Skip to content

fix(fp): fp_run refuses the counts it cannot run with, before any work - #242

Merged
k-yoshimi merged 1 commit into
k-yoshimi:developfrom
HengyuLi-Ozaki-lab:fix/fp-run-refuses-bad-counts
Oct 13, 2026
Merged

k-yoshimi merged 1 commit into
k-yoshimi:developfrom
HengyuLi-Ozaki-lab:fix/fp-run-refuses-bad-counts

Conversation

@HengyuLi-Ozaki-lab

Copy link
Copy Markdown
Collaborator

What

fp_run refuses, before any work, the counts the FP solver cannot run with. fp_param_check
(fp/fp_param_registry.f90) holds the rules in one place and fp_api_run calls it before fp_prep. A violation is
FP_ERR_INVALID with one XX fp_run: ... line per violation on unit 6; the handle stays usable, so a corrected count
runs on the next call.

Count Accepted Otherwise, on develop
NRMAX 1 to FP_MAX_NRMAX (100) below 1 the mesh set-up runs past an array bound; above 100 the run is made and fp_get_state returns rc=3
NPMAX, NTHMAX 2 or more the solver runs past an array bound
LMAXFP 0 to HUGE − 1 below 0 no pass is made and fp_loop tests an error flag that no pass has set (fp/fploop.f90:231; on the build measured the process ends); at HUGE the iteration's exit at convergence, N_IMPL = 1 + LMAXFP (fp/fploop.f90:98), overflows, N_IMPL <= LMAXFP (:81) stays true and the step never ends
NSAMAX at most NSBMAX the collision arrays are NSBMAX wide in both species directions (fp/fpcomm.f90:472-474)
NS_NSA(1..NSAMAX) 1 to MIN(NSMAX, NSBMAX) a species has profiles up to NSMAX, and its distribution is copied into the background array at its species number (fp/fpmpi.f90:65-71)
NS_NSB(1..NSBMAX) 1 to NSMAX profiles exist up to NSMAX

A 0 in either map counts as the slot's own number, which fp_set_nsa_nsb substitutes when the run is prepared.
NSBMAX above NSMAX is not refused in itself: with every NS_NSB entry at most NSMAX it runs (measured, below).
FP_MAX_NRMAX is the API's limit, so fp_api passes it in; the registry module does not depend on fp_state.
fp_get_state's own check stays.

Why

fp_param_set takes these counts as they come, and on develop a value the solver cannot run with is met only deep
inside fp_prep or fp_loop: an array index out of bounds (an abort when the library is built with run-time checks,
undefined behaviour in the default -O3 build), a process that ends, a call that never returns, or a run whose results
fp_get_state cannot return. For a host that holds the library in its own process (the MCP server, a Python session)
each of these takes the host with it. This is validation in one batch before the run (#143) instead of a STOP reached
from the library (#142).

Found while documenting FP's parameters for a downstream client (TASK Web Client); in use there since 2026-10-03.

Measured on libfpapi.so (macOS arm64, gfortran 15, -O0 with run-time checks; one process per case, 10 × 10
momentum cells, fp_run(1) unless stated):

Case develop This branch
NRMAX = 101 run made; fp_get_state rc=3 fp_run rc=1
NRMAX = 101 with fp_run(0) accepted; fp_get_state rc=3 rc=1
NRMAX = 0, -1; NRMAX = 0 with fp_run(0) abort (index of array rm) rc=1
NPMAX = 1, 0; NTHMAX = 1, 0 abort rc=1
LMAXFP = -1 XX mtx_abort: ierr_g= -1, the process ends rc=1
LMAXFP = 2147483647 no return within 120 s rc=1
NSAMAX = 2, NSBMAX = 1, NS_NSA(2) = 1 abort (index 2 of lnlam) rc=1
NS_NSA(1) = 3, NSMAX = 3, NSBMAX = 2 abort (index 3 of fnsb) rc=1
NS_NSA(1) = 3, NSMAX = 2, NSBMAX = 3, NS_NSB = {1, 2, 1} abort (index 3 of plf) rc=1
NS_NSB(2) = 3, NSMAX = 2, NSBMAX = 2 abort (index 3 of plf) rc=1
NSBMAX = 3, NSMAX = 2 (slot 3 is species 3) abort (index 3 of plf) rc=1
NS_NSA(1) = -1 abort (index -1 of modelw) rc=1
NS_NSB(2) = -1, NSMAX = NSBMAX = 2 abort (index -1 of pz) rc=1
NS_NSA(2) = 0 (so 2), NSMAX = 1, NSAMAX = NSBMAX = 2, NS_NSB(2) = 1 abort (index 2 of plf) rc=1
NS_NSB(3) = 0 (so 3), NSMAX = 2, NSBMAX = 3 abort (index 3 of plf) rc=1
NRMAX = 100 runs, state returned the same
NPMAX = 2; NTHMAX = 2 runs the same
LMAXFP = 0, 2147483646 runs the same
NSAMAX = NSBMAX = 2; species 2 evolved alone with NSBMAX = 2 runs the same
NSMAX = 2, NSBMAX = 3, NS_NSB = {1, 2, 1} runs the same
a 0 in both maps where the slot's own number is valid runs the same
the same species in two evolved slots (NSMAX = 1) runs the same
NS_NSB(1) = 2, NSBMAX = 1 (species 1 in no background slot) runs, with XX NS_NSA has no correponding NS_NSB the same

Checked

  • Whole suite, upstream CI's flags (pytest python/ --forked --timeout=120 --timeout-method=signal), the seven
    lib*api.so and libtotapi_mono.so built, MONO_LIB_PATH unset; macOS (Apple silicon), gfortran 15.2.0
    (Homebrew, -O0 -fbounds-check -fcheck=all), Python 3.12.13, pytest 9.0.3, mcp 1.27.1, bpsd develop fb8414f:
    • this branch (develop 4a5d977 and this one commit): 757 passed, 35 skipped, 1 xpassed;
    • develop alone, the same build: 723 passed, 35 skipped, 1 xpassed. The 34 more are the new tests.
  • The 35 skips are the 19 equivalence tests that run only on Linux since fplib + wrxlib equivalence tests fail 1e-10 tolerance on develop (mismatches ~1e-9) #213 ("Equivalence tests are
    Linux-canonical"), the mono tests that need MONO_LIB_PATH (CI's mono job runs them) and three ti_mcp tests
    without their data fixture. They were not run here: upstream's Linux CI is their verdict. Downstream, a Linux CI
    with the same four jobs has run them green on this commit merged with other changes, not on this branch alone. The
    xpass is trlib's test_NSMAX_in_range (tr_tst2 fixture: NSMAX>2 mutation produces NaN scalars (RNU/RTU species 3+ unpopulated) #189), as on develop.
  • The tests: python/fplib/tests/test_run_checks.py, 34 tests, one per case and no subTest (pytest --forked reports a failing unittest subTest as passed (24 test files use subTest) #241), each species
    case breaking one rule only. The runs that must still be made take three steps and assert the time reached
    (3 × DELT with a DELT that is not the default), the extents asked for and a finite, positive density (the
    two-cell runs, whose density is wrong, the time only). One test holds the eight XX fp_run: lines of a run that
    breaks every rule. Against develop's library the refusals fail (the table above: an abort of the forked child, a
    run that is made, a session that ends, no return within the time limit) and the runs that must still be made pass.
  • Mutations of the check: 28 (each bound moved by one or removed, the zero substitution removed or changed in each
    map, each map's loop shortened, a message line removed, the call disabled, a larger limit passed), the library
    rebuilt for each. The tests notice every one.
  • make -C fp fp_api_check_so: OK.
  • The LMAXFP = HUGE and HUGE − 1 cases run in a process of their own (subprocess.run(..., timeout=30)), killed if
    it does not end: a forked test that hangs inside a Fortran loop cannot be stopped from outside (pytest-timeout ends
    the session with an internal error and the child keeps running).

Things to know

  • One refusal changes a run that used to complete. With NRMAX above 100 the run was made, fp_get_state
    returned rc=3, and the results could be read only from unit 6 or from fp_regress.dat (FP_REGRESS_DUMP=1). It is
    now refused before the run; CHANGELOG.md lists it under "Changed". Every other rule is a bound the solver indexes
    by.
  • The standalone fp program is unchanged. The namelist path's fp_check (fp/fpparm.f90) stays empty: the menu
    calls it after every input line, where a rule between two parameters (NSAMAX <= NSBMAX) would refuse the state
    between two lines.
  • One code for every refusal. fp_run returns FP_ERR_INVALID; the reason is the XX fp_run: line on unit 6. A
    fp_validate() that returns the list, as eq_validate does and Parameter validation API: pre-run consistency check across modules #143 proposes for every module, could be built on
    fp_param_check.
  • Not refused, and said so in docs/fp-library/architecture.md: NPMAX = 2 or NTHMAX = 2 (it runs and is too
    coarse for a result); a mesh too large for memory, or whose NRMAX × NPMAX × NTHMAX passes a default integer
    (fp/fpprep.f90:597); an evolved species that no background slot names (reported by fp_set_nsa_nsb, and the run
    is made, without collisions with its own kind).
  • fp_get_state after a refusal reads the last run's arrays with the counts now set, past their end when a count
    was raised, as it does on develop after any fp_set_param without a run. The document now says not to call it
    before the next run that is made; the fix belongs to fp_api_get_state and to a pull request of its own.
  • fp/fp_api.h, the header comment of fp/fp_api.f90 and fp_mcp's tool text do not mention the check, so that
    this branch merges with feat(fp_mcp): expose set_param_str tool for KNAMEQ / MODELG=3 equilibrium load #231, fix(fp_mcp): redirect Fortran WRITE(6,...) to stderr to protect JSON-RPC channel #232 and feat(fp): expose global plasma scalars (Ip, stored energy, powers, volume) in fp_state_t #233 without a hand (they change the lines next to them).
    python/fplib/fplib.py (run) and docs/fp-library/architecture.md document it. Say the word and the three texts
    are added here, or in a follow-up once those are merged.
  • Size. 459 added lines: 309 of tests, 94 of Fortran (83 in the registry, 11 in fp_api.f90), 28 of
    documentation, 21 of CHANGELOG, 7 of a docstring.

Scope

Based on current develop (4a5d977) and standing alone on it: one commit. Independent of #226, #231, #232, #233 and
#240: it merges with each of their heads without a conflict (git merge-tree); it shares one file with #233 only
(fp/fp_api.f90, different hunks) and none with the others.

🤖 Generated with Claude Code

fp_param_set takes NRMAX, NPMAX, NTHMAX, LMAXFP and the species maps as
they come, and fp_run then met values the solver cannot run with only
deep inside fp_prep or fp_loop: an array index out of bounds (an abort
when the library is built with run-time checks, undefined without them),
a process that ends, a call that never returns, or a run whose results
fp_get_state cannot return.

fp_param_check (fp/fp_param_registry.f90) checks them together and
fp_api_run calls it before fp_prep (validation in one batch before the
run, k-yoshimi#143, instead of a STOP reached from the library, k-yoshimi#142). A violation
is FP_ERR_INVALID and one "XX fp_run: ..." line on unit 6; the handle
stays usable, so a corrected count runs on the next call.

  NRMAX              1 .. FP_MAX_NRMAX (the state layout, passed in by
                     fp_api)
  NPMAX, NTHMAX      2 or more
  LMAXFP             0 .. HUGE-1. At HUGE the iteration's exit at
                     convergence, N_IMPL = 1 + LMAXFP (fp/fploop.f90),
                     overflows and N_IMPL <= LMAXFP stays true. Below 0
                     no pass is made and fp_loop tests an error flag
                     that no pass has set
  NSAMAX             at most NSBMAX (the collision arrays are NSBMAX
                     wide in both species directions)
  NS_NSA(1..NSAMAX)  1 .. MIN(NSMAX, NSBMAX)
  NS_NSB(1..NSBMAX)  1 .. NSMAX
  A 0 in either map is the slot's own number, as fp_set_nsa_nsb
  substitutes it. NSBMAX above NSMAX is not refused in itself.

One of these is a change for a run that used to complete: with NRMAX
above FP_MAX_NRMAX the run was made, fp_get_state returned rc=3, and the
results could be read only from unit 6 or fp_regress.dat
(FP_REGRESS_DUMP=1). It is now refused before the run (CHANGELOG:
Changed).

Measured on libfpapi.so (macOS arm64, gfortran, -O0 with run-time checks;
one process per case, 10 x 10 momentum cells, fp_run(1) unless stated):

  case                                   before                   after
  NRMAX = 101                            run; get_state rc=3      rc=1
  NRMAX = 101, fp_run(0)                 accepted; get_state rc=3 rc=1
  NRMAX = 0, -1                          abort (index of rm)      rc=1
  NRMAX = 0, fp_run(0)                   abort (index of rm)      rc=1
  NPMAX = 1, 0; NTHMAX = 1, 0            abort                    rc=1
  LMAXFP = -1                            "XX mtx_abort", exit     rc=1
  LMAXFP = 2147483647                    no return in 120 s       rc=1
  NSAMAX=2, NSBMAX=1, NS_NSA(2)=1        abort (2 of lnlam)       rc=1
  NS_NSA(1)=3, NSMAX=3, NSBMAX=2         abort (3 of fnsb)        rc=1
  NS_NSA(1)=3, NSMAX=2, NSBMAX=3,
    NS_NSB={1,2,1}                       abort (3 of plf)         rc=1
  NS_NSB(2)=3, NSMAX=2, NSBMAX=2         abort (3 of plf)         rc=1
  NSBMAX=3, NSMAX=2 (slot 3: species 3)  abort (3 of plf)         rc=1
  NS_NSA(1) = -1                         abort (-1 of modelw)     rc=1
  NS_NSB(2) = -1, NSMAX = NSBMAX = 2     abort (-1 of pz)         rc=1
  NS_NSA(2) = 0 (so 2), NSMAX = 1,
    NSAMAX = NSBMAX = 2, NS_NSB(2) = 1   abort (2 of plf)         rc=1
  NS_NSB(3) = 0 (so 3), NSMAX = 2,
    NSBMAX = 3                           abort (3 of plf)         rc=1
  NRMAX = 100                            runs, state returned     same
  NPMAX = 2; NTHMAX = 2                  runs                     same
  LMAXFP = 0, 2147483646                 runs                     same
  NSAMAX = NSBMAX = 2                    runs                     same
  NS_NSA(1) = 2, NSBMAX = 2              runs                     same
  NSMAX=2, NSBMAX=3, NS_NSB={1,2,1}      runs                     same
  NS_NSA(2) = NS_NSB(2) = 0,
    NSMAX = NSAMAX = NSBMAX = 2          runs                     same
  NS_NSA = {1,1}, NS_NSB = {1,1},
    NSMAX = 1, NSAMAX = NSBMAX = 2       runs                     same
  NS_NSB(1) = 2, NSBMAX = 1 (species 1
    in no background slot)               runs, with a message     same

The namelist path's fp_check (fp/fpparm.f90) stays empty: the menu
calls it after every input line, where a rule between two parameters
would refuse the state between two lines.

Not checked, and said so in docs/fp-library/architecture.md: a mesh too
large for memory or for a default integer; NPMAX or NTHMAX of 2 (it runs
and is too coarse for a result); an evolved species that no background
slot names (reported by fp_set_nsa_nsb, and the run is made).
fp_get_state after a refusal reads the last run's arrays with the counts
now set, past their end when a count was raised, as after any
fp_set_param: it is not to be called before the next run that is made.

Tests: python/fplib/tests/test_run_checks.py, 34 tests, one per case and
no subTest (under --forked a failing subTest is reported as passed); each
species case breaks one rule only. The runs that must still be made take
three steps and assert the time reached, the extents asked for and a
finite positive density (the two-cell runs, whose density is wrong, the
time only). The LMAXFP = HUGE and HUGE-1 cases run in a process of their
own, killed if it does not end: a forked test cannot be stopped from
outside a Fortran loop. One test holds the eight message lines of a run
that breaks every rule. 28 mutations of the check and of its call (each
bound moved or removed, the zero substitution removed or changed, a loop
shortened, a message dropped, the call disabled), rebuilt one by one: the
tests notice every one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HengyuLi-Ozaki-lab added a commit to HengyuLi-Ozaki-lab/task that referenced this pull request Oct 5, 2026
…-run-checks-proposed

docs(product-line): group I is upstream pull request k-yoshimi#242; what group J waits for, measured

@k-yoshimi k-yoshimi left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at 3eb8f8c4 (based on current develop tip). Approving.

Verified locally (macOS arm64, gfortran 15, repo make.header, pytest 9.1.1 --forked --timeout=120 --timeout-method=signal, with the strict-subTest override from #243 active):

  • Rebuilt fp/libfpapi.so from the PR head (fp_param_check exported) and ran python/fplib + python/mcp-servers/fp_mcp: 119 passed, 3 skipped (the Linux-canonical equivalence cases). All 36 cases in the new test_run_checks.py pass, including the two process-isolated LMAXFP cases.
  • Cross-checked the bounds against the sources they cite: the collision arrays are (…, NSBMAX, NSBMAX) (fp/fpcomm.f90:472-474), FNSB is written at the species number (fp/fpmpi.f90:65-71), fp_loop's DO WHILE (N_IMPL .le. LMAXFP) with N_IMPL = 1 + LMAXFP as the convergence exit, FP_MAX_NRMAX = 100 in both fp/fp_state.f90 and python/fplib/_ffi.py. The registry's set-time caps (NSMAX <= 100, NSAMAX/NSBMAX <= 8) keep the loops' MIN(…, SIZE(NS_NSA)) guards from ever truncating.
  • The check sits after the g_initialized / ntmax < 0 guards and before fp_prep, so a refused handle stays usable, as TestARefusalLeavesTheLibraryUsable shows.

Scope note for the record: the Fortran change is confined to the C-ABI entry (fp_api.f90) and the registry module, no solver change.

Non-blocking:

  • CHANGELOG.md describes the same change under both "Changed" and "Fixed"; one entry (Fixed, with the NRMAX > FP_MAX_NRMAX behaviour change called out inside it) would read cleaner.
  • The refusal reasons go to unit 6 only. Through fp_mcp they are currently shredded into the JSON-RPC stream (the problem #232 addresses), so an LLM client sees only invalid parameter until #232 lands. Not this PR's concern, just the ordering: #232 (after its main() rework) makes this PR's diagnostics visible.

@k-yoshimi

Copy link
Copy Markdown
Owner

Independent Codex review (second reviewer, read-only) found no HIGH items; approval stands. For the record:

@k-yoshimi

Copy link
Copy Markdown
Owner

@cursor review

@cursor

cursor Bot commented Oct 13, 2026

Copy link
Copy Markdown

Bugbot couldn't run

Bugbot is not enabled for your user on this team.

Ask your team administrator to increase your team's hard limit for Bugbot seats or add you to the allowlist in the Cursor dashboard.

@k-yoshimi
k-yoshimi merged commit a0b5840 into k-yoshimi:develop Oct 13, 2026
3 checks passed
HengyuLi-Ozaki-lab pushed a commit to HengyuLi-Ozaki-lab/task that referenced this pull request Oct 14, 2026
…i#245)

The FPCOMM profile arrays (RNT, RWT, RTT, RJT, RPCT, RPWT) keep the extents
of the last fp_prep, while NRMAX / NSAMAX are the current namelist values.
fp_set_param on either count clears g_prepared without reallocating, so
until the next fp_run the two disagree and fp_get_state copied
nsa = 1..NSAMAX, nr = 1..NRMAX out of smaller arrays: a read past the
allocation (silent in the -O3 library build, an abort under -fcheck=all).
k-yoshimi#242's early refusal in fp_run added one more way to be left in that
window.

fp_get_state now copies MIN(NRMAX, SIZE(RNT,1)) x MIN(NSAMAX, SIZE(RNT,2))
(and ntg_last bounded by SIZE(RNT,3)) and reports the copied extents in
state%nrmax / state%nsamax, so the [nsamax][nrmax] slice is always data
that exists: a lowered count truncates the snapshot at once, a raised one
stays capped at the old allocation until the next run. Behaviour before
the first run in a process (nothing allocated: namelist counts, zero
profiles) and the rc=3 refusal above FP_MAX_NRMAX are unchanged.

Tests: python/fplib/tests/test_state_extents.py (9 cases, no subTest):
NSAMAX / NRMAX raised without a run (the two reads past the arrays),
NRMAX / NSAMAX lowered, NRMAX raised then run (the bound follows the
reallocation), NRMAX above the cap still rc=3, fp_run(0), a second handle
in the same process reading before its first run (per-process arrays,
counts reset by fp_init: a 1 x 1 slice of the old data), and the
pre-first-run snapshot; all six profile arrays are checked. The
assertions are on the reported extents and copied values, so they hold
with or without run-time bounds checking. Verified once with fp_api.o
compiled with DFLAGS (-fcheck=all): develop's fp_api.f90 fails the two
raised-count cases, this one passes.

Docs: docs/fp-library/architecture.md (the "do not call fp_get_state in
that window" note from k-yoshimi#242 replaced by the new contract, incl. the
per-process allocation) and the fp state.md tables (en/ja).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TA2KahTs3qLMZL6yjSsWwT
HengyuLi-Ozaki-lab pushed a commit to HengyuLi-Ozaki-lab/task that referenced this pull request Oct 14, 2026
…-yoshimi#189)

tr_tst2 + NSMAX=3 went NaN (WPT/BETA*/TAUE*) after one step. The issue
blamed the equilibrium import copying RNU/RTU(...,3) from an eqdata
built for two species; the tst2 fixture runs MDLUF=0, where tr_prof
builds RN/RT for ns = 1..NSMAX from the namelist pairs PN/PNS and
PT/PTS and RNU/RTU are unused. Species 3 simply keeps the tr_init
defaults PN(3)=PNS(3)=0, so its density is identically zero and the
transport step divides by it (every species' RT goes NaN). Reproduced
on the fixture: PN[3]/PNS[3] set -> finite (even 1e-8), PN[3]=0 with
PNS[3]>0 -> finite, PN[3]<0 -> NaN, PT[3]=0 or PTS[3]=0 -> NaN. NSMAX=2
with PN[2]=PNS[2]=0 failed inside tr_loop (ierr=3) instead.

tr/tr_api.f90: private species_profile_violations (MDLUF=0, ns =
1..MIN(NSMAX, array extents)): negative PN/PNS -> OUT_OF_RANGE, both
zero -> OUT_OF_RANGE_AFTER_DEP, PT<=0 / PTS<=0 -> OUT_OF_RANGE, named in
the registry spelling (PN[3]). tr_api_run calls it whenever it is about
to run tr_prep and returns TR_ERR_INVALID with one "XX tr_run: PN[3]:
..." line per finding on unit 6 (the fp_run contract from k-yoshimi#242; the
handle stays usable). tr_api_validate pushes the same findings as
diagnostics. C-ABI layer only; trprof.f90 untouched. The tr_init
defaults (NSMAX=2) and every fixture / sample input (iter01, m0904:
NSMAX=4 with four positive values each) pass unchanged.

Tests: test_NSMAX_in_range loses its strict xfail (the refusal is the
tolerated controlled failure; k-yoshimi#241's strict subTest keeps the NaN
visible should it return); a positive test runs NSMAX=3/4 finite with
the added species' profiles; python/trlib/tests/test_species_profile_
checks.py (16 cases, one per case): refusal matrix, accepted cases
(incl. PN=0 with PNS>0 and handle reuse after a refusal) and validate()
parity. Docs: python/trlib/README.md, docs/sphinx/modules/tr/{en,ja}/
parameter-setting.md (what validate() checks).

Local (macOS, CI flags): trlib + tr_mcp + totlib 265 passed, 1
pre-existing py3.10 ExceptionGroup failure, 18 pre-existing skips;
`make -C tr tr_api_check_so` OK (the non-PIC tr_api_check_all link
needs the gsaf graphics library, absent on macOS; CI runs it).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TA2KahTs3qLMZL6yjSsWwT
HengyuLi-Ozaki-lab added a commit to HengyuLi-Ozaki-lab/task that referenced this pull request Oct 14, 2026
develop gained k-yoshimi#242, k-yoshimi#243, k-yoshimi#248, k-yoshimi#249, k-yoshimi#251, k-yoshimi#254 and k-yoshimi#255 since this
branch left it. k-yoshimi#248 (fp_get_state bounds its copies by the allocated
extents) touches the same declaration line of fp_api_get_state as this
branch.

One conflict, fp/fp_api.f90: the local variables of fp_api_get_state.
Both sides added names to the same INTEGER statement; the merge keeps
all of them (ntg1_last from this branch, nr_alloc and nsa_alloc from
k-yoshimi#248). The two bodies do not overlap: k-yoshimi#248's copy of the six profiles is
bounded by the allocated extents, and the global scalars of this branch
were already bounded by SIZE(PIT, 1) and SIZE(PIT, 2). In the state k-yoshimi#248
is about (a count changed by fp_set_param after a run) the scalar block
is not executed at all: fp_set_param clears g_prepared, and the scalars
read zero, as this branch's documentation says.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HengyuLi-Ozaki-lab added a commit to HengyuLi-Ozaki-lab/task that referenced this pull request Oct 14, 2026
After the merge of develop, fp_get_state follows two rules in the window
between an fp_set_param and the next run: the six profiles are still the
last run's, within the allocated extents (k-yoshimi#245, k-yoshimi#248), and the seven
global scalars read zero, because they are filled from a prepared state
only. Nothing tested the second in that window; a change of the gate
would have left the suite green.

python/fplib/tests/test_state_scalars_window.py, 8 cases: the state has
the seven; zero before the first run; the volume and the stored energy
after a run, and after a run of zero steps; after a set_param of
NSAMAX + 1, NRMAX + 1 and NRMAX - 1 the seven are zero while timefp and
the profiles are the last run's within the extents k-yoshimi#245 gives (3 x 1,
3 x 1, 2 x 1); the next run fills them again. With the gate changed to
"the arrays are allocated" the three window cases and the last one fail
(measured).

The documents that said "zero until the first run" now also say "and
again after a set_param until the next run that is made, while the
profiles are still those of the last run" (architecture.md, state.md en
and ja, the fplib README, fp_api.h, state.py, the fp_mcp schema text, a
comment in fp_api.f90). "That is made": since k-yoshimi#242 an fp_run can be
refused before it prepares.

CHANGELOG.md gets the entry this pull request did not have.

No code change: the comment in fp_api.f90 is the only Fortran line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HengyuLi-Ozaki-lab added a commit to HengyuLi-Ozaki-lab/task that referenced this pull request Oct 18, 2026
develop gained k-yoshimi#242, k-yoshimi#243, k-yoshimi#248, k-yoshimi#249, k-yoshimi#251, k-yoshimi#254, k-yoshimi#255, k-yoshimi#256, k-yoshimi#257,
k-yoshimi#258, k-yoshimi#259, k-yoshimi#261, k-yoshimi#263 and k-yoshimi#264 since this branch left it.

One conflict, in python/eqlib/_ffi.py, python/fplib/_ffi.py and
python/trlib/_ffi.py alike: the end of load_library. k-yoshimi#261 (k-yoshimi#246) checks
the state layout of the library just loaded and raises
<Mod>libAbiError on a mismatch; this branch closes the directory it
registered with os.add_dll_directory when the load fails. The check is
now inside the block that cleans up, so a library refused for its
layout does not leave its directory in the process-wide DLL search set
either (or an entry in _DLL_DIR_HANDLES). Order as on develop: the
layout check, then the prototypes.

With the check on the load path, the cases of test_platform_libname.py
that fake the loaded library (ctypes.CDLL patched) were refused by it:
24 of 39 failed. Each of the three files now has an autouse fixture that
patches check_state_layout out for them (the check has its own cases in
test_ffi.py, TestLibraryLayout), and one new case per module puts a
check back that raises AbiMismatchError (with the prototypes passing,
so that only the check can refuse the load) and asserts
<Mod>libAbiError, the directory handle closed and nothing registered;
with the check moved after the cleanup block, that case fails (measured
on eq).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HengyuLi-Ozaki-lab added a commit to HengyuLi-Ozaki-lab/task that referenced this pull request Oct 21, 2026
develop now carries the pieces split out of this branch (k-yoshimi#256 eq_save,
k-yoshimi#257 the MCP fd isolation, k-yoshimi#265 the mcp pin) and k-yoshimi#232, k-yoshimi#233, k-yoshimi#240, k-yoshimi#242,
k-yoshimi#243, k-yoshimi#248, k-yoshimi#249, k-yoshimi#251, k-yoshimi#254, k-yoshimi#255, k-yoshimi#258, k-yoshimi#259, k-yoshimi#261, k-yoshimi#263, k-yoshimi#264. 43
files conflicted; each was resolved as follows.

- Taken from develop (36 files): every file where this branch only
  changed the mcp pin to "mcp>=0.9,<2" (develop has ">=1.2,<2", k-yoshimi#265):
  the seven pyproject.toml, six READMEs, the fp, ti, tot, wr and wrx
  servers, the Sphinx MCP pages, the trlib presentation and its
  builder, the 2026-04-18 MCP plan; and the eq_save pieces now in
  develop in their reviewed form: eq/eq_api.f90, eq/eq_api.h,
  python/eqlib/tests/test_save.py, eq_mcp/tests/test_save_tool.py; and
  tr_mcp/server.py, whose changes here were this branch's own fd
  isolation (develop's _fd_isolation replaces it) and the os._exit(0)
  at the end of main().
- The os._exit(0) that ended main() of tr_mcp and eq_mcp on this branch
  (to skip the Python teardown, cited in k-yoshimi#227 item 5) is gone with
  them: develop's main() returns 0, as approved in k-yoshimi#257. The abort it
  avoided was a double DEALLOCATE, which this branch fixed in
  tr/trcomm.f90 (DEALLOCATE_TRCOMM returns when nothing is allocated);
  that fix stays, merged without a conflict.
- eq/eqfile.f90: develop's (EQSAVE without an argument, EQSAVE_IERR
  doing the work, and its kept EQDSKR error), plus this branch's
  MODELG=25 in EQ_READ's EQDSK branch (k-yoshimi#228-2). eq/eqmenu.f90 merged
  without a conflict but kept this branch's "CALL EQSAVE(IERR)"; it is
  develop's "CALL EQSAVE" again, as EQSAVE takes no argument.
- python/eqlib/_ffi.py, eqlib.py, tests/test_eqlib.py: develop's, plus
  this branch's get_psi_rz (the eq_common_get_psi_rz_ prototype, the
  Eq.get_psi_rz method, its closed-handle assertion).
- eq_mcp/server.py: develop's (the save tool of k-yoshimi#256, the shared fd
  isolation), plus this branch's get_psi_rz handler and tool and its
  --print-tools entry (13 tools). eq_mcp/tests/test_server.py: this
  branch's (13 tools with save and get_psi_rz), which contains
  develop's change.
- python/mcp-servers/eq_mcp/tests/test_import_isolation.py (this branch
  only, no conflict) is removed: it pinned this branch's own isolation
  helper (_install_fd_isolation, _FD_ISOLATION_INSTALLED in eq_mcp and
  tr_mcp), which develop's python/_fd_isolation.py replaces; develop's
  tests/test_fd_isolation.py of each of the seven servers checks the
  same contract (an import leaves fd 1 and sys.stdout alone).
- .github/workflows/python-tests.yml: develop's mcp pin; this branch's
  numpy install (the get_psi_rz tests import numpy) and ../task symlink
  step kept.
- CHANGELOG.md did not conflict. The three k-yoshimi#228 entries this branch held
  back in d8b6e4d are added to [Unreleased] Fixed, after its first
  entry, where no other open pull request inserts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants