Repository navigation
Repo - API! - Consolidate the v2 deck sections, driver and HDF5 writers - #602
matt-pharr wants to merge 89 commits into
Conversation
…broken Run main and the scripting API on the same Solovev deck for Galerkin, Riccati Δ′, hdf5 file forcing, the analytic two-pass grid and gal_match_flag matching. Mark the known drifts @test_broken: file-forcing normalization, the analytic two-pass grid, the cut solution implied under gal_match_flag, and the fixed kinetic source. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ount Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Remove the runtimes keyword from run_force_free_states, perturbed_equilibrium, compute_perturbed_equilibrium, run_perturbed_equilibrium and run_slayer_stage. main times every stage from outside: PerturbedEquilibrium, KineticForces and ErrorFields each get one banner and one timer through _timed_stage, so they are no longer timed twice. Info/Runtimes drops the galerkin and forcing_terms sub-stage entries, which only a stage could measure from inside. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The :ray model returns no inner profiles by default since #536, so the cut-solution drift only shows with real-ψ profiles. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…section and ODE tolerances on Ballooning
…Stability] section
… the old key fail as an unknown keyword No backward-compatibility code: remove the EulerLagrangeProblem error for local_stability_flag, its deprecated-key pre-commit entry, the two tests asserting it errors, and the docs sentence naming it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gal_cut_solution is no longer a ForceFreeStatesControl field or a deprecated deck key. Galerkin(; cut_solution=true) passes it to galerkin_solve as a solve option, so only a script can keep the cut solution. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nto refactor/deck-match-in-main
…si, and smoke-test the gpec.h5 plots
…efactor/v2-consolidation
…o refactor/v2-consolidation
… into refactor/v2-consolidation
…nto refactor/v2-consolidation
…actor/v2-consolidation
… refactor/v2-consolidation
…to refactor/v2-consolidation
…efactor/v2-consolidation
…ds for a few stragglers
…a parity test case
|
Sorry for this eltrich horror of a PR 💀 good for halloween I guess? @jhalpern30 @logan-nc @d-burg This is wrapped up now and ready for your feedback. I will do a few cleanup passes to make sure that everything works, but no results were changed in this PR besides a few slayer roots because we got rid of the double conjugate convention with SLAYER that moves roots a tiny bit but is in theory 100% equivalent. This PR achieves a lot of things with open issues, including taking GPEC.jl down to 315 lines, decimating the docstrings, deduping utility functions, helpers, etc, ensuring parity between decks/scripting, fixing scripting syntax, making the hdf5 savable via scripting, and fixing a number of inaccuracies reflected in variable names (omega_Hz and friends were not actually in Hz, docstrings that claim false things, etc). Anything that was labeled as a co-author with claude is something that I did not read 100% of the lines of and mostly amounts to easy develop merges and test fixes, everything else got an attentive eye. This PR achieves 90% of the remainder of what I want to achieve before 2.0.0. Do not feel obligated to review the entire thing; if I requested you it is because you will likely be interested in what is changing/reorganized in this PR and you should look at it and offer any feedback you wish to include so I can implement it. |
…checks, GGJ omega_range and the NTV benchmark
… the V review sweep found untested
|
@matt-pharr Claude and I reviewed the tearing side of this PR: Δ′, SLAYER, GGJ, η, χ and b_crit. The physics holds up. The kernels, layer inputs, critical Δ and b_crit algebra are unchanged, no tolerance moved, and #222 looks closed in the code. Dropping the conjugate also checks out: on the DIII-D-like SLAYER example at develop, Im/|·| is at most 7e-8 on the Δ′ diagonal and 6e-8 on the off-diagonal products. Five things to look at, verified in the code:
Traced but not run, for your cleanup pass:
Landing order: this PR doesn't include #617 (the #614 fix, approved) or #619 ( |
|
@d-burg most of these feel like mechanical things you and your claude can fix proactively. PleSe try to minimize dead time between human iteration by telling you claude to make proactive fixes where the path is clear. Even if a human decision is needed, at least post a clear proposed plan so the next human inthe loop can just say "yes, do as proposed" if they approve of the idea |
… the explicit-output test into the deck round trip Drops struct-echo assertions and a duplicated replay testset. Also lists Deck and ForceBalance in the naming Area table, fixes three harness labels that still said Hz, and stops calling the force-balance problem an Euler-Lagrange system in the API docs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdRsGGg4YRG6E9fgLGbpdF
logan-nc
left a comment
There was a problem hiding this comment.
Tiered review covering structure, physics, conventions and tests. Visual summary: https://claude.ai/artifact/EEN5xY6MXFEBAkftX4HrXD
Pushed 8dda8ca9c, no src/ changes, so the harness stamp holds:
DeckandForceBalanceare now in the naming Area table. This was the only failing CI check.- Three deck kinetic-source error checks now call
parse_deckinstead ofGPEC.main. runtests_ffs_output.jlis folded into the deck round trip, saving one full pipeline run.- Removed the struct-echo assertions and a replay testset that a merge had duplicated.
- Fixed three harness labels that still said Hz.
- Fixed the
api.mdtext that still called the problem an Euler-Lagrange system. - Tests run on alfven at
70fe92e3b; the later amends touch only docs and labels. Results: solve API 165/165, rerun 87/87, deck round trip 113/113.
Physics: the checks pass.
6c42f0364: the new SLAYER coefficients are the exact conjugates of the old ones, evaluated at ĝ instead of conj(ĝ).cole_deltamaps to Δ(iQ) on both sides. The GGJ plane is s = −iQ/τ with ω relabeled. Winding +1 follows.b8f50de22: implements the plan posted on #222, with Sauter/Redl η from the kinetic data, ρ = m_i·n_i and f = Ω_E/2π.
Line comments each carry a fix. The two suggestion blocks and the [GGJ] validation change src/, so they should land together under one harness re-stamp. I can push that batch and re-run the harness on request.
Merging still needs approval from a third-party human reviewer.
| const _ODE_DECK_KEYS = ("eulerlagrange_tolerance", "renorm_threshold", "numsteps_init", "numunorms_init", "save_interval", "nchunks") | ||
| const _SEARCH_DECK_KEYS = ("scan_mode", "Q_re_range", "Q_im_range", "nre", "nim", "amr_passes", "amr_max_cells", "boxes", | ||
| "multi_box_prescreen_n", "pole_threshold", "pole_threshold_adaptive", "filter_above_poles", "filter_outside_re", | ||
| "gamma_gap_threshold", "polish_roots", "validity_rtol", "root_classification", "store_scan") | ||
| const _KINETIC_FILE_HOME = "name the kinetic-profile file once, as kinetic_file (and kinetic_group) in [Equilibrium]" | ||
| const _MOVED_KEYS = Dict{String,Dict{String,String}}( | ||
| "ForceFreeStates" => Dict{String,String}( | ||
| "nn_low" => "set nn = n, or nn = [n1, n2, ...] for consecutive toroidal modes", | ||
| "nn_high" => "set nn = n, or nn = [n1, n2, ...] for consecutive toroidal modes", | ||
| "extended_precision_bvp" => "moved to [Riccati]", |
There was a problem hiding this comment.
nchunks is Riccati-only now that Forward dropped it, but this message still sends it to "[Forward] or [Riccati]". A [ForceFreeStates] nchunks would get pointed at [Forward], which then rejects it.
| const _ODE_DECK_KEYS = ("eulerlagrange_tolerance", "renorm_threshold", "numsteps_init", "numunorms_init", "save_interval", "nchunks") | |
| const _SEARCH_DECK_KEYS = ("scan_mode", "Q_re_range", "Q_im_range", "nre", "nim", "amr_passes", "amr_max_cells", "boxes", | |
| "multi_box_prescreen_n", "pole_threshold", "pole_threshold_adaptive", "filter_above_poles", "filter_outside_re", | |
| "gamma_gap_threshold", "polish_roots", "validity_rtol", "root_classification", "store_scan") | |
| const _KINETIC_FILE_HOME = "name the kinetic-profile file once, as kinetic_file (and kinetic_group) in [Equilibrium]" | |
| const _MOVED_KEYS = Dict{String,Dict{String,String}}( | |
| "ForceFreeStates" => Dict{String,String}( | |
| "nn_low" => "set nn = n, or nn = [n1, n2, ...] for consecutive toroidal modes", | |
| "nn_high" => "set nn = n, or nn = [n1, n2, ...] for consecutive toroidal modes", | |
| "extended_precision_bvp" => "moved to [Riccati]", | |
| const _ODE_DECK_KEYS = ("eulerlagrange_tolerance", "renorm_threshold", "numsteps_init", "numunorms_init", "save_interval") | |
| const _SEARCH_DECK_KEYS = ("scan_mode", "Q_re_range", "Q_im_range", "nre", "nim", "amr_passes", "amr_max_cells", "boxes", | |
| "multi_box_prescreen_n", "pole_threshold", "pole_threshold_adaptive", "filter_above_poles", "filter_outside_re", | |
| "gamma_gap_threshold", "polish_roots", "validity_rtol", "root_classification", "store_scan") | |
| const _KINETIC_FILE_HOME = "name the kinetic-profile file once, as kinetic_file (and kinetic_group) in [Equilibrium]" | |
| const _MOVED_KEYS = Dict{String,Dict{String,String}}( | |
| "ForceFreeStates" => Dict{String,String}( | |
| "nn_low" => "set nn = n, or nn = [n1, n2, ...] for consecutive toroidal modes", | |
| "nn_high" => "set nn = n, or nn = [n1, n2, ...] for consecutive toroidal modes", | |
| "extended_precision_bvp" => "moved to [Riccati]", | |
| "nchunks" => "moved to [Riccati]", |
| error("[KineticForces] is set but neither [PerturbedEquilibrium] (the torque) nor [ForceFreeStates] kinetic = \"KineticForces\" uses it") | ||
| (haskey(inputs, "KineticForces") || kinetic_source) && isempty(get(get(inputs, "Equilibrium", Dict{String,Any}()), "kinetic_file", "")) && | ||
| error("this run needs kinetic profiles ([KineticForces], or kinetic = \"KineticForces\"): set kinetic_file in [Equilibrium]") | ||
| match = match_from_deck(inputs) |
There was a problem hiding this comment.
A [Match] the solve cannot serve (a Forward integrator, Riccati without vac_flag, Galerkin without rpec_flag, or several n) only fails inside MatchProblem, after the full force-free-states solve. The same check fits here with the other cross-section checks, so it stops at parse time like every other deck error. It mirrors the MatchProblem guards:
| match = match_from_deck(inputs) | |
| match = match_from_deck(inputs) | |
| if match !== nothing | |
| (; keywords, algorithm) = force_balance | |
| first(keywords[:nn]) == last(keywords[:nn]) || error("[Match] is single-n: set [ForceFreeStates] nn to one toroidal mode number") | |
| (algorithm isa Galerkin && algorithm.rpec_flag) || (algorithm isa Riccati && get(keywords, :vac_flag, false)) || | |
| error("[Match] needs Δ′: use integrator = \"Galerkin\" with [Galerkin] rpec_flag = true, or \"Riccati\" with vac_flag = true") | |
| end |
Then the [Match] ideal = true case in runtests_solve_api.jl can call parse_deck instead of GPEC.main, which drops one solve from the suite.
| The GGJ layer model of the `[GGJ]` section: `solver` and the backend options, passed to | ||
| `GGJModel` as they are. The default `GGJModel()` when absent. | ||
| """ | ||
| function ggj_model(inputs::AbstractDict) |
There was a problem hiding this comment.
[GGJ] is the only section that skips section_keywords: its keys go straight into GGJModel options. A typo (for example ncx = 64) is not caught here. It surfaces as an "unsupported keyword argument" MethodError from the backend at the first layer solve, after the force-free-states solve. That breaks the promise in the PR body that unknown keys stop the run.
Proposal: give each backend a list of its option names (_ggj_options(::Val{:ray}) etc., from Base.kwarg_decl of the backend's solve_inner/solve_inner_profile/solve_ray methods, or a hand-written tuple) and validate here with section_keywords. This is small, but it changes src/, so it should go in with the [Match] check above under one harness re-stamp. I'm happy to push both if you'd rather not.
| @@ -0,0 +1,284 @@ | |||
| # ForceBalance.jl: the force-balance problem and its solve. The solve prepares the equilibrium with the Tearing | |||
There was a problem hiding this comment.
Structure question, not a blocker for this PR. ForceFreeStates' own solve lives in the top module because preparing the solve calls Tearing.layer_overlap_scan and the KineticForces model. So the scripting entry point of FFS is not in FFS, and a reader looking for it in src/ForceFreeStates/ won't find it. One way out for later: pass those two in as hooks (the pattern already used for calculated_source), so ForceFreeStates owns solve and ForceBalanceProblem and the top module only wires them. Worth a follow-up issue in a later milestone? If you agree I'll file it.
logan-nc
left a comment
There was a problem hiding this comment.
Remaining suggestions are minor non-blocking. @matt-pharr can quickly take them or leave them.
Very impressive!!
This is the GPEC v2.0.0 consolidation. It reshapes the scripting API, the deck and the HDF5 output around one rule: a deck is a script written as TOML. Every object a script builds has one deck section named after it, holding exactly that constructor's keywords, and
mainruns the same public calls a script would. The physics is unchanged: across the 25 regression cases, every tracked quantity that both refs compute is identical (see Regression report).It is a clean break. Nothing is kept for backward compatibility: no aliases, no key mapping, no accepted-and-ignored arguments. An old deck stops on its first moved or removed key, and the error says where the key lives now. The migration tables below list every change.
Closes #222
Closes #372
Closes #374
Closes #388
The plotting half of #326 moves to its own PR, stacked on this one. #613 (a run with a wall mixes a with-wall Λ and a no-wall L) is handled in #620, stacked on this one.
Contents
main,Deck.jl,[Run], replay fromgpec.h5save_to_hdf5, inputs recorded as deck sectionsForceBalanceProblem, integrator objects, kinetic sourcesTearingProblem, GGJ tearingkinetic_torqueerror_fieldsHow to review this PR
The diff is large: about 280 files, +8.2k / −12.4k lines. It was built one change per commit, so reading it commit by commit is much easier than reading the full diff. Each area section below names its commits.
Every commit moves no numbers except
6c42f0364(the growth-rate convention, a root shift of at most about 1e-5 relative, below every harness threshold).The twelve
<Module> - DOCS - Trim docstrings and comments ...commits (ddf311c6dto9a2218338) change only docstrings and comments. A script that strips comments and docstrings confirmed every file is code-identical before and after, apart from one docstring moved onto the function it describes. These can be skimmed.Suggested split by reviewer:
Design rules
These decide where every input lives, and the area sections apply them.
solve(problem, algorithm)separates what from how. An input that can change the converged answer goes on the problem. An input that only changes accuracy or cost (method, tolerance, resolution, scan range) goes on the algorithm.solveis used only where there is a real choice of method:Forward(),Riccati(),Galerkin();GGJModel(),SLAYERModel();Ballooning().Everything else is a plain function that returns a result.
The deck has the same shape as the API. Problems and stage functions get sections such as
[ForceFreeStates],[Match],[Tearing]and[PerturbedEquilibrium]. Algorithms and models get sections named after their type, such as[Riccati],[Galerkin],[GGJ],[SLAYER]and[Ballooning]. A problem with a choice of method names it (integrator = "Galerkin",inner_model = "GGJ"). Including a section runs that stage; there are noenabledflags. Unknown keys stop the run.One save function.
save_to_hdf5(target, result)has a method per result type. Computation never writes files.mainsaves after each stage, and a script saves what it wants.Every capability works from both a deck and the API, with a deck/API parity test (
docs/src/developer_notes.md).Changes by area
Driver and deck
mainused to be a long monolithic driver. It is now a short script over the public API: for each section present in the deck, it builds the object, calls the same function a script would, and saves the result.src/Deck.jl(new) is the only place deck text becomes API objects.parse_deckturns every section into constructor keywords before anything runs. So a misspelled key, a moved key, or a section no stage reads stops the run at once with an error naming the fix. Moved keys say where they went (for example "gal_nxmoved to [Galerkin] as nx").[Run](new) holds the oneoutput_fileand the oneverbosefor the whole run. The per-section copies (HDF5_filename,output_filename,write_outputs_to_HDF5,verbose) are gone. A deck run always writes its file.force_terminationis gone.dir_pathis gone from the problem and result types.nothingfor stages that did not run:(; equil, local_stability, ffs, match, resp, pe, kinetic, error_fields, tearing). The oldslayer,coil_sensitivities,monte_carlo,locking_riskandefc_couplingsfields are nowtearinganderror_fields.gpec.h5: a parsed deck can be written back to the deck text that states it (deck_sections), the inverse of the parser.Rerun.jlreplays anygpec.h5, including one written by a script, because inputs are recorded as deck sections (see Output).Commits:
cb523a196,fb53c7343,a0232cc79,711a9945f,b92ec6b3e,d5b0ef1dc, and the review-sweep fix9ecffadc6.Output
save_to_hdf5(target, result)is declared inUtilities(src/Utilities/Output.jl), and each module adds the method for its own result type.targetis a path or an open file or group. The old driver writerwrite_outputs_to_HDF5is gone.Input/in deck form. A script run and a deck run of the same workflow give the samegpec.h5, apart fromInfo/Runtimes, and either can be replayed.mainwrites each stage's results as soon as the stage finishes, so a crash later in the run keeps the earlier output.Commits:
c6536ef24,d5b0ef1dc,36adc5bc1.Equilibrium
[Solovev],[LargeAspectRatio]and[TJAnalytic]. The section's presence selects the source, and the deck then sets noeq_type.[TJAnalytic] solver = "inverse" | "direct"replaceseq_type = "tj_analytic_direct". The analytic configs are immutable.equil.kinetic, aKineticProfileData), so every consumer reads one source. Previously SLAYER, KineticForces and matching each carried their own copies. These keys are now[Equilibrium]keys:[[Equilibrium.ion_species]],impurity_charge,impurity_mass;zeff_default,chi_e_default,chi_i_default,chi_phi_default,tau_E,resistivity_model,lnLambda_form.grid_type = "log_asymptotic"alias,equilibrium_solver_by_inversion'srefine/β_r/β_zkeywords, and the never-readEquilibriumParameters.b_normandpsi_axis_norm.Commits:
fae914299,daaea0236.Vacuum
[Wall]means no wall.[Wall] shape = ...selects aWallSettingssubtype (ConformalWall,EllipticalWall,DeeWall,ModDeeWall,FileWall), and each takes only its own parameters.WallShapeSettingsandshape = "nowall"are gone.Commit:
14a374549.ForceFreeStates
EulerLagrangeProblemis renamedForceBalanceProblem. With kinetic terms the force operator is not self-adjoint, so the equation is no longer an Euler–Lagrange equation. Internal file and function names are unchanged.Forward(; eulerlagrange_tolerance, renorm_threshold, numsteps_init, numunorms_init, save_interval).Riccatitakes the same plusnchunksandextended_precision_bvp.Galerkintakes the oldgal_*keys without the prefix. Physics and domain settings stay on the problem, and the deck selects the integrator withintegrator = "Forward" | "Riccati" | "Galerkin".nn = 1ornn = [1, 2, 3](consecutive) replacesnn_low/nn_high, matching the API'sIntorUnitRange.kinetickeyword:nothing(ideal), aKineticForcesControl(computed fromequil.kinetic), aKineticMatrices(precomputed), orTestKineticMatrices(; scale)(synthetic, for tests). In a deck that is[ForceFreeStates] kinetic = "KineticForces".kinetic_factoris now only a scale (default 1), not an on/off switch, andkinetic_sourceis gone.kinetic_matrices(prob)evaluates the kinetic matrices once and returns aKineticMatrices. Passing it back askinetic=reuses it across solves, for example in akinetic_factorscan. The internal FKG type is renamedFKGMatrices.frobenius_orderon the problem (default 6) is used by all three integrators.gal_sing_orderis gone, and[Galerkin] frobenius_order_ceilingkeeps Galerkin's per-surface raise.gal_solveris gone.MatchProblemdefaultseta,rhoandrotationfrom the equilibrium's kinetic data, through the same layer closures the tearing solve uses. With no kinetic data, the error names exactly which inputs are missing. This closes Add automatic computation of spitzer and neoclassical resistivities with profiles #222.psilim_from_layer_overlapworks from scripts as well as decks.trueuses a defaultSLAYERModel, and passing aSLAYERModelsets the closures. The scan runs, andForceFreeStates/LayerOverlapis written, only when the cap is on.sing_start(its non-zero branches were never implemented),sing_get_dua, the allocatingsing_matvec/sing_matmulwrappers, and never-read fields onOdeState,SingAsymptotics,FKGMatricesandResistGeometry.Commits:
4f9e4d6fa,de7d87d60,924109e43,464f5304f,b8f50de22,b63a64901.LocalStability
theta_kandalpha_boundary_scanmove fromBallooningontoLocalStabilityProblem.Ballooningkeeps only numerics.verboseis asolvekeyword, defaulting tofalse.[LocalStability]takes the problem keys and[Ballooning]the numerics.Commit:
0671b86b0.InnerLayer and Tearing
One growth-rate convention.
solve_inner(model, p, s)takes s = γ − iω in 1/s for every inner-layer model.layer_response(model, p, z)evaluates a model in its own scan variable (GGJ: s; SLAYER: Q).zero_windingis removed, and every*Windingcount is +1 where it was −1.6c42f0364.Tearing.Runneris folded intoTearing. The entry point issolve(TearingProblem(ffs; ...), model; config=TearingConfig(...)), and the result is aTearingResult. GGJ tearing growth rates now go through the same problem.643a78df3moves each GGJ layer into its surface's E×B frame, so GGJ roots are in the lab frame like SLAYER's.SLAYER's layer closures live on
SLAYERModel:P_perp_model,D_c_type,D_R,D_geo,chi_perp_Dc,theta_sampleandvariant. Physics shared with other modules comes fromequil.kinetic.The deck sections are reorganized:
[Tearing]holds the problem:inner_model,coupling_mode,msing_max,omega_E_kHz.[Tearing.search]holds the root search (TearingConfig).[Tearing.CriticalResonantField]holds the critical resonant field settings.[SLAYER]and[GGJ]hold the models.Units in names are fixed.
omega_Hz/gamma_Hzheld rad/s and 1/s, so they are nowomega/gamma.gap_kHz_thresholdcompared a rad/s value against kHz; it is nowgamma_gap_thresholdin 1/s. This closes SLAYER gap_kHz_threshold compares a rad/s quantity against a kHz threshold (2π error) #372.ggj_growth_rates(; omega_range)bounds ω, the same asTearingConfigand the result.Shared code: the GGJ
ggj_rowsare shared by_physical_uvandRay.ode_matrix. Galerkin and GGJ share one Hermite FEM kernel (Utilities.HermiteFEM).Commits:
345d1d107,58cadc46e,643a78df3,6c42f0364,8e13504e2.PerturbedEquilibrium
Responsefields that were not reconstructed are left out ofgpec.h5, rather than written as empty placeholders.Tearing/PerSurface/luis renamedS(the Lundquist number).b_psi_modesis labeled J·b^ψ. These were mislabeled before; the data are unchanged.regularization_widthis aperturbed_equilibriumkeyword. ThePlasmaResponsefield that was never set is gone.Together with the HDF5 changes, this closes #388. Commit:
36adc5bc1.KineticForces
kinetic_torque(pe, ctrl)evaluates the NTV torque on a perturbed equilibrium and returns aKineticForcesResult, saved withsave_to_hdf5.[KineticForces] nnis gone: n comes from the perturbed equilibrium. The species keys moved to[Equilibrium].Commit:
6375f8874.ErrorFields
error_fields(resp, coil_sets, coil_config; ...)runs the whole assessment on a plasma response and returns oneErrorFieldsResult. That holds the sensitivities, dominant coupling, tolerances, Monte Carlo, locking risk, tolerance scan and correction-coil couplings, eachnothingwhen not requested. Onesave_to_hdf5writes them all at their existing paths.efc_couplingsmoves intoErrorFields, andrun_error_fieldsis gone.Commit:
7d19edf2d.Cleanup
b63a64901,54c2bbb6d).8e13504e2). Each replacement was verified bit-identical. That covers the Hermite FEM basis and grid packing, the GGJ ODE rows, the Riccati renormalization, and the CHEASE reader.src/(ddf311c6dto9a2218338, net −3.5k lines). Each docstring now says what the function does, gives the paper reference, and lists its fields, arguments and return value. Walkthroughs that restated the code, history notes and Fortran line references are gone. Docstrings found wrong along the way are fixed.9ecffadc6,69325f463and the test commit that follows them:Forwardlost annchunksfield that nothing read;Tests, examples, harness and docs
runtests_deck_roundtrip.jlparses every one of them.solvefront half (dd1409f76). Finished one-off scripts are deleted.remove_sectionslets a case drop deck sections, replacing theforce_terminationoverride, and the case TOMLs readTearing/PerSurface/S.test/runtests_solve_api.jl(ec07adbb1).docs/src/api.mddocuments the script form of each stage.Migration
The run errors on every old key, so a deck can be migrated by running it and following the messages. The tables give the full list.
Deck sections and keys
[*] verbose,HDF5_filename,output_filename,write_outputs_to_HDF5[Run] verbose,[Run] output_fileforce_termination[SOL_INPUT],[LAR_INPUT],[TJ_ANALYTIC_INPUT]+eq_type[Solovev],[LargeAspectRatio],[TJAnalytic](noeq_type)eq_type = "tj_analytic_direct"[TJAnalytic] solver = "direct"grid_type = "log_asymptotic"grid_type = "auto"[Wall] shape = "nowall"[Wall][Wall]with unused parameters[ForceFreeStates] nn_low,nn_highnn = nornn = [n1, n2, ...][ForceFreeStates] integrator = "forward"(lowercase)"Forward","Riccati","Galerkin"[ForceFreeStates] eulerlagrange_tolerance,renorm_threshold,numsteps_init,numunorms_init,save_interval[Forward]or[Riccati][ForceFreeStates] nchunks,extended_precision_bvp[Riccati][ForceFreeStates] kinetic_sourcekinetic = "KineticForces";kinetic_factoris only a scale[ForceFreeStates] sing_start[ForceFreeStates] gal_nx,gal_nq,gal_pfac,gal_dx0,gal_dx1,gal_dx2,gal_cutoff,gal_tol,gal_gnstep,gal_dx1dx2_flag,gal_rpec_flag,gal_edge_onesided[Galerkin] nx,nq, ... (no prefix)[ForceFreeStates] gal_solver[ForceFreeStates] gal_sing_order[ForceFreeStates] frobenius_order[ForceFreeStates] gal_sing_order_ceiling[Galerkin] frobenius_order_ceiling[ForceFreeStates] gal_match_flag[Match]section[ForceFreeStates] gal_ideal_flag[Match] ideal[ForceFreeStates] gal_eta,gal_rho,gal_rotation,gal_gamma[Match] eta,rho,rotation,gamma(default from the kinetic data)[ForceFreeStates] gal_inner_solver,gal_inner_xfac,gal_inner_nx,gal_inner_nq,gal_inner_cutoff,gal_inner_kmax[GGJ] solver,xfac,nx,nq,cutoff,kmax[DEBUG] gal_basis_output[Galerkin] basis_output[DEBUG](the rest)[LocalStability] ode_tol,n_scan,max_alpha_scale,alpha_tol,psi_window[Ballooning][KineticForces] ion_species,ion_charge,ion_mass[[Equilibrium.ion_species]]withz,m[KineticForces] impurity_charge,impurity_mass[Equilibrium][KineticForces] kinetic_file,[SLAYER] profile_file,profile_group[Equilibrium] kinetic_file,kinetic_group[KineticForces] nn[SLAYER] enabled[Tearing]section withinner_model = "SLAYER"[SLAYER] inner_model,coupling_mode,msing_max,omega_E_kHz[Tearing](the SLAYER variant is[SLAYER] variant)[SLAYER.scan_grid]and the scan keys (scan_mode,Q_re_range,nre,amr_passes,pole_threshold,polish_roots, ...)[Tearing.search][SLAYER] gap_kHz_threshold[Tearing.search] gamma_gap_threshold, in 1/s[SLAYER.CriticalResonantField][Tearing.CriticalResonantField][SLAYER] zeff,chi_perp_e,chi_perp_i,chi_tor[Equilibrium] zeff_default,chi_e_default,chi_i_default,chi_phi_default[SLAYER] ion_mass,impurity_charge,tau_E,resistivity_model,lnLambda_form[Equilibrium](ion_massviaion_species)[SLAYER] bt[PerturbedEquilibrium] compute_responseRelative paths in a deck are now resolved against the deck's directory, including the coil files (
dat_dir,dat_file,h5_file).API names
EulerLagrangeProblemForceBalanceProblemForceBalanceProblem(...; kinetic_source, kinetic_factor > 0)ForceBalanceProblem(...; kinetic=KineticForcesControl() / KineticMatrices / TestKineticMatrices(; scale))Forward(; ...),Riccati(; ...)Galerkin(; solver)Galerkin()(automatic factorization)Ballooning(; theta_k, alpha_boundary_scan, verbose)LocalStabilityProblem(eq; theta_k, alpha_boundary_scan);solve(...; verbose)WallShapeSettingsnothingor aWallSettingssubtypewrite_outputs_to_HDF5,write_to_hdf5!save_to_hdf5(target, result)Tearing.Runner,SLAYERResult,slayer_control_from_tomlsolve(TearingProblem(ffs; ...), SLAYERModel(...) / GGJModel(...); config=TearingConfig(...))->TearingResultomega_Hz,gamma_Hz(and_secondary)omega,gamma(rad/s, 1/s)solve_inner(model, p, Q)with model-specific conventionssolve_inner(model, p, s)with s = γ − iω;layer_response(model, p, z)zero_windingggj_growth_rates(; omega_range)bounding −ωggj_inner_deltasrun_error_fields)error_fields(resp, coil_sets, coil_config; ...)->ErrorFieldsResultkinetic_torque(pe, ctrl)->KineticForcesResultmain(...)->(; ffs, pe, slayer, local_stability, coil_sensitivities, monte_carlo, locking_risk, efc_couplings)(; equil, local_stability, ffs, match, resp, pe, kinetic, error_fields, tearing)compute_torque_all_methods!species from the controlequil.kinetic(required)Ballooningverbose = trueby defaultverbose = falseby defaultkinetic_matrices,KineticMatrices,layer_overlap_scan,layer_eta,layer_rho,layer_omega_E,IonSpecies,CriticalResonantFieldControl,CriticalResonantFieldResultHDF5 changes
Tearing/PerSurface/luTearing/PerSurface/SPerturbedEquilibrium/Response/*Tearing/**/*WindingForceFreeStates/LayerOverlappsilim_from_layer_overlapcaps the domainKineticForces/ValidityInfo/Runtimes/match[Match]stageInput/b_psi_modesattributesBehavior changes to know about
kinetic_factor = 0still builds the kinetic matrices and pins kinetic-resonance knots in the ψ grid. To run ideal, leave outkinetic.[KineticForces]profile factors.[LocalStability],[Ballooning],[ForcingTerms]or[KineticForces]without[ForceFreeStates]. It also stops on[ForcingTerms]without[PerturbedEquilibrium], and on a[KineticForces]that neither the torque nor the kinetic source uses.psilim_from_layer_overlap = truewithout[SLAYER]caps the domain with the defaultSLAYERModel(it used to warn and do nothing).Release note
*Windingcounts are +1GPEC v2.0.0 makes the deck a script written as TOML: each object a script builds has one deck section,
mainruns the same public calls a script does, and every result saves withsave_to_hdf5and records its inputs so anygpec.h5replays.Regression report
Harness on ITER SDCC,
develop @ 728a7e7d5vsrefactor/v2-consolidation @ b521d76be, the current head. Every value matches the earlier run at54c2bbb6d.The two "changed" cases are not like-for-like. Their overrides use keys that only this branch reads, so develop silently runs a different configuration: the uncoupled per-surface search, and the default GGJ solver. Every case where both refs run the same inputs is unchanged.
Not in this PR
gpec.h5, with a plot registry (Analysis UX: introduce a GPECResult object + precondition-aware plot registry #326). This is a separate PR stacked on this one.gal_*functions into aGalerkinFEMsubmodule.main, and a few latent issues in code this PR does not change.🤖 Generated with Claude Code
https://claude.ai/code/session_014wKUyAaxKdTJ5ch1boMFE1