# Mixed Local/Field view execution model Incomplete! ## Semantics ### Simply typed lambda calculus This execution model is based on the simply-typed lambda-calculus with `int`, `float`, `double` constants and enhanced by let statements. Note: The simply typed lambda calculus does not allow recursion. Good references: - http://web-static-aws.seas.harvard.edu/courses/cs152/2016sp/lectures/lec11-types.pdf - https://www.seas.upenn.edu/~cis500/cis500-f13/sf/Stlc.html (more accessable in the beginning, but not as concise) The following are mathematical definitions that define the required semantics, they still need to be represented / encoded to cope well with the lambda calculus semantics. *Todo*: Encoding of symbol. Currently unique integers are used internally. Put references here for the different choices. *Todo*: Type constructors. There seem to be many different choices in the literature some which are too expressive leading to models with too much flexibility. ### Lambda function / Abstraction __Semantics__ A (anonymous) function mapping (typed) arguments to values. __Grammar__ ```python class Lambda(Function): args: Tuple[SymbolName, ...] expr: Expr ``` __Example__ ```python Lambda(args=[SymbolName(name="x", type_=int), expr=SymbolRef("y")]) ``` ### Index For now just an integer. *Current opinion*: Attributing a type tag to an index makes sense to avoid confusion / disconnection between frontend code and generated code during debugging. ### Index range __Semantics__ A range from a semi-positive index $i_{start}$ to $i_{end}$ with step size one, i.e. $unit\_range(i_{start}, i_{stop}) = \{i \, |\, i \in \mathbb{Z}^{\ge 0} \land i_{start} \le i < i_{end}\}$ __Grammar__ Index ranges are always externally supplied and only appear as symbols in the IR. In order to support what is currently called "for-loops" in GT4Py, i.e. map operations over static dimensions (e.g. a set of tracers), this requirement could be lifted and we could allow something like this: ``` Call(func=Construct(), args=(UnitRange, i_start, i_stop)) ``` ### Index Product set A cartesian product of integer intervals, i.e. $P = (unit\_range(i_{0, start}, i_{0, stop})) \times ... \times (unit\_range(i_{n, start}, i_{n, stop}))$ __Operations__ - Access: $f(P, o_0, ..., o_n) = (i_0+o_0, ..., i_n+o_n)$ (complexity $O(1)$) ### Stencil A lambda function whose closure vars are fields or constants taking indices and returning a value. __Example__ ```python # frontend lambda i, j: (field[i-1, j]-field[i+1, j], field[i, j-1]-field[i, j+1]) # ir delta_x = Call(Sub, Call(GetItem, (SymbolRef("Field"), (Call(Sub, (SymbolRef("I"), -1)), SymbolRef("J")))) delta_x = Call(BuiltInFunction("getitem"), (SymbolRef("field"), Call(BuiltInFunction("sub"), (SymbolRef("I"), Constant(-1))) Lambda(args=(SymbolName("I", int), SymbolName("J", int)), expr=Call(Construct(), (Tuple, Call(BuiltInFunction("sub"), args=Call(BuiltInFunction("getitem"), (SymbolRef("field"), Call(BuiltInFunction("sub"), (SymbolRef("I"), Constant(-1)))) SymbolRef())) ``` ### Array A function $f: X \to Y$ with $X$ a cartesian product of index ranges starting from zero and $Y$ a set of numerical values. __Operations__ - Access: $f(i_0, ..., i_n)$ (complexity $O(1)$) - Map: $map: F \times D \to F$ a higher order function that takes a stencil $s \in F$ and a product set $d \in D$ and returns an array $f \in F$ ## Library All of this is frontend related and not part of the execution model. ### Field In mathematical notation a `Field` may be written as some function $f: X \to Y$ where the *domain* is a *product set* of indices and $Y$ is a set of numerical values. **Operations** - Access: For every element of the domain the field returns a value (complexity $O(1)$) - Domain retrieval - Image retrieval ```python class Field(DataModel, AbstractField): domain: ProductSet image: np.ndarray def __getitem__(self, position): assert position in self.domain origin = self.domain[0, 0, 0] image_idx = tuple(idx-o for idx, o in zip(position, origin)) return self.image[image_idx] def view(self, domain): return apply_stencil(lambda idx: self[idx], domain) def transparent_view(self, domain): return TransparentView(domain, self) ``` ## Transparent View ```python class TransparentView(DataModel, AbstractField): domain: ProductSet field: Field def __getitem__(self, position): assert position in self.field.domain return self.field[position] ``` ## Examples This are frontend examples! Todo: Show IR of the execution model. ### Laplacian ```python def laplacian(field_domain: ProductSet, field_image: Array): def stencil(i, j, k): return -4 * f(i, j) + f(i-1, j) + f(i+1, j) + f(i, j-1) + f(i, j+1) return map(stencil, field.domain) def laplap(field_domain: ProductSet, field_image: Array): laplacian(field_domain, field_image) ``` ```python @tracable def laplacian(field: "Field"): @stencil_decorator(extent=(1, 1)) def stencil(f): return -4 * f(0, 0) + f(-1, 0) + f(1, 0) + f(0, -1) + f(0, 1) return fmap(stencil, field) @tracable def laplap(field): lap = laplacian(field) laplap = laplacian(lap) return laplap domain = UnitRange(0, 5)*UnitRange(0, 5) input = apply_stencil(lambda pos: 1 if 1 < pos[0] < 3 else 0, domain) input_view = input.transparent_view(input.domain.extend(-2, -2)) result = laplap(input_view) ``` ### Upstream FVM This should be possible, but is a draft and untested. ```python vel = [1, 0, 0] vhs = [apply_stencil(lambda pos: vel[dim], grid[idx_space]) for dim, idx_space in enumerate([(I+1/2, J, K), (I, J+1/2, K), (I, J, K+1/2)])] psib = apply_stencil(lambda pos: 1 if pos in grid[(I, J, K), "west"] else 0, grid[(I, J, K)]) psi = apply_stencil(lambda pos: 0, grid[(I, J, K)]) def upwind_dim(grid, psi: Field, psib: Field, vh: Dict[Any, Field], dim: Dimension): def interior_stencil(vh, psi): return vh() * psi(dim-1/2) if (vh() >= 0.0) else vh() * psi(dim+1/2) def boundary_stencil(vh, psib, psi, dir: Union[Literal[-1/2], Literal[1/2]]): return vh() * psib(dim+dir) if (vh() >= 0.0) else vh() * psi(dim+dir) def stencil(position, vh, psi): boundary_names = {I: ["west", "east"], J: ["south", "north"], K: ["bottom", "top"]} if position in grid[position.dimensions[dim-1/2], "interior"]: return interior_stencil(vh, psi, dim) elif position in grid[position.dimensions[dim-1/2], ("prescribed_boundary", boundary_names[dim][0])]: return boundary_stencil(vh, psib, psi, dim, -1/2) elif position in grid[position.dimensions[dim-1/2], ("prescribed_boundary", boundary_names[dim][1])]: return boundary_stencil(vh, psib, psi, dim, 1/2) else: raise ValueError() return apply_stencil(stencil, vh.domain, vh, psi) def upwind(grid, psi, psib, vh): return (upwind_dim(psi, psib, vh[dim], dim=dim()) for dim in (I, J, K) def update_fluxdiv(grid, fxh, fyh, fzh, dt, dx, dy, dz): dsi = [1./dx, 1./dy, 1./dz] def stencil(position, psi, fxh, fyh, fzh): if position in grid[(I, J, K), "physical"]: div = sum([(fh(dim + 1 / 2) - fh(dim - 1 / 2)) * ds for dim, ds, fh in zip([I, J, K], dsi, [fxh, fyh, fzh])]) return psi() - dt * div return psi() return apply_stencil(stencil, grid[(I, J, K)], psi, fxh, fyh, fzh) def fvm_advector(grid, psi, psib, vh, dt, dx, dy, dz): "advect psi for one time step" fxh, fyh, fzh = upwind(grid, psi, psib, vh) return update_fluxdiv(grid, fxh, fyh, fzh, dt, dx, dy, dz) ```