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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions