-
Notifications
You must be signed in to change notification settings - Fork 57
feat[next]: staggered fields #2667
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 7 commits
b5f37da
ed935fa
52fe0fd
32618f6
df11778
5e704e7
544232d
85ba405
21d3c09
e4105b9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| --- | ||
| tags: [] | ||
| --- | ||
|
|
||
| # Staggered Dimensions | ||
|
|
||
| - **Status**: valid | ||
| - **Authors**: Till Ehrengruber (@tehrengruber) | ||
| - **Created**: 2026-07-08 | ||
| - **Updated**: 2026-07-09 | ||
|
|
||
| A **staggered dimension** is a dimension sitting at the **half-integer** | ||
| positions of a base dimension. For example, in a cell-centered 2D Cartesian grid | ||
| with cells located at `I`, `J`, the edges are located at `I − ½`, `J` and | ||
| `I`, `J − ½`. Differentiating between staggered and non-staggered dimensions | ||
| avoids accidental errors when fields defined on different entities are combined, | ||
| and improves readability by staying close to the mathematical formulation. | ||
|
|
||
| ## Context | ||
|
|
||
| Without a distinction between an entity and its staggered counterpart, fields | ||
| living on, e.g., cells and on edges share the same dimension and can be combined | ||
| silently even though they are geometrically different. We want the type system to | ||
| keep them apart, while still letting users move between the two with an intuitive, | ||
| mathematically-flavoured syntax. | ||
|
|
||
| ## Indexing convention | ||
|
|
||
| A shift by a **half-integer** offset moves between a dimension and its staggered | ||
| counterpart; an integer offset shifts within the (non-staggered) dimension as | ||
| usual. | ||
|
|
||
| Let `i_field` be a 1D field of cell values defined on `I`, and `IHalf` the | ||
| corresponding staggered dimension of the edges between those cells. Then: | ||
|
|
||
| - `i_field(IHalf + 0.5)` → maps edges to the cell value above: `(i−½)+½ = i` | ||
| - `i_field(IHalf − 0.5)` → maps edges to the cell value below: `(i−½)−½ = i−1` | ||
|
|
||
| ``` | ||
| ┊ ● ┊ ● ┊ ● ┊ | ||
| I -1 0 1 | ||
| IHalf -1½ -½ +½ +1½ | ||
| ``` | ||
|
|
||
| Symmetrically, let `ihalf_field` be defined on `I − ½` (i.e. on `IHalf`). Then: | ||
|
|
||
| - `ihalf_field(I + 0.5)` → maps cells to the edge above: `i+½` | ||
| - `ihalf_field(I − 0.5)` → maps cells to the edge below: `i−½` | ||
|
|
||
| ### Storage | ||
|
|
||
| `ihalf_field.ndarray[idx]` holds the value for logical index | ||
| `i = domain.start + idx`, which is *interpreted* as sitting at `i − ½`. In the | ||
| example above, `IHalf(0)` therefore sits at `0 − ½ = −½`, i.e. it is the index of | ||
| the edge just below the cell `I(0)`. The memory layout is identical to any normal | ||
| field; the "half" is purely how the index is interpreted geometrically. This | ||
| index arithmetic is encoded in `common.connectivity_for_cartesian_shift`. | ||
|
|
||
| ## Encoding | ||
|
|
||
| A staggered dimension is encoded as its base dimension's name with the internal | ||
| `_Staggered` prefix (`common._STAGGERED_PREFIX`), rather than as a new attribute | ||
| on `Dimension`. The helpers `is_staggered`, `flip_staggered` and | ||
| `as_non_staggered` operate purely on that prefix. | ||
|
|
||
| Dimensions are identified by their **name** and appear throughout the toolchain | ||
| in more than one form: as a `common.Dimension` instance, but also as an | ||
| `itir.AxisLiteral` (and as plain strings in generated backend code). Adding a | ||
| "staggered" flag to `Dimension` would have required threading it through every one | ||
| of these representations and every place that constructs, compares, or lowers a | ||
| dimension — a large, invasive change touching the frontend, the IR, and all | ||
| backends. Carrying the marker in the name instead means the frontend and IR pass | ||
| it through unchanged, without any of those formats having to learn about | ||
| staggering. The leading underscore marks the prefix as internal — user code goes | ||
| through the `is_staggered` / `flip_staggered` / `as_non_staggered` helpers | ||
| (exported from `gt4py.next`) rather than manipulating the name directly. | ||
|
|
||
| ## Dimension Constraints | ||
|
|
||
| **A dimension and its staggered counterpart must not appear together** in the | ||
| same field or domain. This follows directly from the geometric meaning of | ||
| staggered dimension and is enforced by `check_dims` (`common.py`), which | ||
| raises otherwise. Other functions may rely on this, e.g `order_dims` just orders | ||
| by the `as_non_staggered` name. | ||
|
|
||
| ## gtfn backend | ||
|
|
||
| In gtfn a staggered dimension is emitted as a C++ `using` alias of its base | ||
| dimension's tag (`_add_staggered_aliases` in `itir_to_gtfn_ir.py`): e.g. | ||
| `_StaggeredIDim_t` becomes an alias of `IDim_t`, and `visit_AxisLiteral` | ||
| correspondingly emits the base dimension name. | ||
|
|
||
| The reason is that gtfn lowers a shift to an integer offset along a SID axis, and | ||
| a half-integer shift produces exactly such an offset (see | ||
| `connectivity_for_cartesian_shift`). But a staggered shift also *relocates* the | ||
| field onto the staggered dimension, a different axis identity. If the staggered | ||
| dimension had its own tag, reproducing that would require not just shifting the | ||
| SID but also *renaming* its axis from the base tag to the staggered tag — which | ||
| the gtfn shift primitive cannot do. Aliasing the two tags makes the base and its | ||
| staggered counterpart a single axis, so the relocation collapses into the plain | ||
| offset shift and no axis has to be renamed. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -31,7 +31,10 @@ | |
| Field, | ||
| GridType, | ||
| UnitRange, | ||
| as_non_staggered, | ||
| domain, | ||
| flip_staggered, | ||
| is_staggered, | ||
| unit_range, | ||
| ) | ||
| from .constructors import FieldConstructor, as_connectivity, as_field, empty, full, ones, zeros | ||
|
|
@@ -122,6 +125,9 @@ | |
| "Domain", | ||
| "unit_range", | ||
| "UnitRange", | ||
| "is_staggered", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, for example when we construct the various domains for staggered and non-staggered index spaces in PMAP their size depends on staggeredness of a dimension so we use this to distinguish them. There are other properties where staggeredness does not matter, but only the geometric dimension is relevant. In those cases |
||
| "flip_staggered", | ||
| "as_non_staggered", | ||
| # from constructors | ||
| "FieldConstructor", | ||
| "empty", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,7 +12,7 @@ | |
|
|
||
| from gt4py import eve | ||
| from gt4py.eve.extended_typing import Never, cast | ||
| from gt4py.next import utils | ||
| from gt4py.next import common, utils | ||
| from gt4py.next.ffront import ( | ||
| dialect_ast_enums, | ||
| experimental as experimental_builtins, | ||
|
|
@@ -304,19 +304,22 @@ def _visit_shift(self, node: foast.Call, **kwargs: Any) -> itir.Expr: | |
| current_expr = im.as_fieldop( | ||
| im.lambda_("__it")(im.deref(im.shift(offset_name.id, new_index)("__it"))) | ||
| )(current_expr) | ||
| # `field(Dim + idx)` | ||
| # `field(Dim + idx)` (where `idx` is integer or half integer) | ||
| case foast.BinOp( | ||
| op=dialect_ast_enums.BinaryOperator.ADD | dialect_ast_enums.BinaryOperator.SUB, | ||
| left=foast.Name() as dim_name, | ||
| left=foast.LocatedNode(type=ts.DimensionType(dim=common.Dimension() as dim)), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Somewhat, the located property does not matter, but the root node in |
||
| right=foast.Constant(value=offset_index), | ||
| ): | ||
| if arg.op == dialect_ast_enums.BinaryOperator.SUB: | ||
| offset_index *= -1 | ||
| assert isinstance(dim_name.type, ts.DimensionType) | ||
| dim = dim_name.type.dim | ||
| conn = common.connectivity_for_cartesian_shift(dim, offset_index) | ||
| current_expr = im.as_fieldop( | ||
| im.lambda_("__it")( | ||
| im.deref(im.shift(im.cartesian_offset(dim), offset_index)("__it")) | ||
| im.deref( | ||
| im.shift( | ||
| im.cartesian_offset(conn.domain_dim, conn.codomain), conn.offset | ||
| )("__it") | ||
| ) | ||
| ) | ||
| )(current_expr) | ||
| # `field(Off)` | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.