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
a0520f4
Compare
…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>
|
@logan-nc I think the main thing to look at here is the logic/naming for |
|
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
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. |
|
@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 |
There was a problem hiding this comment.
Overly verbose annotation
| - `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 |
There was a problem hiding this comment.
this test seems excessive
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
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.