Skip to content

Make gpec.h5 a self-contained rerun snapshot#217

Merged
github-actions[bot] merged 24 commits into
developfrom
feature/rerun-from-h5
Jun 24, 2026
Merged

Make gpec.h5 a self-contained rerun snapshot#217
github-actions[bot] merged 24 commits into
developfrom
feature/rerun-from-h5

Conversation

@logan-nc

Copy link
Copy Markdown
Collaborator

Closes #171.

Summary

  • Store the full merged TOML, raw equilibrium arrays, and (when enabled) forcing-mode data inside gpec.h5 so a run can be replayed from any directory without needing the original g-file / CHEASE / auxiliary TOML (sol.toml / lar.toml) / forcing.dat.
  • main("run.h5") dispatches to a new main_from_h5 replay entry point. It rebuilds a DirectRunInput / InverseRunInput from the stored raw arrays, or reconstructs a SolovevConfig / LargeAspectRatioConfig from the merged TOML sections, and hands the result to setup_equilibrium via the existing additional_input channel — no sentinel path strings.
  • Default output is <source_basename>_rerun.h5 in pwd() so the source is never overwritten. --output-dir, --output-name, --override-file <toml>, and repeatable --override section.key=value CLI flags let users retarget the output or tweak TOML fields on the fly.
  • The field-by-field input/ForceFreeStates/* and input/EQUIL_CONTROL/* mirrors are replaced with a single input/gpec_toml_raw string (full merged TOML), plus input/raw_inputs/equilibrium/* and (when PE is enabled) input/raw_inputs/forcing_terms/* groups.
  • Scope note: this PR is the ''always recompute'' variant per the issue discussion — every rerun re-runs equilibrium + stability + PE from the stored inputs. Stage skipping (reusing stored ODE state, vacuum data, etc.) is deferred to a follow-up issue, as is snapshotting filepath-based wall polylines (all current examples use built-in wall shapes).
  • Removed group readers: a repo-wide grep confirms nothing in the regression harness, test suite, analysis scripts, or benchmarks/ directory reads input/ForceFreeStates or input/EQUIL_CONTROL. Worth flagging for any downstream notebooks that might.

Test plan

  • Full unit test suite: 681/681 tests pass, including 16 new rerun tests in test/runtests_rerun_from_h5.jl (self-contained replay matches source bit-for-bit; --override ForceFreeStates.eulerlagrange_tolerance=1e-6 propagates; rerun refuses to overwrite its own source)
  • Regression harness vs develop on solovev_n1: 20/20 unchanged, zero diffs
  • Regression harness vs develop on diiid_n1: 26/26 unchanged, zero diffs
  • Manual smoke test — Solovev analytic: source and replay produce bit-for-bit identical et[1], ep[1], ev[1], mpert, msing, singular ψ/q
  • Manual smoke test — DIIID-like direct-EFIT with forcing.dat: source and replay produce bit-for-bit identical et[1], delta_prime, island_half_width, ||resonant flux|| from a temp dir that contains only gpec.h5

🤖 Generated with Claude Code

)

Store the full merged TOML, raw equilibrium arrays, and (when enabled)
forcing-mode data inside gpec.h5 so a run can be replayed from any
directory without the original g-file / CHEASE / auxiliary TOML / forcing
inputs. `main("run.h5")` dispatches to `main_from_h5`, which rebuilds
the `DirectRunInput` / `InverseRunInput` / `SolovevConfig` /
`LargeAspectRatioConfig` directly from the snapshot and hands it to
`setup_equilibrium` via the existing `additional_input` channel. The
rerun output is written as `<basename>_rerun.h5` to guarantee the
source is never overwritten; `--output-dir`, `--output-name`,
`--override-file`, and repeatable `--override key=value` CLI flags let
users retarget or tweak TOML fields without editing the snapshot.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@logan-nc logan-nc added the enhancement New feature or request label Apr 10, 2026
@jhalpern30

Copy link
Copy Markdown
Collaborator

@claude review, keep it short

@claude

claude Bot commented Apr 10, 2026

Copy link
Copy Markdown

Code Review

The overall design is sound: snapshotting raw arrays + merged TOML into gpec.h5 and dispatching via main_from_h5 is a clean way to achieve self-contained reruns. The refactor of mainmain + main_with_inputs is well-executed and the backward-compatibility outer constructors handle pre-existing call sites cleanly.

Issues

1. Test coverage gap: EFIT/CHEASE rerun path has no automated test

The new test suite only exercises the Solovev (analytic/sol) rerun path. The build_direct_from_raw and build_inverse_from_raw round-trips for EFIT and CHEASE equilibria are only manually smoke-tested. These paths include non-trivial array reconstruction logic (spline rebuilding from raw node arrays), and a regression would not be caught by CI. At minimum, add an automated test using the existing regression_diiid_like_ideal_example fixture (or a minimal EFIT case) to cover the kind = "direct" path.

2. Silent string fallback in parse_override_flag can mask type errors

# src/Rerun.jl
parsed_val = try
    parsed = TOML.parse("_rerun_override_ = $rhs")
    parsed["_rerun_override_"]
catch
    # Fall back to string if TOML parse fails (e.g., unquoted bare words).
    rhs
end

A bare catch here silently converts any unparseable RHS to a string. For example, --override ForceFreeStates.npert=blorp stores "blorp" as a String, which passes through apply_toml_overrides! and only errors later (or silently does nothing) when ForceFreeStatesControl tries to use it as an integer. The user sees no warning at the point of the mistake. Replace with catch e and emit a @warn or re-throw with a message indicating that the value was interpreted as a string.

3. Deprecated equil.toml path produces silently non-rerunnable snapshots

When gpec.toml has no [Equilibrium] section and the run falls back to equil.toml, inputs["Equilibrium"] is never populated in the main function's inputs dict. The stored gpec_toml_raw blob therefore has no [Equilibrium] section, and main_from_h5 will crash with a KeyError on inputs["Equilibrium"]. The run completes successfully and writes an input/gpec_toml_raw, so the user has no indication that the snapshot is not rerunnable until they actually try.

Options: either add a @warn "equil.toml runs produce non-rerunnable snapshots; migrate to gpec.toml [Equilibrium]" at snapshot time, or merge the parsed equil.toml content into inputs["Equilibrium"] before calling main_with_inputs.

4. No test for --override-file

The flag is documented and implemented, but there is no test exercising it. Since it follows the same apply_toml_overrides! path as --override, a single test case verifying that a TOML file override propagates (e.g., changes mpert or tolerance) would provide meaningful coverage without much additional setup.

Minor

5. File-level "docstring" in Rerun.jl

"""
    Rerun support: helpers for saving run inputs into gpec.h5 and replaying them.
    ...
"""

A bare string literal at the top of an included file is not a Julia docstring — it is evaluated and discarded. Use # comments for file-level documentation, consistent with the rest of the codebase.

@jhalpern30

Copy link
Copy Markdown
Collaborator

This looks very cool! Going to hold off on reviewing it until #202 is merged in, since there are some conflicts between the two and I've already started reviewing that and working with Calin to get it merged in. Once that's done, I think there's an interesting way to work it into this branch and make the inputs logic concise. Perhaps something like a:

function main(combination of an h5, dd, toml, etc.)
  if toml
    contruct inputs/equil from toml
    main_from_inputs()
  else if h5
    construct inputs/equil from h5 (this branch)
    main_from_inputs()
  else if dd
    construct inputs/equil from dd (@calin1989's branch)
    main_from_inputs()
and so on

@jhalpern30

Copy link
Copy Markdown
Collaborator

@logan-nc since Calin's code is merged in now, I can take a look at this whenever the merge conflicts are resolved

@logan-nc

logan-nc commented Jun 5, 2026

Copy link
Copy Markdown
Collaborator Author

@jhalpern30 I am out on vacation today and the claude max expires tomorrow. I don't think the merge conflict is serious... can you resolve it or have claude do so for you? don't wait on me to review this

@logan-nc

logan-nc commented Jun 5, 2026

Copy link
Copy Markdown
Collaborator Author

note, this capability definitely needs a good regression test so it doesn't get messed up whenever someone adds a new toml var or other io change (example: @d-burg will add profiles needed for slayer soon)

@jhalpern30

Copy link
Copy Markdown
Collaborator

@jhalpern30 I am out on vacation today and the claude max expires tomorrow. I don't think the merge conflict is serious... can you resolve it or have claude do so for you? don't wait on me to review this

I am not currently logged into your claude but I can take a look

@logan-nc

logan-nc commented Jun 5, 2026

Copy link
Copy Markdown
Collaborator Author

ty. I expect the merge conflict is trivial

logan-nc and others added 5 commits June 10, 2026 22:58
#229)

Adds modern coil-geometry inputs alongside the legacy ASCII .dat format:

- Analytic generators make_pf_hoop, make_window_pane (axis-aligned, two [R,Z]
  corners), and make_helical (circular-torus), each producing a CoilSet that
  flows through the existing currents + apply_transforms (shift/tilt) path.
- HDF5 coil I/O: group-based, multi-set, content-tolerant
  (save_coils_to_h5 / load_coils_from_h5_group! / read_coil_h5 / write_coil_h5)
  plus dat<->h5 converters. One subgroup per set; unrelated content ignored.
- CoilSetConfig gains a `source` discriminator (file/pf_hoop/window_pane/helical)
  and analytic params; load_coil_sets dispatches via _build_raw_coil_set with
  .h5/.dat resolved by extension. Legacy .dat behavior unchanged.
- Rerun snapshot: a coil run stores the geometry used under
  input/raw_inputs/coils; main_from_h5 gains --coil-source {forcing-modes|coils}
  so a run can be replayed from gpec.h5 with no original .dat (recomputing the
  field from stored geometry, optionally against a new equilibrium).
- Fixes a pre-existing stale-key bug in runtests_rerun_from_h5.jl (vacuum/et ->
  FreeBoundaryStability/eigenmode_energies).
- Tests for generators, h5 round-trip/multi-set/junk-tolerance, dispatch, and an
  end-to-end coil snapshot+replay that deletes the .dat between runs.
- Docs: forcing_terms.md coil-source + HDF5 schema sections; coil_geometries
  README HDF5 schema and corrected toroidal-rotation note.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- CoilSetConfig docstring: `[rad] (...)` parsed as a markdown link; reword to
  plain prose so makedocs cross-reference check passes.
- forcing_terms.md / coil_geometries README: label the HDF5 schema fence as
  `text` and drop bracket/paren unit annotations.

Verified: docs build clean (no missing_docs / invalid-link); regression harness
diiid_n1 base vs HEAD shows 30/30 quantities unchanged (.dat coil path identical).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Unify main() so each input source builds a ready (inputs, eq_config,
additional_input) and hands it to the renamed main_from_inputs:
build_inputs_from_toml and build_inputs_from_h5. This also fixes a latent
IMAS bug where main(; dd) never threaded dd into the pipeline body.

- equil.toml snapshots are now rerunnable: serialize the built eq_config into
  inputs[Equilibrium] so gpec_toml_raw is complete (equilibrium_config_to_dict).
- parse_override_flag warns instead of silently storing an unparseable value
  as a String.
- Rerun.jl file-level docstring -> comments.
- Slim rerun tests to one source + one replay: analytic Solovev replay now
  diffs all output datasets; direct (EFIT) recovery is checked through the real
  h5 reader without a pipeline run; override flags and parse logic are covered
  pipeline-free. Drop the heavy regression-harness rerun cases.
- Add an IMAS dd-dispatch regression test.
- CLAUDE.md: forbid PR/issue references in source comments.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…control_3d analog)

Adds two reusable Analysis.CoilForcing entry points that replicate GPEC's plot_control_3d:

- plot_surface_3d(equil; color_by, coil_sets, ...) — the 3D plasma boundary surface,
  optionally coloured by ANY control-surface output and/or with coils overlaid. One function
  covers surface-only / +field / +coils / +field+coils; coils-only stays plot_coil_geometry_3d.
- control_surface_scalar(equil, modes, n; ...) — inverse-Fourier-transforms spectral modes
  (b_n, b_n_x, xi_n, …) to a real-space [θ,φ] field, reusing FourierTransforms.inverse and the
  ν/helicity correction from PerturbedEquilibriumModes (no field recomputation, no Biot-Savart).

Conventions:
- Shared :RdBu symmetric scale: a blue (+current) coil reads the same colour as the blue
  (+) field it drives. Sign/handedness chosen and VERIFIED against coil currents so a positive
  coil produces positive b_n_x directly beneath it (the legacy Python mis-tracks toroidally).
- No new Project.toml deps. Coloured 3D surfaces need a PlotlyJS-style backend (already a dep);
  on GR the tool warns and degrades to an uncoloured surface rather than mis-colouring by Z.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@logan-nc

logan-nc commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator Author

@jhalpern30 — merge conflicts with develop are resolved and this is ready for your review.

Unified dispatch (your suggestion). main() now routes every input source through a builder that returns a ready (inputs, eq_config, additional_input), then calls one shared main_from_inputs:

  • build_inputs_from_toml(path; dd) — gpec.toml + IMAS dd
  • build_inputs_from_h5(args) — gpec.h5 snapshot

This also fixed a latent IMAS bug from the #202 merge where main(; dd) accepted dd but never threaded it into the pipeline body (now covered by a regression test).

Bot review issues — all addressed: equil.toml is more fully/clearly deprecated; parse_override_flag warns instead of silently storing an unparseable value as a string; the file-level docstring is now # comments; direct/EFIT recovery and --override-file are now tested.

Rerun drift coverage — kept lightweight. Instead of heavy regression-harness cases (two full pipeline runs each), the guard lives in the unit suite: one source + one replay with an all-datasets source-vs-replay diff (fires if any new TOML var or IO field isn't snapshotted — e.g. future slayer profiles), plus a pipeline-free direct-EFIT recovery check through the real h5 reader. Net rerun unit-test time ~1 min.

Verified: full unit suite green; rerun tests pass; solovev_n1 regression unchanged vs develop (21/21, 0 diff).

logan-nc and others added 4 commits June 11, 2026 17:11
…spatch

equil.toml has been deprecated for a long time and no example or test ships
one; gpec.toml [Equilibrium] is the single supported source. Drop the dead
fallback branch in build_inputs_from_toml (and the equilibrium_config_to_dict
band-aid that was only there to make that path's snapshot rerunnable). The
deeper deprecated EquilibriumConfig(path)/setup_equilibrium(path) overloads in
the Equilibrium module are left for a separate cleanup.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…so the field tracks its coil

control_surface_scalar now extends the SFL Fourier modes to the machine
angle via c(θ,φ)=-Re[f(θ)e^{i n ν(θ)}e^{i n·hel·φ}], the exact inverse of
fourier_decompose_bn under φ=-hel(2π ζ + ν). The +ν correction un-shears the
field into a vertical (square-ish) window-pane band — the prior ζ-based
e^{i n φ} left a residual toroidal shear (the 'helical tendrils'). The e^{i n·hel·φ}
makes the toroidal tracking robust for either helicity, and the leading − is a
documented display orientation giving blue-coil↔blue-surface (verified: +coil →
+b_n_x lobe beneath it on DIII-D).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…upplied m range

The reconstruction uses only the supplied poloidal modes, so a coil placed within ~one grid
cell of the boundary makes b_n near-singular and the truncated series rings (Gibbs — a spurious
dip where a single lobe is expected). Documented so users keep a realistic coil standoff or widen
the m range. Verified against the direct n=1 harmonic: at a sensible standoff the window-pane b_n_x
is a single clean blob peaked on the midplane; only on the separatrix did it split.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…er-matched winding

Add make_window_pane_standoff to place a window-pane array by physical
standoff distance and poloidal angle off the plasma surface, deriving the
frame from surface_point_and_normal instead of explicit R-Z corners.
load_coil_sets now receives the equilibrium so the standoff mode can be
built from the main forcing pipeline.

The standoff tangent is the +90 deg rotation of the outward normal so the
poloidal leg runs lower-Z to higher-Z at the outboard midplane, matching
make_window_pane's c1->c2 winding. This standardizes the two builders to a
single convention: positive current produces positive b_n_x near the coil.
Adds a winding-convention test and documents the standoff mode in the TOML
example.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@jhalpern30 jhalpern30 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I apologize for the long PR review here, but this touches a lot of the code and I wanted to avoid the extra cruft Claude seems persistent to add. Most of the changes are cosmetic, specifically overly verbose comments, with the exception of a few that are more widereaching such as backwards compatibility and how to carry around the raw_data.

I didn't look too in depth at the forcing terms section since I'm not as familiar with it.

Comment thread src/Equilibrium/EquilibriumTypes.jl Outdated
zmax::Float64 # Maximum Z-coordinate of the computational grid [m].
psio::Float64 # The total flux difference |ψ_axis - ψ_boundary| [Weber / radian].
bt_sign::Int # Sign of the toroidal field: +1 or -1 (from fpol sign in g-file)
raw_data::Dict{String,Any} # Raw arrays used to build the splines (for gpec.h5 snapshot/replay)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add this to docstring. Probably no need to duplicate docstring in the comments for this struct

Comment thread src/Equilibrium/EquilibriumTypes.jl Outdated

# Outer constructor: call sites that don't capture raw arrays pass 11 positional args
# (through bt_sign); raw_data defaults to empty (this equilibrium is then not rerun-snapshottable).
DirectRunInput(config, sq_in, psi_in, psi_in_xs, psi_in_ys, rmin, rmax, zmin, zmax, psio, bt_sign) =

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not entirely sure what this is doing / why it needs to be here. I guess this is taking the place of using a @kwdef wrapper on the struct and having raw_data default to none if not specified?

Comment thread src/Equilibrium/EquilibriumTypes.jl Outdated
ro::Float64 # R axis location
zo::Float64 # Z axis location
psio::Float64 # Total flux difference |psi_axis - psi_boundary|
raw_data::Dict{String,Any} # Raw arrays used to build the splines (for gpec.h5 snapshot/replay)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Description needs to be in the docstring again

Comment thread src/Equilibrium/EquilibriumTypes.jl Outdated

# Raw ingest data forwarded from DirectRunInput/InverseRunInput for gpec.h5 snapshot/replay.
# Keys depend on equilibrium kind: "direct", "inverse", or "analytic".
raw_inputs::Dict{String,Any}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docstring. Maybe update your claude.md to note this

Comment thread src/GeneralizedPerturbedEquilibrium.jl Outdated
# Rerun snapshot/replay helpers. Depends on the module-level imports above
# (HDF5, TOML, Equilibrium, ForcingTerms) and the `_BANNER` constant, so this
# include has to come after them but before `main`, which dispatches into
# `build_inputs_from_h5` on .h5 inputs.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like this issue could be easily avoided by just moving the external using statements to the top of the file above the include statements, and then including include("Rerun.jl") with the rest of the the includes. And then this 4 line comment is unnecessary

Comment thread src/Rerun.jl Outdated
return Equilibrium.build_direct_from_raw(eq_config, raw)
elseif kind == "inverse"
return Equilibrium.build_inverse_from_raw(eq_config, raw)
elseif kind == "analytic"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You already explicitly check for analytic before entering this function in build_inputs_from_h5

Comment thread src/Rerun.jl Outdated
# path from the source machine can't trip up later code paths.
eq_config.eq_filename = ""

additional_input = if raw_eq["kind"] == "analytic"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh my logic... I had to read this 3 times to get it, I've never seen logic coded like this before. If you think this is best that's fine, but I think this whole section would be much more readable as

if raw_eq["kind"] == "analytic"
  if eq_config.eq_type == "lar"
        lar_data = get(inputs, "LAR_INPUT", Dict{String,Any}())
        additional_input = Equilibrium.LargeAspectRatioConfig(; (Symbol(k) => v for (k, v) in lar_data)...)
    else
        sol_data = get(inputs, "SOL_INPUT", Dict{String,Any}())
        additional_input = Equilibrium.SolovevConfig(; (Symbol(k) => v for (k, v) in sol_data)...)
    end
 end
elseif raw_eq["kind"] == "direct"
     additional_input = Equilibrium.build_direct_from_raw(eq_config, raw)
elseif raw_eq["kind"] == "inverse"
    additional_input = Equilibrium.build_inverse_from_raw(eq_config, raw)
else
   error("Unknown raw equilibrium kind in gpec.h5: $kind")

I think the repeated additional_input is worth the clarity here. And removing the rebuild_equilibrium_input function

Comment thread src/GeneralizedPerturbedEquilibrium.jl Outdated
# LAR/Solovev carry their parameters in a separate auxiliary TOML referenced
# by eq_filename. Merge those parameters into the in-memory `inputs` dict so
# the snapshot writer emits a self-contained TOML blob.
merge_auxiliary_eq_toml!(inputs, eq_config)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A long term goal might be to just merge this into gpec.toml as a section that only appears if you specify that type of EQ? Seems like this extra toml carrying around is unnecessary

Comment thread src/Rerun.jl

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A general comment on this file - I think its existence is necessary due to the large amount of CLI/override capabilities added, but I think pretty much all of the equilibrium functions can be deleted and in-lined in their respective locations. This will cut the file down a bit

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A suggestion here that you can take or ignore based on how difficult you'd think it'd be to implement - so this PR adds a raw_data struct member to the Input and PlasmaEquilibrium struct, but all this raw_data really contains is the struct data itself in dictionary form? Why don't we just return the input struct itself from the equilibrium setup (or even just make it a part of PlasmaEquilibrium, this would actually be quite easy probably) and then dump that data to the HDF5. I guess you'd still need to load and dump things from an HDF5 so its not perfectly clean, but removes most of the additions to the Equilibrium code

logan-nc and others added 4 commits June 20, 2026 20:22
…oor, guard standoff coils against boundary

Per NilsLeuthold's review of #278:
- make_pf_hoop: raise nsec floor from >=4 to >=361 (default) so a full
  poloidal loop has at least ~1 deg resolution instead of a near-square.
- make_window_pane_standoff: error when a tilted coil would intersect the
  plasma boundary (clearance = standoff - half*|sin(tilt)| <= 0), the case
  Nils flagged (90 deg tilt with standoff shorter than half the leg length).
- Tests: sub-floor nsec throw; standoff intersection throw + valid case.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ure/coil-h5-and-analytic-coils

# Conflicts:
#	src/GeneralizedPerturbedEquilibrium.jl
…ure/coil-h5-and-analytic-coils

# Conflicts:
#	src/Rerun.jl
#	test/runtests_rerun_from_h5.jl
…alytic-coils

ForcingTerms: HDF5 + analytic coil interfaces, with gpec.h5 coil snapshot (issue #229)
@logan-nc

Copy link
Copy Markdown
Collaborator Author

Thanks for the thorough review — worked through all of it. Summary of how each thread was resolved:

Architecture

  • raw_data dict → typed DirectIngest/InverseIngest (ReadEquilibrium.jl:1, EquilibriumTypes.jl:404). Your instinct to "just return/dump the struct" was right; the blocker is that the input structs hold built spline interpolants, which aren't HDF5-serializable, and the raw node arrays weren't stored anywhere else. So the ingest is now a typed struct holding exactly the serializable node arrays, carried as a field on DirectRunInput/InverseRunInput/PlasmaEquilibrium and dumped by iterating fieldnames. A bad key is now a field error, not a silent dict miss. This also let the field-by-field write inline into write_outputs_to_HDF5 (GeneralizedPE:614) and removed write/read_equilibrium_raw_inputs.
  • No backwards-compat / all runs rerunnable (Rerun.jl:56, 78). IMAS and TJ-analytic now capture/regenerate too, so I dropped the isempty guards and the empty-default constructors. The only remaining hard error is for a genuinely pre-snapshot gpec.h5 (no input/gpec_toml_raw), which is a real format check.
  • Inlined/clarified the replay dispatch (Rerun.jl:243, 339, 1). Removed rebuild_equilibrium_input and its analytic => nothing arm. The analytic-vs-direct-vs-inverse branch is now one readable block, and analytic config construction is a shared build_analytic_config used by both the TOML and rerun builders (no more lar/else-Solovev misrouting of other analytic kinds).
  • Side-car analytic TOML → embedded section (GeneralizedPE:123). [SOL_INPUT]/[LAR_INPUT] now live in gpec.toml like [TJ_ANALYTIC_INPUT]; the sol.toml files are deleted and merge_auxiliary_eq_toml! is just a deprecation fallback.

Comments / structure

  • Removed the diff-narration / "dialogue-with-existing-comments" comments (GeneralizedPE:162, 188, 192, 607, the main_from_inputs block) and the stranded TJ-analytic comment.
  • Moved struct-field descriptions into the docstrings, documented the ingest contract, dropped the rot-prone positional-arg counts (EquilibriumTypes.jl:399/404/434/831).
  • Moved external using to the top and include("Rerun.jl") in with the other includes, deleting the 4-line justification (GeneralizedPE:64).

Banner (GeneralizedPE:86) — good call, and it's already what the rerun path does: it prints its own banner rather than the normal one, showing source git: <info/git_version from the HDF5> → current: <git describe of the running checkout> plus source/output paths. So you get exactly the "version the inputs were created on vs. version producing the new outputs" comparison you described. The normal banner still prints on normal runs.

Verification

  • Full test suite green, including a new CHEASE inverse-ingest recovery test alongside the existing direct + analytic round-trips.
  • Regression harness bit-for-bit identical vs develop: solovev_n1 21 unchanged, diiid_n1 30 unchanged (every diff 0.0e+00) — confirms the refactor + TOML migration are numerically inert.

logan-nc and others added 2 commits June 21, 2026 21:04
… analytic TOML

Address PR #217 review:

- Replace the raw_data/raw_inputs Dict{String,Any} on DirectRunInput/
  InverseRunInput/PlasmaEquilibrium with typed DirectIngest/InverseIngest
  structs (the serializable node arrays needed to rebuild the splines). The
  HDF5 writer dumps the struct fields; read_equilibrium_ingest reconstructs
  them generically. Drops the empty-default outer constructors.
- Make every equilibrium kind rerunnable: IMAS and TJ-analytic now capture or
  regenerate correctly. Replace the lar/else-Solovev replay branch (which
  misrouted other analytic kinds to Solovev) with ANALYTIC_EQ_SECTIONS plus a
  shared build_analytic_config used by both the TOML and rerun builders.
  Remove rebuild_equilibrium_input and the backwards-compat guards.
- Embed [SOL_INPUT] directly in gpec.toml (like [TJ_ANALYTIC_INPUT]) and retire
  the side-car sol.toml files; merge_auxiliary_eq_toml! becomes a deprecation
  fallback. Migrate all Solovev examples and test fixtures; fix the internal
  test call sites that bypass main and relied on the side-car.
- Remove diff-narration comments, move struct-field descriptions into
  docstrings, reorder module using/include, slim Rerun.jl.
- Add a CHEASE inverse-ingest recovery test; document struct-field convention
  in CLAUDE.md.

Full test suite green; regression harness bit-for-bit identical vs develop on
solovev_n1 (21 unchanged) and diiid_n1 (30 unchanged).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… registry

Address PR #217 review (maintainability):

- Replace the three parallel analytic-dispatch sites (ANALYTIC_EQ_SECTIONS dict,
  build_analytic_config if/elseif, setup_equilibrium elseif chain) with a single
  Equilibrium.ANALYTIC_EQ registry of AnalyticEqSpec(section, config_type, run_fn).
  Both fresh-run and rerun paths now dispatch off it, so adding a new analytic kind
  is one new row. Drops the unsafe else-defaults-to-TJAnalyticConfig branch.
- Extract EFIT_KINDS for the twice-duplicated efit-family list.
- Document the ingest positional round-trip contract; sharpen two rerun errors.
- Add a guard test asserting every registry entry is well-formed (Dict/String ctors,
  run_fn method, non-empty section) and that build_analytic_config resolves it, so
  future drift fails at test time rather than at a user's rerun.

Type-stable where it matters: setup_equilibrium still infers a concrete
PlasmaEquilibrium and the numerical kernels (sol_run/lar_run/...) stay concretely
typed; dynamic dispatch is confined to the once-per-run config boundary behind a
function barrier. Regression bit-for-bit identical vs pre-refactor HEAD on
solovev_n1 (21) and diiid_n1 (46).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@logan-nc

Copy link
Copy Markdown
Collaborator Author

Follow-up (0a7af46): consolidated the analytic-equilibrium dispatch into a single registry.

Review flagged that analytic kinds were dispatched across three parallel sites that had to stay in lock-step — the ANALYTIC_EQ_SECTIONS dict, the build_analytic_config if/elseif (whose else silently defaulted to TJAnalyticConfig), and the setup_equilibrium elseif chain. With more feature branches adding [SECTION] blocks soon, that was the main extensibility hazard.

Changes:

  • New Equilibrium.ANALYTIC_EQ registry of AnalyticEqSpec(section, config_type, run_fn) as the single source of truth. Both the fresh-run and rerun paths dispatch off it, so adding a new analytic kind is now one row. The per-entry run_fn cleanly handles the tj_analytic / tj_analytic_direct split (shared config, different solver). Removed the unsafe else-defaults-to-TJ branch.
  • Extracted EFIT_KINDS for the twice-duplicated efit-family list; documented the ingest positional round-trip contract; sharpened two rerun error messages to point at the registry.
  • Added a guard test asserting every registry entry is well-formed (Dict/String ctors, run_fn method, non-empty section) and that build_analytic_config resolves it — so future drift fails at test time, not at a user's rerun.

Note: splines are not serialized — only the DirectIngest/InverseIngest node arrays are stored and the splines are rebuilt via cubic_interp on replay (the splines/ HDF5 group is diagnostic output only).

Type stability: setup_equilibrium still infers a concrete PlasmaEquilibrium and the numerical kernels (sol_run/lar_run/…) stay concretely typed; dynamic dispatch is confined to the once-per-run config boundary behind a function barrier.

Verification:

  • Rerun suite green (analytic Solovev e2e 8/8, coil snapshot 8/8, direct 20/20, inverse 20/20, override 5/5, new registry guard 21/21); Equilibrium 261/261; TJ-analytic 16/16.
  • Regression-guardian: bit-for-bit identical vs pre-refactor HEAD across all tracked quantities — solovev_n1 (21 unchanged) and diiid_n1 (46 unchanged).

@jhalpern30 jhalpern30 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I cleaned up some minor things myself, but I think two things still need attention regarding backwards compatibility (sorry for the second round).

  1. Did you mean to add a section for deprecated TOML parameters? This seems unnecessary at this time
  2. Did you mean to still support an auxiliary equilibrium file? This seems like another one we can just get rid of

# tangent plane supports the (convex) boundary, so the nearest leg corner's clearance from the
# boundary along the normal is standoff - half*|sin(tilt)| — the tilt tips the legs toward the
# surface, while tangential displacement leaves the normal clearance unchanged.
clearance = standoff - half * abs(sin(deg2rad(poloidal_tilt)))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems out of place in this PR? Just want to make sure you meant to put this here

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The custom coils in general? Yes, it was a sub-branch that came back into here. Why? The main objective was to be able to re-run coils from an h5 without having to carry around the matching .dat files, which is a necessary condition for this PR to be counted as complete. The custom coils made from toml params was a little missing creep but was a straightforward port of existing OMFIT python code and the only new part was the toml IO, which was tied up in this PR's architecture. So there it is.

If I had more bandwidth, I think you are right that the custom coils should be a separate PR branching off this but landing to develop after this merges. I wish. Next time 👍

Comment thread src/ForcingTerms/ForcingTerms.jl Outdated
include("Rerun.jl")
# Drop deprecated [ForceFreeStates] keys (e.g. banded-matrix removal) so legacy gpec.toml files
# keep parsing instead of throwing an unknown-keyword error.
function _drop_deprecated_ffs_keys!(table)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this something we actually want in here? I could see this list getting very long... I thought our approach was just to ignore backwards compatibility until after a first julia release

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I vote we include it hear as the architecture is needed for v2.0.0 and having something there is needed to test whether it works. In the final cleanup of v2.0.0 we can clobber the list to stat fresh if it feels unwieldy. I could be convinced otherwise though... thoughts @jhalpern30 and @matt-pharr?

Comment thread src/GeneralizedPerturbedEquilibrium.jl Outdated
@logan-nc

Copy link
Copy Markdown
Collaborator Author

@jhalpern30 to address the top level comments:

  1. Yes, I intended the section of deprecated toml parameters because it seemed like something we need for @matt-pharr's v2.0.0 milestone. Major versions are supposed to maintain backwards compatibility (https://semver.org/) so future 2.1.0 2.2.0 etc. should technically be able to re-run an old case run with 2.0.0 but @matt-pharr assures me that it is acceptable to count a deprecation message and ignored inputs as "compatible".
  2. I guess I was just cruising on the logic of (1) but... yes, we are still in the un-versioned wild west and are free to not maintain backwards compatibility now if we don't want to. I suppose I was trying not to break things for @d-burg too much but if he is ok with a minor effort to update his TJ workflows to this then yeah, let's make it a hard policy change.

…readers

Make amplitude_imag a required input across the ASCII, HDF5-file, and
HDF5-group forcing readers, mirroring amplitude_real, instead of silently
defaulting it to zero. Unify the ASCII column terminology on
amplitude_real/amplitude_imag to match the HDF5 dataset names across the
docstrings, the forcing.dat header, and the example TOML mode-table comments.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jhalpern30

Copy link
Copy Markdown
Collaborator

@jhalpern30 to address the top level comments:

  1. Yes, I intended the section of deprecated toml parameters because it seemed like something we need for @matt-pharr's v2.0.0 milestone. Major versions are supposed to maintain backwards compatibility (https://semver.org/) so future 2.1.0 2.2.0 etc. should technically be able to re-run an old case run with 2.0.0 but @matt-pharr assures me that it is acceptable to count a deprecation message and ignored inputs as "compatible".
  2. I guess I was just cruising on the logic of (1) but... yes, we are still in the un-versioned wild west and are free to not maintain backwards compatibility now if we don't want to. I suppose I was trying not to break things for @d-burg too much but if he is ok with a minor effort to update his TJ workflows to this then yeah, let's make it a hard policy change.

I see. Isn't the point of that article that major versions can break backwards compatibility though, and minor/patch versions must maintain it?

I agree backwards compatibility is necessary, but thought is needed to what it means for our case. i.e. in this PR, backwards compatibility is introduced for the banded matrix parameters that I removed in #286 - I would classify this more as "legacy code that was converted last year but no one is/ever really used", so its not like we're preserving someone's workflow, we're just preserving their ability to run with their existing input file without removing a line they probably weren't looking at anyway. And anyone currently working with the Julia code has pretty much signed up for this level of variation in the day to day code, so I don't think we should support this. I would vote to remove it from this PR and make it a new issue/PR for how to intelligently deprecate things such that we can create something that is sustainable in the long term that can deal with more complicated backwards compatible fixes outside of just "ignore this input".

@logan-nc

logan-nc commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator Author

I see. Isn't the point of that article that major versions can break backwards compatibility though, and minor/patch versions must maintain it?

Exactly. 2.0.0 will break compatibility with fortran v1.5 but will need to maintain compatibility with itself moving forward (i..e 2.0.1, 2.1.0 2.2.0, etc.). Thus, it will very quickly need this deprecation guard. I want it in place so no one goes rogue and deprecates something with no guard. It being there makes it easy to note "you didn't guard your deprecation" in the PR and easy for them to do so instead of replying "what do you mean? how should I?".

If you want to move it to another PR and/or champion some other guard technique, I support you doing that. If you don't want to see mband ever used as an example, then also ok. It's weird to add the capability without at least one example though - how about the mer_flag that was changed to local_stability_flag in #234?

Analytic equilibria (sol/lar/tj_analytic) now resolve their parameters only
from the embedded [SOL_INPUT]/[LAR_INPUT]/[TJ_ANALYTIC_INPUT] section of
gpec.toml. Every in-repo deck already uses the embedded form and the side-car
files were removed, so the deprecation bridge is dead weight pre-release.

Remove merge_auxiliary_eq_toml! and its call, the eq_filename side-car
fallback in setup_equilibrium (analytic kinds now require their *Config as
additional_input, with a clear error otherwise), and the now-orphaned
*Config(::String) file constructors. Tighten the surviving *Config(::Dict)
docstrings. Drop the side-car hasmethod assertion from the registry test.

No numerical change: regression harness solovev_n1 (21/21) and diiid_n1
(46/46) identical local vs branch HEAD.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@logan-nc

Copy link
Copy Markdown
Collaborator Author

ok @jhalpern30 I removed the side-car TOML file support for analytic equilibria (your item 2). Item 1 is in your court.

@jhalpern30

Copy link
Copy Markdown
Collaborator

Exactly. 2.0.0 will break compatibility with fortran v1.5 but will need to maintain compatibility with itself moving forward (i..e 2.0.1, 2.1.0 2.2.0, etc.). Thus, it will very quickly need this deprecation guard. I want it in place so no one goes rogue and deprecates something with no guard. It being there makes it easy to note "you didn't guard your deprecation" in the PR and easy for them to do so instead of replying "what do you mean? how should I?".

If you want to move it to another PR and/or champion some other guard technique, I support you doing that. If you don't want to see mband ever used as an example, then also ok. It's weird to add the capability without at least one example though - how about the mer_flag that was changed to local_stability_flag in #234?

Got it. I support mer_flag because I think that's a good example of an input where the name was changed but the functionality is still there and someone might try to use it. I will 1) modify this PR to use that for deprecation instead of the banded inputs sometime today that way we have something in the repo already and 2) make an issue/think about the best way to do deprecation moving forward.

@jhalpern30

Copy link
Copy Markdown
Collaborator

Ok, deprecation is added. The merge conflicts are due to what looks like the half-area weighting, which I don't know much about so I am not going to touch that to avoid breaking it. Once they are resolved, I think this is ready to merge

@logan-nc logan-nc added the auto-merge Automatically merge PR when CI passes label Jun 24, 2026
@github-actions github-actions Bot enabled auto-merge June 24, 2026 01:02
logan-nc and others added 2 commits June 23, 2026 21:14
The rerun refactor dropped deprecated analytic side-car TOML support, so
analytic equilibria must carry their parameters in an embedded [SOL_INPUT]
section. The regression_solovev_kinetic_nuzero fixture still pointed at a
sol.toml side-car, so build_analytic_config errored ("requires a [SOL_INPUT]
section") and the full-run test failed. Migrate the Solovev parameters into
gpec.toml (matching the calculated sibling) and drop the dead side-car.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…_field

The merge renamed the singular-coupling output resonant_flux ->
resonant_area_weighted_field (now b^r = Φ^r/A^r in tesla). The coil-geometry
snapshot test in runtests_rerun_from_h5.jl still read the old key, so the
HDF5 read returned an empty array and the non-empty / max-amplitude
assertions failed. Point the helper at the new key and rename it accordingly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot merged commit cfb7693 into develop Jun 24, 2026
4 checks passed
@github-actions github-actions Bot deleted the feature/rerun-from-h5 branch June 24, 2026 02:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

auto-merge Automatically merge PR when CI passes enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add capability to rerun GPEC from an HDF5 file

2 participants