feat(fp): expose global plasma scalars (Ip, stored energy, powers, volume) in fp_state_t - #233
Open
HengyuLi-Ozaki-lab wants to merge 1 commit into
Open
Conversation
…lume) in fp_state_t
Motivation
----------
`fp_get_state` returned only six per-unit-volume profile arrays
(RNT/RWT/RTT/RJT/RPCT/RPWT) plus mesh sizes. In the common configuration
(MODELD=0, no wave model, no current drive) those moments are computed
grid-point-locally, so the equilibrium enters the physics *only* through
the volume element VOLR and the grid metric -- neither of which was in
the state. Measured on the fp_iter01 fixture, swapping the analytic
equilibrium for an EQ-produced eq.bin with a very different geometry
(RR 3.0->3.6, RA 1.0->1.2, BB 3.0->4.5, RIP 2.0, RKAP 1.8) leaves all
six arrays **bit-for-bit identical**, while the underlying plasma volume
nearly doubles. EQ->FP handoff therefore worked in the library but was a
silent no-op to any MCP client.
The quantities that *do* respond were already computed by FPWRTGLB --
they were just WRITE(6,...)'d and thrown away. This commit routes them
into the state struct.
New fields (all species-summed at the latest NTG1 sample)
--------------------------------------------------------
TOTAL_IP [MA] sum PIT; PIT = sum_NR RJS*VOLR/(2*pi*RR).
RJS carries the *1.D-6 A->MA factor at
fp/fpsave.f90:1310; printed at fpsave.f90:311.
STORED_ENERGY [MJ] sum PWT; PWT = sum_NR RWS*VOLR.
RWS is *1.D-6 J->MJ at fp/fpsave.f90:1377;
printed as the W column of fpsave.f90:252.
COLLISION_POWER [MW] sum PPCT; RPCS *1.D-6 at fpsave.f90:1509,
printed at fpsave.f90:307.
ABSORPTION_POWER [MW] sum PPWT; RPWS *1.D-6 at fpsave.f90:1510,
printed at fpsave.f90:305.
ABSORPTION_WR [MW] sum PWRT -- the "WR:" term of fpsave.f90:305.
ABSORPTION_WM [MW] sum PWMT -- the "WM:" term of fpsave.f90:305.
PLASMA_VOLUME [m^3] TVOLR = sum_NR VOLR, fp/fpprep.f90:231-234,
printed in the DEVICE banner at fpprep.f90:237.
All seven are zero until the first run: TVOLR and the P*T(NSA,NTG1)
accumulators only exist after fp_prep, and only carry a sample once
FPSGLB has bumped NTG1 (fp/fploop.f90:200). fp_get_state guards on
g_prepared + NTG1 >= 1 + ALLOCATED so it never exposes uninitialised
memory.
Why a `scalars` block
---------------------
The Python wrapper groups them under a top-level `scalars` key, matching
`trlib.state.TrState.to_dict()`. FP was the odd one out: consumers that
branch on `state.get("scalars")` (e.g. task-web's mcp_bridge
steady-state detection) silently saw None for every fp state. Same shape
now means the same code path works for both modules.
ABI
---
The 7 doubles are APPENDED to fp_state_t, so every pre-existing member
keeps its offset. libfpapi.so MUST be rebuilt (`make -C fp libfpapi.so`)
-- a stale .so read through the longer ctypes layout returns garbage in
the trailing members. Three declarations are kept in lockstep:
fp/fp_state.f90, fp/fp_api.h, python/fplib/_ffi.py::FpStateC; a new test
asserts the scalars are the last 7 fields and all c_double.
Verification (fp_iter01 fixture, NTMAX=2)
-----------------------------------------
G1 analytic G2b eq.bin ratio
PLASMA_VOLUME 28.42446 53.12014 1.8688
STORED_ENERGY 5.368302 10.33949 1.9260
COLLISION_POWER -0.2130797 -0.4073574 1.9118
TOTAL_IP 1.497e-13 1.635e-13 (noise; no E-field)
ABSORPTION_* 0 0 (no wave model)
Every value agrees with the corresponding Fortran WRITE(6,...) line of
the same run to full printed precision, which is the correctness check
for both the units and the struct layout. TOTAL_IP was separately
exercised with E0=0.05: state 45.24848 vs printed 4.5248E+01.
The six profile arrays are bit-for-bit unchanged versus a run against
the pre-change .so, confirming this is purely additive and does not
touch the computation path.
Tests: fplib 47 passed, fp_mcp 46 passed. The single
test_equivalence::test_iter01 failure (37 profile[*].RPCT mismatches at
1e-10) is PRE-EXISTING -- reproduced identically on a pristine rebuild
of HEAD, and _to_baseline_shape() does not read the new `scalars` key.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds the seven geometry-dependent global plasma scalars that FP already computes to
fp_state_t, exposed as a TR-stylescalarsblock:TOTAL_IPΣ_NSA PIT(NSA,NTG1)—fpsave.f90:110,146,286, printed:311STORED_ENERGYΣ_NSA PWT(NSA,NTG1)—fpsave.f90:112, printed in theWcolumn:252COLLISION_POWERΣ_NSA PPCT(NSA,NTG1)—fpsave.f90:114,278, printed:307ABSORPTION_POWERΣ_NSA PPWT(NSA,NTG1)—fpsave.f90:115,277, printed:305ABSORPTION_WRΣ_NSA PWRT(NSA,NTG1)—fpsave.f90:284, same print lineABSORPTION_WMΣ_NSA PWMT(NSA,NTG1)—fpsave.f90:285, same print linePLASMA_VOLUMETVOLR—fpprep.f90:231-234, banner:237All seven were already available as
fpcommmodule data (PIT/PWT/PPCT/PPWT/PWRT/PWMTare(NSAMAX, NTG1M)allocatables;TVOLRis a scalar). None is computed at print time, so all are retrievable afterfp_run. The MW/MA conversions are the ones already applied in the moment integrators (fpsave.f90:1310,:1377,:1509,:1510).Motivation
FP's
get_statecurrently returns only the six per-species profile arrays and the mesh dimensions. Those six are local per-unit-volume moments: withMODELD=0and no wave or current-drive model, each radial cell is an independent 0-D collisional relaxation depending on local n, T, Z and E — not on B, R or ε.The practical consequence: loading a completely different equilibrium changes nothing observable. Running
fp_iter01against an analytic equilibrium versus against aneq.binwritten byeq'ssavetool with substantially perturbed geometry (RR 3.0→3.6, RA 1.0→1.2, BB 3.0→4.5, RIP 1.0→2.0, RKAP 1.5→1.8) produces bit-identicalRNT/RWT/RTT/RJT/RPCT/RPWT.The equilibrium is being consumed —
eqgetbwrites the loaded geometry back into FP, the mesh metrics move (rsrhon0.805→1.0264,bpm0.3506→0.1829,ql2.2961→7.0134), and the global integrals scale with the volume ratio — but it enters only through the volume element and the mesh metrics, andfp_state_texposed neither. So an MCP/library client driving theMODELG=3equilibrium-load path had no way to tell whether the file had been used at all.(Ruled out as a stale-state artifact: changing
PTPR/PTPP,PNorNTMAXdoes move the arrays. Two further attempts to surface a difference — a strong loop voltage drivingRJT, and radial transport withMODELD=1— were also bit-identical, with a control confirming the perturbedeq.binhad been loaded in that arm.)Why a
scalarsblocktrlib'sTrState.to_dict()already returns a top-level"scalars"key;fplib's did not. Beyond exposing the new values, this removes that shape inconsistency — downstream code that readsstate["scalars"]generically (for example steady-state detection that looks up a named scalar) silently gotNonefor FP.Verification
Rebuilt with the repo's current
make.header(-fbounds-check -fcheck=all, unmodified).1 — values agree with the Fortran's own output. Each scalar was compared against the
WRITE(6,...)lines of the same run at full precision:TVOLR 0.284245E+02,W 5.3683E+00, collision-2.1308E-01,IP 1.4970E-13, absorption0.0000E+00. This simultaneously validates the unit conversions and the struct layout — a layout error would read garbage. A supplementary probe withE0=0.05droveTOTAL_IPto45.24848, matching the printed4.5248E+01.2 — the equilibrium is now observable, and the ratios reproduce what the Fortran diagnostics independently showed:
eq.bin(perturbed)PLASMA_VOLUMESTORED_ENERGYCOLLISION_POWERThe six profile arrays remain bit-identical between those two runs — i.e. the underlying physics is unchanged and it is the new scalars that make the difference visible.
3 — no regression. State captured with the pre-change
.soand with the rebuilt one is bit-identical across all six arrays, the five integer dimensions andTIMEFP. Purely additive.4 — initialisation safety. After
initwithoutrun, all seven scalars read 0 (theg_preparedguard prevents an uninitialisedTVOLRfrom leaking); afterrun, they are populated. Filling is bounds-guarded withMIN(NSAMAX, SIZE(PIT,1))/MIN(NTG1, SIZE(PIT,2)).5 — tests.
fplib47 passed (was 45),fp_mcp46 passed (was 45). New coverage asserts the seven fields are appended last inFpStateC._fields_and are allc_double, thatfrom_c/to_dictcarry them, and that the MCPSTATE_SCHEMAstays in sync withSCALAR_FIELDS.6 — determinism. 5/5 fresh runs of each arm, byte-identical outputs.
Honest caveats
ABSORPTION_POWER/ABSORPTION_WR/ABSORPTION_WMare verified only at zero. I could not drive them non-zero through the C ABI:MODEL_WAVE=1withPABS_LH=1.0still yields 0, and the LH/EC resonance-mesh parameters (DELNPR,NPRMAX, …) are not infp_param_registry.f90, so they are unreachable from this path. The summation follows exactly the same pattern as the two power fields that are verified against non-trivial values, but a reviewer with a working wave configuration should sanity-check them.fp_state_t, solibfpapi.somust be rebuilt;python/fplib/_ffi.pyis updated in lockstep.test_equivalence.py::test_iter01fails on this branch — pre-existing, not introduced here. Verified by stashing the entire change, rebuilding, and re-running: the same failure appears with 37 mismatches, all inprofile[*].RPCTat rel_err ~1e-9–1e-10. Structurally it cannot be affected either:_to_baseline_shape()reads only the six arrays, the integer dimensions andTIMEFP, never the newscalarsblock.Files
fp/fp_state.f90,fp/fp_api.f90,fp/fp_api.h,python/fplib/_ffi.py,python/fplib/state.py,python/mcp-servers/fp_mcp/server.py, plus tests inpython/fplib/tests/andpython/mcp-servers/fp_mcp/tests/.Independent of the two companion
fp_mcpPRs (#231set_param_str, #232 stdout guard); this one may need a trivial rebase if #231 lands first, since both touchfp_mcp/server.pyin different places.🤖 Generated with Claude Code