Skip to content

feat(export): IMAS-shaped replay export adapter (#49) - #71

Merged
obvious-autobuild[bot] merged 3 commits into
mainfrom
feat/imas-export
Oct 12, 2026
Merged

obvious-autobuild[bot] merged 3 commits into
mainfrom
feat/imas-export

Conversation

@obvious-autobuild

Copy link
Copy Markdown
Contributor

Fixes #49.

Human author: Tapan Ganatma Nakkina ([email protected])

Why

Issue #49 asks FusionLab's replay bundle to speak to IMAS dictionaries. Today the export surfaces are derived CSV/JSON, OpenUSD, and the replay bundle itself; none names quantities the way the fusion community's data dictionary does. The backlog program's locked decision: IMAS is an export direction only — an adapter at the edge, never an internal data spine and never an import path. This PR ships that adapter.

What

  • fusionlab/imas_export.py (new, on the usd_export.py pattern): a pure function over the same bundle dict usd_export.export_shot takes, mapping the ShotBundle v1 arrays to IMAS data-dictionary paths via a static, in-repo table. Units convert once at this edge — house units (m, T, MA, 1e20 m^-3, keV, MW, MJ) → the dictionary's (A, W, m^-3, eV) — with api_replay._UNIT as the conversion table's test oracle. Nothing is recomputed: every emitted value is the bundle's own.
  • GET /replay/{shot_id}/imas (sibling of the USD route): a strict-JSON attachment, imas_{id}.json, whose values equal the /replay bundle path-for-path, plus the sibling fusionlab_annotations.json in the same body: the catalog's useful/abort flags, the per-series provenance labels (measured/model/learned/computed), the machine-specific eq_inputs block, and the series the dictionary has no home for (n_e20 line density, Thomson radii, R_m, P_rad_MW, dWdt_MW, midplane-radius q_mid). The IMAS payload points at the sidecar via ids_properties.comment and carries no flag or label as data.
  • Graceful degradation inherited: groups the bundle lacks are omitted, never emitted as empty IDS stubs — the same rule /replay applies.
  • Dictionary verification (flagged as training knowledge in the spec, verified this session against the IMAS Data Dictionary, DD 4.2.x build, imas-data-dictionary.readthedocs.io, accessed 2026-10-12): equilibrium.time_slice.global_quantities.ip in A; q_95/beta_tor_norm dimensionless; boundary.elongation/minor_radius in m; profiles_1d.f in T.m with psi in Wb; profiles_2d.psi in Wb on a rectangular grid (grid_type.index = 1, dim1=R, dim2=Z); pf_active coil rectangle geometry fields r/z/width/height (geometry_type.index = 2); wall limiter unit[].outline r/z in m; ids_properties.comment exists as free text. Two honest omissions: vacuum_toroidal_field.r0 is left out because the bundle does not record the reference radius B_T was measured at (noted in the IDS comment); the wall description.type enum is left unfilled because no option honestly describes a single open limiter.
  • Docs: DATA_CONTRACT.md's "future adapter" paragraph now states the export direction and the import-never-as-spine rule; .obvious route maps updated.
  • No new dependency (omas not taken, not even as an extra — a dev cross-check can be added later if wanted); base pyproject.toml untouched.

How to Review

  • The mapping table + sidecar lists in fusionlab/imas_export.py are the whole design — everything else is mechanics. test_mapping_table_partitions_the_spec guarantees every future SPEC field gets classified (IMAS home or sidecar entry), never silently unexported.
  • tests/fixtures/imas/golden_30166.json is a committed full-value snapshot (~6 MB, one value per line). When the export changes deliberately, regenerate with the one-liner in the test module's docstring and review the diff — the snapshot diff is the failure mode.
  • Intentionally excluded: IMAS import, omas as a dependency, any change to /replay or the bundle spec.

Test Evidence

  • Full suite on the rebased tree (main @ a7a6e87): 539 passed, 1 skipped (main alone: 526; +13 new — 11 in tests/test_imas_export.py, 2 in tests/test_api.py), uv run ruff check clean, uv run mypy fusionlab clean.
  • New tests cover: the golden snapshot of shot 30166; conversion-vs-oracle; equality of the attachment with the /replay bundle at IMAS paths; degraded bundles (minimal and LCFS-only) exporting core traces only; sidecar contents; the comment-pointer rule; GET-only + clean 404s; no non-finite float in the attachment.
  • Backend-only change — no rendered UI surface is touched (the download affordance is an attachment header, same style as the USD/export routes), so visual dogfood evidence is not applicable.

🔗 Obvious Project · 🧵 Obvious Thread · 🔀 Obvious PR Artifact

ObviousApp and others added 3 commits October 12, 2026 16:53
Export-only adapter over ShotBundle v1: a static in-repo mapping from the
bundle's arrays to IMAS data-dictionary paths, served by a new sibling of
the USD route, GET /replay/{shot_id}/imas. The payload is the replay
bundle's own values path-for-path - units converted once at this edge
(MA->A, 1e20 m^-3 -> m^-3, keV->eV, MW->W, MJ->J) - never a recomputation.

What the dictionary has no honest home for rides a sibling
fusionlab_annotations.json: the catalog's useful/abort flags, the
per-series provenance labels (measured/model/learned/computed), the
machine-specific eq_inputs block, and the series IMAS cannot carry
(n_e20 as line density, the Thomson radii, R_m, P_rad_MW, dWdt_MW).
The IMAS payload points at the sidecar through ids_properties.comment
and carries no flag or label as data.

Groups the bundle lacks are omitted, never emitted as empty IDS stubs -
the same graceful degradation /replay applies. Dictionary conventions
were verified against the IMAS Data Dictionary (DD 4.2.x) before the
conversion table locked; the vacuum_toroidal_field r0 is omitted because
the bundle does not record the B_T reference radius. No new dependency.

Fixes #49

Co-authored-by: Tapan Ganatma Nakkina <[email protected]>
…for the IMAS adapter

- tests/test_imas_export.py: the mapping table partitions the SPEC exactly
  once (an IMAS home or a sidecar entry - a future field fails here until
  its home is decided); the conversion table is pinned against the
  api_replay._UNIT oracle the house already keeps; a committed golden
  snapshot of the shot 30166 export pins shape and values (a snapshot
  diff is the failure mode; regenerate with the one-liner in the module
  docstring); minimal and LCFS-only synthetic bundles prove degraded
  shots export core traces only - no empty IDS stubs; the sidecar test
  asserts flags, provenance, meta and unmapped series stay out of the
  IMAS payload, which mentions them only in the comment.
- tests/test_api.py: the /replay/{id}/imas attachment equals the replay
  bundle at IMAS paths (factor-1 traces exactly, Ip within rounding),
  carries the replay's own archive flags and provenance map, is GET-only
  with clean 404s, and never emits a non-finite float.

Full suite: 489 passed, 1 skipped (baseline 476 + 1); ruff and mypy clean.

Co-authored-by: Tapan Ganatma Nakkina <[email protected]>
DATA_CONTRACT.md's "future adapter" paragraph framed IMAS as producing
bundles (the import side); it now states the export direction - the
/replay/{id}/imas adapter emits the bundle's own values at IMAS paths
with units converted once at that edge - and the rule that IMAS is never
an internal format or an import spine. Route maps in .obvious pick up
the new endpoint and the imas_export module.

Co-authored-by: Tapan Ganatma Nakkina <[email protected]>
@obvious-autobuild
obvious-autobuild Bot marked this pull request as ready for review October 12, 2026 16:57
@obvious-autobuild
obvious-autobuild Bot merged commit 7d172fd into main Oct 12, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BACKLOG-6: OMAS/IMAS export adapter

2 participants