Skip to content

S1 RTC per-acquisition previews break on non-monotonic time axis — render by datetime, not index #192

Description

@lhoupert

Problem

Per-tile S1 RTC GeoZarr cubes store one acquisition per time slice (per orbit group, with overview levels r10m…r720m). The per-acquisition STAC items render their preview via titiler using sel=time={INTEGER index} — a positional index into the time axis.

This is fragile and not scalable:

  • Cubes go non-monotonic. ingest_s1tiling_acquisition (src/eopf_geozarr/conversion/s1_ingest.py:634-680) always appends a new acquisition at the physical end. A cross-run append of an earlier-dated scene therefore pushes the time axis out of chronological order.
  • Once non-monotonic, titiler fails to render the out-of-order slice (HTTP 400), so the per-acquisition item has no preview.
  • Keeping positional indices correct would require reordering the cube data and re-registering every item on every append — O(N) data rewrite + O(N) item churn per new acquisition.

Evidence

  • Tile 31TEH, descending group: time axis is [idx0 = 2026-06-08T05:43:23, idx1 = 2026-06-07T05:52:23]decreasing.
  • Item s1-rtc-31TEH-20260607t055223 (idx1) renders HTTP 400 "Could not find any valid variables in '/descending:vv'", while idx0 renders fine.
  • The idx1 slice is not empty (100% finite, real backscatter) — so it is purely an ordering/selection problem, not missing data.
  • Root metadata gap: …/descending/r10m/time/zarr.json has empty attributes — the time array is a bare int64 with no CF datetime metadata, so readers can't treat it as a datetime index.

Root cause

  1. time arrays carry no CF datetime encoding (units/calendar), so xarray/titiler can't expose them as a datetime index — the time dimension falls back to an integer index (which is why sel=time=0 works but sel=time=<datetime> 500s).
  2. ingest_s1tiling_acquisition appends at the physical end with no chronological guarantee.

Proposed resolution — render by datetime, not index

The deployed titiler v0.5.0 already does label-based selection: da.sel({dim: value}, method=method) (titiler/eopf/reader.py:589,597) with a method::value DSL. The only gap is the cube metadata.

Encode time as a CF datetime coordinate (units = "nanoseconds since 1970-01-01", calendar = "proleptic_gregorian", standard_name = "time"; values unchanged). Then each per-acquisition item can render via sel=time={its own acquisition datetime}. An exact-label .sel(time=<datetime>) works even on a non-monotonic axis (only nearest/slice need monotonicity), so:

  • No cube data reorder, ever.
  • Append a new acquisition → register only that one item with its datetime; existing items untouched.
  • Order-immune and O(1) per append.

Tracking checklist

  • Spike validated (datetime sel renders the 31TEH 06-07 slice, correct, on the non-monotonic axis)
  • data-model: CF datetime encoding on time (create/append/conditions paths) + tests
  • data-pipeline: per-acquisition render links use sel=time={datetime} + incremental registration
  • Existing-cube metadata backfill (CF time attrs) + one-time item re-registration
  • Release (eopf-geozarr pin bump + image tag) and deploy
  • Fleet migrated + verified

Fallback if the spike fails: sort the cube on append (reorder + re-register) — kept as a backup approach only.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions