You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Strengthens the Staggered part of connectivities as types
to the static guarantee havogt's dimension-generic-fields prototype has, without
disturbing the local-dimension design.
The change
Two levels below DimensionIndex:
DimensionIndex # the root; the tree's annotations say this
├── AnyCartesianAxisIndex # a cell class of a Cartesian axis
│ ├── CartesianAxisIndex # ← declared; what users subclass
│ └── Staggered[D: CartesianAxisIndex] # derived
├── LocalDimensionIndex # unchanged
└── (direct subclasses) # mesh locations V/E/C, geometry-less index spaces
Staggered[Staggered[K]], Staggered[V2E.Local] and Staggered[C] become static [type-var] errors rather than runtime TypeErrors. Because the levels sit below
the root, Staggered[K] stays a DimensionIndex, so no type[DimensionIndex]
annotation in the gt4py tree widens and LocalDimensionIndex keeps its exact
position — the objection the note raises against a sibling root does not apply.
staggered_probe.py pins this down: mypy 2.3.1 and pyright 1.1.414 report exactly
the eleven EXPECT-ERROR lines and nothing else, so every must be ACCEPTED line
(including type[DimensionIndex] positions) holds. P4 covers the __add__
self-type, including the two definition-site suppressions it costs.
Confined to PR A (GridTools/gt4py#2899), which already introduces Staggered[D];
PR B's LocalDimensionIndex slots in unchanged.
Why, in domain terms
The levels are motivated as a concept, not a typing trick. A declared axis and its Staggered[·] are the two cell classes of a 1-dimensional CW complex (a path, or a
cycle if periodic); Staggered is the involution that swaps them; in more than one
dimension the grid is the product complex, so a cell class is a per-axis bit vector
and Dims[...]is that vector. Consequences recorded:
per-axis staggering is finer than a form degree (k = 1 in 2D conflates the two
edge families a C-grid must separate);
the absence of orientation data follows from the product structure, not from an
omission;
mesh locations are separate classes because the complex does not factor as a product.
Staggered[Staggered[K]] = K is semantically right — Staggered is an involution on
the two cell classes — but it is not expressible, so the two would be mutually
incompatible nominal types. Making the nested form unrepresentable sidesteps the
equation rather than asserting it. (An earlier revision of this description argued the
equation was also wrong, on the grounds that two half-shifts compose to K + 1; that
conflated the class swap with the index shift and was retracted in e743269.)
Other decisions
Extents are declared, never derived. Absolute UnitRange(start, stop) already
carries the degree assignment (which range is wider) and the periodic case; the
interleaving invariant belongs to the grid, since nothing here sees both ranges.
The alignment convention is antisymmetric, so both conventions are already
reachable by choosing which member of the pair to declare. Cost is cosmetic (the
derived member's tag).
DimensionKind.LOCAL leaves the enum in PR B, derived from the class; kind
becomes HORIZONTAL | VERTICAL.
dual/Dual reserved for the full Hodge dual; the per-axis swap keeps flip_staggered.
Rejected (recorded in Alternatives considered)
A sibling root above DimensionIndex; a Protocol + ClassVar[Literal[False]]
discriminator; a second partner constructor (StaggeredAbove[D] makes flip_staggered partial, so I + 0.5 cannot compute its own codomain); declaring both
members of a pair; Staggered[D: AnyCartesianAxisIndex].
Open questions added
Parameterizing the alignment (must be static — it changes the emitted stencil — and it
hits the rule-4 fingerprint hole); kind's remaining two jobs; a static cell degree
conditioned on an exterior-calculus consumer.
Also
Cross-links both ways with the surface-syntax note
§3.3/§6.4, which develops the same two-cell-class structure as half-index notation and
the Arakawa placement map. Index keywords synced with both notes' tags.
Status stays draft — the content wants a human review pass.
Add two levels below `DimensionIndex` — `AnyCartesianAxisIndex` (either cell
class of a Cartesian axis) and `CartesianAxisIndex` (a declared axis) — and
bound `Staggered[D: CartesianAxisIndex]` on the declared one. Doubly staggered
dimensions, staggered local dimensions and staggered mesh locations become
static `[type-var]` errors instead of runtime `TypeError`s, and because the
levels sit below the root no `type[DimensionIndex]` annotation widens and
`LocalDimensionIndex` keeps its position. Verified in `staggered_probe.py`
with mypy 2.3.1 and pyright 1.1.414.
Motivate the levels as a domain concept rather than a typing trick: a declared
axis and its `Staggered[.]` are the two cell classes of a 1-dimensional CW
complex, `Staggered` is the involution that swaps them, and in more than one
dimension `Dims[...]` is the per-axis bit vector of the product complex. Record
that this is finer than a form degree, that the absence of orientation data
follows from the product structure, and that mesh locations are separate
classes because the complex does not factor.
Also decided: extents are declared and never derived (absolute `UnitRange`s
already carry the degree assignment and the periodic case; the interleaving
invariant belongs to the grid); the alignment convention is antisymmetric, so
both conventions are reachable by choosing which member of the pair to declare;
`DimensionKind.LOCAL` leaves the enum in PR B, derived from the class.
Rejected in the note: a sibling root above `DimensionIndex`, a `Protocol` +
`Literal` discriminator, a second partner constructor (`StaggeredAbove[D]`
makes `flip_staggered` partial), declaring both members of a pair, and a static
cell `degree` for now. Open questions added for parameterizing the alignment,
`kind`'s remaining two jobs, and a `degree` conditioned on an exterior-calculus
consumer. Reserve `dual`/`Dual` for the full Hodge dual; the per-axis swap keeps
`flip_staggered`. Cross-links both ways with the surface-syntax note.
…ppendix
Audit the design against the two community conventions for describing mesh
topology and grid staggering in netCDF/CF: UGRID for unstructured meshes and
SGRID for structured staggered grids.
The structural finding: UGRID's locations are cell *degrees*
(node/edge/face/volume) while SGRID's are per-axis *bit vectors*
(node/edge1/edge2/face in 2D, eight values in 3D). The two conventions split
exactly along the product-versus-non-product line the Cartesian-axis section
derives from the CW complex — SGRID must distinguish edge1 from edge2, UGRID
cannot — which arrives as external evidence for encoding the two cases
differently. `Dims[Staggered[I], J]` derives what SGRID enumerates as 2^n names.
SGRID's four `padding` values decode into single absolute `UnitRange`s under one
alignment convention, so the decision that extents are declared and never
derived is vindicated and the range model is strictly more expressive than the
enum (`padding: low` is exactly the halo cell the note already names). The same
decoding shows `low`/`both` use ADR 0026's alignment and `none`/`high` the other
one, which is standardized evidence for the alignment follow-up.
Enhancements recorded: UGRID's location vocabulary gives `LocationIndex` content
and is where a cell `degree` belongs, since a degree is canonical for a mesh
location and only declarational for a Cartesian axis; `check_neighbor_table`
could validate a bound table against the codomain's absolute range, making
UGRID's `start_index` a non-issue; UGRID's anticlockwise node ordering would make
incidence signs derivable rather than supplied. Two findings for havogt's mesh
and field-data proposals (fields on a non-contiguous subset of a location, and a
field knowing its mesh) are recorded in the appendix rather than edited into
another contributor's working area.
New open question 9 on the index origin of a bound table; open questions 5-8
updated with the convention evidence.
egparedes
changed the title
personal/egparedes: connectivities as types — Cartesian axis dimensions
personal/egparedes: connectivities as types — Cartesian axis dimensions, and UGRID/SGRID alignment
Oct 1, 2026
Second commit: alignment with the UGRID and SGRID conventions
Adds connectivities-as-types_conventions.md, an audit of this design against UGRID (unstructured mesh
topology) and SGRID (structured staggered grids),
plus the follow-through edits in the main note. PR title widened accordingly.
The structural finding
UGRID's locations are cell degrees; SGRID's are per-axis bit vectors.
locations
reading
UGRID
node, edge, face, volume
degree 0–3
SGRID 2D
node, edge1, edge2, face
2² bit vectors
SGRID 3D
node, edge1–edge3, face1–face3, volume
2³ bit vectors
The two conventions split exactly along the product-versus-non-product line the
Cartesian-axis section derives from the CW complex: SGRID must distinguish edge1 from edge2, UGRID cannot. That is the note's claim arriving as
external evidence, and Dims[Staggered[I], J]derives what SGRID enumerates as
2ⁿ location names.
SGRID padding vs "extents are declared, never derived"
Decoding the spec's four values into array positions (n = node count):
padding
cells
cell j spans nodes
at node-position
as one absolute UnitRange
none
n−1
(j, j+1)
j + ½
[1, n)
low
n
(j−1, j)
j − ½
[0, n)
high
n
(j, j+1)
j + ½
[1, n+1)
both
n+1
(j−1, j)
j − ½
[0, n+1)
All four are one UnitRange under a single alignment convention, because a
gt4py range carries a start and a netCDF dimension does not. Deriving extents
would have been wrong for three of the four. The range model also reaches
arbitrary halo depth, which padding cannot express.
padding: low is exactly the halo cell the note already names
(Staggered[I](0) is the first cell outside the complex).
low/both use ADR 0026's alignment, none/high the other — so both are
standardized and in production, which is stronger evidence for open question 5
than the note previously had.
Other agreements worth recording
face_node_connectivity ≡ NeighborConnectivity[Face, Node] — UGRID names
connectivities by entity pair, matching the Domain/Codomain rename.
_FillValue / nMax… ↔ skip_value / max_neighbors; min_neighbors is
strictly more informative. ICON's 12 pentagons among hexagons make V2E min_neighbors=5, max_neighbors=6, which UGRID cannot say.
SGRID uses the samepadding syntax for vertical_dimensions as for
horizontal, so it needs no vertical kind — support for open question 6.
face_dimensions specifies padding per axis, supporting a per-axis flag.
Enhancements
LocationIndex gets a vocabulary (open question 8): UGRID's
node/edge/face/volume gated by topology_dimension. And a cell degree is canonical for a mesh location but only declarational for a Cartesian axis —
so open question 7's degree is redirected to LocationIndex.
New open question 9: validate a bound table against the codomain's absolute
range rather than [0, size), which makes UGRID's start_index ∈ {0,1} a
non-issue for one comparison in check_neighbor_table.
Orientation could be derivable: UGRID mandates anticlockwise face node
ordering, which makes the incidence signs of div = ⋆d⋆ derivable from face_node_connectivity instead of materialized as geofac_div.
External evidence for open question 6: UGRID needed face_dimension / edge_dimension attributes because a global array-ordering rule was
insufficient — the same failure as constraint F4.
For havogt's proposals (recorded, not edited in)
Fields on a non-contiguous subset of a location (UGRID's location_index_set) are inexpressible with a dense UnitRange, yet ICON needs
them (owned vs halo, per-region index lists). And both conventions put mesh= / grid= on every data variable, whereas a gt4py field knows its dims but not its
mesh — which is the unresolved multi-table case of open question 2. Left in the
appendix with wikilinks rather than edited into personal/havogt/.
Caveat
Both are netCDF interchange conventions — a location is a string attribute and
a mesh is a dummy variable referenced by name, the pattern this proposal replaces
with types. What transfers is vocabulary and invariants, not mechanism.
…review
Fixes found by an independent review of the whole proposal.
Wrong claims:
- `Staggered[Staggered[K]] = K` is semantically *right* — `Staggered` is an
involution on the two cell classes, as the note itself states. Only its
expressibility is in question, so the two would be incompatible nominal types.
The previous "not the right equation either" argument conflated the type
constructor with the index shift.
- The extent invariant said the two ranges "differ in size by exactly one when
bounded", which the conventions appendix disproves: SGRID's `low` and `high`
are bounded with equal sizes. Interleaving gives at most one, and halo
extension relaxes even that.
- "Both assignments occur" was supported by two examples of the *same*
assignment. Replaced the first with a vertex-indexed structured grid.
- The degree bit-vector table silently assumed the declared axis indexes its
0-cells, contradicting "the types do not say which class has degree 0" two
paragraphs earlier. The assumption is now stated.
- Both checkers reject the `__add__` self-type at the definition site, not only
mypy, and with different diagnostics, so it costs two separately-spelled
suppressions. `staggered_probe.py` gains P4 covering the fourth row of the
"four checks become static" table — the one row it did not reach — and both
suppressions. The mypy wording quoted from `dimension-generic-fields` was stale.
- Scan redesign never appeals to `kind`; "rather than from `kind`" was this
note's inference presented as that note's position.
- The surface-syntax citation moved from §6.4 to §6.5 for the placement map, and
now says its §3.3 cross-link was added alongside this note, so it is a pointer
and not independent corroboration.
- UGRID *recommends* anticlockwise face nodes ("should"), not requires, which
weakens the derivable-incidence-signs suggestion accordingly. SGRID does have
absolute numbering via integer coordinate variables, so the `UnitRange` column
is a correct relative decoding and "more expressive" is qualified. The
`_FillValue` and F4-analogy readings are corrected.
Rendering and accounting:
- The Implementation table rows for PRs A and B spanned two source lines, so GFM
truncated both. Back onto single lines.
- "PR 1" was never defined; it is GridTools/gt4py#2898.
- The concept-count parenthetical summed to 23, not 25; the appendix's
"connectivity type classes" row was missing.
- The A1-A10 accounting left two constraints unexplained: A9 also dissolves, A10
remains, re-expressed over `tag`.
- The Sketch showed the PEP 695 `Staggered` as if it were the runtime form.
- `ts.ShiftType` appeared in both "What it deletes" and "Kept"; the provider-key
rule for `{V2E.tag: table}` was unparseable as written.
- `typing_probe.py` is not error-clean and now says so.
New open question 9 on `DimensionMeta.__eq__` versus the "equality is `is`"
identity rule, which the note asserts in one place and overrides in another.
…e + appendices
Restructure following the review: a shorter core that can be accepted or rejected
on its own, with the derivations, mechanics and rejected options moved out. All
moved text is verbatim; the core keeps a one-paragraph statement plus a wikilink
wherever a block left.
Four new appendices:
- `_staggering.md` — the CW-complex derivation, the product-complex bit vector,
the alignment convention and its antisymmetry, why extents are declared, the
axis-versus-mesh-location decision, the relation to `dimension-generic-fields`
and the `dual`/`flip_staggered` naming constraint, the six axis-level
alternatives, and the alignment / `kind` / `degree` follow-ups.
- `_identity.md` — the mechanics behind Identity rules 1 and 3-6: reconstruction
by import, the one `copyreg` hook, fingerprinting, the injective codegen
mangling, and the `Staggered[D]` tag grammar. The eight rules keep their numbers
in the core, each reduced to its statement.
- `_typing.md` — every decision made by running a checker: why `Local` is
annotated nowhere and must be a `TypeAlias` when shared, what the `__add__`
self-type costs, the relation to `Local[V2E]`, and the two alternatives those
settled. The probes are its attachments.
- `_alternatives.md` — the alternatives rejected on grounds other than the axis
levels.
Also in the core:
- The two 240- and 190-word cells of *What stays, and why* become prose; the table
keeps one line each.
- The two executed experiments are a six-line summary plus a link; the transcripts
were already in the constraints appendix verbatim.
- Open questions 5-8 and 10 are statements with links rather than arguments.
- The closed-PR sentence goes (history, not design), as does `Domain` from the
"lands in B" list — it is a type-parameter name, never an artifact.
Core 1162 -> 874 lines. Verified: no dangling wikilink, anchor or table row across
all seven documents; Identity rules 1-8 and open questions 1-10 keep their
numbering; index keywords still byte-identical to the note's tags; appendices
correctly absent from the index; both probes unchanged (mypy 12 diagnostics over
11 marked lines, pyright 11).
`quartz.config.ts` had:
```ts
baseUrl: "graitools.github.io/gt4py_knowledge",
```
but the live Pages URL for this repo is
`https://gridtools.github.io/gt4py_knowledge/`:
```console
$ gh api repos/GridTools/gt4py_knowledge/pages --jq '{html_url, cname}'
{"cname":null,"html_url":"https://gridtools.github.io/gt4py_knowledge/"}
```
`graitools` is a transposition of `gridtools`. AGENTS.md states that
`baseUrl` must
match the final GitHub Pages URL of the repo, and Quartz uses it for
canonical URLs,
the sitemap, RSS and open-graph metadata — so absolute links the
published site emits
currently point at a host that does not resolve.
One line, no content change. Found while investigating something
unrelated in
`quartz.config.ts`; kept separate from #36 deliberately.
`connectivities-as-types_slides.md`: twelve slides for the design review — the
four-string problem and which path uses which string, the class-based declaration,
identity, what a Python type can and cannot say, the dimension hierarchy, the four
checks that become static, why two axis levels, what it deletes, the PR stack, and
the open questions. Every slide links the section it compresses and the deck adds
no facts of its own, so drift degrades to staleness rather than contradiction; it
carries a visible "as of commit 21e6b8f" marker.
Vanilla Markdown, so one file serves as both a Quartz page and a real deck. No
change to `quartz.config.ts`, `quartz.layout.ts` or `deploy.yml`, no committed
artifact and no CI step.
Verified locally, both ways:
- `marp-cli` 4.5.1: 12 slides, 121,695-byte self-contained HTML with zero external
references, and a 12-page 960x540 PDF 1.7 whose title and author come from the
frontmatter. 3 tables and 3 code blocks render.
- Quartz 4.5.2 (the version `deploy.yml` clones from `v4`, commit `d25a6ea`), built
against this repo's `content/` and config: 36 files processed, 270 emitted, exit
0. Inside `<article>` the page has exactly 11 `<hr>`, 11 `<h2>` and 1 `<h1>` — so
every `---` became a slide break rather than a setext heading — 0 leaked `<style>`
and no `[[wikilink]]` text. All 10 anchors the deck points at exist in the built
main page, and the staggering-appendix link resolves.
The render command needs `--no-stdin`: without it marp-cli waits on stdin and looks
like a hang. That is recorded in the deck itself, together with the note that a cold
`npx` run spends minutes downloading before it converts anything.
connectivities-as-types_slides.md — twelve slides for the design review. Vanilla
Markdown, so one file is both a Quartz page and a real deck: no change to quartz.config.ts, quartz.layout.ts or deploy.yml, no committed artifact, no CI
step. Rendered on demand:
As a deck (marp-cli 4.5.1 / marp-core 4.4.0): 12 slides; a 121,695-byte
self-contained HTML with zero external src=/href= references; a 12-page
960×540 PDF 1.7 whose Title and Author come from the frontmatter. The 3 tables and 3
code blocks render.
As a published page (Quartz 4.5.2 — the version deploy.yml clones from v4,
commit d25a6ea — built against this repo's real content/ and config): 36 files
processed, 270 emitted, exit 0. Inside <article>:
check
result
<hr>
11 — every --- is a slide break, not a setext <h2>
<h2> / <h1>
11 / 1, exactly the deck's headings
leaked <style>
0
literal [[wikilink]] text
none (the deck uses [text](path#anchor) throughout)
anchors into the main page
10 of 10 resolve in the built HTML
Two things worth knowing if you render it
--no-stdin is required. Without it marp-cli prints "Currently waiting data
from stdin stream" and hangs indefinitely. This is in the deck's own render line.
A cold npx -y run spends minutes downloading before it converts anything, which
also looks like a hang. Second run is under a second.
Drift
The deck carries no facts of its own — every slide ends with a link to the section it
compresses, and slide 1 says "summarises that note as of commit 21e6b8f". So
staleness degrades to an out-of-date pointer rather than a contradiction. No CI sync
check: this is a meeting artifact, and the repo already has a retirement lifecycle.
Per AGENTS.md the deck is not indexed in content/index.md — it is listed in the
proposal's ## Appendices, like the other six.
… layout
Reviewing the rendered PDF page by page turned up four layout defects that only
show up once rendered:
- The appendix banner became slide 1 and pushed the title off the bottom edge.
The title slide now leads, with a compact banner under the subtitle, and the
render instructions move to a new closing slide where they read better anyway.
- Marp treats a source newline as a hard break, so hard-wrapping at 80 columns
produced ragged lines throughout. Paragraphs, blockquotes, list items and the
footer links are now one source line each.
- The "four checks become static" table overflowed, cutting off its footer link.
Shortened to one line per row.
- The render command needs `--no-stdin`, without which marp-cli waits on stdin
and looks like a hang. Recorded on the closing slide.
13 slides, verified page by page in the rendered PDF. Quartz still builds clean
(36 files, 270 emitted): inside `<article>` the page has 12 `<hr>`, 12 `<h2>` and
1 `<h1>`, no leaked `<style>`, no wikilink text, and all 11 anchors resolve in the
built main page.
Six inline comments, all valid. Two were regressions introduced when the
mega-cells and the Identity rules were condensed in the core/appendix split.
- `NeighborTableType` fallback: an undeclared table still *gets* a
`NeighborTableType`; only its `connectivity` field falls back to the structural
`ConnectivityType`, so `dtype`, `skip_value` and `max_neighbors` survive for
compilation. The condensed prose had read as if the record itself were replaced.
- Identity rule 4: a dimension is fingerprinted by reference, so redefining one
under the same name with a different `kind` does *not* invalidate artifacts —
which is the hole open question 5 names. Only a connectivity declaration, which
is fingerprinted additionally by domain, codomain, `Local` and counts, does. The
condensation had made the claim general and so contradicted open question 5.
- `order_dimensions`: the proposed `(is_local, kind, base.tag)` key would move
local dimensions from between horizontal and vertical to last, turning
`(Cell, V2E.Local, K)` into `(Cell, K, V2E.Local)` and changing sparse-field
layout. The note now says the existing rank is preserved and only the source of
localness changes.
- The `Staggered[Staggered[K]] = K` paragraph now says explicitly that the
involution is on cell *classes*, a different operation from composing two
half-integer shifts.
- `min_neighbors` is not a `NeighborTableType` field; it is the optional count on
the declaration's `Local`. Corrected in the conventions comparison table.
- The `start_index` suggestion was underspecified: a codomain is a dimension class
and carries no range, `check_neighbor_table` gets only a declaration and a table
or type, and the `table_types` path has no entries to inspect. Conventions §4b is
retitled to *record the index origin* and lists the three prerequisites; open
question 10 and the §6 round-trip claim are qualified to match.
All six were valid and are fixed in 40f538f. Two were regressions I introduced
in the core/appendix split (21e6b8f), where condensing a mega-cell and the Identity
rules dropped qualifications the longer text had:
Comment
Verdict
Fix
NeighborTableType fallback
regression
the record survives; only its connectivity field falls back, so dtype/skip_value/max_neighbors reach compilation
Identity rule 4 fingerprints
regression
dimensions fingerprint by reference (a kind change is invisible to the cache); only declarations capture counts. Now cross-references open question 5
order_dimensions sort key
valid
(is_local, kind, base.tag) was my invention and would reorder horizontal/local/vertical → horizontal/vertical/local, moving sparse-field layout. Now: keep today's rank, change only the source of localness
Staggered[Staggered[K]] = K
valid
the inconsistency was in the PR description, not the files (fixed e743269). Description corrected; the paragraph now states that the involution is on cell classes, not on indexed positions
min_neighbors attribution
valid
it is a ClassVar on LocalDimensionIndex, not a NeighborTableType field
start_index → codomain range
valid
the weakest of my three suggestions. §4b retitled to record the index origin and now lists the three missing prerequisites; open question 10 and the §6 claim qualified
Each thread has a reply with specifics.
Note on verifiability
Three of these comments cite the gt4py PR #2907 implementation (_unbound_table_type, order_dimensions' rank). gt4py is not checked out in this repository, so I accepted
those on the strength of internal evidence — the pre-split text of this note said
the same thing in two cases, and in the third the safer wording (preserve today's rank)
is correct regardless of what the current rank is. A reviewer with the gt4py tree at b3c53fa7e should still spot-check them, along with the eight source claims listed in
the review thread.
CI
This repository has no PR-level CI: .github/workflows/deploy.yml triggers only on push to main and workflow_dispatch, and gh pr checks reports none. Validation is
therefore local — I re-ran the link, anchor and table-row checks across all eight
documents (0 problems) after these edits.
Absolute ranges do not generally determine cell degree: on the periodic axis in staggering appendix §3, both classes can use [0, n) under either degree assignment. Halo padding also prevents inference from relative widths. A static degree assignment does not require periodicity; deriving extents requires periodicity and halo policy. Correct this rationale here and in staggering appendix §7 (lines 228–233), retaining the lack-of-consumer rationale for leaving degree out.
Specify edge orientations and qualify mimetic-weight claims
Counterclockwise face-node ordering fixes the face orientation, not each globally indexed edge's orientation or the edge field's sign convention. For the same face (0, 1, 2), reversing edge (0, 1) changes its incidence coefficient from +1 to −1 without changing face_node_connectivity. State the required edge orientations and face-to-edge mapping. Also qualify the later mimetic-weight claim: a C2V orientation marker alone supplies neither these prerequisites nor the metric factors described in surface-syntax note §3.6.
Qualify cell-degree inference from derived-axis bits
Counting derived-axis bits gives the cell degree only when every declared axis indexes 0-cells. The linked design also permits declared axes to index 1-cells, as with ICON's full-level KDim; deriving its partner then changes degree from 1 to 0. Qualify the set-bit statement so the cross-link does not conflate declared-versus-derived status with degree.
Both references use a Markdown link with a destination beginning [[, rather than a complete wikilink. They therefore target invalid paths instead of the main proposal's sections. Use [[path#heading|label]], following AGENTS.md:71–72.
This Markdown destination starts with [[ but never closes a wikilink, so it does not reach the alignment follow-up. Use a complete wikilink to the main note's Open questions section, following AGENTS.md:71–72.
Five findings in code unchanged since the first review. All valid.
- **Three malformed links.** The core/appendix split rewrote same-file anchors
into wikilinks by substituting the link *destination*, producing
`[label]([[path#anchor)` — a Markdown link whose destination is an unclosed
wikilink, reaching nothing. Two in the identity appendix, one in the staggering
appendix, now proper `[[path#anchor|label]]`. My link checker matched neither
`](#...)` nor `[[...]]` and so missed the hybrid; it now tests for it.
- **The degree/extent rationale was backwards.** Open question 7 claimed a static
`degree` "duplicates what the ranges say" and "needs periodicity". Neither holds:
a periodic axis gives both cell classes `[0, n)` under either assignment and halo
padding makes the widths arbitrary, so degree is *not* inferable from extents; and
a static degree needs no periodicity at all. It is *deriving extents* that needs
the degree plus a periodicity and halo policy. The only surviving reason to leave
it out is the absence of a consumer. Corrected here and in staggering appendix §7,
and the appendix's "which one is wider is the degree assignment" now carries its
bounded, halo-free precondition.
- **Incidence signs need three orderings, not one.** Conventions §4(c) claimed
anticlockwise face-node order makes the signs derivable. That fixes the *face*
orientation only: for face `(0, 1, 2)`, reversing edge `(0, 1)` flips its
coefficient with `face_node_connectivity` unchanged. Deriving a sign needs the
face ordering, each edge's own `edge_node_connectivity` ordering, and the
`face_edge_connectivity` mapping — and even then only the topological `d`, never
the metric `⋆`. The `C2V` orientation-marker suggestion is qualified accordingly.
- **The cross-link overstated popcount.** The surface-syntax note's paragraph said
the cell degree is the number of set bits; that holds only when every declared
axis indexes its 0-cells. ICON's full-level `KDim` indexes 1-cells, so deriving
its partner lowers the degree. Qualified, so declared-versus-derived is not
conflated with degree.
The re-review confirmed the first six resolved and surfaced five more in code that
hadn't changed. All five valid, fixed in 80f9d04. These had no inline threads, so
replies are collected here.
Three malformed links (_identity.md:46-47, _staggering.md:192) — a real bug,
and mine. The core/appendix split rewrote same-file anchors into wikilinks by
substituting the link destination, producing [label]([[path#anchor): a Markdown
link whose destination is an unclosed wikilink, reaching nothing. Now proper [[path#anchor|label]] per AGENTS.md. My validator matched neither ](#…) nor [[…]], so the hybrid slipped through both patterns; it now tests for it explicitly.
The degree/extent rationale was backwards (:785, _staggering.md §7). You are
right on both halves. Open question 7 claimed a static degree duplicates what the
ranges say and "needs periodicity" — neither holds. A periodic axis gives both cell
classes [0, n) under either assignment, and halo padding makes the widths arbitrary,
so degree is not inferable from extents; and a static degree needs no periodicity at
all. The dependency runs the other way: deriving extents needs the degree plus a
periodicity and halo policy. Rewritten so the only surviving reason is the absence of a
consumer, as you suggested. §3's "which one is wider is the degree assignment" now
carries its bounded, halo-free precondition.
Incidence signs need three orderings, not one (_conventions.md:203). Your
counterexample is decisive — for face (0, 1, 2), reversing edge (0, 1) flips the
coefficient with face_node_connectivity unchanged. §4(c) now lists all three
prerequisites (face node ordering, each edge's own edge_node_connectivity, the face_edge_connectivity mapping) and states that even with all three only the
topological d becomes computable — ⋆ stays mesh geometry. The C2V
orientation-marker suggestion is qualified to say it supplies one of the three and none
of the metric factors.
The cross-link overstated popcount (discretization-independent-fd-syntax.md:133).
Correct, and it was an inconsistency with this note: the main note's degree table had
already been given the "declared = 0-cells" caveat in e743269, but I failed to
propagate it to the cross-link paragraph. ICON's full-level KDim is exactly the
counterexample. Qualified so declared-versus-derived is not conflated with degree.
Validation
Link, anchor, relative-link and table-row checks across the 11 files touched: 2
findings, both <slug> placeholders in index.md's authoring template, which are
intentional. Zero ]([[ remain anywhere under content/.
…render slide
Move "Rendering this deck" from last to first, so a reader landing on the page
sees how to render it before the content — the role the `> **Appendix** to …`
banner plays in the sibling appendices. The title slide's pointer is updated from
"on the last slide" to "on the slide before this one".
Still 13 slides. Re-rendered and checked: slides 1 and 2 both fit, and the Quartz
page is unchanged structurally (12 `<hr>`, 12 `<h2>`, 1 `<h1>`, 3 tables, no leaked
`<style>`), with "Rendering this deck" now the first heading. All 14 deck links
still resolve.
…ion's questions
Answers to the gt4py implementation agent's seven questions, recorded where the
note was silent, ambiguous or wrong.
- **`kind` on a local is `None`**, annotated `ClassVar[DimensionKind | None]`, and
`kind=` on a local declaration is a `TypeError`. `None` rather than `HORIZONTAL`
so every `kind == HORIZONTAL` / `kind != VERTICAL` site in gtfn, nanobind and
DaCe keeps its meaning instead of counting a sparse field's local axis as
horizontal. Since `None` is not orderable against the enum, `order_dimensions`
ranks horizontal/local/vertical explicitly rather than sorting on `kind`.
`common.is_local_dimension` replaces the `kind is LOCAL` checks.
- **Index arithmetic is restricted statically *and* at runtime**, matching
`Staggered[C]`: `__add__`/`__sub__` carry the self-type and also raise, and FOAST
rejects `C + 1` / `as_offset(C, f)` with a `DSLError`. Hand-written iterator IR
stays unchecked.
- **"ranges" in the sketch was wrong.** Only `+`/`-` and `as_offset` are restricted
to an axis; comparison still builds a `Domain` on any dimension, which
`concat_where` needs over `CellDim`/`EdgeDim`. Comment corrected.
- **`__eq__` versus `is` is decided**, not open: `DimensionMeta.__eq__` returns
`NotImplemented` for a dimension operand so `I == J` falls back to `is` and is
always a `bool`; `I == <int>` building a `Domain` is the one exception, and
`Domain.__bool__` raises. The residual — an integer key colliding with a
dimension-class key in one dict — is stated and accepted. Now Identity rule 8;
open question 9 is retired and the old 10 renumbered.
- **The migration heuristic is specified** in staggering appendix §4 as an ordered
rule, including `as_offset` use and a referenced staggered counterpart as axis
evidence, and reporting rather than guessing on a conflict.
- `CartesianAxisIndex` and `AnyCartesianAxisIndex` are both exported from `gtx`.
- Identity rule 4 now states the fingerprint hole itself instead of deferring to
open question 5, which only mentions it in passing.
…29 and 0030
gt4py main merged #2808, which takes 0028 for "Plain Builders Instead of
Factories", so the stack's two ADRs shift up:
- ADR 0028 "Dimensions as nominal types" -> **ADR 0029**
(`0029-Dimensions_As_Nominal_Types.md`), still PR A
- ADR 0029 "Connectivities as types" -> **ADR 0030**
(`0030-Connectivities_As_Types.md`), still PR B
22 references updated across the note, the identity appendix and the slides, in
the order 0029->0030 then 0028->0029 so the two do not collide. ADR 0019, 0023 and
0026 are untouched, and no reference to ADR 0028 remains — it now means something
else.
…ed stack
Aligns the note with what the four branches now implement.
- The migration rule in staggering appendix §4 matches the script: axis evidence is
`kind=VERTICAL`, the source of a Cartesian `FieldOffset`, the first argument of
`as_offset`/`flip_staggered`, **index arithmetic `D ± <number>` in the source**
(v1.2.2 could already write `KDim + 1`), or a `"_Staggered<value>"` string
constant; mesh-location evidence is a neighbor `FieldOffset`'s source or target
domain dimension; a conflict is declared `DimensionIndex` and reported.
- ICON4Py at the same snapshot, from a fresh run: `KDim` classified as a
`CartesianAxisIndex`, the three horizontal locations as `DimensionIndex`, all 15
locals as `LocalDimensionIndex`, 0 undecided and 0 conflicts. The reported
`isinstance(..., Dimension)` figure becomes 5 sites in 2 modules, superseding the
earlier 10-in-5, and the new category of 6 `DimensionKind.LOCAL` uses in 5
modules is recorded.
- `as_offset` is restricted to an `AnyCartesianAxisIndex`, so a staggered partner is
still accepted; `DSLError` in a field operator, `TypeError` in embedded. Noted as
landing in PR C rather than A.
- `Staggered[K]`'s only base is `Staggered`, which derives from
`AnyCartesianAxisIndex` — it is an axis by inheritance, not by direct base.
- Implementation rows record the artifacts that now pin these claims: the mypy
typing case `cartesian_axis_levels` (A) and pyright with
`reportUnnecessaryTypeIgnoreComment: error` (B).
Held pending clarification: the three places that say `AxisLiteral` drops its
stored `kind`, which the implementation report contradicts.
…le is closed
- `AxisLiteral` keeps storing only the tag, as the note said; `kind` and `dim` are
read-only properties off the resolved class, and the derived `kind` is optional
because a local dimension's is `None`. Recorded in Identity rule 1 and the
identity appendix; A10 is unaffected.
- **Identity rule 4 rewritten.** A dimension is now deconstructed as its
by-reference name *and* its `kind`, so redefining one under the same name with
another kind does invalidate artifacts, and a staggered dimension goes through
its base. `base` cannot be flipped in place at all, since `Staggered[K]` is
interned by base identity and its tag embeds the base tag. The axis level is
deliberately not fingerprinted: it changes what is accepted, never what is
emitted.
- Open question 5 and staggering appendix §7 lose the "fingerprint hole" objection
to an alignment keyword. The cost is now that the keyword would have to join
`kind` in the dimension fingerprint — a pattern rule 4 establishes rather than an
open hole.
…ites
Verified against gt4py `main` at `b3c53fa7e`: `foast_to_gtir.py:305` is
`im.shift(offset_name.id, ...)`, the shift lowering, while `:331` is
`im.as_fieldop_neighbors(str(offset_name), ...)`, the neighbors lowering. The
Problem table cited both under "shift", which the research appendix already
contradicted by placing `:325-331` in the reduction path.
Both leak N2, so the claim was right and the attribution was not: the compiled
reduction path leaks the Python variable name into `as_fieldop_neighbors` and
*then* reads N3 in `unroll_reduce`. The reduction row now records both strings.
…rift
An independent review found 14 items, almost all drift introduced when the note
was split into appendices and then revised: the last two commits each corrected
the core and left a stale copy of the same fact behind a link.
Factual:
- The identity appendix's rule 4 still read "fingerprinted by reference" with no
`kind` — the version `606429a` was written to retire — while being the page the
core points at for that rule "in full". Rewritten to match.
- Open question 6 had lost its argument entirely: the split dropped the layout-is-a-
field-property / F4 / `order_dimensions`-wants-an-order / vertical-is-a-role
reasoning, leaving only the conclusion and a pointer to a section that never
carried it. Restored verbatim from `94d1726` as staggering appendix §7 item 6, and
the core now also links the conventions appendix §4d, which is literally titled
"External evidence for open question 6".
- The deck's reduction row omitted the N2 leak that `4583164` had just added to the
core, losing the point that N2 leaks on both compiled paths.
- The deck presented `__eq__` versus `is` as open on its closing slide; `96caa0e`
settled it as Identity rule 8. Replaced with a genuinely open item.
- The deck double-counted: "four strings … a fifth constraint is hidden", when the
hidden one is N2, already inside the four.
- Three counts were wrong: "twelve-slide" (thirteen), "eleven alternatives" (eight,
plus two in the typing appendix and six in the staggering appendix), and "about
ten" axis-vs-location decisions in ICON4Py's `dimension.py` (four, per the
measured run).
- `GridTools/gt4py#2917` 404s while #2916 and #2918 resolve, so the stack reference
is dead; dropped in favour of the per-PR table.
Structural:
- Two conventions-appendix citations attributed to "the main note" text that lives
in the staggering appendix.
- The typing appendix opened on "The last row", with no table in that file.
- "Three levels" / "four levels" for a hierarchy whose new classes are siblings;
the core and slide 8 now say three new classes.
- The open-questions list says which four items are actually open.
- The core's three named alternatives now carry a why-clause each, so the section is
decidable without opening an appendix.
Nits: `updated` date, probe stamps to mypy 2.4.0 (re-verified: both probes report
exactly their documented sets), probes grouped at the end of the appendix list,
plain-text cross-references in the alternatives appendix made into wikilinks, the
provider-key sentence given a referent, and the deck's as-of marker replaced by a
branch reference rather than a SHA that goes stale on every push.
…mmit missed
A second review pass found that one of the fixes `560d613` claimed had never been
written, one landed only in the core, and one created a fresh contradiction.
- **"about ten" was never changed.** `560d613`'s message says it became "four", but
the only hunk it applied to the staggering appendix was the open-question-6
insertion; the number stood untouched, still contradicting the note's own measured
run. It is now four, with the reason the other 15 declarations do not raise the
question.
- **`GridTools/gt4py#2917` was removed from the core but left on the deck's title
slide.** It 404s while #2916 and #2918 resolve; slide 12 already carries the
per-PR table, so nothing is lost by dropping it.
- **Open question 9 is open.** `560d613` added a lead sentence calling items 3, 4, 7,
8 and 9 settled, and in the same commit put item 9 on the deck's "Open, and worth
arguging about" slide. Item 9 is the half that was misclassified: unlike item 8 it
carries no out-of-scope marker, and the conventions appendix §4b treats it as live
("the note's open question 9 should say so"). The open set is now 1, 2, 5, 6 and 9.
Nits: the restored open-question-6 text said scan-redesign "already takes" the scan
range from the output domain, which reads as endorsement of something that note
lists as P1 and wants to replace — reworded to say the range does not come from the
dimension either way; item 7's "the next item" now names the main note's item 8;
§7's list spacing made consistent; the typing appendix no longer italicises the
deck's slide title as if quoting the core.
…review
Non-blocking items from the approving pass:
- `staggered_probe.py` quoted "four checks become static" as the note's wording;
that string is the deck's slide title. The previous commit fixed exactly this in
the typing appendix and left it in the probe. Now matches the note's own label.
- The restored open-question-6 text made Scan redesign the grammatical agent of
behaviour it describes and wants to replace. Reworded so today's behaviour is the
subject and the proposal's P1 is attributed to it.
- Reflowed an awkward source wrap in staggering appendix item 7.
…e assumption
The implementation reports it made no decision on the index origin: the stack
assumes 0-based tables, `NeighborTableType` carries no `start_index`, the loader
performs no rebase, and nothing validates entries against a codomain range.
Open question 9 now states that assumption instead of only noting the absent
field, which changes what is being asked: not whether to validate, but whether to
*record* an assumption the stack currently ships silently. Validation stays
described as the separate, larger change it is. The question stays open at the
implementation's request — recording the origin is egparedes's call, not the
stack's.
Conventions appendix §4b asked open question 9 to say this; it now does, so the
cross-reference reads as satisfied rather than pending. PR A's row says
`AxisLiteral` stores only its tag and derives `kind`/`dim`, rather than the
shorthand "drops `kind`" that was once read as dropping the accessor too.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Strengthens the
Staggeredpart ofconnectivities as types
to the static guarantee havogt's
dimension-generic-fieldsprototype has, withoutdisturbing the local-dimension design.
The change
Two levels below
DimensionIndex:Staggered[Staggered[K]],Staggered[V2E.Local]andStaggered[C]become static[type-var]errors rather than runtimeTypeErrors. Because the levels sit belowthe root,
Staggered[K]stays aDimensionIndex, so notype[DimensionIndex]annotation in the gt4py tree widens and
LocalDimensionIndexkeeps its exactposition — the objection the note raises against a sibling root does not apply.
staggered_probe.pypins this down: mypy 2.3.1 and pyright 1.1.414 report exactlythe eleven
EXPECT-ERRORlines and nothing else, so everymust be ACCEPTEDline(including
type[DimensionIndex]positions) holds. P4 covers the__add__self-type, including the two definition-site suppressions it costs.
Confined to PR A (GridTools/gt4py#2899), which already introduces
Staggered[D];PR B's
LocalDimensionIndexslots in unchanged.Why, in domain terms
The levels are motivated as a concept, not a typing trick. A declared axis and its
Staggered[·]are the two cell classes of a 1-dimensional CW complex (a path, or acycle if periodic);
Staggeredis the involution that swaps them; in more than onedimension the grid is the product complex, so a cell class is a per-axis bit vector
and
Dims[...]is that vector. Consequences recorded:k = 1in 2D conflates the twoedge families a C-grid must separate);
omission;
Staggered[Staggered[K]] = Kis semantically right —Staggeredis an involution onthe two cell classes — but it is not expressible, so the two would be mutually
incompatible nominal types. Making the nested form unrepresentable sidesteps the
equation rather than asserting it. (An earlier revision of this description argued the
equation was also wrong, on the grounds that two half-shifts compose to
K + 1; thatconflated the class swap with the index shift and was retracted in
e743269.)Other decisions
UnitRange(start, stop)alreadycarries the degree assignment (which range is wider) and the periodic case; the
interleaving invariant belongs to the grid, since nothing here sees both ranges.
reachable by choosing which member of the pair to declare. Cost is cosmetic (the
derived member's tag).
DimensionKind.LOCALleaves the enum in PR B, derived from the class;kindbecomes
HORIZONTAL | VERTICAL.dual/Dualreserved for the full Hodge dual; the per-axis swap keepsflip_staggered.Rejected (recorded in Alternatives considered)
A sibling root above
DimensionIndex; aProtocol+ClassVar[Literal[False]]discriminator; a second partner constructor (
StaggeredAbove[D]makesflip_staggeredpartial, soI + 0.5cannot compute its own codomain); declaring bothmembers of a pair;
Staggered[D: AnyCartesianAxisIndex].Open questions added
Parameterizing the alignment (must be static — it changes the emitted stencil — and it
hits the rule-4 fingerprint hole);
kind's remaining two jobs; a static celldegreeconditioned on an exterior-calculus consumer.
Also
Cross-links both ways with
the surface-syntax note
§3.3/§6.4, which develops the same two-cell-class structure as half-index notation and
the Arakawa placement map. Index keywords synced with both notes' tags.
Status stays
draft— the content wants a human review pass.