# Canonical internal data schema — FROZEN 2026-09-24 Frozen after inspecting a real ECDS `s2s-reforecasts` GRIB (`data/Tx_05-01-2022.grib`, from the user's `test.py`). Evidence: `metadata/grib_inspection.md`, `metadata/ecmwf_reforecast_inventory.csv`. ## Confirmed archive structure (from the real GRIB) | Aspect | Confirmed value | |---|---| | Format / centre | GRIB2, `ecmf`, `dataType=cf` (control forecast), `marsClass=s2`, `marsStream=enfo`, `marsType=cf` | | Grid | `regular_ll`, native **1.5°** (`iDir=jDir=1.5`); e.g. `Ni=33` lon, `Nj=21` lat | | Lat ordering | **DESCENDING, north→south** (`30 → 0`); `scanningMode=0`, `jScansPositively=0` | | Lon | ascending east (`81 → 129`), degrees_east | | Variable (this file) | `mx2t6` (paramId **121**), "Maximum temperature at 2 m in the last 6 hours", `stepType=max`, `typeOfLevel=heightAboveGround`, `level=2` | | Units | **K** (convert to °C for temperature targets) | | Steps | `stepUnits=hours`; **184 six-hourly windows** `0-6, 6-12, …, 1098-1104` = 46 days × 4/day (covers LT0–LT45) | | Init (reforecast) date | `dataDate` = `mars.hdate` (e.g. **2022-01-05** 00Z) | | Model-version date | `mars.date` (e.g. **2023-01-05**) — metadata only, tracked for cycle consistency | | Valid time | `validityDate/Time = init + endStep` (UTC). Verified: `0-6`→2022-01-05 06Z, `18-24`→2022-01-06 00Z, `1098-1104`→2022-02-20 00Z | | Member | single `cf` (no ensemble spread) | ## Conversion path (D6): GRIB → NetCDF → xarray `cdo -f nc copy in.grib out.nc` yields CF-1.6 NetCDF with: `time` (UNLIMITED, `hours since 00:00` = **valid time**), `lon`, `lat`, `height`, and the field named by shortName (e.g. `mx2t6`, units K). cfgrib not required. cdo/eccodes CLI confirmed working. ## Canonical representation (what preprocessing produces) `CanonicalField` (see `src/data/interfaces.py`), per initialization: - `values`: `(lead, lat, lon)` float32, **units °C** (K−273.15 for temperature). - `lat`: **ascending** degrees_north (GRIB lat is flipped on load). - `lon`: degrees_east. - `init_time`: datetime64 = reforecast init (`dataDate`/`mars.hdate`), e.g. 2022-01-05T00. - `model_version_date`: datetime64 = `mars.date` (cycle-consistency tracking). - `lead`: end-step hours `[6,12,…,1104]`; `lead_unit="h"`. - `valid_time`: `init + lead` (UTC) via `utils.timealign.valid_time`. - `level_type`: `sfc` (single level) | `pl`; `level`: hPa for pl. - `member_type`: `cf`. - `stepType`: `max` for `mx2t6` (max over each 6 h window). ## Daily-max Tx from `mx2t6` (UTC → local day) Observed Tx (VnGAD) is a **local UTC+7 calendar-day maximum**. Forecast daily Tx = **max of the four `mx2t6` six-hour windows whose valid time falls in each local (UTC+7) day**, using `utils.timealign.local_calendar_day` on the window valid times. LT0's local day may be partial (`partial_first` flag). This mapping is the one that must be unit-tested (leap years, day boundaries) before aggregating. ## Coordinate / unit standardization rules (frozen) 1. **Flip latitude to ascending** on load (GRIB is N→S). 2. **K → °C** for temperature variables used as Tx targets/inputs. 3. Keep predictors on the **native coarse grid** (1.5°); never regrid to 0.25° before the encoder (D8). 4. Align everything by **actual valid date**, never array index.