Skip to content

personal/egparedes: connectivities as types — Cartesian axis dimensions, and UGRID/SGRID alignment - #36

Merged
egparedes merged 20 commits into
mainfrom
explain-staggered-connectivities
Oct 2, 2026
Merged

egparedes merged 20 commits into
mainfrom
explain-staggered-connectivities

Conversation

@egparedes

@egparedes egparedes commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

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 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
@egparedes

Copy link
Copy Markdown
Contributor Author

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 same padding 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).
egparedes added a commit that referenced this pull request Oct 1, 2026
`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.
@egparedes

Copy link
Copy Markdown
Contributor Author

Fourth commit: a slide-deck appendix

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:

npx -y @marp-team/marp-cli@4 --no-stdin connectivities-as-types_slides.md --html -o /tmp/deck.html

Verified locally, both ways

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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Substantive inaccuracies in the design documentation could mislead implementation and need correction.

Review effort: Balanced
Findings: 6 Medium severity

Open (6)
What changed in this PR

Refines the knowledge base’s draft connectivity proposal with Cartesian-axis typing and UGRID/SGRID alignment.

Changes:

  • Introduces stricter staggering bounds and a typing probe.
  • Separates supporting explanations into appendices and a slide deck.
  • Updates cross-links and index keywords.
File Description
content/​personal/​egparedes/​discretization-independent-fd-syntax.md Links half-index notation to axis typing.
content/​personal/​egparedes/​connectivities-as-types/​typing_probe.py Clarifies checker diagnostics.
content/​personal/​egparedes/​connectivities-as-types/​staggered_probe.py Probes static staggering restrictions.
content/​personal/​egparedes/​connectivities-as-types/​connectivities-as-types.md Updates the hierarchy and proposal summary.
content/​personal/​egparedes/​connectivities-as-types/​connectivities-as-types_typing.md Collects typing constraints and decisions.
content/​personal/​egparedes/​connectivities-as-types/​connectivities-as-types_staggering.md Explains alignment, extents, and alternatives.
content/​personal/​egparedes/​connectivities-as-types/​connectivities-as-types_slides.md Adds a design-review deck.
content/​personal/​egparedes/​connectivities-as-types/​connectivities-as-types_identity.md Extracts identity and serialization details.
content/​personal/​egparedes/​connectivities-as-types/​connectivities-as-types_conventions.md Compares UGRID and SGRID conventions.
content/​personal/​egparedes/​connectivities-as-types/​connectivities-as-types_alternatives.md Collects other rejected alternatives.
content/​index.md Synchronizes proposal keywords.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread content/personal/egparedes/connectivities-as-types/connectivities-as-types.md Outdated
Comment thread content/personal/egparedes/connectivities-as-types/connectivities-as-types.md Outdated
Comment thread content/personal/egparedes/connectivities-as-types/connectivities-as-types.md Outdated
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.
@egparedes

Copy link
Copy Markdown
Contributor Author

Copilot review addressed — all six comments

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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Cell-degree reasoning and the prerequisites for deriving incidence signs need substantive clarification.

Review effort: Balanced
Findings: None

Resolved since last review (6)
Previously missed (5)

In code that hasn't changed since last review

Medium severity Correct degree inference rationale for periodic ranges

content/​personal/​egparedes/​connectivities-as-types/​connectivities-as-types.md:785

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.

Medium severity Specify edge orientations and qualify mimetic-weight claims

content/​personal/​egparedes/​connectivities-as-types/​connectivities-as-types_conventions.md:203

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.

Medium severity Qualify cell-degree inference from derived-axis bits

content/​personal/​egparedes/​discretization-independent-fd-syntax.md:133

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.

Low severity Fix malformed wikilink destinations

content/​personal/​egparedes/​connectivities-as-types/​connectivities-as-types_identity.md:47

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.

Low severity Complete the wikilink to the alignment follow-up

content/​personal/​egparedes/​connectivities-as-types/​connectivities-as-types_staggering.md:192

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.
@egparedes

Copy link
Copy Markdown
Contributor Author

Second Copilot pass addressed — 5 of 5

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.
…f marker

The deck summarises the note as of 8292eff, the commit that syncs it with the
implemented stack.
…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.
@egparedes
egparedes merged commit 701bebd into main Oct 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants