Skip to content
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- sort/hash grouping refactor (#30) ([#33](https://github.com/englacial/zagg/pull/33)) by @espg
- Rectilinear grid: chunk-driven auto-padding + run enablement ([#32](https://github.com/englacial/zagg/pull/32)) by @espg

## [0.46.0] - 2026-08-17

- unindexed shard-map builds cover at `parent_order` and intersect before decoding records (#445) ([#447](https://github.com/englacial/zagg/pull/447)) by @espg
- An unpinned unindexed HEALPix mortie `swath` build now covers at the output
grid's `parent_order` instead of its chunk order, so `metadata["mortie_order"]`
in newly built manifests records the shard order (e.g. 9, not 13). Assignment
is measurably identical at the production order pairs (the
`bench/neon_order_sweep.py` invariant, verified at 555,867 granules); at
coarse grids the new default is a documented conservative superset, never a
subset. The order an explicit `mortie_order=` pin resolves to is unchanged
(it is still honored literally and still validated against `parent_order`).
- Unindexed builds cover from the catalog's WKB column and intersect before
materializing granule records. At clone scale (555,867 granules) the
unpinned default — the case the bullet above changes — goes 1,075 s -> 39 s
(~27x), no longer covering every footprint at the chunk order to answer a
shard-order question; a build that already pinned `mortie_order=9` goes
86.6 s -> 39.3 s (~2.2x).
- Disclosed: an explicit `mortie_order=` pin always covers live, indexed or not,
so a MultiPolygon footprint assigns as a union-of-parts superset; single-part
CMR granules are unaffected.

## [0.3.0] - 2026-06-11

- Add bring-your-own-role path for IAM-constrained deploys; creds handling for external s3 bucket writes ([#27](https://github.com/englacial/zagg/pull/27)) by @espg
Expand Down
13 changes: 10 additions & 3 deletions tests/data/benchmark/configs/s2_neon_o9.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@
# the shipped src/zagg/configs/sentinel2_l2a.yaml except ``parent_order: 9`` --
# the o9 dispatch shard matching the point-pipeline legs (~12.8 km shards, 4
# over the SERC box, 4^10 order-19 cells each; the same override espg's
# operational S2 SERC run pins).
# operational S2 SERC run pins) -- and the harness-local ``pyramid: false``
# opt-out below. Both are packaging: the semantic hash (D19) of this config
# equals the shipped one, so the leg measures the shipped product.
#
# Layout notes: ``store_layout: hive`` -- the production default for HEALPix
# raster since issues #247/#253 (issue #237 promoted, ratified by @espg on
# issue #272; the flat interop profile is deprecated). ``sharded`` does not
# apply to the (time, cells) slab writes (raster is never sharded, issue #247).
# issue #272; the flat interop profile is deprecated), restated here, not a
# divergence. ``sharded`` does not apply to the (time, cells) slab writes
# (raster is never sharded, issue #247).
data_source:
reader: raster
bands:
Expand All @@ -30,6 +33,10 @@ output:
# ratified issue #272). One leaf zarr object per array per dispatch shard.
store_layout: hive
pyramid: false # overview sweep opted out pending Phase E fleet sizing (issue #201)
# Time axis as mortie toc words (spec §8, issue #443) -- output-defining, so
# it tracks the shipped config (issue #451): a store born on toc is a
# different product from a legacy-axis one.
time_encoding: toc
grid:
type: healpix
indexing_scheme: nested
Expand Down
38 changes: 38 additions & 0 deletions tests/test_raster_benchmark.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,44 @@ def test_raster_targets_manifest_consistent():
assert get_store_layout(cfg) == "hive"


def test_s2_neon_o9_tracks_the_shipped_sentinel2_config():
# The leg's config claims to be the shipped src/zagg/configs/sentinel2_l2a.yaml
# with the o9 dispatch shard (issue #451); hold it to that mechanically so a
# knob added to one and not the other (as time_encoding was, issue #443)
# cannot drift silently. Divergence budget: parent_order and the harness's
# pyramid opt-out -- both packaging, so the semantic hash (D19) matches.
from zagg.config import get_store_layout, load_config
from zagg.semantics import semantic_hash

manifest, base = rrb.load_targets(str(BENCH / "targets_raster_neon.json"))
bench = load_config(str(base / manifest["targets"]["raster_s2_neon_2025"]["config"]))
shipped = load_config(str(REPO / "src" / "zagg" / "configs" / "sentinel2_l2a.yaml"))

assert bench.data_source == shipped.data_source
assert bench.aggregation == shipped.aggregation
assert semantic_hash(bench) == semantic_hash(shipped)

def normalized(cfg):
# store is a run-local output path (the harness overrides it); grid
# indexing_scheme is descriptive-only (config.py rejects any other value).
out = {k: v for k, v in cfg.output.items() if k not in ("store", "grid", "pyramid")}
out["store_layout"] = get_store_layout(cfg)
out["grid"] = {k: v for k, v in cfg.output["grid"].items() if k != "indexing_scheme"}
return out

b, s = normalized(bench), normalized(shipped)
assert b["grid"].pop("parent_order") == 9
assert s["grid"].pop("parent_order") == 11
assert b == s
# pyramid is dropped from the dict comparison above, so pin both sides of it
# explicitly: the shipped config growing an overview declaration (the issue
# #382 grammar) would otherwise turn the family on for the product while the
# leg keeps measuring it off, at an unchanged semantic hash -- output.pyramid
# is not in semantic_core, so the D19 assertion cannot see it either.
assert bench.output["pyramid"] is False

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

🤖 from Claude (review)

The divergence budget is enforced one-sidedly: pyramid is dropped from the comparison on both sides but asserted only on the bench side, so the shipped config can grow any pyramid declaration and this test still passes.

normalized() excludes "pyramid" (line 65), and line 74 pins only bench.output["pyramid"] is False. Nothing constrains shipped.output.

Verified by mutation on this branch — a harness reproducing the test body verbatim (data_source/aggregation equality, semantic_hash, normalized(), the parent_order pops, b == s, the line-74 pin), run against load_config-loaded copies of both configs:

PASS   baseline
PASS   shipped gains pyramid: {spacing: 2}
PASS   shipped gains pyramid: false          # divergence disappearing also unnoticed
FAIL   shipped gains emit_cell_ids           # correct
FAIL   bench pyramid becomes a mapping       # correct
FAIL   shipped reverts time_encoding         # correct

The hole is not incidental: the one output key exempted from the dict comparison is the same key the docstring names as half the divergence budget ("Divergence budget: parent_order and the harness's pyramid opt-out"). The stated purpose — "a knob added to one and not the other (as time_encoding was, issue #443) cannot drift silently" — holds for every knob except this one. And pyramid is not inert like the two other drops: src/zagg/configs/sentinel2_l2a.yaml acquiring pyramid: {overviews: [...]} (the issue #382 grammar) turns the overview family on for the shipped product while the leg keeps measuring it off, at an unchanged semantic hash — output.pyramid never enters semantic_core, so the D19 assertion above cannot see it either.

One line next to 74 closes it: assert "pyramid" not in shipped.output.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

🤖 from Claude

Fixed in 52b2531 — your one-liner, with a comment naming why the hole exists (the pyramid drop from normalized() plus output.pyramid being outside semantic_core, so neither the dict comparison nor D19 can see it):

    assert bench.output["pyramid"] is False
    assert "pyramid" not in shipped.output

The normalized() drop stays on both sides as-is; only the assertion is now two-sided. uv run --extra test pytest tests/test_raster_benchmark.py -q → 11 passed.

Separately in 44bc654, this test now resolves the bench config through the targets manifest (base / manifest["targets"]["raster_s2_neon_2025"]["config"]) instead of the hardcoded BENCH / "configs" / "s2_neon_o9.yaml", matching test_raster_targets_manifest_consistent beside it — so a manifest repoint cannot leave this test parity-checking a stale file.

assert "pyramid" not in shipped.output


def test_pinned_s2_catalog_carries_raster_entries():
# The pinned catalog is the fixed granule set (offline, no STAC): 2025
# Earth Search c1 items over the SERC box, with the raster entry fields
Expand Down
Loading