Repository navigation
Comparing changes
Open a pull request
base repository: EOPF-Explorer/data-model
base: v0.10.2
head repository: EOPF-Explorer/data-model
compare: v0.11.0
- 8 commits
- 87 files changed
- 6 contributors
Commits on Aug 10, 2026
-
fix(docs): update documentation to reflect GeoZarr conventions (#230)
* fix(docs): update references from GeoZarr-spec 0.4 to GeoZarr format across documentation * fix(docs): update evolution note to reference GeoZarr 0.4 draft
Configuration menu - View commit details
-
Copy full SHA for 0015598 - Browse repository at this point
Copy the full SHA 0015598View commit details
Commits on Aug 11, 2026
-
feat: Sentinel-3 OLCI GeoZarr export (#212)
* fix the types * refactor: build all conventions via zarr_cm.create_many Route multiscales, spatial, and proj convention metadata through a single utils.build_convention_attrs() helper that delegates to zarr_cm.create_many, so zarr-cm validates each convention and emits its CMO. Previously the CMOs were hand-placed and the spatial:/proj:/multiscales keys assembled by hand, which skipped zarr-cm validation (e.g. multiscales' layout>=1 and derived_from=>transform rules) and duplicated key strings. Type the helper precisely with zarr-cm's TypedDicts (SpatialAttrs, GeoProjAttrs, MultiscalesAttrs, MultiConventionAttrs) and a CRSLike Protocol instead of dict[str, Any]. Multiscales data is still produced by the project's MultiscaleMeta model (it also covers the TMS encoding zarr-cm doesn't model), but its CMO and validation now go through zarr-cm. Output is byte-identical to before (verified against the golden-file snapshot tests). Add tests/test_conversion/test_convention_attrs.py covering the helper, including that zarr-cm now rejects an invalid multiscales layout. Co-Authored-By: Claude Opus 4.8 <[email protected]> * chore: gitignore local scratch files and .claude/ Ignore root-level scratch files (test.py, tmp.json, cli_test.sh) and the machine-local .claude/ session directory so they stop showing in git status. Co-Authored-By: Claude Opus 4.8 <[email protected]> * Revert "chore: gitignore local scratch files and .claude/" This reverts commit 4a5b6e4. * fix type checking errors * wire up mypy correctly in pre-commit * build: migrate type checker mypy -> pyright; upgrade zarr-cm zarr-cm's convention TypedDicts now use PEP 728 `extra_items`, which mypy does not support. Switch the type checker to pyright (which does) and upgrade to the zarr-cm main git dep that also ships the supporting fixes: - Mapping-covariant aggregate API + exported JsonType/JsonValue/JsonDict, so build_convention_attrs no longer needs a private `_core` import or a cast. - PEP 695 `type JsonValue` alias, so pydantic resolves MultiscaleGroupAttrs' ConventionMetadataObject field directly (removed the model_rebuild workaround). Toolchain: - pyproject: replace [tool.mypy] with [tool.pyright]; mypy -> pyright dev dep. - .pre-commit-config / CI lint job: run `uv run --frozen pyright`. Type fixes across src/ and tests/ to reach a clean pyright run (0 errors): - remove stale mypy `# type: ignore[...]` comments (pyright-unnecessary); - narrow NotRequired TypedDict access (`.get()` + guard) instead of making keys Required, preserving runtime validation of optional Sentinel-1/2 members; - replace casts on external/untyped data with runtime isinstance/validation; - model construction via `Model.model_validate(...)` instead of `**dict`. No `typing.Any` introduced. Runtime behavior unchanged. Co-Authored-By: Claude Opus 4.8 <[email protected]> * refactor: replace unverified casts with runtime checks Three casts asserted a type derived from Any or a union without verifying it. Replace each with an isinstance guard that raises TypeError on violation, so the assumption is enforced at runtime instead of only asserted to the checker: - sentinel1_reprojection: rio.write_crs() returns Any -> verify xr.Dataset. - s2_multiscale: output_group[base_path] is Array | Group -> verify zarr.Group. - s2_multiscale: client.compute() returns Any -> verify distributed.Future. The remaining casts bridge types on data we already validated (model_dump / create_many output), satisfy protocol/TypeVar binding on `self`, or widen a TypedDict for a third-party API — a runtime check there adds no safety. Co-Authored-By: Claude Opus 4.8 <[email protected]> * chore: use latest version of zarr-cm * docs: design for Sentinel-3 OLCI L1 EFR -> GeoZarr export First Sentinel-3 exporter, scoped to OLCI L1 EFR. Grounded in a real EOPF product introspected from the EODC sample store: 21 radiance bands on a single ~300m curvilinear swath grid geolocated by per-pixel 2D lat/lon (no projected CRS). Decision: preserve native swath geometry with CF 2D coordinates (no reprojection); /2 decimation pyramid; measurements-first scope; mirror the S2 package + data_api model + CLI auto-detection. Co-Authored-By: Claude Opus 4.8 <[email protected]> * docs: implementation plan for Sentinel-3 OLCI export 10-task TDD plan mirroring the S2 exporter: OLCI band mapping, data_api model, swath /2 decimation, structural detection, swath GeoZarr metadata, convert_olci_optimized entry point, CLI auto-detect + convert-s3-olci-optimized, real-product JSON fixture, golden-file snapshot, and verification/docs. Co-Authored-By: Claude Opus 4.8 <[email protected]> * chore: regenerate uv.lock for released zarr-cm 0.4.1 pyproject already pins zarr-cm>=0.4.1; update the lockfile from the git-main dev build to the released 0.4.1. Co-Authored-By: Claude Opus 4.8 <[email protected]> * feat(s3-olci): add OLCI band mapping Co-Authored-By: Claude Opus 4.8 <[email protected]> * feat(s3-olci): add Sentinel3OlciRoot data-api model Co-Authored-By: Claude Opus 4.8 <[email protected]> * fix(s3-olci): remove Any from data-api model (use precise opaque-group type) Co-Authored-By: Claude Opus 4.8 <[email protected]> * feat(s3-olci): add swath /2 decimation for overviews Co-Authored-By: Claude Opus 4.8 <[email protected]> * fix(s3-olci): move xarray import to TYPE_CHECKING; strengthen decimation test Co-Authored-By: Claude Opus 4.8 <[email protected]> * feat(s3-olci): add OLCI product detection Co-Authored-By: Claude Opus 4.8 <[email protected]> * feat(s3-olci): swath spatial-convention attrs (no transform) Add swath_spatial_attrs() function to return GeoZarr spatial: convention data for curvilinear swath geometry with pixel registration but no affine transform or bbox (geolocation carried by 2-D lat/lon arrays). Co-Authored-By: Claude Opus 4.8 <[email protected]> * feat(s3-olci): add convert_olci_optimized entry point Co-Authored-By: Claude Opus 4.8 <[email protected]> * fix(s3-olci): correct overview min_dimension guard; document returned DataTree omits overviews Co-Authored-By: Claude Opus 4.8 <[email protected]> * feat(s3-olci): CLI auto-detect + convert-s3-olci-optimized command Co-Authored-By: Claude Opus 4.8 <[email protected]> * test(s3-olci): real OLCI product fixture + round-trip detection Add structure-dump fixture (shapes shrunk to 16x16 / tie-points 16x4) from S3A_OL_1_EFR NT product fetched via HTTPS .zmetadata, add s3_olci_group_example conftest fixture, and extend test_s3_olci.py with is_sentinel3_olci_dataset and Sentinel3OlciRoot round-trip tests. Also update Sentinel3OlciMeasurementsMembers to include time_stamp and fix quality/conditions member types to allow nested GroupSpec children. Co-Authored-By: Claude Opus 4.8 <[email protected]> * test(s3-olci): golden-file snapshot of converted OLCI structure Co-Authored-By: Claude Opus 4.8 <[email protected]> * fix(s3-olci): strip inherited v2 encoding before v3 write; fix fixture time_stamp rows Co-Authored-By: Claude Opus 4.8 <[email protected]> * docs(s3-olci): document Sentinel-3 OLCI export support Add a Supported Products section to README.md and a dedicated Sentinel-3 OLCI L1 EFR Conversion section to docs/converter.md, covering: auto-detection via `eopf-geozarr convert`, the dedicated `convert-s3-olci-optimized` command with all key flags, output layout (measurements + r2/r4/... overviews + conditions/quality passthrough), and the v1 scope note (encoding wiring is a follow-up). Also add `# test: skip` to the first code block in the OLCI implementation plan doc so test_docs.py skips the non-runnable planning snippets. Co-Authored-By: Claude Opus 4.8 <[email protected]> * fix(s3-olci): fill-aware block-avg overviews, multiscales CMO, orphans passthrough, fixture dims FIX 0 — reduce_swath: genuine 2×2 fill-aware block-average for radiance vars (OLCI_BANDS), stride-decimate for 2-D coord arrays (lat/lon/alt); replaces literal [::2,::2] decimation of radiance that added no information. FIX 1 — declare overviews as GeoZarr multiscales: build_convention_attrs now receives a MultiscalesAttrs layout (asset "." + "r2"/"r4"/… with relative scale=[2,2] transform) and writes the CMO to measurements attrs. FIX 2 — copy measurements/orphans through (was silently dropped because to_dataset() discards child groups); iterate dt_input["/measurements"].children and _copy_subtree each one. FIX 3 — fixture dim consistency: renamed tie-point 'columns' → 'tie_columns' in geometry/meteorology arrays, fixed orphan 2-D arrays from [16,16] to [16,4] (removed_pixels=4), fixed instrument 2-D arrays to [bands,detectors] at consistent sizes, fixed 3-D meteo arrays to [4,16,4], fixed nb_removed_pixels shape; xr.open_datatree now opens the whole tree cleanly. Snapshot regenerated (conditions, quality, measurements/orphans, r2 overview). FIX 4 — wire in OLCI_BANDS: is_sentinel3_olci_dataset uses OLCI_BANDS[0], reduce_swath identifies radiance vars via OLCI_BANDS frozenset. FIX 5 — remove --enable-sharding / --keep-scale-offset from README and docs/converter.md copy-paste example; keep them in flags table with "accepted but not yet wired" note. All 31 OLCI tests pass; pyright 0 errors; ruff clean; no typing.Any. Co-Authored-By: Claude Opus 4.8 <[email protected]> * fix(s3-olci): restore faithful 'columns' dim name in OLCI fixture (revert tie_columns); regen snapshot Co-Authored-By: Claude Opus 4.8 <[email protected]> * fix(s3-olci): open with mask_and_scale=False; sanitize stale attrs; regen snapshot - CLI and snapshot test now open OLCI source with mask_and_scale=False so radiance stays uint16 with scale_factor/_FillValue in .attrs; CF decoding no longer silently strips scale/offset or widens dtype to float64. - Add _sanitize_data_vars() in olci_converter.py: strips _eopf_attrs, dtype, valid_min, valid_max from measurement data-var attrs before writing to GeoZarr store (both native and overviews); preserves _FillValue, scale_factor, add_offset, units, standard_name, coordinates. - Update sanitize_array_attrs() in conversion/utils.py: always strip _eopf_attrs/dtype/valid_min/valid_max; only strip _FillValue when is_decoded_float=True (raw-integer path keeps it in attrs for downstream fill handling). - Update _clear_encoding docstring: clarify converter expects raw (non-mask-scaled) input; only Zarr v2 codec encoding is stripped. - Regenerate snapshot: oa01_radiance dtype=uint16, scale_factor/add_offset/ _FillValue present, no _eopf_attrs/dtype/valid_min/valid_max. - Add _assert_radiance_dtype_and_attrs() regression guard in snapshot test. Co-Authored-By: Claude Opus 4.8 <[email protected]> * fix: revert shared sanitize_array_attrs to pre-OLCI semantics; add OLCI-local attr sanitizer The OLCI work modified the shared sanitize_array_attrs in conversion/utils.py, changing S2/S1/generic output attrs and breaking 3 S2 golden-file snapshot tests. - Restore sanitize_array_attrs to c613e24 original: always strips _eopf_attrs + _FillValue; strips dtype/fill_value/valid_min/valid_max and rewrites digital_counts units only when is_decoded_float=True. - Add _sanitize_olci_array_attrs in olci_converter.py: strips _eopf_attrs/dtype/ valid_min/valid_max while preserving _FillValue (needed for raw uint16 data opened with mask_and_scale=False). - Replace sanitize_array_attrs usage in _sanitize_data_vars with the new helper. - Add unit test asserting _FillValue is preserved and stale keys are stripped. Co-Authored-By: Claude Opus 4.8 <[email protected]> * fix(s3-olci): align coordinate decimation with coarsen trim on odd dimensions reduce_swath used slice(None, None, factor) (i.e. [::factor]) for coordinate arrays, which yields ceil(N/factor) elements when N is odd. coarsen with boundary="trim" yields floor(N/factor). For real OLCI products with 4865 columns (odd), this caused a 1-element mismatch between radiance and coordinate arrays in every overview group, making xr.open_dataset raise: ValueError: conflicting sizes for dimension 'columns': length 2433 on 'altitude' and length 2432 on 'oa01_radiance' Fix: pre-compute dim_trim = (N // factor) * factor for each swath dimension and change coordinate isel slices to slice(0, dim_trim, factor) so both paths produce exactly floor(N/factor) elements for any N. Add 4 unit tests (rows=7, cols=5) and 1 integration test (rows=10, cols=9) that confirm consistent shapes across radiance and coordinate arrays after reduction, and that overview groups open without conflicting-sizes errors. Co-Authored-By: Claude Opus 4.8 <[email protected]> * docs(s3-olci): add Sentinel-3 OLCI GeoZarr demonstration notebook Notebook walks through opening an EOPF OLCI L1 EFR product (real EODC sample, with an offline fallback to the bundled fixture), detecting it, converting to a multiscale GeoZarr store, and visualizing the pyramid by splitting one field of view into four quadrants each rendered from a different overview level. Adds a `notebooks` dependency group (jupyter, matplotlib, nbformat). Executed end-to-end against the real product; outputs (incl. the quadrant figure) are saved in the notebook. Co-Authored-By: Claude Opus 4.8 <[email protected]> * fix(s3-olci): open raw input in convert auto-detect path; guard overview rounding The OLCI auto-detect branch of convert_command passed the CF-decoded DataTree (mask_and_scale=True default) to convert_olci_optimized, whose documented precondition is raw uint16 radiance with scale_factor / _FillValue in attrs. Fill-aware masking in reduce_swath then saw no _FillValue, and np.round quantized decoded physical radiance in every overview level. Re-open the input with mask_and_scale=False in that branch, matching convert_s3_olci_optimized_command, and round in reduce_swath only when packing back into an integer dtype. Also include nested ancillary groups (e.g. conditions/geometry) in the DataTree returned by convert_olci_optimized — they were silently absent because only top-level groups with direct arrays were assembled — and add a routing test asserting the OLCI converter is selected and receives packed (non-CF-decoded) input. Assisted-by: ClaudeCode:claude-fable-5 * fix(s3-olci): forward --min-dimension in auto-detect path; avoid fill collisions The OLCI auto-detect branch of convert_command silently used the default min_dimension=256 instead of the user's --min-dimension; forward the flag so both entry points agree, and assert the forwarding in the routing test. In reduce_swath, a valid block whose mean rounds to the fill sentinel was recoded as fill in the overview; nudge such values one step back into the valid domain on the side the unrounded mean came from. Assisted-by: ClaudeCode:claude-fable-5 * fix(s3-olci): add --no-s3-olci-optimized opt-out; clarify divergent sanitizers Mirror the S2 escape hatch for the OLCI auto-detect path: a new --no-s3-olci-optimized flag forces the generic conversion path, with a routing test asserting the opt-out. The CF-decoded detection tree is now closed before re-opening the input raw, and the convert subcommand description documents the OLCI auto-routing. Rename the OLCI attr sanitizer to _sanitize_olci_array_attrs_keep_fill so its divergence from the shared sanitize_array_attrs (which strips _FillValue) is explicit at call sites, and cross-reference the two helpers in both docstrings so edits to one aren't assumed to apply to the other. Assisted-by: ClaudeCode:claude-fable-5 * fix(s3-olci): clamp fill-collision nudge to dtype range; document strict detection The fill-collision nudge in reduce_swath could wrap for a sentinel at a dtype bound (e.g. fill 0 on an unsigned dtype nudging to -1 → dtype max). Clamp the nudge candidates so both sides resolve to the in-range neighbour when the sentinel sits at a bound. Document that OLCI auto-detection is intentionally conservative (strict structural validation against Sentinel3OlciRoot): near-miss products fall through to the generic path, and convert-s3-olci-optimized is the explicit bypass. Noted in both detection helpers and the convert subcommand description. Assisted-by: ClaudeCode:claude-fable-5 * chore: remove superpowers planning docs from version control Working plan/design documents under docs/superpowers/ are local scaffolding, not project documentation; untrack them and gitignore the directory. The files remain on disk (and in history). Assisted-by: ClaudeCode:claude-fable-5 * fix(s3-olci): warn on unwired converter options; NaN-aware fill masking convert_olci_optimized accepts enable_sharding, spatial_chunk, compression_level and keep_scale_offset but does not yet apply them; it now logs a warning when a non-default value is passed, the dedicated subcommand's help marks those flags as not yet applied, and the convert auto-detect path stops forwarding them (it would otherwise warn on every run since convert's defaults differ). In reduce_swath, a float source with a NaN _FillValue was masked via an equality comparison that is a no-op for NaN; mask explicitly with notnull() in that case instead of relying on coarsen's implicit NaN skipping. Assisted-by: ClaudeCode:claude-fable-5 * fix(s3-olci): defensive return-tree assembly; test fill-collision nudge DataTree.from_dict enforces parent/child dimension consistency, so an ancillary group reusing a swath dim name at a different size could make the return-tree assembly fail even though the store was written correctly. Catch that case and fall back to a measurements-only view with a warning instead of masking a successful write. Add direct tests for the reduce_swath fill-collision branch: a valid block averaging exactly to an interior fill sentinel is nudged one step into the valid domain, and an all-fill block keeps the sentinel. (Bound sentinels such as 0/65535 are unreachable by a mean of one-sided valid values; the dtype clamp remains defensive.) Assisted-by: ClaudeCode:claude-fable-5 * perf(s3-olci): materialize coarsened mean once; never raise from detection The radiance branch of reduce_swath evaluated the dask-backed coarsen+mask graph twice (once via isnull().values for the valid mask, again via fill_da.values) — roughly doubling the heaviest compute in the converter for real products. Compute the averaged array once up front. is_sentinel3_olci_dataset only caught ValueError, but from_zarr / model_dump can raise other types on malformed stores; broaden the guard so the classifier never raises for direct callers (the CLI wrapper already swallowed everything). Assisted-by: ClaudeCode:claude-fable-5 * fix(s3-olci): clear error for missing swath dims; finer return-tree fallback Validate up front that the measurements group has the rows/columns swath dimensions the whole OLCI pipeline assumes, raising a clear ValueError instead of a bare KeyError deep in the converter. When return-tree assembly hits a DataTree dimension-consistency conflict, re-add groups greedily and exclude only the offending ones instead of collapsing to a measurements-only view. On the convert auto-detect path, warn when a user's --spatial-chunk or --enable-sharding is dropped (those options are not yet applied by the OLCI converter), instead of ignoring them silently. Assisted-by: ClaudeCode:claude-fable-5 * fix(s3-olci): explicit skipna in fill-aware mean; depth-sort tree fallback Pass skipna=True explicitly to the coarsen mean so the fill-aware promise (one fill pixel must not contaminate its block) does not hinge on a version-dependent default, and add a test asserting a partially filled block averages only its valid pixels. Sort the return-tree greedy fallback by path depth so parents are considered before their nested children, preventing an implicit empty parent from shadowing the real data-bearing one. Assisted-by: ClaudeCode:claude-fable-5 * fix(s3-olci): truncate store on re-run; handle transposed swath dims Truncate any pre-existing store before the first write so re-running convert_olci_optimized against the same output path cannot leave stale overview or ancillary groups from a prior run (the per-group writes were mode="w" scoped to measurements plus mode="a" appends). Tested by converting twice with different min_dimension values. Make swath detection in reduce_swath order-insensitive: a band stored transposed as (columns, rows) is normalized to (rows, columns) and fill-aware block-averaged instead of silently falling through to stride decimation. Document the origin-vs-center offset between decimated overview coordinates and block-averaged radiance in the reduce_swath docstring. Assisted-by: ClaudeCode:claude-fable-5 * docs(s3-olci): plot each pyramid level on its own native grid in notebook The quadrant visualization nearest-neighbour upsampled every overview back onto the native pixel grid, so nothing in the figure was actually shown at its stored resolution. Replace it with a 2x2 panel figure where each panel pcolormeshes one level's radiance against that level's own decimated 2-D latitude/longitude — same swath coverage per panel, visibly larger native cells per level, no resampling. Levels are picked dynamically from the finest affordable (<~600k quads) to the coarsest. Also make the offline fixture fallback independent of the kernel's working directory (nbconvert starts in docs/notebooks/), and silence consolidated-metadata warnings when opening levels. Assisted-by: ClaudeCode:claude-fable-5 * feat(s3-olci): overview lat/lon are geodesic block centroids, not decimated Overview coordinates were stride-decimated, labeling each block-averaged radiance cell with its block's top-left corner pixel — up to half a block (~77 km at r512) away from the centroid of the pixels actually averaged, and never converging to the swath center under repeated reduction. reduce_swath now reduces the 2-D latitude/longitude pair (matched by CF standard_name, falling back to variable name) jointly: pixel positions are mapped to unit vectors on the sphere, block-averaged with fill awareness (a pixel fill in either array is excluded from both), and the mean direction is converted back to degrees. Each overview cell is therefore geolocated at the geodesic centroid of its source block, stable across the antimeridian and at the poles, and collapsing the whole swath to one cell yields its geodesic center. Because the converter runs on un-decoded data and OLCI packs geolocation as int32 microdegrees (scale_factor=1e-06), values are CF-unpacked before the trigonometry and re-packed after, preserving dtype and attrs; feeding raw packed integers into the trig collapses every coordinate to ~(0, 0) (caught on the real EODC product, covered by regression test). Other non-band swath variables (e.g. altitude) keep stride decimation. Notebook re-executed against the real product; prose updated to describe the centroid coordinates. Assisted-by: ClaudeCode:claude-fable-5 * fix(s3-olci): raw return tree; block-average altitude in overviews Address both findings from roborev job 456: The DataTree returned by convert_olci_optimized was reopened with default CF decoding, handing callers float radiance while the store (and the converter's own input) holds packed uint16. Reopen with mask_and_scale=False and document the raw contract. Overview altitude was stride-decimated, sitting ~half a block from the geodesic-centroid lat/lon locating the same cell. Route altitude (matched by standard_name, falling back to name) through the same fill-aware block mean as radiance: it is linear, so the mean commutes with CF packing and is exact on raw values. Assisted-by: ClaudeCode:claude-fable-5 * perf(cli): check --no-s3-olci-optimized before running OLCI detection Short-circuit the opt-out flag first so an explicit --no-s3-olci-optimized skips the structural detection (a full model validation of the store) instead of running it and discarding the result. Roborev job 457, finding 1. Assisted-by: ClaudeCode:claude-fable-5 * docs(s3-olci): draw swath quadrants from different pyramid levels over a map Replace the four-panel figure with a single cartopy PlateCarree map: the swath is split into four geographic quadrants and each quadrant is drawn from a different pyramid level via xarray's curvilinear plot.pcolormesh against that level's own centroid lat/lon, over coastlines, borders, and labeled gridlines with a shared robust color scale. The seamless tiling across quadrant boundaries makes the geodesic-centroid coordinate scheme visible: levels agree on where things are even though every quadrant is a different stored grid. Adds cartopy to the notebooks dependency group. Uses explicit set_xticks/set_yticks with cartopy's Longitude/LatitudeFormatter for axis labels: Gridliner auto-labels (draw_labels=True) blank the whole GeoAxes when a colorbar axes is present under cartopy 0.25 with matplotlib 3.11. Assisted-by: ClaudeCode:claude-fable-5 * chore: re-lock uv.lock after merging main Co-Authored-By: Claude Fable 5 <[email protected]> * fix(s3-olci): open OLCI sources with native-chunk-aligned reads Port #220 to the OLCI converter paths: both OLCI source opens (convert routing re-open and convert-s3-olci-optimized) used chunks="auto", which lets dask sub-split stored zarr chunks into multiple read tasks — duplicate egress/decompression and cache races under EOPF-Explorer/data-pipeline#339. open_source_datatree gains a mask_and_scale passthrough since the OLCI converter requires raw packed input. Co-Authored-By: Claude Fable 5 <[email protected]> * fix(s3-olci): write native resolution to measurements/r0 so the store opens with xr.open_datatree The converter previously wrote the full-resolution arrays directly in measurements/ with overview groups (r2, r4, ...) nested beneath, declared as "asset": "." in the multiscales layout. Overview children then inherited the parent's 2-D latitude/longitude coordinates over the shared rows/columns dims at mismatched sizes, so xr.open_datatree — and any generic GeoZarr reader, e.g. titiler — rejected the store with an alignment error (reported on PR #212 from a real S3A scene). Native-resolution arrays now live in measurements/r0 as a named sibling of the overview groups (the Sentinel-2 pattern); measurements/ itself carries only the multiscales/spatial convention metadata, with the layout referencing "r0" instead of ".". This makes the hand-built DataTree workaround (which silently omitted overview children) unnecessary: convert_olci_optimized now returns xr.open_datatree(output) directly, and a new acceptance test pins that the whole exported store opens cleanly. Snapshot regenerated; docs and demo notebook updated and re-executed against the real EODC product. Assisted-by: ClaudeCode:claude-fable-5 * docs: design for OLCI reprojection to regular grid with CRS Assisted-by: ClaudeCode:claude-fable-5 * docs: implementation plan for OLCI reprojection and cross-product CRS contract Assisted-by: ClaudeCode:claude-fable-5 * feat(s3-olci): add swath-to-grid reprojection via rasterio geolocation arrays Assisted-by: ClaudeCode:claude-sonnet-5 * fix(s3-olci): guarantee _FillValue on all warped variables; test dims error Float variables with no pre-existing _FillValue warped to NaN nodata but were skipping the _FillValue attr, breaking the "every warped variable records its fill" contract. Always write _FillValue on the warped path (NaN included for float dtypes) and clarify the docstring to scope the guarantee to warped variables only. Also add the missing test for the 2-D latitude/longitude shape-mismatch ValueError, per the one-test-per-error-case convention. Assisted-by: ClaudeCode:claude-sonnet-5 * refactor(s3-olci): parameterize reduce/decimate over spatial dims for gridded pyramids Add keyword-only dims parameter to decimate_swath and reduce_swath, defaulting to SWATH_DIMS=("rows", "columns") to preserve existing behavior. Enables Task 4 to create pyramids on reprojected grids with dims ("y", "x"). Assisted-by: ClaudeCode:claude-haiku-4-5 * feat(s3-olci): add gridded spatial-convention attrs with affine transform Assisted-by: ClaudeCode:claude-haiku-4-5 * feat(s3-olci): reproject measurements to regular EPSG:4326 grid with per-level CRS Warp the swath once after sanitize via reproject_olci, build the r0/r2/... pyramid on the grid with reduce_swath(dims=(y, x)), and write per-level spatial/geo-proj convention attrs plus multiscales on the parent. Replace the degenerate collinear lat/lon in the synthetic test fixture with a real axis-aligned grid and restructure size assertions relative to the observed warped r0. swath_spatial_attrs is deleted (replaced by grid_spatial_attrs). The golden-snapshot test intentionally fails until the next commit seeds real geolocation into the zero-filled fixture. Assisted-by: ClaudeCode:claude-sonnet-5 * feat(s3-olci): --target-crs flag, regenerated snapshot, gridded-layout docs and notebook Seed real geolocation into the zero-filled golden fixture inside the snapshot test (the warp rejects degenerate extents), regenerate the snapshot for the gridded layout, add --target-crs to the CLI subcommand, update README/converter.md for the reprojected output, and re-execute the demo notebook against the real EODC product. Mark the implementation-plan doc's code blocks test-skip so pytest-examples does not execute them. Assisted-by: ClaudeCode:claude-sonnet-5 * test: cross-product contract - every converted product is a regular grid with CRS Adds tests/test_geozarr_output_contract.py, a parametrized test over all three converters (OLCI, S1, S2) asserting the hard requirement that every multiscale level is a regular grid with a declared, pyproj-round-trippable CRS. OLCI passes with no marks. S1/S2 whole-store open_datatree is xfailed (legacy asset='.' nested layout). S1/S2 per-level CRS detection is also xfailed: the generic converter (geozarr.py) only stamps zarr-cm proj:/ spatial: convention attrs on the base-resolution group, not on overview sub-groups, so rioxarray can't auto-detect CRS there on a plain xr.open_dataset read. Also extracts the Sentinel-2 fixture body into an importable build_sample_sentinel2_datatree() so the contract test can build the same sample tree without needing the pytest fixture machinery. Assisted-by: ClaudeCode:claude-sonnet-5 * docs(s3-olci): fix stale notebook intro to describe reprojected grid Assisted-by: ClaudeCode:claude-fable-5 * fix(test): open contract levels with decode_coords=all, drop bogus S1/S2 xfail Opening each pyramid level with the default xarray zarr decode never promotes a CF grid_mapping/spatial_ref reference into a coordinate, so ds.rio.crs came back None for S1/S2 overview levels and I wrongly attributed that to a converter gap and added an xfail. The converter's own read path already uses decode_coords="all" for exactly this reason (geozarr.py:1132, :1541). Passing the same flag in the test makes CRS detection succeed for every product's every level (S2 r2 -> EPSG:32632, S1 r2 -> EPSG:4326), so REGULAR_GRID_XFAIL is removed entirely - test_every_level_is_regular_grid_with_crs now genuinely passes for olci/s1/s2 with no marks. DATATREE_XFAIL (asset='.' nested layout) is unrelated and unchanged. Assisted-by: ClaudeCode:claude-sonnet-5 * test(s3-olci): cover projected --target-crs end-to-end (EPSG:3857) Assisted-by: ClaudeCode:claude-fable-5 * docs(s3-olci): re-execute notebook against merged converter Assisted-by: ClaudeCode:claude-fable-5 * feat(s3-olci): restore geodesic-centroid overview geolocation and swath spatial attrs Restores the upstream machinery (d69bd45) dropped when reprojection became unconditional, parameterized over the reduction dims so the gridded pipeline is unaffected. Groundwork for the native output_grid mode. Assisted-by: ClaudeCode:claude-sonnet-5 * feat(s3-olci): output_grid parameter — native instrument grid by default, opt-in regridding convert_olci_optimized(..., output_grid: str = "native") replaces target_crs: str = "EPSG:4326". "native" (default) skips the warp and builds the r0/r2/... pyramid on (rows, columns) with swath-mode CRS-free spatial attrs; any other value parses as a CRS and runs the existing warp pipeline unchanged. Unparseable values raise ValueError naming output_grid. Also rewires the convert-s3-olci-optimized CLI's --target-crs flag to --output-grid (default native), since the converter signature change broke the subcommand's call site; updates the matching one-line flag docs in README.md and docs/converter.md. Assisted-by: ClaudeCode:claude-sonnet-5 * test(s3-olci): cover --output-grid CLI flag end-to-end in both modes Assisted-by: ClaudeCode:claude-haiku-4-5 * test(s3-olci): contract + golden snapshots cover native and regridded modes The regular-grid+CRS contract entry for OLCI now opts into output_grid=EPSG:4326 explicitly; a new native-mode contract test asserts the swath store opens as a DataTree with 2-D geolocation and no fabricated CRS. The golden-snapshot test is parametrized over both modes with per-mode golden files replacing the single regridded one. Assisted-by: ClaudeCode:claude-sonnet-5 * docs(s3-olci): document native-default output_grid; notebook demonstrates both modes README and converter.md prose still described the old always-warped behavior even though the --output-grid flag already defaulted to native, contradicting the flag table. Rewrite the "What is converted" bullets, the converter.md intro/flag-table/layout-tree/metadata prose, and the demo notebook to describe native swath geometry as the default with regridding as an opt-in EPSG:4326 warp. Notebook gains a new "Opt-in regridding" section and was re-executed end-to-end against the live EODC sample product. Assisted-by: ClaudeCode:claude-sonnet-5 * docs(s3-olci): fix remaining README intro claiming unconditional warp Assisted-by: ClaudeCode:claude-fable-5 * docs(s3-olci): scope proj: convention claim to CRS output mode Assisted-by: ClaudeCode:claude-fable-5 * fix(s3-olci): reject min_dimension < 1 before touching the output store _overview_levels floor-halves dimensions; min(r, c) // 2 >= min_dimension never terminates for min_dimension <= 0, so --min-dimension 0 hung the converter after truncating the store. Found by adversarial review. Assisted-by: ClaudeCode:claude-fable-5 * fix(s3-olci): unpack CF-packed geolocation before warping reproject_olci fed raw lat/lon values to src_geoloc_array as degrees, but the pipeline contract is mask_and_scale=False and real OLCI stores geolocation as int32 microdegrees with scale_factor=1e-6 — regridded output was silently georeferenced a factor of 1e6 off on real products. Unpack scale/offset (and mask declared fill to NaN) exactly like the native pipeline's geodesic path does. Found by adversarial review. Assisted-by: ClaudeCode:claude-fable-5 * fix(s3-olci): warp transposed (columns, rows) bands instead of dropping them Found by adversarial review; mirrors the native pipeline's normalization. Assisted-by: ClaudeCode:claude-fable-5 * fix(s3-olci): edge-align regridded overview georeferencing with the r0 grid Radiance is block-averaged, so an overview coordinate is the center of the block it aggregates; stride-decimating the 1-D coords shifted every level's recomputed spatial:transform/bbox by (2^l - 1)/2 base pixels and contradicted the multiscales layout's {scale: [2,2], translation: [0,0]}. Overview y/x are now derived from the r0 transform (common origin, pixel size doubling per level). Snapshot seeding writes raw int32 microdegrees (matching the real product's CF packing, exercised by the geolocation unpacking fix); both goldens regenerated. Found by adversarial review. Assisted-by: ClaudeCode:claude-fable-5 * docs(s3-olci): re-execute notebook against corrected regridded georeferencing Assisted-by: ClaudeCode:claude-fable-5 --------- Co-authored-by: Claude Opus 4.8 <[email protected]> Co-authored-by: Loïc Houpert <[email protected]>
Configuration menu - View commit details
-
Copy full SHA for 0bd880d - Browse repository at this point
Copy the full SHA 0bd880dView commit details -
feat: GeoZarr minispec validator, compliant converter output, and doc…
… fixes - #7 (#237) * feat(validation): validate stores against the GeoZarr minispec Replace the legacy per-variable CF attribute checks in `eopf-geozarr validate` with a declarative validator built on the pydantic-zarr store models. The validator snapshots the hierarchy with GroupSpec.from_zarr and checks every normative minispec requirement: - store root: zarr_conventions declaration, spatial:bbox, exactly one CRS key - multiscale datasets: all three conventions declared, spatial:bbox / spatial:dimensions / CRS present, layout entries carry spatial:shape and spatial:transform (and transform when derived_from is set), layout assets resolve to members, and every level qualifies as a GeoZarr dataset - any node using proj:/spatial: convention keys: keys validate and the convention is declared (group declarations are inherited by direct child arrays) - Zarr V3 only; stores with no convention-bearing nodes are reported The store models in data_api/geozarr/store.py are tightened to carry these requirements (root zarr_conventions, multiscale group conventions/CRS/ dimensions, transform-with-derived_from). The CLI exits non-zero on a non-compliant store and reports each violation with its node path. Refs #233, #163, #88 Assisted-by: ClaudeCode:claude-fable-5 * fix(conversion): make converter output minispec-compliant Fixes surfaced by the new minispec validator, reproducing issue #233: - create_geozarr_dataset now writes the store-root spatial footprint (zarr_conventions + spatial:bbox + proj:code in EPSG:4326); the root writer moved to conversion.utils and is shared with convert_s2_optimized, which previously wrote the bbox but not the conventions declaration - calculate_overview_levels always emits level 0, so groups smaller than min_dimension no longer lose their multiscales/proj/spatial metadata to a swallowed empty-layout validation error - overview level groups (r2, r4, ...) now carry their own spatial + proj convention attributes, as the minispec requires of every layout member - multiscales layout entries always carry spatial:transform (previously skipped when the transform was degenerate) - sanitize_array_attrs strips geo-proj convention keys (proj:code, proj:wkt2, proj:projjson) that source products carry on arrays without the required declaration; legacy external keys like proj:epsg are kept Integration tests assert both converter paths produce validator-clean stores; the recorded example structures are regenerated accordingly. Fixes #233 Assisted-by: ClaudeCode:claude-fable-5 * docs: fix S2 group paths and describe conventions compliance The Python API samples pointed at /measurements/r10m, but Sentinel-2 products nest reflectance under /measurements/reflectance/r10m; following the samples verbatim silently skipped every group and produced a store with no GeoZarr metadata at all (issue #233). All samples now use the real paths. Also replaces the remaining GeoZarr-0.4-era requirement lists (_ARRAY_DIMENSIONS, mandatory CF names) with the conventions the converter actually writes, documents the S2 auto-routing to the optimized layout, and describes the new minispec validator. Refs #233 Assisted-by: ClaudeCode:claude-fable-5 * fix: address adversarial-review findings in validator and converters Findings from a multi-agent adversarial review of the minispec branch, each independently reproduced before fixing: Validator (data_api/geozarr): - malformed zarr_conventions values (wrong type, non-mapping entries) are now diagnosed as validation issues instead of raising TypeError - convention declarations are inherited by direct child arrays only; child groups no longer inherit them (matches the minispec inheritance rule) - empty multiscales.layout is rejected (spec: layout must not be empty) - the store root accepts redundant CRS encodings; the invented at-most-one rule (inherited from the pre-existing model) is relaxed to the spec's at-least-one - new structural Members/DataArray checks: no scalar arrays (0-D CF grid-mapping containers tolerated), unique string dimension_names, and a matching 1-D coordinate array for every data-variable dimension Converters: - write_store_root_geo_metadata reprojects with transform_bounds (edge densification) instead of two corner points, resolves the CRS from proj:code/proj:wkt2/proj:projjson instead of assuming degrees when the code is absent, skips-with-warning groups whose CRS cannot be resolved, falls back to the full longitude range for antimeridian-crossing footprints, and derives S3 storage options from the output path; the create_geozarr_dataset call site is guarded so a late failure cannot abort a finished conversion - the CRS of a source variable is captured (proj:epsg, then proj:code/wkt2) before sanitize_array_attrs strips the convention keys - overview datasets carry over 1-D non-spatial coordinates (e.g. time) so every data-variable dimension keeps its coordinate array - the optimized path drops scalar (0-D) coordinates such as source band labels when writing dataset groups Tests: error cases for each fix, the level-0 overview fix is pinned directly (mutation-detected), the CLI validate non-compliant exit path is covered, and the recorded example structures are regenerated. Assisted-by: ClaudeCode:claude-fable-5 * fix(deps): require aiohttp >= 3.14.3 pip-audit blocks CI on aiohttp 3.14.1 (PYSEC-2026-3545, PYSEC-2026-3546, PYSEC-2026-3547, all fixed by 3.14.3). Raise the floor and update the lock; the lock also picks up the 0.10.2 package version from the release commit, which had not been re-locked. Assisted-by: ClaudeCode:claude-fable-5
Configuration menu - View commit details
-
Copy full SHA for da604dd - Browse repository at this point
Copy the full SHA da604ddView commit details
Commits on Sep 10, 2026
-
deps: bump Python dependencies and pinned GitHub Actions (#264)
* deps: bump all Python dependencies to latest Refresh the whole `uv.lock` with `uv lock --upgrade`, superseding the open Dependabot PRs #260, #259, #255, #252, #241, #234, #228 and #227. pyright 1.1.414 narrows the `MISSING` sentinel more precisely than 1.1.410 did, so two call sites gain the same `reportArgumentType` ignore the neighbouring line already carries. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JEbpfA8CPcBQJzMpVJBViF * ci(deps): bump pinned GitHub Actions Supersedes Dependabot PR #225. Same SHAs it proposed, with the two stale `# v6` comments on `actions/setup-python` corrected to `# v7.0.0`. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JEbpfA8CPcBQJzMpVJBViF --------- Co-authored-by: Claude Opus 5 <[email protected]>
Configuration menu - View commit details
-
Copy full SHA for 86202d7 - Browse repository at this point
Copy the full SHA 86202d7View commit details
Commits on Sep 18, 2026
-
feat(s1): Sentinel-1 GRD RTC GeoZarr ingestion, data model and STAC b…
…uilders (#216) * feat: Phase 0 — S1 GRD RTC design, S1Tiling integration, and real-data GeoTIFF→GeoZarr V3 prototype - Implementation plan with Zarr V3 store structure, conventions, and phased roadmap - S1Tiling Docker instructions with EODAG 4.0.0 patch (5 breaking changes fixed) - GeoTIFF inspector script for S1Tiling output validation - Synthetic prototype validating sharding, resize/append, overview generation - Real-data conversion: 3 acquisitions (31TCH) → 1.8 GB Zarr store with 6 overview levels - Key findings: chunk divisibility constraint, datetime format, coverage variability * phase 0: integrate all lessons learned into plan and docs - Update §2 array metadata example to actual zarr-python 3.1.1 on-disk format (sharding codec in codecs array, chunk_grid holds shard shape, inner chunks 366) - Update conditions group: per-orbit naming (gamma_area_{orbit}), mark LIA and incidence_angle as aspirational (not confirmed as S1Tiling outputs) - Add Operational Findings subsection: EODAG 4.0.0 patch, DEM tile extent, GAMMA_AREA filename format - Fix docker instructions: CALIBRATION tag is GammaNaughtRTC (not gamma_naught_rtc) - Mark Phase 0 checklist fully complete - Update §9 API corrections to reflect plan is now corrected * phase 1: S1 RTC Pydantic models aligned with S2 pattern - Add src/eopf_geozarr/data_api/s1_rtc.py — Zarr V3 Pydantic models for S1 GRD γ0T RTC GeoZarr stores, using pyz.v3 GroupSpec/ArraySpec with TypedDict members (same pattern as s2.py uses pyz.v2) - Models: S1RtcRoot, S1RtcOrbitGroup, S1RtcNativeResolutionDataset, S1RtcOverviewResolutionDataset, S1RtcConditionsGroup - Validation: convention UUIDs, spatial:dimensions, multiscales layout, required data arrays (vv/vh/border_mask), gamma_area presence - Add tests/_test_data/s1_rtc_examples/s1-grd-rtc-31TCH.json — realistic fixture with 3 timesteps, 6 overview levels, 3 gamma_area conditions - Add tests/test_data_api/test_s1_rtc.py — 11 tests: round-trip, structure validation, negative cases (missing orbit, r10m, UUIDs, etc.) - Add conftest fixture s1_rtc_json_example parametrized over all fixtures * refactor: improve Pydantic model definitions and streamline imports in S1 RTC module * docs: update Phase 1 checklist, add Phase 1 findings, link tracking issue #139 * fix: standardize spatial dimensions to lowercase in S1 RTC models and test cases * fix: add 1D spatial coordinate arrays and use lowercase dimension names in S1 GRD RTC prototype * feat: add detailed implementation plan for Phase 2 GeoTIFF ingestion pipeline * fix: update Phase 2 GeoTIFF ingestion plan to include 1D spatial coordinate arrays and enforce lowercase dimension names * feat: Phase 2 — S1 GRD RTC GeoTIFF ingestion pipeline Productionise the GeoTIFF → Zarr V3 ingestion pipeline for S1Tiling gamma-naught RTC outputs. New module: src/eopf_geozarr/conversion/s1_ingest.py (~500 lines) - extract_geotiff_metadata(): rasterio-based metadata extraction with tag validation and S1Tiling datetime normalisation - create_s1_store(): Zarr V3 store creation with full GeoZarr conventions (multiscales, proj:, spatial:), sharded arrays, 1D spatial coordinate arrays at every resolution level - ingest_s1tiling_acquisition(): main API — create-or-open store, read GeoTIFFs, generate overviews (average/nearest), write all levels, append coordinate variables. CRS/shape consistency validation on append. - consolidate_s1_store(): post-batch metadata consolidation - discover_s1tiling_acquisitions(): file discovery with grouping Tests: tests/test_s1_rtc_ingest.py (27 tests) - Metadata extraction, store creation, ingestion (create + append), data integrity, xarray roundtrip, CRS/shape mismatch rejection, consolidation, file discovery Uses zarr_cm CMO dicts for convention metadata (not hardcoded UUIDs). Reuses calculate_aligned_chunk_size() from utils.py. Exports wired in conversion/__init__.py. Refs: #139 * refactor: use zcm.Multiscales typed model per reviewer feedback - Replace dict[str, Any] multiscales field with zcm.Multiscales import - Remove inline MultiscalesTransform/ScaleLevel/Multiscales classes - Update spatial:dimensions validator to lowercase ['y', 'x'] (Python 3.12) - Remove typing_extensions import (native in Python 3.12) - Update test assertions for Pydantic model attribute access - Auto-format fixes from ruff (imports, fixture parens, line length) * fix: configure ruff TC001 for Pydantic runtime-evaluated base classes - Add [tool.ruff.lint.flake8-type-checking] runtime-evaluated-base-classes for pydantic.BaseModel so Pydantic field type imports aren't flagged - Remove 4 stale noqa comments auto-fixed by ruff * feat: implement S1Tiling conditions ingestion and discovery commands in CLI * feat(stac): add build_s1_rtc_stac_item and generate-stac-s1 CLI (#173) * feat(stac): add build_s1_rtc_stac_item and generate-stac-s1 CLI command Adds eopf_geozarr.stac.s1_rtc.build_s1_rtc_stac_item, which reads a consolidated S1 GRD RTC Zarr V3 store and returns a pystac.Item with SAR/SAT/projection extensions, WGS84 bbox derived via pyproj, and vv/vh asset sub-paths. Ascending orbit is preferred when both are present. Also adds the generate-stac-s1 CLI subcommand that prints the item as JSON. 8 unit tests cover all acceptance criteria from the plan. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * fix: support s3:// URIs in S1Tiling discovery and ingest (#175) Four functions in s1_ingest.py silently failed for s3:// paths: - discover_s1tiling_acquisitions / discover_s1tiling_conditions used pathlib.Path.glob(), which normalises s3:// to s3:/ and returns 0 matches - ingest_s1tiling_acquisition / ingest_s1tiling_conditions coerced paths via Path(), corrupting s3:// URIs, and called .exists() which always returns False for S3 (existence checks now use path_exists() from fs_utils) Adds three private helpers: - _list_tifs(): uses s3fs.S3FileSystem.glob for s3:// prefixes - _coerce_input_path(): preserves str for s3://, returns Path otherwise - _rasterio_env(): rasterio.Env(AWSSession) context for s3:// paths; strips https:// from AWS_ENDPOINT_URL since GDAL expects hostname only Tested: rasterio.open("s3://...") confirmed working on OVH S3 with AWS_S3_ENDPOINT or AWS_ENDPOINT_URL set; hostname-only required by GDAL. Refs #139 Co-authored-by: Claude Sonnet 4.6 <[email protected]> * fix(stac): point vv/vh asset hrefs to orbit group root; add tile_matrix_set Per Emmanuel's review on #173: - vv/vh asset hrefs now point to {store}/{orbit} (the zarr group that carries multiscales), matching how S2 reflectance assets point to measurements/reflectance. TiTiler reads tile_matrix_set from that group's multiscales attributes. - create_s1_store now writes tile_matrix_set into the orbit group's multiscales so TiTiler can discover the tiling scheme without error. - Updated test_asset_hrefs to assert the new orbit-group href. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * fix(ingest): add proj:code to resolution group attrs for TiTiler compatibility TiTiler's _validate_zarr checks ds.rio.crs on the native resolution group (r10m). Without proj:code in the resolution group's attrs, rioxarray returns None and the group is excluded → groups=[] → bounds unpack error. S2 zarr already has proj:code at the resolution group level; mirror that for S1. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * fix(s1): write CF grid_mapping, drop tile_matrix_set from RTC store (#176) The deployed titiler-eopf reader resolves a group's CRS via rioxarray, which reads a CF spatial_ref/crs_wkt coordinate -- not the GeoZarr proj:code attr. S1 GRD RTC stores carried only proj:code, so rio.crs was None and every multiscale group was rejected (HTTP 500 on render/info). The S2 converter already writes a spatial_ref grid-mapping; this brings S1 in line. Also stop writing tile_matrix_set into the orbit-group multiscales: the data-model owner confirmed it is not part of the S1 GRD RTC data model, and the data_api reader already treats it as optional (MISSING default). - add _add_grid_mapping() using pyproj CRS.to_cf() (the same source rioxarray uses, so no hard-coded projection) and call it for every resolution level (both store-creation paths) and the conditions group - remove _create_tile_matrix_set() and its multiscales entry - tests: assert no tile_matrix_set, spatial_ref present + rio.crs resolves to the native EPSG, conditions group carries grid_mapping This fixes at the source what data-pipeline currently works around in ingest_v1_s1_rtc.py (_patch_cf_grid_mapping / _patch_tile_matrix_limits). Co-authored-by: Claude Opus 4.8 <[email protected]> * Merge #172 (deprecate v0, fix fill values, sanitize attrs) into s1-rtc (#178) * chore: release 0.9.0 * feat: implement scale-offset and data type casting via codecs * fix: fix dependency declaration * chore: use latest version of cast value * chore: make cast-value a project dependency * test: expand test coverage * fix: upgrade pytest to 9.0.3 (CVE-2025-71176) Fixes insecure /tmp directory handling on UNIX systems. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * - Use zarr-python's presumptive implementation of scale-offset - use scalar map for handling NaN - ensure that downsampled arrays use scalar map + cast value - improve tests across parametrization of relelvant functions * feat: add store-root spatial:bbox and tighten minispec requirements (#164) * feat: add store-root spatial:bbox and tighten minispec requirements Introduces a GeoZarr "Store Root" layer in the minispec so clients can read a summary footprint without walking into child groups, and tightens the multiscale profile so `spatial:bbox` at the root and `spatial:transform` + `spatial:shape` on every layout entry are mandatory. Adds a new `geozarr.store` pydantic module (`GeoZarrStoreAttrs`, `GeoZarrMultiscaleGroupAttrs`, `GeoZarr`) enforcing the tightened profile, and updates the S2 converter to union child-group bboxes into an EPSG:4326 footprint written at the store root. Closes #156. Addresses the clear parts of #163; the array-level and non-multiscale-group parts of that issue need further clarification. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * feat: update GeoZarr store model and optimize S2 converter for improved bbox handling * Addresses review feedback on #164: - Adds a "GeoZarr Hierarchy & Identification" subsection to the Store Root section of the minispec, codifying d-v-b's proposed rules: single root, root prefix ends with `.zarr`, suffix occurs at most once in the hierarchy, and explicit terminal-path conditions. - Promotes the store-root CRS to mandatory: at least one of `proj:code`, `proj:wkt2`, `proj:projjson` MUST be set; there is no implicit EPSG:4326 default. Per @vincentsarago's review. - Promotes `zarr_conventions` declaration at the store root from RECOMMENDED to required. - Converter now writes `proj:code: "EPSG:4326"` at the root alongside `spatial:bbox`. - Pydantic `GeoZarrStoreAttrs` enforces the new CRS-required rule. Cross-links the new hierarchy + spatial:extent follow-ups upstream: zarr-developers/geozarr-spec#132 (hierarchy & root identification, also addresses #124 URL parsing) and #133 (STAC-style spatial:extent). --------- Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]> * chore: uv.lock * refactor: use zarr-python 3.2.0 * chore: lockfile * fix: pin fill value based on data type instead of relying on xarray * test: update tests to consistently use nan fill value for floats * chore: bump urllib3 to 2.7.0 * chore: skip quicklook groups (#165) * chore: group dependabot updates for actions and pip (#160) * chore: group dependabot updates for actions and pip - Group all GitHub Actions bumps (minor, patch, major) into one PR - Group minor and patch pip bumps into one PR; major bumps remain ungrouped for individual review Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * chore: change dependabot schedule day to wednesday * ci: add comment_on option to security auditing action * ci: switch dependabot Python ecosystem from pip to uv uv ecosystem reads uv.lock directly, enabling Dependabot to raise PRs for lockfile-pinned versions including security patches. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> --------- Co-authored-by: Claude Sonnet 4.6 <[email protected]> * fix: derive coarse spatial transforms from coordinates (#168) * fix: derive coarse spatial transforms from coordinates * refactor: improve function definitions for clarity and consistency * chore: release 0.10.0 (#162) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * fix(converter): align convert layout to S2, strip _eopf_attrs, fix _FillValue (#171) - Remove TMS layout and /0 /1 /2 numeric overview groups from the general 'convert' CLI; produce S2-style sibling r{2**level} overviews instead. 'convert-s2-optimized' continues to emit the same layout. - Drop the '--tile-width' CLI option. - Sanitize array attrs in all converters: strip stale '_eopf_attrs' and source-only '_FillValue' / 'dtype' / 'valid_min' / 'valid_max' on decoded floats; rewrite 'units: digital_counts' -> 'units: 1'. The sanitizer now returns a new dict and callers reassign 'attrs', so stale keys are actually removed (a previous '.update()' pattern left them in place). - Sanitize coord attrs too (datetime coords in /conditions/meteorology were leaking '_eopf_decode_datetime64' via '_eopf_attrs'). - Set '_FillValue' properly for float measurements so xarray's CF encoder produces the base64-NaN representation needed for 'use_zarr_fill_value_as_mask=True' round-trip (xarray#11345). Fixes #171. - Remove dead 'utils.encode_cf_fill_value' (no callers). - Tests: open each r{N} multiscale level separately; migrate 'test_multiscales_round_trip' from deprecated tms.Multiscales to zcm.Multiscales (with tuplify_json to handle extra='allow' fields). - Regenerate snapshot fixtures; verified 0 '_eopf_attrs', 'tile_matrix*', or 'digital_counts' markers across S2A/S2B/S2C. * test: add guardrails for converter output attrs (#171) Walks the snapshotted GroupSpec JSON fixtures and asserts: - no '_eopf_attrs' anywhere; - no 'tile_matrix*' markers (TMS removal); - no 'units: digital_counts' on float arrays (decoded scale/offset); - every float array under '/measurements/' has '_FillValue' set (required for CF NaN-mask round-trip, xarray#11345). * docs: deprecate v0 references, document r{N} overview layout (#171) - Drop 'tile_width' parameter from API examples (option removed from CLI/API). - Replace '/measurements/r10m/{0,1,2}' nested-pyramid examples with the current flat 'r{2**level}' sibling-group layout. - Remove the V0 vs V1 split in converter.md / examples.md / quickstart.md; the general 'convert' command now produces the same flat layout as 'convert-s2-optimized'. The S2-optimized section is reframed as a feature description rather than a 'V1 vs V0' comparison. - Update architecture.md multiscales snippet to the new model ('layout' / 'asset' / 'derived_from' / 'transform'). - Update faq.md inspection snippet to walk the new layout. * refactor: clean up code formatting and enhance attribute handling in geozarr conversion * fix(cli-e2e): drop deprecated --tile-width flag from tests and example * refactor: remove deprecated v0 layout, sanitize attributes, and fix _FillValue handling * test: add _FillValue masking roundtrip test (#172) Addresses @vincentsarago's PR review: assert float arrays written with the converter's _FillValue convention round-trip through xarray's use_zarr_fill_value_as_mask=True so NaN cells come back masked. * test: drive _FillValue masking test through create_geozarr_dataset (#172) Replace the standalone xarray write with a real converter run: build a small float DataTree, invoke create_geozarr_dataset, then reopen the output with use_zarr_fill_value_as_mask=True and assert masking on the nodata patch. Exercises the converter's _FillValue + encoding path end-to-end, per @vincentsarago's review. * fix: green CI for #172 — S2 convert flat layout + idna CVE (#177) * fix(security): bump idna 3.13 -> 3.17 to clear CVE-2026-45409 pip-audit in the Security Audit workflow blocks on idna 3.13 (CVE-2026-45409, incomplete fix of CVE-2024-3651; fixed in 3.15). Bump the transitive pin via uv lock. pip-audit now reports no known vulnerabilities. Co-Authored-By: Claude Opus 4.8 <[email protected]> * fix(cli): route Sentinel-2 convert through the optimized flat layout Generic `convert` emitted GeoZarr-0.4 multiscales with native data at the group root and overviews as nested r{2**N} sub-groups. That tree cannot be opened by xr.open_datatree (an overview child's x/y conflict with the parent's inherited x/y), so the `info` and `validate` CLI commands crashed on the converter's own Sentinel-2 output. Detect Sentinel-2 inputs and delegate to the existing, tested convert_s2_optimized path, which emits flat sibling r{N}m levels (the layout PR #172 documents). Non-S2 inputs keep the generic create_geozarr_dataset path. Detection is best-effort and never aborts conversion. Regenerate the geozarr_examples structure snapshots to the flat layout. Co-Authored-By: Claude Opus 4.8 <[email protected]> * test: make CLI e2e assertions debuggable and fix multiscale round-trip - The CLI logs errors to stdout via structlog; assert on stdout+stderr in test_cli_e2e so a non-zero exit shows the actual error instead of an empty stderr. - test_multiscale_attrs_round_trip tuplified only one side of its equality, so flat-layout level groups carrying list-valued attrs (e.g. spatial:bbox) broke a check that is only about JSON list/tuple normalisation. Tuplify both sides; the model still round-trips losslessly. Co-Authored-By: Claude Opus 4.8 <[email protected]> --------- Co-authored-by: Claude Opus 4.8 <[email protected]> --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Davis Vann Bennett <[email protected]> Co-authored-by: Claude Sonnet 4.6 <[email protected]> Co-authored-by: Emmanuel Mathot <[email protected]> --------- Co-authored-by: Claude Sonnet 4.6 <[email protected]> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Davis Vann Bennett <[email protected]> Co-authored-by: Emmanuel Mathot <[email protected]> * Revert "feat(stac): add build_s1_rtc_stac_item and generate-stac-s1 CLI (#173)" (#179) This reverts commit f749368ac3870790a69b250926f0af1aa6b58d0e. * fix(s1-ingest): discover multi-frame acquisitions with a masked timestamp (#183) (#184) discover_s1tiling_acquisitions returned 0 acquisitions for multi-frame S1Tiling products: their GeoTIFF filenames mask the acquisition time as '…txxxxxx', which the filename regex (\d{8}t\d{6}) rejected, so parse_s1tiling_filename returned None and every file was skipped — silently, with no error. - Relax the acq_stamp pattern to accept a masked time (\d{6}|x{6}), so masked files are no longer dropped. - In discover, when the stamp is masked, resolve the real YYYYMMDDtHHMMSS stamp from the GeoTIFF ACQUISITION_DATETIME tag (via extract_geotiff_metadata) and use it for grouping + downstream STAC datetime. This is the durable upstream fix for the data-pipeline #237 workaround (which renamed masked products before discovery); that workaround can now be retired. Closes #183. Co-authored-by: Claude Opus 4.8 <[email protected]> * chore: merge main into s1-tiling (#190) * chore: release 0.9.0 * feat: implement scale-offset and data type casting via codecs * fix: fix dependency declaration * chore: use latest version of cast value * chore: make cast-value a project dependency * test: expand test coverage * fix: upgrade pytest to 9.0.3 (CVE-2025-71176) Fixes insecure /tmp directory handling on UNIX systems. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * - Use zarr-python's presumptive implementation of scale-offset - use scalar map for handling NaN - ensure that downsampled arrays use scalar map + cast value - improve tests across parametrization of relelvant functions * feat: add store-root spatial:bbox and tighten minispec requirements (#164) * feat: add store-root spatial:bbox and tighten minispec requirements Introduces a GeoZarr "Store Root" layer in the minispec so clients can read a summary footprint without walking into child groups, and tightens the multiscale profile so `spatial:bbox` at the root and `spatial:transform` + `spatial:shape` on every layout entry are mandatory. Adds a new `geozarr.store` pydantic module (`GeoZarrStoreAttrs`, `GeoZarrMultiscaleGroupAttrs`, `GeoZarr`) enforcing the tightened profile, and updates the S2 converter to union child-group bboxes into an EPSG:4326 footprint written at the store root. Closes #156. Addresses the clear parts of #163; the array-level and non-multiscale-group parts of that issue need further clarification. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * feat: update GeoZarr store model and optimize S2 converter for improved bbox handling * Addresses review feedback on #164: - Adds a "GeoZarr Hierarchy & Identification" subsection to the Store Root section of the minispec, codifying d-v-b's proposed rules: single root, root prefix ends with `.zarr`, suffix occurs at most once in the hierarchy, and explicit terminal-path conditions. - Promotes the store-root CRS to mandatory: at least one of `proj:code`, `proj:wkt2`, `proj:projjson` MUST be set; there is no implicit EPSG:4326 default. Per @vincentsarago's review. - Promotes `zarr_conventions` declaration at the store root from RECOMMENDED to required. - Converter now writes `proj:code: "EPSG:4326"` at the root alongside `spatial:bbox`. - Pydantic `GeoZarrStoreAttrs` enforces the new CRS-required rule. Cross-links the new hierarchy + spatial:extent follow-ups upstream: zarr-developers/geozarr-spec#132 (hierarchy & root identification, also addresses #124 URL parsing) and #133 (STAC-style spatial:extent). --------- Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]> * chore: uv.lock * refactor: use zarr-python 3.2.0 * chore: lockfile * fix: pin fill value based on data type instead of relying on xarray * test: update tests to consistently use nan fill value for floats * chore: bump urllib3 to 2.7.0 * chore: skip quicklook groups (#165) * chore: group dependabot updates for actions and pip (#160) * chore: group dependabot updates for actions and pip - Group all GitHub Actions bumps (minor, patch, major) into one PR - Group minor and patch pip bumps into one PR; major bumps remain ungrouped for individual review Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * chore: change dependabot schedule day to wednesday * ci: add comment_on option to security auditing action * ci: switch dependabot Python ecosystem from pip to uv uv ecosystem reads uv.lock directly, enabling Dependabot to raise PRs for lockfile-pinned versions including security patches. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> --------- Co-authored-by: Claude Sonnet 4.6 <[email protected]> * fix: derive coarse spatial transforms from coordinates (#168) * fix: derive coarse spatial transforms from coordinates * refactor: improve function definitions for clarity and consistency * chore: release 0.10.0 (#162) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * enh: deprecate v0, fix fill values and sanitize attributes (#172) * fix(converter): align convert layout to S2, strip _eopf_attrs, fix _FillValue (#171) - Remove TMS layout and /0 /1 /2 numeric overview groups from the general 'convert' CLI; produce S2-style sibling r{2**level} overviews instead. 'convert-s2-optimized' continues to emit the same layout. - Drop the '--tile-width' CLI option. - Sanitize array attrs in all converters: strip stale '_eopf_attrs' and source-only '_FillValue' / 'dtype' / 'valid_min' / 'valid_max' on decoded floats; rewrite 'units: digital_counts' -> 'units: 1'. The sanitizer now returns a new dict and callers reassign 'attrs', so stale keys are actually removed (a previous '.update()' pattern left them in place). - Sanitize coord attrs too (datetime coords in /conditions/meteorology were leaking '_eopf_decode_datetime64' via '_eopf_attrs'). - Set '_FillValue' properly for float measurements so xarray's CF encoder produces the base64-NaN representation needed for 'use_zarr_fill_value_as_mask=True' round-trip (xarray#11345). Fixes #171. - Remove dead 'utils.encode_cf_fill_value' (no callers). - Tests: open each r{N} multiscale level separately; migrate 'test_multiscales_round_trip' from deprecated tms.Multiscales to zcm.Multiscales (with tuplify_json to handle extra='allow' fields). - Regenerate snapshot fixtures; verified 0 '_eopf_attrs', 'tile_matrix*', or 'digital_counts' markers across S2A/S2B/S2C. * test: add guardrails for converter output attrs (#171) Walks the snapshotted GroupSpec JSON fixtures and asserts: - no '_eopf_attrs' anywhere; - no 'tile_matrix*' markers (TMS removal); - no 'units: digital_counts' on float arrays (decoded scale/offset); - every float array under '/measurements/' has '_FillValue' set (required for CF NaN-mask round-trip, xarray#11345). * docs: deprecate v0 references, document r{N} overview layout (#171) - Drop 'tile_width' parameter from API examples (option removed from CLI/API). - Replace '/measurements/r10m/{0,1,2}' nested-pyramid examples with the current flat 'r{2**level}' sibling-group layout. - Remove the V0 vs V1 split in converter.md / examples.md / quickstart.md; the general 'convert' command now produces the same flat layout as 'convert-s2-optimized'. The S2-optimized section is reframed as a feature description rather than a 'V1 vs V0' comparison. - Update architecture.md multiscales snippet to the new model ('layout' / 'asset' / 'derived_from' / 'transform'). - Update faq.md inspection snippet to walk the new layout. * refactor: clean up code formatting and enhance attribute handling in geozarr conversion * fix(cli-e2e): drop deprecated --tile-width flag from tests and example * refactor: remove deprecated v0 layout, sanitize attributes, and fix _FillValue handling * test: add _FillValue masking roundtrip test (#172) Addresses @vincentsarago's PR review: assert float arrays written with the converter's _FillValue convention round-trip through xarray's use_zarr_fill_value_as_mask=True so NaN cells come back masked. * test: drive _FillValue masking test through create_geozarr_dataset (#172) Replace the standalone xarray write with a real converter run: build a small float DataTree, invoke create_geozarr_dataset, then reopen the output with use_zarr_fill_value_as_mask=True and assert masking on the nodata patch. Exercises the converter's _FillValue + encoding path end-to-end, per @vincentsarago's review. * fix: green CI for #172 — S2 convert flat layout + idna CVE (#177) * fix(security): bump idna 3.13 -> 3.17 to clear CVE-2026-45409 pip-audit in the Security Audit workflow blocks on idna 3.13 (CVE-2026-45409, incomplete fix of CVE-2024-3651; fixed in 3.15). Bump the transitive pin via uv lock. pip-audit now reports no known vulnerabilities. Co-Authored-By: Claude Opus 4.8 <[email protected]> * fix(cli): route Sentinel-2 convert through the optimized flat layout Generic `convert` emitted GeoZarr-0.4 multiscales with native data at the group root and overviews as nested r{2**N} sub-groups. That tree cannot be opened by xr.open_datatree (an overview child's x/y conflict with the parent's inherited x/y), so the `info` and `validate` CLI commands crashed on the converter's own Sentinel-2 output. Detect Sentinel-2 inputs and delegate to the existing, tested convert_s2_optimized path, which emits flat sibling r{N}m levels (the layout PR #172 documents). Non-S2 inputs keep the generic create_geozarr_dataset path. Detection is best-effort and never aborts conversion. Regenerate the geozarr_examples structure snapshots to the flat layout. Co-Authored-By: Claude Opus 4.8 <[email protected]> * test: make CLI e2e assertions debuggable and fix multiscale round-trip - The CLI logs errors to stdout via structlog; assert on stdout+stderr in test_cli_e2e so a non-zero exit shows the actual error instead of an empty stderr. - test_multiscale_attrs_round_trip tuplified only one side of its equality, so flat-layout level groups carrying list-valued attrs (e.g. spatial:bbox) broke a check that is only about JSON list/tuple normalisation. Tuplify both sides; the model still round-trips losslessly. Co-Authored-By: Claude Opus 4.8 <[email protected]> --------- Co-authored-by: Claude Opus 4.8 <[email protected]> --------- Co-authored-by: Loïc Houpert <[email protected]> Co-authored-by: Claude Opus 4.8 <[email protected]> * ci(deps): bump the actions group across 1 directory with 8 updates (#167) Bumps the actions group with 8 updates in the / directory: | Package | From | To | | --- | --- | --- | | [actions/checkout](https://github.com/actions/checkout) | `5.0.1` | `6.0.2` | | [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) | `7.6.0` | `8.1.0` | | [actions/cache](https://github.com/actions/cache) | `5.0.4` | `5.0.5` | | [codecov/codecov-action](https://github.com/codecov/codecov-action) | `5.5.3` | `6.0.0` | | [googleapis/release-please-action](https://github.com/googleapis/release-please-action) | `4.4.0` | `5.0.0` | | [actions/upload-artifact](https://github.com/actions/upload-artifact) | `6.0.0` | `7.0.1` | | [actions/download-artifact](https://github.com/actions/download-artifact) | `7.0.0` | `8.0.1` | | [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) | `1.13.0` | `1.14.0` | Updates `actions/checkout` from 5.0.1 to 6.0.2 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/93cb6efe18208431cddfb8368fd83d5badbf9bfd...de0fac2e4500dabe0009e67214ff5f5447ce83dd) Updates `astral-sh/setup-uv` from 7.6.0 to 8.1.0 - [Release notes](https://github.com/astral-sh/setup-uv/releases) - [Commits](https://github.com/astral-sh/setup-uv/compare/37802adc94f370d6bfd71619e3f0bf239e1f3b78...08807647e7069bb48b6ef5acd8ec9567f424441b) Updates `actions/cache` from 5.0.4 to 5.0.5 - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/668228422ae6a00e4ad889ee87cd7109ec5666a7...27d5ce7f107fe9357f9df03efb73ab90386fccae) Updates `codecov/codecov-action` from 5.5.3 to 6.0.0 - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/1af58845a975a7985b0beb0cbe6fbbb71a41dbad...57e3a136b779b570ffcdbf80b3bdc90e7fab3de2) Updates `googleapis/release-please-action` from 4.4.0 to 5.0.0 - [Release notes](https://github.com/googleapis/release-please-action/releases) - [Changelog](https://github.com/googleapis/release-please-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/googleapis/release-please-action/compare/16a9c90856f42705d54a6fda1823352bdc62cf38...45996ed1f6d02564a971a2fa1b5860e934307cf7) Updates `actions/upload-artifact` from 6.0.0 to 7.0.1 - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/b7c566a772e6b6bfb58ed0dc250532a479d7789f...043fb46d1a93c77aae656e7c1c64a875d1fc6a0a) Updates `actions/download-artifact` from 7.0.0 to 8.0.1 - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/37930b1c2abaa49bbe596cd826c3c89aef350131...3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c) Updates `pypa/gh-action-pypi-publish` from 1.13.0 to 1.14.0 - [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases) - [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e...cef221092ed1bacb1cc03d23a2d87d1d172e277b) --- updated-dependencies: - dependency-name: actions/cache dependency-version: 5.0.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions - dependency-name: actions/checkout dependency-version: 6.0.2 dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions - dependency-name: actions/download-artifact dependency-version: 8.0.1 dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions - dependency-name: actions/upload-artifact dependency-version: 7.0.1 dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions - dependency-name: astral-sh/setup-uv dependency-version: 8.1.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions - dependency-name: codecov/codecov-action dependency-version: 6.0.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions - dependency-name: googleapis/release-please-action dependency-version: 5.0.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions - dependency-name: pypa/gh-action-pypi-publish dependency-version: 1.14.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * deps(deps-dev): bump mypy from 1.20.2 to 2.1.0 (#170) Bumps [mypy](https://github.com/python/mypy) from 1.20.2 to 2.1.0. - [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md) - [Commits](https://github.com/python/mypy/compare/v1.20.2...v2.1.0) --- updated-dependencies: - dependency-name: mypy dependency-version: 2.1.0 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * deps(deps): bump the uv-minor-patch group across 1 directory with 4 updates (#174) Bumps the uv-minor-patch group with 4 updates in the / directory: [pydantic](https://github.com/pydantic/pydantic), [numpy](https://github.com/numpy/numpy), [cf-xarray](https://github.com/xarray-contrib/cf-xarray) and [pymdown-extensions](https://github.com/facelessuser/pymdown-extensions). Updates `pydantic` from 2.13.3 to 2.13.4 - [Release notes](https://github.com/pydantic/pydantic/releases) - [Changelog](https://github.com/pydantic/pydantic/blob/main/HISTORY.md) - [Commits](https://github.com/pydantic/pydantic/compare/v2.13.3...v2.13.4) Updates `numpy` from 2.4.4 to 2.4.6 - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst) - [Commits](https://github.com/numpy/numpy/compare/v2.4.4...v2.4.6) Updates `cf-xarray` from 0.11.0 to 0.11.1 - [Release notes](https://github.com/xarray-contrib/cf-xarray/releases) - [Commits](https://github.com/xarray-contrib/cf-xarray/compare/v0.11.0...v0.11.1) Updates `pymdown-extensions` from 10.21.2 to 10.21.3 - [Release notes](https://github.com/facelessuser/pymdown-extensions/releases) - [Commits](https://github.com/facelessuser/pymdown-extensions/compare/10.21.2...10.21.3) --- updated-dependencies: - dependency-name: pydantic dependency-version: 2.13.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: uv-minor-patch - dependency-name: numpy dependency-version: 2.4.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: uv-minor-patch - dependency-name: cf-xarray dependency-version: 0.11.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: uv-minor-patch - dependency-name: pymdown-extensions dependency-version: 10.21.3 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: uv-minor-patch ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * fix(deps): bump aiohttp to >=3.14.0 to resolve CVE-2026-34993 and CVE-2026-47265 (#182) The Security Audit job's pip-audit step flagged aiohttp 3.13.5 for CVE-2026-34993 and CVE-2026-47265 (both fixed in 3.14.0). Raise the floor and refresh the lock to 3.14.1. Co-authored-by: Claude Opus 4.8 <[email protected]> * chore: release 0.10.1 (#181) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * ci(deps): bump the actions group with 3 updates (#185) Bumps the actions group with 3 updates: [actions/checkout](https://github.com/actions/checkout), [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) and [codecov/codecov-action](https://github.com/codecov/codecov-action). Updates `actions/checkout` from 6.0.2 to 6.0.3 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/de0fac2e4500dabe0009e67214ff5f5447ce83dd...df4cb1c069e1874edd31b4311f1884172cec0e10) Updates `astral-sh/setup-uv` from 8.1.0 to 8.2.0 - [Release notes](https://github.com/astral-sh/setup-uv/releases) - [Commits](https://github.com/astral-sh/setup-uv/compare/08807647e7069bb48b6ef5acd8ec9567f424441b...fac544c07dec837d0ccb6301d7b5580bf5edae39) Updates `codecov/codecov-action` from 6.0.0 to 6.0.1 - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/57e3a136b779b570ffcdbf80b3bdc90e7fab3de2...e79a6962e0d4c0c17b229090214935d2e33f8354) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 6.0.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions - dependency-name: astral-sh/setup-uv dependency-version: 8.2.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions - dependency-name: codecov/codecov-action dependency-version: 6.0.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --------- Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Davis Vann Bennett <[email protected]> Co-authored-by: Claude Sonnet 4.6 <[email protected]> Co-authored-by: Emmanuel Mathot <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * feat(s1-rtc): STAC item builders + ingest fixes for TiTiler rendering (#180) * feat(stac): add build_s1_rtc_stac_item and generate-stac-s1 CLI command Adds eopf_geozarr.stac.s1_rtc.build_s1_rtc_stac_item, which reads a consolidated S1 GRD RTC Zarr V3 store and returns a pystac.Item with SAR/SAT/projection extensions, WGS84 bbox derived via pyproj, and vv/vh asset sub-paths. Ascending orbit is preferred when both are present. Also adds the generate-stac-s1 CLI subcommand that prints the item as JSON. 8 unit tests cover all acceptance criteria from the plan. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * fix: support s3:// URIs in S1Tiling discovery and ingest (#175) Four functions in s1_ingest.py silently failed for s3:// paths: - discover_s1tiling_acquisitions / discover_s1tiling_conditions used pathlib.Path.glob(), which normalises s3:// to s3:/ and returns 0 matches - ingest_s1tiling_acquisition / ingest_s1tiling_conditions coerced paths via Path(), corrupting s3:// URIs, and called .exists() which always returns False for S3 (existence checks now use path_exists() from fs_utils) Adds three private helpers: - _list_tifs(): uses s3fs.S3FileSystem.glob for s3:// prefixes - _coerce_input_path(): preserves str for s3://, returns Path otherwise - _rasterio_env(): rasterio.Env(AWSSession) context for s3:// paths; strips https:// from AWS_ENDPOINT_URL since GDAL expects hostname only Tested: rasterio.open("s3://...") confirmed working on OVH S3 with AWS_S3_ENDPOINT or AWS_ENDPOINT_URL set; hostname-only required by GDAL. Refs #139 Co-authored-by: Claude Sonnet 4.6 <[email protected]> * fix(stac): point vv/vh asset hrefs to orbit group root; add tile_matrix_set Per Emmanuel's review on #173: - vv/vh asset hrefs now point to {store}/{orbit} (the zarr group that carries multiscales), matching how S2 reflectance assets point to measurements/reflectance. TiTiler reads tile_matrix_set from that group's multiscales attributes. - create_s1_store now writes tile_matrix_set into the orbit group's multiscales so TiTiler can discover the tiling scheme without error. - Updated test_asset_hrefs to assert the new orbit-group href. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * fix(ingest): add proj:code to resolution group attrs for TiTiler compatibility TiTiler's _validate_zarr checks ds.rio.crs on the native resolution group (r10m). Without proj:code in the resolution group's attrs, rioxarray returns None and the group is excluded → groups=[] → bounds unpack error. S2 zarr already has proj:code at the resolution group level; mirror that for S1. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * fix(s1): write CF grid_mapping, drop tile_matrix_set from RTC store (#176) The deployed titiler-eopf reader resolves a group's CRS via rioxarray, which reads a CF spatial_ref/crs_wkt coordinate -- not the GeoZarr proj:code attr. S1 GRD RTC stores carried only proj:code, so rio.crs was None and every multiscale group was rejected (HTTP 500 on render/info). The S2 converter already writes a spatial_ref grid-mapping; this brings S1 in line. Also stop writing tile_matrix_set into the orbit-group multiscales: the data-model owner confirmed it is not part of the S1 GRD RTC data model, and the data_api reader already treats it as optional (MISSING default). - add _add_grid_mapping() using pyproj CRS.to_cf() (the same source rioxarray uses, so no hard-coded projection) and call it for every resolution level (both store-creation paths) and the conditions group - remove _create_tile_matrix_set() and its multiscales entry - tests: assert no tile_matrix_set, spatial_ref present + rio.crs resolves to the native EPSG, conditions group carries grid_mapping This fixes at the source what data-pipeline currently works around in ingest_v1_s1_rtc.py (_patch_cf_grid_mapping / _patch_tile_matrix_limits). Co-authored-by: Claude Opus 4.8 <[email protected]> * Merge #172 (deprecate v0, fix fill values, sanitize attrs) into s1-rtc (#178) * chore: release 0.9.0 * feat: implement scale-offset and data type casting via codecs * fix: fix dependency declaration * chore: use latest version of cast value * chore: make cast-value a project dependency * test: expand test coverage * fix: upgrade pytest to 9.0.3 (CVE-2025-71176) Fixes insecure /tmp directory handling on UNIX systems. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * - Use zarr-python's presumptive implementation of scale-offset - use scalar map for handling NaN - ensure that downsampled arrays use scalar map + cast value - improve tests across parametrization of relelvant functions * feat: add store-root spatial:bbox and tighten minispec requirements (#164) * feat: add store-root spatial:bbox and tighten minispec requirements Introduces a GeoZarr "Store Root" layer in the minispec so clients can read a summary footprint without walking into child groups, and tightens the multiscale profile so `spatial:bbox` at the root and `spatial:transform` + `spatial:shape` on every layout entry are mandatory. Adds a new `geozarr.store` pydantic module (`GeoZarrStoreAttrs`, `GeoZarrMultiscaleGroupAttrs`, `GeoZarr`) enforcing the tightened profile, and updates the S2 converter to union child-group bboxes into an EPSG:4326 footprint written at the store root. Closes #156. Addresses the clear parts of #163; the array-level and non-multiscale-group parts of that issue need further clarification. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * feat: update GeoZarr store model and optimize S2 converter for improved bbox handling * Addresses review feedback on #164: - Adds a "GeoZarr Hierarchy & Identification" subsection to the Store Root section of the minispec, codifying d-v-b's proposed rules: single root, root prefix ends with `.zarr`, suffix occurs at most once in the hierarchy, and explicit terminal-path conditions. - Promotes the store-root CRS to mandatory: at least one of `proj:code`, `proj:wkt2`, `proj:projjson` MUST be set; there is no implicit EPSG:4326 default. Per @vincentsarago's review. - Promotes `zarr_conventions` declaration at the store root from RECOMMENDED to required. - Converter now writes `proj:code: "EPSG:4326"` at the root alongside `spatial:bbox`. - Pydantic `GeoZarrStoreAttrs` enforces the new CRS-required rule. Cross-links the new hierarchy + spatial:extent follow-ups upstream: zarr-developers/geozarr-spec#132 (hierarchy & root identification, also addresses #124 URL parsing) and #133 (STAC-style spatial:extent). --------- Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]> * chore: uv.lock * refactor: use zarr-python 3.2.0 * chore: lockfile * fix: pin fill value based on data type instead of relying on xarray * test: update tests to consistently use nan fill value for floats * chore: bump urllib3 to 2.7.0 * chore: skip quicklook groups (#165) * chore: group dependabot updates for actions and pip (#160) * chore: group dependabot updates for actions and pip - Group all GitHub Actions bumps (minor, patch, major) into one PR - Group minor and patch pip bumps into one PR; major bumps remain ungrouped for individual review Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * chore: change dependabot schedule day to wednesday * ci: add comment_on option to security auditing action * ci: switch dependabot Python ecosystem from pip to uv uv ecosystem reads uv.lock directly, enabling Dependabot to raise PRs for lockfile-pinned versions including security patches. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> --------- Co-authored-by: Claude Sonnet 4.6 <[email protected]> * fix: derive coarse spatial transforms from coordinates (#168) * fix: derive coarse spatial transforms from coordinates * refactor: improve function definitions for clarity and consistency * chore: release 0.10.0 (#162) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * fix(converter): align convert layout to S2, strip _eopf_attrs, fix _FillValue (#171) - Remove TMS layout and /0 /1 /2 numeric overview groups from the general 'convert' CLI; produce S2-style sibling r{2**level} overviews instead. 'convert-s2-optimized' continues to emit the same layout. - Drop the '--tile-width' CLI option. - Sanitize array attrs in all converters: strip stale '_eopf_attrs' and source-only '_FillValue' / 'dtype' / 'valid_min' / 'valid_max' on decoded floats; rewrite 'units: digital_counts' -> 'units: 1'. The sanitizer now returns a new dict and callers reassign 'attrs', so stale keys are actually removed (a previous '.update()' pattern left them in place). - Sanitize coord attrs too (datetime coords in /conditions/meteorology were leaking '_eopf_decode_datetime64' via '_eopf_attrs'). - Set '_FillValue' properly for float measurements so xarray's CF encoder produces the base64-NaN representation needed for 'use_zarr_fill_value_as_mask=True' round-trip (xarray#11345). Fixes #171. - Remove dead 'utils.encode_cf_fill_value' (no callers). - Tests: open each r{N} multiscale level separately; migrate 'test_multiscales_round_trip' from deprecated tms.Multiscales to zcm.Multiscales (with tuplify_json to handle extra='allow' fields). - Regenerate snapshot fixtures; verified 0 '_eopf_attrs', 'tile_matrix*', or 'digital_counts' markers across S2A/S2B/S2C. * test: add guardrails for converter output attrs (#171) Walks the snapshotted GroupSpec JSON fixtures and asserts: - no '_eopf_attrs' anywhere; - no 'tile_matrix*' markers (TMS removal); - no 'units: digital_counts' on float arrays (decoded scale/offset); - every float array under '/measurements/' has '_FillValue' set (required for CF NaN-mask round-trip, xarray#11345). * docs: deprecate v0 references, document r{N} overview layout (#171) - Drop 'tile_width' parameter from API examples (option removed from CLI/API). - Replace '/measurements/r10m/{0,1,2}' nested-pyramid examples with the current flat 'r{2**level}' sibling-group layout. - Remove the V0 vs V1 split in converter.md / examples.md / quickstart.md; the general 'convert' command now produces the same flat layout as 'convert-s2-optimized'. The S2-optimized section is reframed as a feature description rather than a 'V1 vs V0' comparison. - Update architecture.md multiscales snippet to the new model ('layout' / 'asset' / 'derived_from' / 'transform'). - Update faq.md inspection snippet to walk the new layout. * refactor: clean up code formatting and enhance attribute handling in geozarr conversion * fix(cli-e2e): drop deprecated --tile-width flag from tests and example * refactor: remove deprecated v0 layout, sanitize attributes, and fix _FillValue handling * test: add _FillValue masking roundtrip test (#172) Addresses @vincentsarago's PR review: assert float arrays written with the converter's _FillValue convention round-trip through xarray's use_zarr_fill_value_as_mask=True so NaN cells come back masked. * test: drive _FillValue masking test through create_geozarr_dataset (#172) Replace the standalone xarray write with a real converter run: build a small float DataTree, invoke create_geozarr_dataset, then reopen the output with use_zarr_fill_value_as_mask=True and assert masking on the nodata patch. Exercises the converter's _FillValue + encoding path end-to-end, per @vincentsarago's review. * fix: green CI for #172 — S2 convert flat layout + idna CVE (#177) * fix(security): bump idna 3.13 -> 3.17 to clear CVE-2026-45409 pip-audit in the Security Audit workflow blocks on idna 3.13 (CVE-2026-45409, incomplete fix of CVE-2024-3651; fixed in 3.15). Bump the transitive pin via uv lock. pip-audit now reports no known vulnerabilities. Co-Authored-By: Claude Opus 4.8 <[email protected]> * fix(cli): route Sentinel-2 convert through the optimized flat layout Generic `convert` emitted GeoZarr-0.4 multiscales with native data at the group root and overviews as nested r{2**N} sub-groups. That tree cannot be opened by xr.open_datatree (an overview child's x/y conflict with the parent's inherited x/y), so the `info` and `validate` CLI commands crashed on the converter's own Sentinel-2 output. Detect Sentinel-2 inputs and delegate to the existing, tested convert_s2_optimized path, which emits flat sibling r{N}m levels (the layout PR #172 documents). Non-S2 inputs keep the generic create_geozarr_dataset path. Detection is best-effort and never aborts conversion. Regenerate the geozarr_examples structure snapshots to the flat layout. Co-Authored-By: Claude Opus 4.8 <[email protected]> * test: make CLI e2e assertions debuggable and fix multiscale round-trip - The CLI logs errors to stdout via structlog; assert on stdout+stderr in test_cli_e2e so a non-zero exit shows the actual error instead of an empty stderr. - test_multiscale_attrs_round_trip tuplified only one side of its equality, so flat-layout level groups carrying list-valued attrs (e.g. spatial:bbox) broke a check that is only about JSON list/tuple normalisation. Tuplify both sides; the model still round-trips losslessly. Co-Authored-By: Claude Opus 4.8 <[email protected]> --------- Co-authored-by: Claude Opus 4.8 <[email protected]> --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Davis Vann Bennett <[email protected]> Co-authored-by: Claude Sonnet 4.6 <[email protected]> Co-authored-by: Emmanuel Mathot <[email protected]> * feat(s1-stac): add render extension with dual-pol RGB composite preview The previous S1 RTC preview rendered a single VH band as grayscale with an incorrect rescale (0,219), unsuitable for linear gamma0 RTC values. Declare a render-extension `renders.rgb` config producing the standard dual-pol false- colour composite (R=VV, G=VH, B=VV/VH ratio) with rescale 0–0.1 and tilesize 256, referencing the preferred orbit group. Downstream titiler-based services consume this to generate previews/tiles/tilejson. Co-Authored-By: Claude Opus 4.8 <[email protected]> * refactor(s1-stac): emit a single rescale pair in the RGB render A single [min, max] pair applies to all bands in titiler, so emitting three identical pairs was redundant (and forced collapse logic in the consumer). Same rendered output. Co-Authored-By: Claude Opus 4.8 <[email protected]> * fix(s1-rtc): TEMPORARY name store as s1-rtc-{tile} to match titiler render path (#246) titiler-eopf reconstructs the store path as s3://{bucket}/tests-output/{collection}/{item_id}.zarr and ignores the STAC asset href, so the GeoZarr cube must be named after the item id (s1-rtc-{tile}) for new tiles to preview. Parse the tile from the s1-rtc- prefix (was s1-grd-rtc-); the item id (s1-rtc-{tile}) is unchanged, so filename == item id by construction. Pairs with the data-pipeline direct-write change (store written at the tests-output path) which replaces the #250 auto-copy. Dev-phase, no consumers — disposable test data. Revert to s1-grd-rtc- when titiler-eopf#108 (resolve store from href) lands. Co-Authored-By: Claude Opus 4.8 <[email protected]> * fix(s1-rtc): build STAC item from a non-consolidated cube store build_s1_rtc_stac_item opened the cube with zarr.open_consolidated, which raises `ValueError: Consolidated metadata ... not found` when the store lacks root consolidated metadata. A per-tile cube grown by appending a time-slice to an existing same-orbit group ends up in exactly that state — re-consolidating an append on the S3 store is unreliable — so live STAC registration failed on the 2nd+ same-orbit acquisition for a tile (it broke the Pyrenees S1 RTC soak). The builder must not require consolidated metadata: titiler reads these stores fine without it. Fall back to a direct zarr.open_group(mode="r") read when the consolidated metadata is absent; re-raise any other ValueError. Adds a regression test: build from a 2-slice same-orbit store created with consolidate=False (fails before the fix in open_consolidated, passes after). Co-Authored-By: Claude Opus 4.8 <[email protected]> * fix(s1-rtc): CF-encode `time` at every level for datetime-based rendering (#192) (#193) Per-acquisition previews render via titiler `sel=time={INTEGER index}` — a positional index that breaks once a cube's time axis goes non-monotonic (a cross-run append of an earlier-dated scene; proved on 31TEH: `[06-08, 06-07]` → the 06-07 item has no preview). Keeping positional indices correct would need cube reorder + full re-registration on every append (not scalable). The deployed titiler (v0.5.0) already does label-based `da.sel(..., method=...)` via `open_datatree(decode_times=True)`; the only gap is the cube's `time` array, which is a bare int64 with no CF datetime metadata, so it can't be a datetime index. Encoding it lets previews select by exact datetime, which works even on a non-monotonic axis — no reorder, register only the new item. titiler picks a multiscale level by zoom (previews use a coarse level), so `time` must resolve there. Putting it only at the orbit-group level and inheriting fails to open while r10m stays bare int64 (AlignmentError: int64 vs datetime64 on the shared dim). So encode `time` consistently at EVERY level: - new `_create_time_coordinate_array` + `TIME_CF_ATTRS` (units/calendar/ standard_name); created at every level in create + append-group paths. - the append write resizes/writes `time` at every level (was r10m only). - absolute_orbit/relative_orbit/platform stay at r10m (not selected on). - stored dtype stays int64 ns, so register_per_acquisition's raw read is unaffected. Adds TestTimeCFDatetime incl. exact datetime `.sel` on a non-monotonic axis at the native and a coarse level. 55 passed. Refs EOPF-Explorer/data-model#192 Co-authored-by: Claude Opus 4.8 <[email protected]> * fix(s1-rtc): enrich STAC items & consolidate construction into the library (#195) (#196) * fix(s1-rtc): enrich cube STAC item + fix vv/vh asset model (#195) The S1 RTC cube STAC item carried duplicate vv/vh assets with byte-identical hrefs (bug #1), exposed no asset for the descending orbit group of a dual-orbit cube (bug #2), and was missing most of the metadata the S2 L2A reference carries. Rework `build_s1_rtc_stac_item`: - Asset model: replace the two indistinguishable vv/vh assets with one `gamma0-rtc-backscatter-{asc,desc}` asset per present orbit group, exposing VV/VH as named STAC 1.1 `bands` (no more duplicate hrefs), plus a `border-mask-{orbit}` asset for the valid-data mask. Each γ⁰ asset carries data_type/nodata/unit/gsd. - Identity: add `constellation`, `instruments: [c-sar]`, `gsd` (platform stays per-acquisition — a cube can mix S1A/S1C). - Projection: add proj:bbox, and proj:shape/proj:transform (best-effort from the r10m group attrs). - Datacube extension: cube:dimensions (time as a bounded extent — not a values list, the cube grows by appending — plus x/y) + cube:variables. - Descriptive: title/description; created (earliest acquisition, stable across rebuilds) / updated (build time) via the timestamps extension. - Fold the corrected render rescale (0.0,0.2) into `_rgb_render`. Out of scope (tracked follow-ups): statistics (#157), processing:software / DEM lineage, and per-product CDSE fields are not in the store; per-acquisition item construction is moved in a follow-up commit. Co-Authored-By: Claude Opus 4.8 <[email protected]> * feat(s1-rtc): move per-acquisition + coverage STAC construction into the library Consolidate the *pure* S1 RTC STAC construction in eopf_geozarr.stac.s1_rtc so item shape has a single owner (the library); registration (STAC-API upsert, S3 alternates, gateway rewrite, TiTiler links, CDSE discovery) stays in data-pipeline. Adds, ported from the data-pipeline scripts (deployment URLs stripped): - acquisition_id(tile_id, when) - build_s1_rtc_per_acquisition_items(store, *, orbit, collection_id) -> list[pystac.Item]: one single-datetime item per cube time slice of an orbit, reoriented to that orbit (sat:orbit_state + render expression + only that orbit's γ⁰/mask assets), per-slice platform, datacube structure dropped (a single acquisition is not a cube). Deployment-agnostic: carries the render config + datetime; the registration layer derives the cube-endpoint TiTiler links (sel=time={datetime}) from it. - Slice / pick_slice / slice_coverages: coverage-based preview-slice selection (reads border_mask at r720m). Ported the construction unit tests (pick_slice, slice_coverages, and the per-acquisition construction subset); the link/thumbnail/alternate-asset and rescale-override tests stay in data-pipeline as registration concerns. Follow-up (separate data-pipeline PR): bump the data-model pin, delete the moved functions from register_per_acquisition.py / register_v1_s1_rtc.py, and rewire them to consume this library output. Co-Authored-By: Claude Opus 4.8 <[email protected]> * fix(s1-rtc): per-acquisition items carry their own orbit footprint Review follow-up. A per-acquisition item cloned the cube base, so on a dual-orbit cube every item inherited the WGS84 union bbox/geometry of both orbits and the preferred (ascending) orbit's proj:bbox — advertising a footprint wider than, and a data bbox from the wrong orbit relative to, the acquisition's own orbit. Recompute bbox/geometry/proj:bbox from the run orbit's spatial:bbox (factored the bbox->polygon construction into `_bbox_to_geometry`, shared with the cube builder), and give per-acquisition items a single-acquisition description rather than the cube's "datacube" wording. Tests: per-acq footprint = run orbit (not the union); `created` = earliest acquisition (idempotent across rebuilds); ValueError on an invalid or absent orbit. Co-Authored-By: Claude Opus 4.8 <[email protected]> * fix(s1-rtc): omit orbit_state on dual-orbit cubes; expose datacube dimension sizes Two review observations from inspecting the live items: - sat:orbit_state is single-valued, so on a dual-orbit cube (both ascending and descending groups present) a single value mislabels half the slices. Set it (and declare the SAT extension) only for a single-orbit cube; per-acquisition items, which are single-orbit, still carry the real per-orbit value. Ensure per-acq items declare the SAT extension even when cloned from a dual-orbit base that omitted it. - cube:dimensions previously carried only `extent`, so the number of elements per dimension wasn't visible. The time axis is irregularly sampled, so list its discrete `values` (count = number of acquisitions); the regular x/y axes get a `step` (size derivable from extent; exact pixel count is also in proj:shape). Co-Authored-By: Claude Opus 4.8 <[email protected]> * fix(s1-rtc): datacube time axis carries extent only (drop values enumeration) Enumerating every acquisition as cube:dimensions.time.values does not scale as the cube is appended over the mission. No STAC extension provides a scalable temporal element-count field (datacube temporal_dimension offers only values/step/extent, and step is null for S1's irregular sampling; projection proj:shape is spatial-only; raster has no dimension shape). So the time axis carries a bounded `extent` only. x/y element counts remain available via proj:shape (exact) and the datacube x/y extent+step. The number of acquisitions is obtained from the per-acquisition collection (STAC search count), which stays correct as the cube grows. Co-Authored-By: Claude Opus 4.8 <[email protected]> * fix(s1-rtc): correct datacube variable_type, platform value, created, time values Review of the live items surfaced several metadata issues: - cube:variables used `type` (a non-standard, silently-ignored key) instead of the datacube extension's `variable_type`; border_mask is now `auxiliary`, vv/vh `data`. - per-acquisition `platform` carried the store's short code (e.g. `s1a`); normalize to the STAC convention `sentinel-1a` (mirrors the Sentinel-2 reference). - `created` (timestamps ext) was set to an acquisition time, which misuses the field (it means the item's creation instant); omit it and keep `updated` = build time. - the cube's irregular time axis now lists its discrete `values` (the acquisition instants) so the number of time steps is visible; the regular x/y axes stay extent + step (their pixel count is in proj:shape). Co-Authored-By: Claude Opus 4.8 <[email protected]> * refactor(s1-rtc): robust EPSG parse for datacube reference_system + clarify per-acq proj inheritance Review follow-ups (no behaviour change for the EPSG:NNNN stores in use): - derive the datacube reference_system via pyproj.CRS(...).to_epsg() instead of splitting the proj:code string, so non-"EPSG:NNNN" CRS forms don't break it. - comment why per-acquisition items recompute proj:bbox from their orbit but inherit proj:code/shape/transform (the MGRS grid is shared across orbits). Co-Authored-By: Claude Opus 4.8 <[email protected]> * docs(s1-rtc): note the dual-orbit time axis spans both orbits in the datacube A dual-orbit cube merges two per-orbit sub-cubes (disjoint time axes) onto a shared grid. Modelling orbit as a dimension would imply a sparse, mostly-empty orbit×time grid; instead orbit stays an attribute of each acquisition, conveyed via the per-orbit assets. Add a `description` on the time dimension so the merged axis isn't misread. Single-orbit cubes …
Configuration menu - View commit details
-
Copy full SHA for f19f423 - Browse repository at this point
Copy the full SHA f19f423View commit details -
deps(deps): update zarr[cast-value-rs] requirement (#269)
Updates the requirements on [zarr[cast-value-rs]](https://github.com/zarr-developers/zarr-python) to permit the latest version. - [Release notes](https://github.com/zarr-developers/zarr-python/releases) - [Changelog](https://github.com/zarr-developers/zarr-python/blob/main/docs/release-notes.md) - [Commits](zarr-developers/zarr-python@v3.2.0...v3.3.0) --- updated-dependencies: - dependency-name: zarr[cast-value-rs] dependency-version: 3.3.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Configuration menu - View commit details
-
Copy full SHA for df9f27a - Browse repository at this point
Copy the full SHA df9f27aView commit details -
ci(deps): bump astral-sh/setup-uv in the actions group (#268)
Bumps the actions group with 1 update: [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv). Updates `astral-sh/setup-uv` from 9.0.0 to 10.0.1 - [Release notes](https://github.com/astral-sh/setup-uv/releases) - [Commits](astral-sh/setup-uv@c771a70...20cfd1b) --- updated-dependencies: - dependency-name: astral-sh/setup-uv dependency-version: 10.0.1 dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Configuration menu - View commit details
-
Copy full SHA for 3e65de4 - Browse repository at this point
Copy the full SHA 3e65de4View commit details -
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Configuration menu - View commit details
-
Copy full SHA for 7d662ed - Browse repository at this point
Copy the full SHA 7d662edView commit details
This comparison is taking too long to generate.
Unfortunately it looks like we can’t render this comparison for you right now. It might be too big, or there might be something weird with your repository.
You can try running this command locally to see the comparison on your machine:
git diff v0.10.2...v0.11.0