You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Tracking issue for making jbbutler/antarctic_AR_dataset consume zagg's temporal pipeline for its heavy per-storm aggregations. The engine itself landed via #70 (with #73 registries and #76 dispatch); this issue covers the remaining work: the event interchange/catalog layer plus a handful of capability gaps found by auditing every attribute_utils.py kernel against zagg.temporal built-ins. Supersedes the remaining scope of #12. The prior downstream attempt, jbbutler/antarctic_AR_dataset#1, was closed unmerged — its aggregation_registry.py design already lives in src/zagg/temporal.py, and its catalog/auth ideas are folded into the plan below.
Attribute coverage audit
All 14 field-based attributes (the ones that stream MERRA-2) expressed as temporal specs, vs. what current main supports. The ~10 mask-geometry attributes (max/mean area, duration, start/end dates, southward extent, cumulative space-time, landfall region, trajectory, elevation gradient) need no MERRA-2 reads and stay client-side in artools (decided; trajectory is per-timestep and doesn't fit the tabular row model anyway).
output
variable / collection
spatial_func × reducer
mask
needs
max_T2m_ais
T2M / slv
max × max
ais
works today
max_T2M_anomaly_ais
T2M / slv
max × max (anomaly)
ais
works today
max_IWV_ais
TQV / slv
max × max
ais
works today
max_IWV_anomaly_ais
TQV / slv
max × max (anomaly)
ais
works today
avg_vIVT_ais
VFLXQV / flx
weighted_mean × weighted_mean (negate)
ais
gap 3
max_vIVT_ais
VFLXQV / flx
max × max (negate)
ais
gap 3
max_vIVT
VFLXQV / flx
max × max (negate)
full
gap 3
min_SLP
SLP / slv
min × first_landfall
ocean
gap 2
max_SLP_gradient
SLP / slv
max_gradient × first_landfall
ocean
gap 2
max_landfalling_v850hPa
V850 / v850
max × first_landfall (negate)
ocean
gaps 2, 3
avg_landfalling_v850hPa
V850 / v850
weighted_mean × first_landfall (negate)
ocean
gaps 2, 3
avg_landfalling_minomega
OMEGA / omega
min_over_levels × first_landfall
ais
gaps 2, 3
cumulative_rainfall_ais
PRECCU+PRECLS / flx
weighted_sum × sum
ais
gaps 3, 4, 5
cumulative_snowfall_ais
PRECSN / flx
weighted_sum × sum
ais
gaps 3, 4, 5
4 of 14 run on current main unchanged; the rest need the gaps below. No artools-side zagg plugin appears necessary — every gap is generic enough to be a zagg built-in or config feature.
Capability gaps
Gap 1 — event interchange + catalog (the core of this issue). The temporal pipeline requires callers to pass events= directly (runner.py_run_lambda_events; masks by URI on Lambda). The storm catalogs are monolithic pandas-HDF5 files (one per ST-DBSCAN hyperparameter setting, data_array column of masks) — pytables, not byte-range addressable, unreadable by zagg.temporal.open_dataset. Needed:
an exporter that explodes one monolithic catalog into per-storm NetCDF event files — writing both the raw mask and the 24-h-lookahead augmented mask (augment_storm_da semantics: extend 24 h past storm end + 9-step rolling max) — plus a catalog table (parquet) with event_key, time range, bbox, mask URIs, and hyperparameter provenance;
an event catalog builder: bulk CMR/earthaccess query per collection DOI × the catalog's full time span → {collection: {date: uri}} index (the approach from [WIP] Lambda orchestrator jbbutler/antarctic_AR_dataset#1's catalog.py), then per-event granule URI lists (precip collections resolve against the augmented time range);
events_from_catalog() emitting local tuples (local backend) or URI dicts (Lambda).
Open sub-question: exporter/catalog placement — zagg.catalog.events (generic: "monolithic mask catalog → event files" is domain-neutral) vs. artools (it knows the pandas-HDF5 schema). Proposal: split — generic event-catalog + events_from_catalog() in zagg, the pytables-specific parser in artools.
Gap 2 — first_landfall event trigger.EVENT_TRIGGERS (registry.py:182) has no built-ins; process_event supports trigger: gating but nothing registers one. Without it, the first_landfallreducer captures the first timestep of the storm, not the first landfall — wrong for 5 specs. Add a built-in generic first_intersection trigger (first timestep where event ∩ a named static mask is non-empty; default ais_mask), registered also under the alias first_landfall. Storms that never intersect → no triggered timesteps → NaN (matches the existing is_landfalling filter semantics; test explicitly).
Gap 3 — per-collection time alignment + multi-granule collections. Two reader-level features:
collection_uris is {name: single_uri} (lambda_handler.py:85, runner.py:2031) but a multi-day storm spans several MERRA-2 daily granules — accept {name: [uris]} and concat along time in read_temporal_inputs.
MERRA-2 FLX/V850 collections are 1-hourly with half-hour timestamps; the storm masks are 3-hourly on the hour, so process_event's .sel(time=batch_times) KeyErrors. Add per-collection time_offset (e.g. -30min). This requires data_source.collections to become a mapping with metadata (currently a list of names in merra2_storm.yaml) — keep the list form working.
Gap 4 — precip processing branch.resample: {freq: 3h, how: sum, scale: 3600} (1-hourly kg m⁻² s⁻¹ rates → 3-hourly totals) and derived: variables (rainfall: PRECCU + PRECLS) as per-collection reader config. Modeled as a second logical collection sharing the FLX DOI so vIVT (no resample) and precip (resample) don't collide.
Gap 5 — per-spec event mask selection. Precip specs aggregate under the augmented mask, whose time axis extends past the primary mask's. Event payload gains named alternates (event_masks: {augmented: uri}) and specs a event_mask: key (default primary). Simplest process_event shape: group specs by event mask and run the streaming loop per group (the augmented group re-reads only the precip collection for the extra timesteps); avoids a union-time-axis loop.
Gap 6 — GES DISC credentials._run_local hardwires NSIDC credentials (runner.py:1183); MERRA-2 direct-S3 needs the GES DISC creds endpoint. Add a gesdisc credential provider via the existing credential_provider registry and a config key selecting the provider. Note this is a parameterization, not new machinery — earthaccess/EDL already speaks to GES DISC today (the downstream repo already streams MERRA-2 through it, and jbbutler/antarctic_AR_dataset#1 fetched GES DISC S3 creds directly); the announced 2026 GES DISC→Earthdata web consolidation changes portals, not the CMR/S3 access path. GES DISC S3 is us-west-2, same region as the Lambda deployment.
Draft downstream config
ar_attributes.yaml (keys marked [NEW] are the gaps above)
Phase 2 — collections-as-mapping config + multi-URI collections with time-concat + time_offset (gap 3). Update merra2_storm.yaml to the mapping form; keep list form valid.
Phase 5 — event catalog: granule index builder (DOI × time span via earthaccess), event catalog table format, events_from_catalog() for local/Lambda (gap 1, zagg half).
Downstream PR (jbbutler/antarctic_AR_dataset, after the above): monolithic-H5 → per-storm NetCDF exporter (raw + augmented masks, catalog parquet with hyperparameter provenance; source data assumed at ../antarctic_AR_catalogs/); ar_attributes.yaml; thin driver; parity test vs attribute_utils.py on a storm subset; demo notebook (local backend, ~5 storms); geometry attributes remain in artools; close jbbutler/antarctic_AR_dataset#1 with a pointer. Lambda-scale validation run is operator-executed.
Decisions made (maintainer)
Exporter ships both raw and augmented masks per storm (no in-worker mask transform seam).
Mask-geometry attributes stay client-side in artools.
Storm catalogs parsed locally from ../antarctic_AR_catalogs/ monolithic HDF5 (HF dataset is monolithic per hyperparameter setting; no per-storm layout exists upstream).
Questions for review / parity risks
Weighted-mean semantics differ.attribute_utils.compute_average computes a per-timestep area-weighted ratio, then an unweighted mean over timesteps (mean-of-ratios). zagg's WeightedMeanAccumulator accumulates global sum(weighted)/sum(weights) (ratio-of-sums, i.e. area-time weighting). These diverge whenever storm area varies over time — which is always. Affects avg_vIVT_ais. Options: (a) add a mean_of_ratios reducer for bit-parity, or (b) accept ratio-of-sums as the (arguably better-defined) estimator and document the delta. Needs a call before the parity test is written.
NaN-aware weights.compute_avg_landfalling_wind excludes NaN cells from the weight sum (fillna + notnull weights); spatial_weighted_mean_parts does not. Matters only where V850 is NaN under the mask (mask is ocean, so likely negligible — verify in parity).
Exporter/catalog placement (gap 1 sub-question): generic halves in zagg, pytables parser in artools — confirm or relocate.
spatial_max_gradient masks the sin(lat)=0 equator row that the downstream kernel lets blow up to inf — no-op for lat ∈ [−86, −39], noted for completeness.
🤖 from Claude
Tracking issue for making jbbutler/antarctic_AR_dataset consume zagg's temporal pipeline for its heavy per-storm aggregations. The engine itself landed via #70 (with #73 registries and #76 dispatch); this issue covers the remaining work: the event interchange/catalog layer plus a handful of capability gaps found by auditing every
attribute_utils.pykernel againstzagg.temporalbuilt-ins. Supersedes the remaining scope of #12. The prior downstream attempt, jbbutler/antarctic_AR_dataset#1, was closed unmerged — itsaggregation_registry.pydesign already lives insrc/zagg/temporal.py, and its catalog/auth ideas are folded into the plan below.Attribute coverage audit
All 14 field-based attributes (the ones that stream MERRA-2) expressed as temporal specs, vs. what current
mainsupports. The ~10 mask-geometry attributes (max/mean area, duration, start/end dates, southward extent, cumulative space-time, landfall region, trajectory, elevation gradient) need no MERRA-2 reads and stay client-side in artools (decided; trajectory is per-timestep and doesn't fit the tabular row model anyway).4 of 14 run on current
mainunchanged; the rest need the gaps below. No artools-side zagg plugin appears necessary — every gap is generic enough to be a zagg built-in or config feature.Capability gaps
Gap 1 — event interchange + catalog (the core of this issue). The temporal pipeline requires callers to pass
events=directly (runner.py_run_lambda_events; masks by URI on Lambda). The storm catalogs are monolithic pandas-HDF5 files (one per ST-DBSCAN hyperparameter setting,data_arraycolumn of masks) — pytables, not byte-range addressable, unreadable byzagg.temporal.open_dataset. Needed:augment_storm_dasemantics: extend 24 h past storm end + 9-step rolling max) — plus a catalog table (parquet) with event_key, time range, bbox, mask URIs, and hyperparameter provenance;{collection: {date: uri}}index (the approach from [WIP] Lambda orchestrator jbbutler/antarctic_AR_dataset#1'scatalog.py), then per-event granule URI lists (precip collections resolve against the augmented time range);events_from_catalog()emitting local tuples (local backend) or URI dicts (Lambda).Open sub-question: exporter/catalog placement —
zagg.catalog.events(generic: "monolithic mask catalog → event files" is domain-neutral) vs. artools (it knows the pandas-HDF5 schema). Proposal: split — generic event-catalog +events_from_catalog()in zagg, the pytables-specific parser in artools.Gap 2 —
first_landfallevent trigger.EVENT_TRIGGERS(registry.py:182) has no built-ins;process_eventsupportstrigger:gating but nothing registers one. Without it, thefirst_landfallreducer captures the first timestep of the storm, not the first landfall — wrong for 5 specs. Add a built-in genericfirst_intersectiontrigger (first timestep where event ∩ a named static mask is non-empty; defaultais_mask), registered also under the aliasfirst_landfall. Storms that never intersect → no triggered timesteps → NaN (matches the existingis_landfallingfilter semantics; test explicitly).Gap 3 — per-collection time alignment + multi-granule collections. Two reader-level features:
collection_urisis{name: single_uri}(lambda_handler.py:85,runner.py:2031) but a multi-day storm spans several MERRA-2 daily granules — accept{name: [uris]}and concat along time inread_temporal_inputs.process_event's.sel(time=batch_times)KeyErrors. Add per-collectiontime_offset(e.g.-30min). This requiresdata_source.collectionsto become a mapping with metadata (currently a list of names inmerra2_storm.yaml) — keep the list form working.Gap 4 — precip processing branch.
resample: {freq: 3h, how: sum, scale: 3600}(1-hourly kg m⁻² s⁻¹ rates → 3-hourly totals) andderived:variables (rainfall: PRECCU + PRECLS) as per-collection reader config. Modeled as a second logical collection sharing the FLX DOI so vIVT (no resample) and precip (resample) don't collide.Gap 5 — per-spec event mask selection. Precip specs aggregate under the augmented mask, whose time axis extends past the primary mask's. Event payload gains named alternates (
event_masks: {augmented: uri}) and specs aevent_mask:key (default primary). Simplestprocess_eventshape: group specs by event mask and run the streaming loop per group (the augmented group re-reads only the precip collection for the extra timesteps); avoids a union-time-axis loop.Gap 6 — GES DISC credentials.
_run_localhardwires NSIDC credentials (runner.py:1183); MERRA-2 direct-S3 needs the GES DISC creds endpoint. Add agesdisccredential provider via the existingcredential_providerregistry and a config key selecting the provider. Note this is a parameterization, not new machinery — earthaccess/EDL already speaks to GES DISC today (the downstream repo already streams MERRA-2 through it, and jbbutler/antarctic_AR_dataset#1 fetched GES DISC S3 creds directly); the announced 2026 GES DISC→Earthdata web consolidation changes portals, not the CMR/S3 access path. GES DISC S3 is us-west-2, same region as the Lambda deployment.Draft downstream config
ar_attributes.yaml (keys marked [NEW] are the gaps above)
Plan
zagg PR (this repo, phased):
first_landfall/first_intersectionbuilt-in event trigger + never-landfall NaN semantics (gap 2). Small, unblocks 5 specs.time_offset(gap 3). Updatemerra2_storm.yamlto the mapping form; keep list form valid.resample+derivedper-collection reader features (gap 4).event_maskspayload alternates, specevent_mask:key, per-group streaming loop) (gap 5).events_from_catalog()for local/Lambda (gap 1, zagg half).gesdisccredential provider + provider selection in config (gap 6).Downstream PR (jbbutler/antarctic_AR_dataset, after the above): monolithic-H5 → per-storm NetCDF exporter (raw + augmented masks, catalog parquet with hyperparameter provenance; source data assumed at
../antarctic_AR_catalogs/);ar_attributes.yaml; thin driver; parity test vsattribute_utils.pyon a storm subset; demo notebook (local backend, ~5 storms); geometry attributes remain in artools; close jbbutler/antarctic_AR_dataset#1 with a pointer. Lambda-scale validation run is operator-executed.Decisions made (maintainer)
../antarctic_AR_catalogs/monolithic HDF5 (HF dataset is monolithic per hyperparameter setting; no per-storm layout exists upstream).Questions for review / parity risks
attribute_utils.compute_averagecomputes a per-timestep area-weighted ratio, then an unweighted mean over timesteps (mean-of-ratios). zagg'sWeightedMeanAccumulatoraccumulates globalsum(weighted)/sum(weights)(ratio-of-sums, i.e. area-time weighting). These diverge whenever storm area varies over time — which is always. Affectsavg_vIVT_ais. Options: (a) add amean_of_ratiosreducer for bit-parity, or (b) accept ratio-of-sums as the (arguably better-defined) estimator and document the delta. Needs a call before the parity test is written.compute_avg_landfalling_windexcludes NaN cells from the weight sum (fillna + notnull weights);spatial_weighted_mean_partsdoes not. Matters only where V850 is NaN under the mask (mask isocean, so likely negligible — verify in parity).spatial_max_gradientmasks thesin(lat)=0equator row that the downstream kernel lets blow up to inf — no-op for lat ∈ [−86, −39], noted for completeness.