Skip to content

Repo - API! - Read kinetic profiles once into the equilibrium and derive every kinetic view from them - #581

Merged
logan-nc merged 12 commits into
developfrom
feature/kinetic-profiles-on-equilibrium
Oct 29, 2026
Merged

logan-nc merged 12 commits into
developfrom
feature/kinetic-profiles-on-equilibrium

Conversation

@matt-pharr

@matt-pharr matt-pharr commented Oct 24, 2026 •

Copy link
Copy Markdown
Collaborator

Release note

  • Audience: users
  • Numerical impact: Any two-pass deck that names kinetic_file now refines its grid with the attached profiles, ideal δW included. SLAYER-only decks on the two-pass auto grid, which previously refined without the profiles, gain one ψ knot on the DIII-D-like cases: their tearing growth rates move by up to about 1% (diiid_slayer_n1, diiid_slayer_n1_chi_perp_e and diiid_slayer_n1_winding γ 0.57%, the coupled γ 0.98%) and the truncation case's energies by about 1e-5. Every other case is unchanged (harness @ 255586d)
  • Migration:
    • [KineticForces] kinetic_file and [SLAYER] profile_file are removed. Name the kinetic-profile file once, as kinetic_file in [Equilibrium]. [SLAYER] profile_group becomes kinetic_group in [Equilibrium]. There is no default kinetic.dat any more. A deck that still sets one of the old keys stops with a message naming the new home.
    • Equilibrium.load_kinetic_profiles(path; …) is now kinetic_profile_splines(read_kinetic_file(path); …).
    • resolve_ntv_species takes KineticProfileData instead of a path.
    • TearingProblem(ffs) takes its profiles from the equilibrium (no profile_file keyword).
    • efc_couplings drops its kinetic-profiles argument.
    • A gpec.h5 replayed with equilibrium_from_h5 re-reads the stored deck's kinetic_file from disk; if the file has moved, the restored equilibrium carries no profiles and a warning says so.

The kinetic-profile file is now read once into the equilibrium (equil.kinetic), set by the new [Equilibrium] kinetic_file key or PlasmaEquilibrium(…; kinetic_file=…), or attached with attach_kinetic_profiles!. NTV, the calculated kinetic matrices, SLAYER, the layer-overlap scan, error-field couplings and grid refinement each derive their own view from it, instead of up to four separate reads. solve() can now run the calculated kinetic source.

Regression report

Run on feynman from the pushed head; the winding case is included because develop added it after the branch last merged.

Cases: diiid_n1, diiid_slayer_n1, diiid_slayer_n1_coupled, diiid_slayer_n1_chi_perp_e, diiid_slayer_n1_winding,
       diiid_truncation_n1, diiid_error_field, solovev_kinetic_calculated, solovev_kinetic_ntv,
       solovev_kinetic_multiion, solovev_kinetic_nuzero
Ref 1: origin/develop  @ 5b2258e5a (2026-10-29)
Ref 2: feature/kinetic-profiles-on-equilibrium  @ 255586db5 (2026-10-29)
       env: julia 1.11.7, x86_64-linux-gnu, manifest 6f3e1b5f (pinned), 4 threads/4 BLAS, jit build

diiid_n1                     Summary: 53 unchanged
diiid_error_field            Summary: 10 unchanged
solovev_kinetic_calculated   Summary: 20 unchanged
solovev_kinetic_ntv          Summary: 7 unchanged
solovev_kinetic_multiion     Summary: 6 unchanged
solovev_kinetic_nuzero       Summary: 14 unchanged

Regression Report: diiid_slayer_n1
Quantity                             Ref 1     Ref 2     Diff               Status
SLAYER surface indices               [6 elem]  [6 elem]  0.0e+00            OK
SLAYER poloidal m                    [6 elem]  [6 elem]  0.0e+00            OK
SLAYER toroidal n                    [6 elem]  [6 elem]  0.0e+00            OK
SLAYER minor radius rs               [6 elem]  [6 elem]  3.504e-09 (0.00%)  ** CHANGED **
SLAYER r-based shear                 [6 elem]  [6 elem]  1.149e-02 (0.01%)  ** CHANGED **
SLAYER Lundquist S                   [6 elem]  [6 elem]  9.230e+03 (0.01%)  ** CHANGED **
SLAYER D_norm                        [6 elem]  [6 elem]  3.362e-05 (0.00%)  ** CHANGED **
SLAYER P_perp                        [6 elem]  [6 elem]  5.031e-07 (0.00%)  ** CHANGED **
SLAYER tauk                          [6 elem]  [6 elem]  6.201e-11 (0.00%)  ** CHANGED **
SLAYER iota_e                        [6 elem]  [6 elem]  1.134e-07 (0.00%)  ** CHANGED **
SLAYER Q_e                           [6 elem]  [6 elem]  1.890e-06 (0.00%)  ** CHANGED **
SLAYER Q_i                           [6 elem]  [6 elem]  4.092e-06 (0.00%)  ** CHANGED **
SLAYER Q_root [2/1,3/1,4/1]          [3 elem]  [3 elem]  2.165e-04 (0.01%)  ** CHANGED **
SLAYER ω_Hz [2/1,3/1,4/1]            [3 elem]  [3 elem]  4.4e-01            OK
SLAYER γ_Hz [2/1,3/1,4/1]            [3 elem]  [3 elem]  7.957e+00 (0.57%)  ** CHANGED **
SLAYER no_root flags [2/1,3/1,4/1]   [3 elem]  [3 elem]  0.0e+00            OK
CRF b_r/B_φ crit [2/1,3/1,4/1]       [3 elem]  [3 elem]  2.727e-07 (0.02%)  ** CHANGED **
CRF Q_peak [2/1,3/1,4/1]             [3 elem]  [3 elem]  8.6e-08            OK
CRF Q0 [2/1,3/1,4/1]                 [3 elem]  [3 elem]  2.870e-07 (0.00%)  ** CHANGED **
CRF b_r crit in tesla [2/1,3/1,4/1]  [3 elem]  [3 elem]  5.305e-07 (0.02%)  ** CHANGED **
SLAYER enabled flag                  1         1         0.0e+00            OK
Summary: 14 changed, 7 unchanged

Regression Report: diiid_slayer_n1_winding
(row for row identical to diiid_slayer_n1, including γ_Hz 7.957e+00 (0.57%))
Summary: 14 changed, 7 unchanged

Regression Report: diiid_slayer_n1_chi_perp_e
Quantity                             Ref 1     Ref 2     Diff               Status
SLAYER surface indices               [6 elem]  [6 elem]  0.0e+00            OK
SLAYER poloidal m                    [6 elem]  [6 elem]  0.0e+00            OK
SLAYER toroidal n                    [6 elem]  [6 elem]  0.0e+00            OK
SLAYER minor radius rs               [6 elem]  [6 elem]  3.504e-09 (0.00%)  ** CHANGED **
SLAYER r-based shear                 [6 elem]  [6 elem]  1.149e-02 (0.01%)  ** CHANGED **
SLAYER Lundquist S                   [6 elem]  [6 elem]  9.230e+03 (0.01%)  ** CHANGED **
SLAYER D_norm                        [6 elem]  [6 elem]  3.362e-05 (0.00%)  ** CHANGED **
SLAYER P_perp                        [6 elem]  [6 elem]  2.161e-06 (0.00%)  ** CHANGED **
SLAYER tauk                          [6 elem]  [6 elem]  6.201e-11 (0.00%)  ** CHANGED **
SLAYER iota_e                        [6 elem]  [6 elem]  1.134e-07 (0.00%)  ** CHANGED **
SLAYER Q_e                           [6 elem]  [6 elem]  1.890e-06 (0.00%)  ** CHANGED **
SLAYER Q_i                           [6 elem]  [6 elem]  4.092e-06 (0.00%)  ** CHANGED **
SLAYER Q_root [2/1,3/1,4/1]          [3 elem]  [3 elem]  1.858e-04 (0.01%)  ** CHANGED **
SLAYER ω_Hz [2/1,3/1,4/1]            [3 elem]  [3 elem]  2.3e-01            OK
SLAYER γ_Hz [2/1,3/1,4/1]            [3 elem]  [3 elem]  6.529e+00 (0.57%)  ** CHANGED **
SLAYER no_root flags [2/1,3/1,4/1]   [3 elem]  [3 elem]  0.0e+00            OK
CRF b_r/B_φ crit [2/1,3/1,4/1]       [3 elem]  [3 elem]  3.135e-07 (0.02%)  ** CHANGED **
CRF Q_peak [2/1,3/1,4/1]             [3 elem]  [3 elem]  7.678e-04 (0.11%)  ** CHANGED **
CRF Q0 [2/1,3/1,4/1]                 [3 elem]  [3 elem]  2.870e-07 (0.00%)  ** CHANGED **
CRF b_r crit in tesla [2/1,3/1,4/1]  [3 elem]  [3 elem]  6.097e-07 (0.02%)  ** CHANGED **
SLAYER enabled flag                  1         1         0.0e+00            OK
Summary: 15 changed, 6 unchanged

Regression Report: diiid_slayer_n1_coupled
Quantity                     Ref 1     Ref 2     Diff               Status
SLAYER tauk                  [6 elem]  [6 elem]  6.201e-11 (0.00%)  ** CHANGED **
SLAYER q_shift               [6 elem]  [6 elem]  3.917e-07 (0.00%)  ** CHANGED **
SLAYER coupled Q_root        [1 elem]  [1 elem]  1.688e-04 (0.01%)  ** CHANGED **
SLAYER coupled ω             [1 elem]  [1 elem]  7.1e-02            OK
SLAYER coupled γ             [1 elem]  [1 elem]  2.845e+00 (0.98%)  ** CHANGED **
SLAYER coupled no_root flag  [1 elem]  [1 elem]  0.0e+00            OK
SLAYER enabled flag          1         1         0.0e+00            OK
Summary: 4 changed, 3 unchanged

Regression Report: diiid_truncation_n1
Quantity                               Ref 1          Ref 2          Diff               Status
layer-overlap cap applied              1              1              0.0e+00            OK
layer-overlap psilim                   9.985081e-01   9.985081e-01   2.6e-12            OK
first overlapping surface index        7              7              0.0e+00            OK
diffusive-resistive layer widths (Δψ)  [7 elem]       [7 elem]       6.0e-09            OK
qlim                                   6.200000e+00   6.200000e+00   0.0e+00            OK
psilim                                 9.949767e-01   9.949767e-01   1.576e-10 (0.00%)  ** CHANGED **
mpert                                  37             37             0.0e+00            OK
# singular surfaces                    5              5              0.0e+00            OK
singular q values                      [5 elem]       [5 elem]       0.0e+00            OK
singular psi locations                 [5 elem]       [5 elem]       1.1e-09            OK
plasma energy Re(ep[1])                -1.377422e+00  -1.377408e+00  1.467e-05 (0.00%)  ** CHANGED **
total energy Re(et[1])                 7.978561e-01   7.978614e-01   5.296e-06 (0.00%)  ** CHANGED **
Summary: 3 changed, 9 unchanged

Notes for reviewers

  • Why the SLAYER cases move.
    • Before, the two-pass grid refinement saw kinetic profiles only when a [KineticForces] section loaded them.
    • Now it sees them whenever the equilibrium carries them, on the deck and the API path alike. The deck and API share one grid path, and the API couldn't express the old gate.
    • On these DIII-D-like profiles that adds one ψ knot (217 → 218), and the tearing growth rates shift by up to about 1%.
    • The SLAYER calculation itself reads the same profiles as before. The six cases that already loaded profiles through [KineticForces] are bit-identical.
  • Data and views. PlasmaEquilibrium.kinetic holds the raw KineticProfileData on the file's own ψ grid: densities, eV temperatures, omega_E, chi_e/chi_i/chi_phi, zeff and per-species densities. Consumers build their views from it:
    • KineticForces.ntv_profiles / ntv_species: the 101-point NTV splines and species, with the [KineticForces] charges, masses and scale factors (unchanged maths);
    • the SLAYER view (Runner._slayer_profiles): the table plus the χ⊥,e/χ⊥,i/χ_φ and Z_eff splines, then _slayer_chi_inputs for the scalar fallbacks;
    • grid refinement and resonance nodes;
    • the calculated kinetic matrices;
    • efc_couplings.
  • One read, kept through the re-form. The file is read once at construction and Equilibrium.reform carries it through the two-pass re-form rather than re-reading it.
  • Absent stays absent. Missing datasets are nothing, never zero-filled, in the data. A missing omega_E stays optional for SLAYER as on develop: the view records it (has_omega_E) and TearingProblem warns that those surfaces assume Ω_E = 0 unless omega_E_kHz overrides them.
  • Fail before the solve. An enabled [SLAYER] section with no kinetic profiles errors before the force-free-states solve; a disabled one only skips the overlap scan. Profiles that lack a χ the P_perp_model needs skip the scan with a warning, as on develop.
  • One schema table. The HDF5 reader and writer iterate a single schema table, so a new profile is one table row plus its struct field (chi_i came in this way). A test keeps the table and the struct in step.
  • Scale factors stay [KineticForces] knobs applied only to the NTV view, as before. solve() has no [KineticForces] section, so it uses the defaults.
  • solve(). It now runs kinetic_source = "calculated" when the equilibrium carries profiles. A parity test checks it against the TOML-driven run. The Riccati()/Galerkin() kinetic guard from develop is kept.
  • Tests:
    • attach and re-form carry-through;
    • NTV and SLAYER views equal the data-based functions, including chi_perp_e/chi_perp_i;
    • a view without omega_E carries zero rotation and says so;
    • the transport-profile selection per P_perp_model, now on attached data;
    • calculated-source deck/API parity;
    • attached profiles change the DIII-D two-pass grid on the path main and solve() share;
    • the overlap-scan inputs come from attached data, and an enabled section without profiles errors.
  • Out of scope: the absent-χ rule (whether chi_perp_e/chi_tor keep their scalar 1.0 fallbacks), charge-constant, Z_eff and μᵢ unification, and the resistivity default (later PRs). MatchProblem defaults from the profiles are a small follow-up.

🤖 Generated with Claude Code

@matt-pharr matt-pharr self-assigned this Oct 24, 2026
@github-actions github-actions Bot added api Config key, output dataset or exported name changed changed-results Results move or an interface breaks - read before upgrading labels Oct 24, 2026
@matt-pharr
matt-pharr marked this pull request as ready for review October 24, 2026 21:58
@matt-pharr
matt-pharr marked this pull request as draft October 24, 2026 22:00
@matt-pharr matt-pharr added this to the GPEC v2.0.0 milestone Oct 26, 2026
Base automatically changed from bugfix/coupled-slayer-tauk-rescale-and-rotation to develop October 26, 2026 19:59

@logan-nc logan-nc 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.

Collision map now that #463 (its base) merged: against today's develop a three-way merge shows no conflicting hunks but 20 files changed on both sides, of which the src/ ones are Equilibrium.jl, EquilibriumTypes.jl, ForceFreeStates/CoreTypes.jl, GeneralizedPerturbedEquilibrium.jl, KineticForces/CalculatedKineticMatrices.jl, KineticForces/KineticForcesStructs.jl, plus regression-harness/src/runner.jl. Three things cross it: #551 moves the two Equilibrium files; #580 changes the efc_couplings/run_error_fields signatures that this PR also changes (species list in, profiles arg out); and the rates-v1 kinetic format from the parity plan (#591, A9) needs the [Equilibrium] kinetic_file home this creates, so I will build that on this branch once it is out of draft.

@matt-pharr

Copy link
Copy Markdown
Collaborator Author

@logan-nc this was a draft pending #501, #463, #548, #551. I will rebase once #551 is merged.

@matt-pharr
matt-pharr marked this pull request as ready for review October 27, 2026 14:54
@matt-pharr
matt-pharr requested review from d-burg and logan-nc October 27, 2026 14:54
@matt-pharr

Copy link
Copy Markdown
Collaborator Author

@logan-nc @d-burg this is now ready for your review.

logan-nc and others added 2 commits October 29, 2026 10:10
…tored equilibrium

The replay constructor still passed the pre-kinetic positional arguments, so every post-hoc entry
point built on equilibrium_from_h5 failed with a MethodError. The stored deck's kinetic_file is
re-read against the h5's directory; a file that has since moved warns and leaves the profiles off.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0186CLfjeJeSusNTva5RMfGU
…nder KineticForces or SLAYER and fix the docs cross-reference

A deck carrying the removed KineticForces kinetic_file or SLAYER profile_file/profile_group keys now
stops with the new home named, in main and on replay, and the example-deck hook rejects the SLAYER
keys. The scripting-API page qualifies the attach_kinetic_profiles! reference, which only the
Equilibrium module exports.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0186CLfjeJeSusNTva5RMfGU

@logan-nc logan-nc 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.

Thorough read of the ready version at a54a78889, checked against the source rather than the diff summary. The design is right and I would like this in for 2.0.0. Three things were red in CI, all mechanical, so rather than write them up as review comments I pushed the fixes to the branch on top of a fresh develop merge (post-#593): 7bee5a62c (merge), 559d96d20 (replay constructor) and 255586db5 (retired keys, docs cross-reference, tests). CI is green at that head and the body carries the new harness stamp. Numbers from my end are below.

Blocking today (fixed in the pushed commits)

  1. Both test matrices failed on the replay constructor. src/Rerun.jl:367 still called the positional PlasmaEquilibrium with the seventeen pre-kinetic arguments; the struct now has eighteen, so equilibrium_from_h5 and everything built on it (ResonantDriveContext, the error-field post-hoc path) threw the MethodError that runtests_error_fields.jl:56 reported on 1.11 and 1.x alike. Fix: the restored equilibrium re-reads the stored deck's kinetic_file, resolved against the h5's directory like eq_filename; a file that has since moved warns and leaves kinetic = nothing so a post-hoc analysis that never needed profiles still works. Round-trip test added to runtests_frozen_equilibrium.jl.
  2. Documentation failed on one cross-reference. docs/src/api.md:55 writes [attach_kinetic_profiles!](@ref); the page has no CurrentModule and the top module re-exports Equilibrium but not that function, so Documenter cannot resolve the binding. Qualified as Equilibrium.attach_kinetic_profiles!.
  3. Old decks got an unhelpful stop. [KineticForces] kinetic_file, [SLAYER] profile_file and profile_group are removed, but a deck that still set them failed on an unknown-keyword error from the control constructor rather than through the retired-key stop #569 built. Added them to the retired-key lists with a remedy that names the new home (name the kinetic-profile file once, as kinetic_file (and kinetic_group) in [Equilibrium]), so main and the gpec.h5 replay both stop early with the move spelled out, and extended the example-deck pre-commit hook for the SLAYER keys. Tests in runtests_equil.jl.

The harness stamp afa8e7063 was also stale by the conventions check (develop moved under it); the body is re-stamped at 255586db5 with the run below, and I added the one-sentence rule I ask for next to the release note while I was in there. Adjust the wording if you disagree.

The design

The equilibrium owning its profiles, and the two-pass grid resolving them whenever they are attached, is the right coupling, and the one-schema-table reader/writer is exactly how a new profile should cost one row. I checked the result movement claim: log_asymptotic is the two-pass alias (GridRefinement.jl:192), so the SLAYER decks really do gain the extra knot, and the six cases that already loaded profiles through [KineticForces] being bit-identical is the right control. One point for the release note: the new behaviour reads better as a rule than as two cases. Any two-pass deck that names kinetic_file now refines its grid with the profiles, ideal δW included, so a user who adds the key to an ideal deck in preparation for SLAYER will see their energies move at the 1e-5 level. That is what people will hit, so the note now says it in one sentence.

Deliberately left for later, not requests on this PR

  • solve() cannot take a KineticForcesControl, so the calculated source on the API path is pinned to the default species, charges and scale factors. A kinetic= keyword is a small follow-up once the API settles in #602.
  • The replay re-reads the kinetic file from disk. Storing the raw profiles under Input/ in gpec.h5 would make a moved h5 self-contained, and the schema table makes that a few lines; it belongs with the #388 schema work.

Harness, feynman, 4 threads, Julia 1.11.7, pinned manifest

Run at 255586db5 against origin/develop (5b2258e5a), eleven cases: the body's ten plus diiid_slayer_n1_winding, which develop added since the branch last merged. No stale-baseline warning. The full report is in the body.

case rows changed what moved
diiid_n1 53 0
diiid_error_field 10 0
solovev_kinetic_calculated 20 0
solovev_kinetic_ntv 7 0
solovev_kinetic_multiion 6 0
solovev_kinetic_nuzero 14 0
diiid_slayer_n1 21 14 γ 0.57 %, the rest at or below 0.02 %
diiid_slayer_n1_winding 21 14 row for row identical to diiid_slayer_n1
diiid_slayer_n1_chi_perp_e 21 15 γ 0.57 %, CRF Q_peak 0.11 %, the rest at or below 0.02 %
diiid_slayer_n1_coupled 7 4 coupled γ 0.98 %
diiid_truncation_n1 12 3 plasma energy 1.5e-5, total energy 5.3e-6, psilim 1.6e-10

Same shape as the body's run, with two differences worth naming. The growth rates move a little less than in the 10-27 run (0.57 % against 0.68 %), most likely because develop itself moved under the branch since then (the singfac_min change among others; I have not isolated it), and the critical-resonant-field rows that develop added since the stamp move with everything else, at or below 0.11 %. The six cases that already loaded profiles through [KineticForces] stay bit-identical, which is the control for the claim that only the grid changed.

Tests for the files the fixes touch, at 255586db5: runtests_frozen_equilibrium.jl 32/32, runtests_rerun_from_h5.jl 85/85, runtests_error_fields.jl 291/291 (the file CI failed on before the fix), and runtests_equil.jl 334/335. The one failure there is the i-file re-form equality check at runtests_equil.jl:265, which fails identically on develop 5b2258e5a in the same environment (feynman, native CPU target, 2 or 4 threads) while passing in CI; it is independent of this PR and I will raise it separately. A local docs/make.jl build passes with the cross-reference fix.

@logan-nc
logan-nc merged commit 10ec452 into develop Oct 29, 2026
17 of 18 checks passed
@logan-nc
logan-nc deleted the feature/kinetic-profiles-on-equilibrium branch October 29, 2026 17:35
matt-pharr added a commit that referenced this pull request Oct 29, 2026
Brings #581 as merged, with its two final fixes. The deck stops for the kinetic
file named under [KineticForces] (kinetic_file) or [SLAYER] (profile_file,
profile_group) move to src/Deck.jl's moved-key list with develop's remedy, and the
hook rejects the [SLAYER] keys. Rerun takes develop's _replay_kinetic_profiles.

Also fixes tests stale since the tearing rework: the dispersion tests use
Tearing.Dispersion and GrowthRateResult's omega/gamma, and the multi-ion species
test reads ion_species from [Equilibrium].
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api Config key, output dataset or exported name changed changed-results Results move or an interface breaks - read before upgrading

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants