---
# System prepended metadata

title: GT4Py Execution Model discussions (leading to new functional model)
tags: [discussion, archive]

---

# GT4Py Execution Model discussions (leading to new functional model)

###### tags: `archive` `discussion`

## Discussion session 1 (10.03.2021)

### Core definitions

- Grid: set of connected points.
- Field: mapping from grid points to quantities. Inside a stencil can also be viewed as a function that can be called with an offset and returns a value(s).
- Stencil: computation using neighborhood shapes to compute output value(s) from a set of input fields.
    + Each input field may be read with a different neighborhood shape.
- Computation area: set of points (or subset of a grid) where a stencil is applied.
- Fencil: sequence of _(stencil, computation area)_ pairs, usually computed together for optimization.
    + In the current DSL, the computation area for each stencil is indirectly defined as a function of a rectangle _domain_ and the stencil itself:
        *  ComputationArea = Function(Stencil, Domain)
    + Instead, we could define the ComputationArea explictly as the subset of grid points where this stencil will be applied.

```
- Grid: Set[Items]
    e.g. grid = {p0, p1, p2, ... p_n, e0, ... e_n ...}

- ComputationArea: Set[Items] # subset of Grid

- Field: Mapping[[Grid], Quantity]

- Accessor: Callable[[Optional[Offset], Quantity]  # Field bound to an application point inside a stencil 

- Stencil: Callable[[Accessor, ...]], Quantity]
    e.g. a(x, y, z) =>  x[1] + y[0] + 2* z[3]

- Fencil: List[Tuple[Stencil, ComputationArea]]
    e.g. [(a(x), comp_area_1), (b(lift(a)(x)), comp_area_2)]
```


### Model

Assumptions in the internal representation of a stencil to guarantee a well-defined interpretation:

- Fields used in a stencil are either input or output (not both).
- Symbols are only assigned once (SSA).
- Assignment to temporaries can be considered as _lifting_ a stencil (RHS expression) to a field.
- The computation area of a stencil is computed explicitly as a function of the domain rectangle passed at run-time.

A fencil is implemented as the sequential application of its stencils in the corresponding computation areas. A _call_ from a fencil (_caller_) to another fencil (_callee_) represents the concatenation of the list of    stencil-computation pairs of the _callee_ in the call site of the _caller_ fencil.


## Possible issues

- Definition of computation areas for each stencil might be more complicated than simply slicing a rectangular domain:
    + how to define a disjoint computation area for a stencil?
    + how to define non-rectangular computation areas?
    + how to use named computation areas defined outside of the stencil?

- Grid concept should be explicitly specified as an argument in the stencil definition.

- _Halo/ghost cells_ and _domain_ specification should be explicitly defined for any object implementing the _Field_ concept (e.g. gt4py storages)

- If a single domain (rectangle?) is passed to the fencil at runtime, what happens when output fields have different domain sizes (e.g. staggered fields)?

- What should be the scope of symbol names in the DSL: global (currently), per computation or per region?

- If assignment to temporaries is considered as just a "lifting", doesn't it complicate the optimization of assigning to a temporary field and reusing the result from several places? In other words, isn't it easier to demote temporaries to scalars than promote computations to temporaries?

- A grid is not just a set of points but several (related) sets of points: vertices, faces, edges (in cartesian grids, even two sets of edges: I-edges and J-edges). In principle, stencils applied on a computation area defined on one of the sets, should only write to output fields defined on that grid item.

- How this model can be extended to solvers?

- How does the execution model relate to the current GridTools C++ execution model?


## Discussion session 2

Preliminar: So far it is been discussed how to represent the internal model in a way that makes sense for a non-ambiguos implementation. The internal model is lower-level than the current DSL (SSA, lifting, etc.), under the assumption of that the current DSL can be regarded as syntatic sugar on top of the model. What I think is still missing in the discussion is the full description of the transforamation to go from the DSL to our model (the _de-sugarization_ of the DSL). It could also be important to re-examine if the current DSL syntax represents sufficiently well the NWP domain concepts.

```mermaid
flowchart LR
    MWP[NWP equations] --> DSL
    DSL --> Model[Internal model]
    Model --> Implemenation
```
------------------------------

## Scratchpad


```python=
@gt.stencil
def stencil_1(in_field_1: InputField, out_field_2: OutputField):

    with computation(), interval(...):
        with horizontal_region([3:3, :]):
            out_field_1 = 0.5 * (in_field_1[I+1] + in_field_1[I-1])


def foo(in_field_1: InputField, in_field_2: InputField):
   tmp <<= in_field_1[J+1]
   out_field_1 = in_field_1[I+1] + in_field_1[I-1] + in_field_2[I+2] + in_field_2[I-2],
   out_field_2 = out_field_1[I+1] + out_field_1[I-1] + in_field_2[I+2] + in_field_2[I-2]
   return out_field_1, out_field_2

def bar(x):
  return x[I + 0] + 1 


def baz(x: acc) -> value:
   tmp : acc = bar(x) # bar' = lift(bar)
   return x[I + 0] + tmp[I - 1]

@true_stencil 
def bazz(x: acc) -> value:
   tmp = bar(x) # 
   return x[I + 0] + tmp # x[I + 0] + bar(x) vs. x[I + 0] + lift(bar)(x)[I + 0]

def lap(x):
  return ...
  
  
def foo(x):
  tmp <= lap(x)
  return lap(tmp)
  
  
  
  


@gt.stencil
def stencil_2(domain, in_field_1: InputField, in_field_2: InputField,
            out_field_1: OutputField, out_field_2: OutputField):
    with computation(), interval(...):
        with horizontal_region(domain[:, :]) as ff:

    with computation(), interval(...):
        with horizontal_region([:, :]) as ff:
            a, b = foo(in_field_1, in_field_2)
            tmp = baz(in)
            
        with horizontal_region([2:-2, :]):
            out = bar(in) + baz(in)
            out2 = bar(tmp)
 
    #stencil_1(domain[2:-2], in_field_1, out_field_1)
 

stencil(if1, if2, of1, of2,
        domain=[100, 100]) # origin=(2,0)) #dict(in_field_1=(1,0), in_field_2=(2,0))


def s(in):
    return in[1, 1, 1] + in[0, 0, 0]

stencil is a function from accessor(s) to value(s)


fencil:
 [ {a(x), comp_area}, {b(lift(a)(x) ), comp_area_2 }]
 
 
 y = a(x)
 z = b(y)
 return y, z
 
 
 
 @fencil
 def aa(x) :
  return [(a(x),  a0), (b(x), a1)] 
 
 bb = [(c(x),  a0}, (d(x), a1)]
 
 aa(bb(x)) => [(a(c(x)),  a0}, (b(d(x), a1)] 



```


```python
@stencil
def laplacian(psi: Accessor) -> double:
  return -4 * delta_psi + delta_psi[I+1] + delta_psi[I-1] + delta_psi[J+1] + delta_psi[J-1]

@fencil
def laplace_operator(psi: InField) -> OutField:
  with computation(PARALLEL), interval(...):
    with horizontal_region([1:-1, 1:-1])
      lap_psi = laplacian(psi)
  return lap_psi

@fencil
def average_laplacian_operator(psi: InField) -> OutField:
  """Compute the average of the laplacian of the I+-1, J+-1 neighbors"""
  with computation(PARALLEL), interval(...):
    with horizontal_region([1:-1, 1:-1])
      # reuse of stencils
      avg_lap_psi = laplacian(psi[I+1]) + laplacian(psi[I-1]) + laplacian(psi[J+1]) + laplacian(psi[J-1])
  return delta_psi

@fencil
def laplap(psi: InField) -> OutField:
  # composition of fencils
  return laplacian(laplacian(psi))
  
---

@stencil
def nabla(psi: Accessor) -> Tuple[double, double]:
  return [
    psi[I+1]-psi[I-1],
    psi[J+1]-psi[J-1]
  ]

@stencil
def divergence(vec: Tuple[Accessor, Accessor]): # arg
  return vec[0][I+1]-vec[0][I-1] + vec[0][J+1]-vec[0][J-1]

@fencil
def laplacian_2(psi: InField):
  with computation(PARALLEL), interval(...):
    with horizontal_region([1:-1, 1:-1]):
      nab = nabla(psi)
      lap = divergence(nab)
  return lap
```

## List of samples
- gtbench without periodic boundary conditions
- ??

