Uh oh!
There was an error while loading. Please reload this page.
GPEC - IMPROVEMENT - Make all input control structs immutable - #367
Conversation
…ForcingTerms control structs Continues the issue #139 struct cleanup that already froze ForceFreeStatesControl: a control struct built from the input TOML should be built once and then left alone, so a reader at a call site can tell nothing behind it changes. - PerturbedEquilibriumControl and KineticForcesControl had no writers in src/ at all. - ForcingTermsControl was filled in two steps because [[ForcingTerms.coil_set]] parses to a Vector{Dict} that cannot ride along in the scalar kwarg splat; coil_sets_raw is now passed as an explicit keyword after the splat instead of assigned afterwards. - SLAYERControl was already immutable; no change needed. - The one test that mutated a control (check_psi_quadrature_convergence) builds a second control with the nonzero atol_psi rather than rewriting the first. No intended numerical change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… the run inputs EquilibriumConfig is now an immutable struct: it holds the input request and is never written to after construction. The separatrix clamp previously wrote its result back into config.psihigh, which was load-bearing through aliasing (PlasmaEquilibrium.config is raw_profile.config). The resolved value now lives on the run-input structs as psihigh_resolved, defaulted to config.psihigh by an outer constructor so existing readers construct unchanged. resolve_psihigh! replaces the two duplicated clamp blocks in setup_equilibrium, the three solvers form their psi grid from psihigh_resolved, and the efit_by_inversion mid-solve InverseRunInput rebuild forwards it explicitly instead of relying on the config alias. EquilibriumParameters.psihigh_resolved carries it onto the finished equilibrium for sing_lim! and the driver. Two other config mutations had to go for the struct to freeze: the eq_filename path resolution now folds into the dict before construction, and the rerun path clears eq_filename on a dict copy so the re-serialized inputs keep the user's original. EquilibriumConfig(path::String) delegates to the dict constructor rather than duplicating it. setup_equilibrium's signature is unchanged, so no caller, harness or benchmark entry point moves. No intended numerical change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
a932227 to
a0520f4Compare…ntrol The flag only ever gated a warning saying the reform was not implemented, so it could not change any result. Drops the field, its docstring entry, and the dead branch in the driver. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
jhalpern30
commented
Aug 14, 2026
@logan-nc I think the main thing to look at here is the logic/naming for |
jhalpern30
commented
Aug 14, 2026
Regression harness: no change.
Every compared quantity is bit-for-bit identical (diff |
…put-control-structs
newq0 is a target on-axis safety factor, not an index: the solver forms f0fac = f0^2*((newq0/q0)^2 - 1) from it. Declaring it ::Int admitted only integers plus the -1 sentinel and threw InexactError on any realistic request such as 1.05. The direct solver also wrote the resolved sentinel back into the config (equil_params.newq0 = -q0, where equil_params is raw_profile.config), which the immutability change turns into a setfield! error. Resolve into a local instead, matching equilibrium_solver(::InverseRunInput). No result can move: a non-integer newq0 threw at construction, so no working run used one, and TOML's `newq0 = 0` converts to 0.0 unchanged. Also note on DirectRunInput that IMAS equilibria are not in EFIT_KINDS and so are never clamped, since the psihigh_resolved name implies otherwise. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dh1NCejnd3fYMmcRKoQRcG
The control was removed from ForceFreeStatesControl, but the struct is built by splatting the whole [ForceFreeStates] table as kwargs, so any existing gpec.toml still carrying the key now dies with an unknown-keyword MethodError -- as does replaying an older gpec.h5, whose stored TOML blob goes through the same path. _drop_deprecated_keys! runs inside main_from_inputs, so one tuple entry covers both. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dh1NCejnd3fYMmcRKoQRcG
EquilibriumParameters is dumped generically by field name, so adding psihigh_resolved adds a gpec.h5 dataset automatically. Without a metadata entry the schema walk in h5_metadata_check.jl fails it for missing long_name and units. No EQUIL_H5_NAMES entry is needed -- the writer falls back to the field name, which reads consistently next to Info/psilim -- and annotate! defaults units to "1". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dh1NCejnd3fYMmcRKoQRcG
…ozen copy The mpsi scan still did deepcopy(eq_config) + cfg.mpsi = N, which errors now that EquilibriumConfig is immutable; only reference_q() had been converted. Carry the equilibrium dict out of reference_q() and build a fresh config per N, as reference_q() already does. Not covered by CI. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dh1NCejnd3fYMmcRKoQRcG
…inverse readers newq0 != 0 had no coverage at all, which is why the broken override stayed green. The -1 sentinel makes f0fac vanish, so ffac is exactly -1 and the revised q-profile is the negated baseline -- asserted exactly. A non-integer target covers the type fix. Also assert psihigh_resolved reaches params on the CHEASE inverse path (where the separatrix clamp never runs), and that a deprecated ForceFreeStates key warns and is dropped rather than killing the run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dh1NCejnd3fYMmcRKoQRcG
logan-nc
commented
Aug 15, 2026
ReviewThe design is right, and I'd have pointed at the same precedent you did. Moving the clamped I've updated the branch with On the naming questionKeep What I pushed
Two notes, neither blocking
VerificationTests: Regression harness — no change.
Every compared quantity is bit-for-bit identical (diff VerdictThe code is in good shape — the four defects are fixed, the suite and harness are green on the merged state, and I have no outstanding objections. But this still needs a third-party human reviewer's approval before it merges: I authored the fixes on this branch, so my sign-off cannot stand in for an independent review of them. |
matt-pharr
commented
Aug 15, 2026
@jhalpern30@logan-nc I will suggest we rebase this once #393 is done, it will be easier since that is when I will have finished the integrator abstraction work. #394 details a follow-up pass I want to do where we move around a bunch of functions into a new file hierarchy for forcefreestates to make more sense now that the integrators are abstracted, and also de-slop all the FFS comments and docstrings and code. |
logan-nc
left a comment
There was a problem hiding this comment.
Looks good to me! Just a few minor cleanups ID'd in human read through
| "log_asymptotic" is a legacy alias for "auto") | ||
| - `psilow::Float64` - Lower limit of normalized flux coordinate | ||
| - `psihigh::Float64` - Upper limit of normalized flux coordinate | ||
| - `psihigh::Float64` - Requested upper limit of normalized flux coordinate. For efit-family |
| - `ro::Union{Nothing,Float64}` - R-coordinate of the magnetic axis [m] | ||
| - `zo::Union{Nothing,Float64}` - Z-coordinate of the magnetic axis [m] | ||
| - `psio::Union{Nothing,Float64}` - Total flux difference |ψ_axis - ψ_boundary| [Wb/rad] | ||
| - `psihigh_resolved::Union{Nothing,Float64}` - Outer flux limit the equilibrium was formed on, |
There was a problem hiding this comment.
Again overly verbose. Stop calling out that it is "clamped" to the separatrix - I think a psin<=1 clamp is obvious to any physicist and doesn't need to be reiterated all over the place
| @test plasma_eq_ascii isa GeneralizedPerturbedEquilibrium.Equilibrium.PlasmaEquilibrium | ||
| end | ||
| @testset "Resolved psihigh (inverse readers)" begin |
Uh oh!
There was an error while loading. Please reload this page.
The scan now runs whenever kinetic profiles are reachable, with only the enforcement gated by psihigh_from_layer_overlap, so the file always records what the layer physics wanted for the domain. Equilibrium/psihigh_resolved domain actually reconstructed Equilibrium/psihigh_resistive_layers where adjacent layers start to overlap Equilibrium/ResistiveLayers/ per-surface widths for every scored surface rational_psi, rational_m, rational_n, r_s, delta_s_abs [m], delta_s_psi [1], extrapolated Comparing the two scalars answers "did the domain honour the layer physics", which previously required reading the run log. Both width forms are kept deliberately: delta_s_abs is the physical thickness and delta_s_psi is the same width in normalized flux, which is what the overlap criterion compares against surface spacing. delta_s_abs matches the leaf name already used in Tearing/LayerWidths per the one-name-per-quantity rule. psihigh_resolved matches the name PR #367 (immutable input structs) introduces. It is written explicitly here only because that PR has not landed; once it does, EquilibriumParameters carries the field and the EQUIL_H5_NAMES loop writes it, so the explicit line must be deleted or the dataset is created twice. The write site says so. Also threads resistivity_model and lnLambda_form from SLAYERControl into the scan; it had hardcoded SauterNeoModel/:nrl, so a deck selecting another closure would get a different eta -- and hence a different delta_s -- from the SLAYER stage. h5 schema test passes (the metadata contract covers the new datasets). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSrf6JCViFfVzqzkQ66o6b
logan-nc
commented
Aug 18, 2026
Correction to my h5 naming call aboveI got the dataset name wrong in my previous comment, and the reasoning I gave for it was wrong too. Pushed a fix in 49d72d1. I said no rename and no
So the dataset is now Two notes on the verification, since I over-claimed there as well:
The struct field stays |
…d drop dead deck keys A develop merge into this branch silently reverted PR #367's immutability freeze on KineticForcesControl, because the multi-species loop mutated a deepcopy of the run control. That made the branch carry an undocumented revert of an upstream refactor and depend on the mutability upstream removed ("KineticForcesControl had no writers in src/ at all" — this loop was exactly such a writer, and the only one left). Build each species' control with the @kwdef keyword constructor instead, the idiom the freeze commit documents and that check_psi_quadrature_convergence already uses, and restore @kwdef struct. Threading species identity through explicit arguments was rejected: ctrl is passed whole into integrate_psi_quadgk and onward, so that would touch several nested signatures. Also drop six keys from the multi-ion deck that develop removed when it replaced the integrator-selection axis with integrator= (mat_flag, ode_flag, force_wv_symmetry, use_parallel, parallel_threads, populate_dense_xi). They only emitted deprecation warnings, but populate_dense_xi in particular implied a guarantee it no longer provides — had integrator= been dropped on that belief, the run would have fallen back to the riccati default, which does not populate dense xi, and PE/NTV would have gone quietly wrong. Document the PerSpecies layout in the h5 schema table. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Continues issue #139, following the already-merged
ForceFreeStatesControlfreeze. (#358 has merged; this now targetsdevelopdirectly.)Why
A control struct built from the input TOML should be built once and then left alone, so a reader at a call site knows nothing behind it changes.
What
PerturbedEquilibriumControl,KineticForcesControlandForcingTermsControlare nowstructrather thanmutable struct. The first two had no writers;ForcingTermsControlwas filled in two steps because[[ForcingTerms.coil_set]]parses to aVector{Dict}that cannot ride in the scalar kwarg splat —coil_sets_rawis now an explicit keyword after the splat.EquilibriumConfig: the one that needed real work
setup_equilibriumread the EFIT g-file, binary-searched for the outermost closed flux surface, and wrote the result back into the config:That write was load-bearing through aliasing —
PlasmaEquilibrium.configisraw_profile.config, which is the only reasonSing.jlsaw the clamped value — so it could not simply be deleted.Fix: the resolved value moves onto the run-input structs, which are already mutable and already carry read-time-resolved values (
bt_signis the same pattern).DirectRunInput/InverseRunInputgainpsihigh_resolved, defaulted toconfig.psihighby an outer constructor, so all existing construction sites are untouched.resolve_psihigh!replaces the two duplicated clamp blocks; the solvers form their ψ grid frompsihigh_resolved, and theefit_by_inversionmid-solveInverseRunInputrebuild now forwards it as an explicit argument instead of relying on the config alias.EquilibriumParameters.psihigh_resolvedcarries it onto the finished equilibrium forsing_lim!and the driver.config.psihighis now the user's request and is never written. Two other config writes had to go:eq_filenamepath resolution folds into the dict before construction, and the rerun path clearseq_filenameon a dict copy (the caller'sinputsis re-serialized intoinput/gpec_toml_raw).EquilibriumConfig(path::String)now delegates to the dict constructor.setup_equilibrium's signature is unchanged, so no caller, benchmark or harness entry point moves.Trade-off
config.psihighis no longer authoritative — code wanting the plasma edge must readparams.psihigh_resolved. The distinct field name makes a wrong read look wrong at the call site, and a new test assertspsihigh_resolved == rzphi_xs[end] <= config.psihighacross all three efit solvers plus that apsihigh=1.0request survives unmodified on the config.Also
reform_eq_with_psilimremoved — it only ever gated a warning saying the reform was not implemented, so it could not affect a result.gpec.h5is unchanged: the config struct is never serialized, so the storedpsihighis the request and every replay re-clamps, as before.Testing
No intended numerical change. Passing:
runtests_equil271/271 ·runtests_kinetic277/277 ·runtests_rerun_from_h570/70 ·runtests_sing76/76 ·runtests_fullruns17/17 ·runtests_coils·runtests_grid_refinement·runtests_parallel_integration·runtests_imas.Regression harness: no change.
localvsorigin/develop@0b196ed8, identical env (julia 1.12.6, manifest pinned):diiid_n1efit_fixedbdy_separatrixsolovev_n1Every compared quantity is bit-for-bit identical (diff
0.0e+00, profile checksums identical), including theefit_fixedbdy_separatrixcase that exercises the separatrix/psihighpath this PR reworks.