###### tags: `frontend`, `shaping` [TOC] GT4Py Frontend design - Evaluation test cases ============================================= ## Examples overview A collection of code snippets to cover all patterns we would like to express in the frontend. The snippets should be expressed using the *semantic IR* as well as the proposed frontend syntax flavors. __[WIP]__ This document is work-in-progress. We are not trying to design "the" frontend, but a user-friendly (see [goals](https://hackmd.io/ZGMjwyALQW2iW0784Fp5ew#Goals) in the main document) proposal that maps easily to the *semantic IR*. ## Driver code example ```python= T = TypeVar("T") DT = TypeVar("DT", bound=gt.ScalarType) # TODO: Field typing conventions field: Field[[*Dimensions], DT] # # Axes names: V or Vertex, V2V or VV or V_V # ... # TODO(tehrengruber): tentative # mapping from axis to connectivity / neighbor table # user can provide something different (e.g. diamond connectivity) connectivities = { # Connectivity[From, To, max_neighbors, has_skip_values] E2V: Connectivity[E, V, 2, False](range(0, 10), np.array([[0, 1], ...])) # ... } # -- Beautified iterator view -- # Decorator to define local operations (i.e. mapping iterators to values) @gt.beautified_iterator_view def my_local_op(field: Iterator[Vertex, Vertex]) -> Value: if field() < 0: return -2. * field() else: return 0.5 * field() # Feature for debugging & testing: direct execution of stencils new_field = my_local_op[my_vertex_domain](input_field) # -- Lifted iterator view -- # Decorator to define operations from fields to a field @gt.lazy_field_view def my_field_op( field_a: Field[Vertex], field_b: Field[Vertex], ) -> Field[Vertex]: return field_a + field_b # Program/fencil on the fly feature: # outputs = my_local_op[vertex_domain](*inputs) # note that you can't pass the out kwarg here # -- Entry point to the toolchain from Python ('fencil') -- # Input fields can only be updated inside fencils @gt.program def my_program( field_a: Field[Vertex], field_b: Field[Vertex], out_field: Field[Vertex], ): my_field_op(field_a, field_b, out=[out_field]) vertex_domain = gt.IndexRange(Vertex, 0, 3) my_local_op[vertex_domain](*inputs, out=[outputs]) @gt.program def my_dynamic_program( field_a: Field[Vertex], field_b: Field[Vertex], out_field: Field[Vertex], point_a: int, point_b: int ): vertex_domain = gt.IndexRange(Vertex, point_a-3, point_a + 2) my_local_op[other_domain](*inputs, out=[outputs]) # If we later want to allow the creation of temporaries # inside the program, we can add the feature in a more or less # backward compatible way like this: # tmp_field = new_temp() # my_local_op[vertex_domain](*inputs, out=[tmp_field]) ``` ## Frontends overview Possible frontends - Plain iterator: _deref()_, _shift()_ and _lift()_ as explicit functions - Beautified iterator: + `name()` -> _deref(name)_ + `name(offset)` -> _deref(shift(offset)(name))_ + `func_name[...]` -> _lift(func_name)_ - Lifted iterator (aka _Lazy Field_): everything within an expression is dereferenced, put in lambda and lifted (i.e. everything is an iterator). ### Beautified Iterator Features: - Trivial translation to Iterator IR as there are only syntactic changes How to lower: - combined offset `O[0] == (O, 0) == O + 0` - `it(offsets, ...)` expands to `deref(shift(offsets, ...)(it))` - trivial: `it()` -> `deref(shift()(it)) == deref(it)` - `fun[...]` -> `lift(fun)` ### Lifted Iterator (aka (Lazy)Field) Features: - can be constructed from the Iterator Model - no need for `lift()` in the syntax as stencils take iterators and return iterators How to lower: - in every expression: - expand `it(offsets, ...)` to `deref(shift(offsets.reverse(), ...)(it))` <!-- discuss(till): it is not sufficient to just deref as it was written before, as you need to be able to shift/remap inside the expression. The reverse it is needed for (C2E, E2V) to read as from vertex to edge to cell. Alternatively we could disallow such shifts.--> - put the expression in a lambda that has all iterators as parameters - _lift_ the lambda - call the lifted lambda with the original iterators as arguments ### Optional syntactic sugar - reductions with list comprehension: - `sum(fun(it1(n), ..., itN(n)) for n in Offset)` -> `reduce(lambda acc, val1, ..., valN: acc + fun(va1, ..., valN), 0)(shift(Offset)(it1), ..., shift(Offset)(itN))` ## Examples ### Regular horizontal stencil (id: S-V4) + _Plain Iterator:_ ```python= @fundef def sum_of_all_vertex_neighbors_of_a_vertex(inp): # length(V2V) == 4 return ( deref(shift(V2V, 0)(inp)) + deref(shift(V2V, 1)(inp)) + deref(shift(V2V, 2)(inp)) + deref(shift(V2V, 3)(inp)) ) ``` + _Beautified iterator:_ ```python= @gt.beautified_iterator_view def sum_of_all_vertex_neighbors_of_a_vertex(inp: Iterator[Vertex, Vertex]) -> DT: return inp(V2V[0]) + inp(V2V[1]) + inp(V2V[2]) + inp(V2V[3]) ``` + _Lifted iterator view:_ ```python= def sum_of_all_vertex_neighbors_of_a_vertex(inp: Field[Vertex]) -> Field[Vertex]: return ( inp(V2V[0]) + inp(V2V[1]) + inp(V2V[2]) + inp(V2V[3]) ) # It would be an out-of-bounds error if `V2V[i]` is not pointing to a valid position def sum_of_all_vertex_neighbors_of_a_vertex(inp: Field[Vertex]): # not all length(V2V) have to be the same return sum(inp(v) for v in V2V) ``` ### Regular horizontal stencil with shifting (id: S-C2E2V) Semantic: Compute the value of the first vertex neighbor of the first edge neighbor of the current cell. ``` Mesh Iterator Field v3 * * * / \ / o\ / X \ e3 / c1 \ e2 / ↓ \ / ↑ \ *------* *------* *-------* v1 e1 v2 X ← o → ``` + _Plain iterator:_ ```python= @fundef def c2e2v_stencil(inp): # iterator pointing to Cell for Vertex field return deref(shift(E2V, 0)(shift(C2E, 0)(inp))) ``` + _Beautified iterator:_ ```python= @gt.beautified_iterator_view def c2e2v_stencil(inp: Iterator[Cell, Vertex]) -> DT: # note: it is not possible to write this as two seperate shift # without a lift operation, as the call operator automatically # dereferences # return inp(C2E[0])(E2V[0]) # not allowed (trying to shift a value) # return inp(C2E[0], E2V[0]) # don't do it now, because need extra explanation ``` + _Lifted iterator view:_ ```python= @fundef def c2e2v_stencil(inp: Field[Vertex]) -> Field[Cell]: # inp(E2V[0])(C2E[0]) # inp(E2V[0], C2E[0]) tmp1: Field[Edge] = inp(E2V[0]) # (lambda inp: inp(E2V[0]))[...] tmp2: Field[Cell] = tmp1(C2E[0]) # todo: discuss # inp(C2E[0], E2V[0]) return tmp2 ``` ```python= def c2e2v_stencil(inp: Iterator[Cell, Vertex]): edge_neigh_of_cell: Iterator[Edge, Vertex] = shift(C2E, 0)(inp) vertex_neigh_of_edge: Iterator[Vertex, Vertex] = shift(E2V, 0)(edge_neigh_of_cell) return deref(vertex_neigh_of_edge) def c2e2v_stencil(inp): vertex_neigh_of_edge = lift(lambda x: deref(shift(E2V, 0)(x)))(inp) edge_neigh_of_cell = lift(lambda x: deref(shift(C2E, 0)(x)))(vertex_neigh_of_edge) return deref(edge_neigh_of_cell) def c2e2v_stencil(inp: Field[Vertex]) -> Field[Cell]: vertex_neigh_of_edge: Field[Edge] = inp(E2V[0]) edge_neigh_of_cell: Field[Cell] = vertex_neigh_or_edge(C2E[0]) return edge_neigh_of_cell ``` ### Regular horizontal stencil with shifting and lifting (id: S-C2E2V-lifted) Note: this is the same computation as above, but with an additional lifting operation. + _Plain iterator:_ ```python= # inp is an iterator with E-position @fundef def first_vertex_nb(inp): # shift(E2V, 0)(inp) returns an iterator on a Vertex position # what type of buffer inp refers to is undefined at this point return deref(shift(E2V, 0)(inp)) @fundef def c2e2v_stencil(inp): # lift(first_vertex_nb) takes an iterator on an edge position # an iterator on an edge position # note: The following can not be expressed in the beautified and # lifted iterator view as a shift always involves a deref. return first_vertex_nb(shift(C2E,0)(inp)) return deref(lift(first_vertex_nb)(shift(C2E, 0)(inp))) return deref(shift(C2E, 0)(lift(first_vertex_nb)(inp))) ``` + _Beautified iterator view_: ```python= @gt.beautified_iterator_view def first_vertex_nb(inp: Iterator[Edge, Vertex]) -> DT: return inp(E2V[0]) @gt.beautified_iterator_view def c2e2v_stencil(inp: Iterator[Cell, Vertex]) -> DT: # first_vertex_nb[...](inp) returns an iterator of type # Iterator[Cell, Edge], where Cell originates from the # inp argument and Edge comes from first_vertex_nb signature. # Note that the position type of all arguments of a shiftable # stencil need to agree. # first_cell_nb[...](inp) can also be seen as an edge field # (originating from inp) that we can access by shifting our # current position to an edge in the vicinity. return first_vertex_nb[...](inp)(C2E[0]) ``` + _Lifted iterator view:_ ```python= def first_vertex_nb(inp: Field[Vertex]) -> Field[Edge]: return inp(E2V[0]) def c2e2v_stencil(inp: Field[Vertex]) -> Field[Cell]: return first_vertex_nb(inp)(C2E[0]) ``` <!-- ```python= def stencil(vertex_field: Iterator[Edge, Vertex]) -> DT: return field(E2V[0]) def bar(vertex_field: Iterator[Vertex, Vertex]): field_edge = stencil[...](vertex_field) field_vertex = field_edge(V2E[0]) return field_vertex def bar_without_lift(): # note how the ordering changes return field_vertex.shift(V2E[0]).shift(E2V[0]) ``` ```python= def stencil(vertex_field: Field[Vertex]) -> Field[Edge]: # the value at the 0-th edge for all vertices that E2V maps to return field(E2V[0]) def bar(vertex_field: Field[Vertex]): field_edge = stencil(vertex_field) field_vertex = field_edge(V2E[0]) return field_vertex ``` --> ### Regular horizontal stencil with reduction (id: S-VRED) + _Plain iterator:_ ```python= @fundef def sum_of_all_vertex_neighbors_of_a_vertex(inp): return reduce(lambda a, b: a+b, shift(V2V)(inp)) ``` + _Beautified iterator view_: ```python= @gt.beautified_iterator_view def sum_of_all_vertex_neighbors_of_a_vertex(inp: Iterator[Vertex, Vertex]) -> DT: return sum(inp(v) for v in V2V) ``` + _Lifted iterator view:_ ```python= def sum_of_all_vertex_neighbors_of_a_vertex(inp: Field[Vertex]): return sum(inp(v) for v in V2V) ``` ### Sparse-field (Hannes) (id: S-SF) + _Plain iterator:_ ```python= def sum_of_all_vertex_neighbors_of_a_vertex(inp): # inp is an iterator in the sparse field state return reduce(lambda a, b: a+b, 0)(inp) def driver(inp) # non sparse field return sum_of_all_vertex_neighbors_of_a_vertex(shift(V2V)(inp)) ``` + _Lifted iterator view:_ ```python= def sum_of_all_vertex_neighbors_of_a_vertex(inp: Iterator[Vertex, ?]) -> Iterator[Vertex]: return reduce(lambda a, b: a+b, 0)(inp) def driver(inp: Iterator[Vertex]) -> Iterator[Vertex] return sum_of_all_vertex_neighbors_of_a_vertex(inp(V2V)) ``` ### Nabla (id: S-FVM-NBL) + _Plain iterator:_ ```python= @fundef def compute_zavgS(pp, S_M): zavg = 0.5 * (deref(shift(E2V, 0)(pp)) + deref(shift(E2V, 1)(pp))) # zavg = 0.5 * reduce(lambda a, b: a + b, 0)(shift(E2V)(pp)) # zavg = 0.5 * library.sum()(shift(E2V)(pp)) return deref(S_M) * zavg @fundef def compute_pnabla(pp, S_M, sign, vol): zavgS = lift(compute_zavgS)(pp, S_M) # pnabla_M = reduce(lambda a, b, c: a + b * c, 0)(shift(V2E)(zavgS), sign) # pnabla_M = library.sum(lambda a, b: a * b)(shift(V2E)(zavgS), sign) pnabla_M = library.dot(shift(V2E)(zavgS), sign) return pnabla_M / deref(vol) ``` + _Beautified iterator view:_ ```python= @gt.beautified_iterator_view def compute_zavgS(pp: Iterator[Edge, Vertex], S_M: Iterator[Edge, Edge]) -> DT: zavg = 0.5 * (pp(E2V[0]) + pp(E2V[1])) return S_M() * zavg @gt.beautified_iterator_view def compute_pnabla( pp: Iterator[Vertex, Vertex], S_M: Iterator[Vertex, Edge], sign: Iterator[Vertex, ?], vol: Iterator[Vertex, Vertex] ) -> DT: zavgS: Iterator[Vertex, Edge] = compute_zavgS[...](pp, S_M) # todo: sign() could return Iterator[Vertex, Edge] pnabla_M = sum(zavgS(e)*sign()(e) for e in V2E) return pnabla_M / vol() ``` + _Lifted iterator view:_ ```python= def compute_zavgS(pp: Field[Vertex], S_M: Field[Edge]) -> Field[Edge]: zavg: Field[Edge] = 0.5 * (pp(E2V[0]) + pp(E2V[1])) # zavg = lift(lambda a, b: 0.5 * (deref(shift(E2V, 0)(a)) + deref(shift(E2V, 1)(a))))(pp) return S_M * zavg # return lift(lambda a,b: deref(a)*deref(b))(S_M, zavg) def compute_pnabla(pp: Field[Vertex], S_M: Field[Edge], sign, vol: Field[Vertex]) -> Field[Vertex]: zavgS: Field[Edge] = compute_zavgS(pp, S_M) pnabla_M: Field[Vertex] = sum( zavgS(v) * sign[i] for i, v in enumerate(V2E) #zavgS(e) * sign[i] for i, e in enumerate(V2E) ) # TODO I don't like adressing the sign field with `i`, dusk solves this by inprecise syntax # pnabla_M = lift(reduce( lambda a, b, c: a+b*c, 0))(shift(V2E)(zavgS), sign) return pnabla_M / vol # return lift(lambda a,b: deref(a)/deref(b))(pnabla_M, vol) ``` ### ICON reductions & dusk/dawn weights (id: S-ICO-DRED) + _Beautified iterator:_ ```python= @gt.beautified_iterator_view def reductions_and_weights( cells_a: Iterator[Edge, Cell], vertices_a: Iterator[Edge, Vertex] ) -> DT: # simple difference between neighboring cells of an edge: edges_a: DT = cells_a(E2C[1]) - cells_a(E2C[0]) # difference between the upper and lower vertex of the diamond edges_a: DT = vertices_a(E2C[1], C2V[0]) - vertices_a(E2C[0], C2V[0]) # sum of the upper and lower vertex of the diamond (selective/filtered sum) edges_a: DT = vertices_a(E2C[0], C2V[0]) + vertices_a(E2C[1], C2V[0]) return edges_a ``` + _Lifted iterator_ ```python= def reductions_and_weights( cells_a: Field[Cell], vertices_a: Field[Vertex] ) -> Field[Edge]: # simple difference between neighboring cells of an edge: edges_a: Field[Edge] = cells_a(E2C[1]) - cells_a(E2C[0]) # difference between the upper and lower vertex of the diamond edges_a: Field[Edge] = vertices_a(E2C[1], C2V[0]) - vertices_a(E2C[0], C2V[0]) # sum of the upper and lower vertex of the diamond (selective/filtered sum) edges_a: Field[Edge] = vertices_a(E2C[0], C2V[0]) + vertices_a(E2C[1], C2V[0]) return edges_a ``` + dusk syntax: ```python= @stencil def reductions_and_weights( edges_a: Field[Edge], cells_a: Field[Cell], vertices_a: Field[Vertex], ): with domain.upward: # These examples all involve the following two triangle shape: # (we call this the diamond shape, orientation is irrelevant) # # * # / \ # / \ # *-----* # \ / # \ / # * # simple difference between neighboring cells of an edge: edges_a = sum_over(Edge > Cell, cells_a, weights=[-1, 1]) # difference between the upper and lower vertex of the diamond edges_a = sum_over(Edge > Cell > Vertex, vertices_a, weights=[-1, 1, 0, 0]) # sum of the upper and lower vertex of the diamond (selective/filtered sum) edges_a = sum_over(Edge > Cell > Vertex, vertices_a, weights=[1, 1, 0, 0]) ``` ### [WIP] Scan pass (id: S-KSUM) + _Plain iterator:_ ```python= @fundef def sum_scanpass(state, inp): return if_(is_none(state), deref(inp), state + deref(inp)) @fundef def ksum(inp): return scan(sum_scanpass, True, None)(inp) @fendef(column_axis=KDim) def ksum_fencil(i_size, k_size, inp, out): closure( domain(named_range(IDim, 0, i_size), named_range(KDim, 0, k_size)), ksum, [out], [inp], ) ``` + _Beautified iterator:_ ```python= @gt.beautified_iterator_view def sum_scanpass(state: Optional[DT], inp: Iterator[Vertex]): if state is None: return inp() return state+inp() @gt.column_operator(column_axis=K) def ksum(inp: Iterator[[V, K], [V, K]]): c: Iterator[V] = scan(sum_scanpass, forward=True, init=None)(inp) return c ``` + _Indexing proposal:_ ```python= # option 1 @gt.local_operator def sum_scanpass(state: Optional[DT], inp: Field[], *, position): (v, k) = position if state is None: return inp[v, k] return state+inp[v, k] # option 2 @gt.scan_pass(forward=True, init=None, column_axis=K) def sum_solver(state, inp, *, position): # ... same as before @gt.column_operator(column_axis=K) def ksum(inp: Field[[V, K]], inp_2d: Field[[V]]): (v,) = position # option 1 return scan(sum_scanpass, forward=True, init=None)(inp, position=position) # option 2 return sum_solver(inp, position=position) @gt.program(...) def ksum_fencil(i_size, k_size, inp, out): domain = IndexRange[IDim](0, i_size)*IndexRange[IDim](0, k_size) return ksum[domain](inp, out=out) ``` ### Tridiag solver (id: S-TRI) + _Plain iterator:_ ```python= @fundef def tridiag_forward(state, a, b, c, d): # not tracable # if is_none(state): # cp_k = deref(c) / deref(b) # dp_k = deref(d) / deref(b) # else: # cp_km1, dp_km1 = state # cp_k = deref(c) / (deref(b) - deref(a) * cp_km1) # dp_k = (deref(d) - deref(a) * dp_km1) / (deref(b) - deref(a) * cp_km1) # return make_tuple(cp_k, dp_k) # variant a # return if_( # is_none(state), # make_tuple(deref(c) / deref(b), deref(d) / deref(b)), # make_tuple( # deref(c) / (deref(b) - deref(a) * nth(0, state)), # (deref(d) - deref(a) * nth(1, state)) # / (deref(b) - deref(a) * nth(0, state)), # ), # ) # variant b def initial(): return make_tuple(deref(c) / deref(b), deref(d) / deref(b)) def step(): return make_tuple( deref(c) / (deref(b) - deref(a) * nth(0, state)), (deref(d) - deref(a) * nth(1, state)) / (deref(b) - deref(a) * nth(0, state)), ) return if_(is_none(state), initial, step)() @fundef def tridiag_backward(x_kp1, cp, dp): # if is_none(x_kp1): # x_k = deref(dp) # else: # x_k = deref(dp) - deref(cp) * x_kp1 # return x_k return if_(is_none(x_kp1), deref(dp), deref(dp) - deref(cp) * x_kp1) @fundef def solve_tridiag(a, b, c, d): tup = lift(scan(tridiag_forward, True, None))(a, b, c, d) cp = nth(0, tup) dp = nth(1, tup) return scan(tridiag_backward, False, None)(cp, dp) @fendef def fen_solve_tridiag(i_size, j_size, k_size, a, b, c, d, x): closure( domain( named_range(IDim, 0, i_size), named_range(JDim, 0, j_size), named_range(KDim, 0, k_size), ), solve_tridiag, [x], [a, b, c, d], ) ``` + _Beautified iterator:_ ```python= @gt.beautified_iterator_view def tridiag_forward(state: Optional[DT], a, b, c, d): if state == None: cp_k = c() / b() dp_k = d() / b() else: cp_km1, dp_km1 = state cp_k = c() / (b() - a() * cp_km1) dp_k = (d() - a() * dp_km1) / (b() - a() * cp_km1) return (cp_k, dp_k) @gt.beautified_iterator_view def tridiag_backward(x_kp1: Optional[DT], cp, dp): if x_kp1 == None: x_k = dp() else: x_k = dp() - cp() * x_kp1 return x_k @gt.column_operator(column_axis=K) def ksum(inp: Iterator[[V, K], [V, K]]): c: Iterator[V] = scan(sum_scanpass, forward=True, init=None)(inp) return c ``` ### Laplacian Cartesian (id: S-L4) + _Plain iterator:_ ```python= @fundef def laplacian(inp): return -4.0 * deref(inp) + ( deref(shift(I, 1)(inp)) + deref(shift(I, -1)(inp)) + deref(shift(J, 1)(inp)) + deref(shift(J, -1)(inp)) ) ``` + _Beautified Iterator:_ ```python= @gt.beautified_iterator_view @fundef def laplacian(inp: Iterator[(I, J), (I, J)]) -> DT: return -4.0 * inp() + inp(I+1) + inp(I-1) + inp(J+1) + inp(J-1) ``` + _Lifted iterator:_ ```python= @fundef def laplacian(inp: Field[I, J]) -> Field[I, J]: return -4.0 * inp() + inp(I+1) + inp(I-1) + inp(J+1) + inp(J-1) ``` ### Horizontal diff Cartesian (id: S-HD) + _Plain iterator:_ ```python= @fundef def flux_x(inp): lap = lift(laplacian)(inp) flux = deref(shift(I, -1/2)(lap)) - deref(shift(I, 1/2)(lap)) return if_(flux * deref(shift((I, 1/2))(inp)) - deref(shift((I, -1/2))(inp)) > 0, 0, flux) # code without staggered dimensions looks like this: def flux_x(inp): lap = lift(laplacian)(inp) flux = deref(lap) - deref(shift(I, 1)(lap)) return if_(flux * (deref(shift(I, 1)(inp)) - deref(inp)) > 0.0, 0.0, flux) ``` + _Beautified Iterator:_ ```python= @gt.beautified_iterator_view def laplacian(inp: Iterator[(I, J), (I, J)]): return ... @gt.beautified_iterator_view def flux_x(inp: Iterator[(I+1/2, J), (I, J)]): lap = laplacian[...](inp) flux = lap(I-1/2) - lap(I+1/2) if flux * inp(I+1/2) - I(I-1/2) > 0.0: return 0 return flux flux_x[i_staggered_domain](inp) ``` + _Lifted Iterator:_ ```python= def flux_x(inp: Field[(I, J)]): lap = laplacian[...](inp) flux = lap(I-1/2) - lap(I+1/2) if flux * inp(I+1/2) - I(I-1/2) > 0.0: return 0 return flux ``` Note: it is not possible in this proposal to specify that this operator was formulated to be posed on the staggered domain. ### [WIP] BCs Cartesian (id: S-L4-BC) + _Accessor proposal:_ ```python= @gt.local_operator(domain=(I, J)) def laplacian(boundary: IndexRange[I, J], inp: Accessor[[I, J], DT]): if i, j in boundary: return 0. return -4.0 * inp() + (inp(I+1) + inp(I-1) + inp(J+1) + inp(J-1)) @gt.local_operator(domain=(I, J)) def laplacian(inp: Accessor[[I, J], DT]): if i, j in gt.domain[1:-1, 1:-1]: return 0. return -4.0 * inp() + (inp(I+1) + inp(I-1) + inp(J+1) + inp(J-1)) ``` ### [WIP] BCs Unstructured ```python= @gt.local_operator def laplacian(inp: Field[[V], DT], on_boundary: Field[[V], bool], *, position: Position[V]): (v,) = position if on_boundary: return 0. return -4.0 * inp[v] + sum(inp[v2] for v2 in neighbors(v, V2V)) ``` ```python= @gt.local_operator def laplacian(inp: Field[[V], DT], *, position: Position[V]): (v,) = position if v in boundary_vertices: return 0. return -4.0 * inp[v] + sum(inp[v2] for v2 in neighbors(v, V2V)) ``` ### [WIP] PBCs Cartesian ### [WIP] Elliptic solver (id: S-ES) *Positional with field composition* ```python= @gt.local_operator def laplacian(domain, phi: Field[(I, J), DT], *, position: Position[I, J]): if position not in domain[1:-1, 1:-1]: return -(phi[i-1, j]-2*phi[i, j]+phi[i+1, j])/(dx*dx) - (phi[i, j-1]-2*phi[i, j]+phi[i, j+1])/(dy*dy) return 0. @gt.field_operator def step(domain, β: DT, L: Field[(I, J), DT], ϕ: Field[(I, J), DT], r: Field[(I, J), DT]): ϕ = ϕ + β * r # update solution r = r + β * L_ # update residual return ϕ, r # driver code def steepest_descent(ϕ_0, L, R, ϵ=0.01, max_it=10000): ϕ = ϕ_0 # solution r = L(ϕ)-R # residual # ensure BCs are respected r[0, :]=r[:, 0]=r[-1, :]=r[:, -1]=0 for it in range(0, max_it): L = laplacian[domain](domain, r) β: DT = - sum(r*r)/sum(r * L_) # step size step(β, L, ϕ, r, out=(ϕ, r)) # debug output print(f"it: {it}, ||r||_L2 = {np.linalg.norm(r)}") #if plotting_enabled: # plot(ϕ) # break when converged if np.linalg.norm(r) <= ϵ: break return ϕ ``` ### [WIP] Average (id: S-AVG) *Positional with field composition* ```python= def backward_diff(inp): return forward_diff(inp)[i-1] @gt.local_operator def laplacian(inp): return -4*inp[i, j]+inp[i-1, j]+inp[i+1, j]+inp[i, j-1]+inp[i, j+1] return (inp[i-1] - inp) - (+inp - inp[i+1]) + (...) return forward_diff(inp)[i-1] - forward_diff(inp) return backward_diff(inp) - forward_diff(inp) @gt.local_operator def forward_diff_i(inp): return inp[i+1, j]-inp[i, j] @gt.local_operator def central_diff_i(inp): return inp[i+1, j]-inp[i-1, j] @gt.field_operator def laplacian(inp): @gt.local_operator def average(inp: Field[(Edge, K), DT], *, position: Position[Vertex, K]) -> DT: v, k = position return sum(inp[e, k] for e in neighbors(v, V2E)) @gt.local_operator def average_same(inp: Field[(Vertex, K), DT], *, position: Position[Vertex, K]): v, k = position return sum(inp[v, k] for v in neighbors(v, V2V)) @gt.field_operator def average_composition(vertex_domain, inp: Field[(Edge, K), DT]): tmp: Field[Vertex, DT] = average[vertex_domain](inp) return average_same[vertex_domain](tmp) def driver(): vertex_domain = Domain((Vertex, 0, 10), (K, 0, 10)) edge_domain = Domain((Edge, 0, 10), (K, 0, 10)) inp_edge = Field.from_function(edge_domain, lambda e, k: 42) out_vertex = Field.from_function(vertex_domain, lambda v, k: 0) average[vertex_domain](inp_edge, out=out_vertex) average_composition(vertex_domain, inp_edge, out=out_vertex) ``` ### Different iterator positions in the same stencil (id: S-EY) *Beautified Iterator* ```python= @gt.beautified_iterator_view def foo(it0, it1): return it0() + it1() @gt.beautified_iterator_view def bar(inp): foo(inp, shift(I+1)(inp)) ``` *Positional* ```python= @local_operator def foo(inp, inp2, *, position): return inp[position] + inp2[position] @local_operator def shift(inp, dim, offset, *, position): pos = position.shift(dim, offset) return inp[pos] @field_operator def bar(inp): foo[...](inp, shift[...](inp, I, 1 )) ``` ### "Chiasm" stencil (id: S-CHI) This example illustrates a valid stencil (call) that can not be lifted and shifted. Namely stencils for which the position type of some argument iterators is different. *Beautified Iterator* ```python= @gt.beautified_iterator_view def foo(it0: Iterator[Vertex, Edge], it1: Iterator[Edge, Edge]): return it0(V2E(0)) + it1() @gt.beautified_iterator_view def bar(it0: Iterator[Vertex, Edge]): # we have not introduced a shift syntax-wise for this model # but we can always read it as (lambda inp: inp(E2V, 0))[...] it1: Iterator[Edge, Edge] = it0.shift(E2V[0]) tmp = foo[...](it0, it1) return tmp() # perfectly valid # not possible with whatever shift: # return tmp(some_shift) # Proof by contradiction: Let it0 and it1 be iterators # located on different positions. By definition of the # semantics of a call to a lifted stencil this must # be equal to: # foo(it0.shift(some_shift), it1.shift(some_shift)) # However by the definition of shift it0 and it1 must # be located at the "from" dimension of some_shift. # This contradics the assumption. # This can also be seen by inling the above example: # it0(some_shift, V2E(0)) + it0(E2V(0), some_shift) # Consequently some_shift is from Vertex (2. call) to # Vertex (1. call). As such the first call shifts it0 # from a vertex to an edge, but the second call shifts # it from an edge to a vertex. An iterator can however # either point to a vertex or an edge, but not both. # "fencil" bar[vertex_domain](edge_field) ``` ### [WIP] Nested reductions (id: S-NR) ### [WIP] Sign field FVM (id: S-FVM-SGN) ### Conditional - _Plain iterator_: ```python= def conditional(cond, a, b): return if_(deref(cond), deref(a), deref(b)) ``` - _Beautified iterator (with if beautification):_ ```python= def conditional(cond, a,b): if cond(): tmp2 = a() tmp = tmp2 else: tmp = b() return tmp ``` - _Lifted iterator:_ ```python= def conditional(cond: Field, a: Field, b: Field): return if_(cond, a, b) ```