Skip to content

fix(tr): refuse active species with an unevolvable namelist profile (#189) - #254

Merged
k-yoshimi merged 1 commit into
developfrom
fix/tr-species-profile-check-189
Oct 13, 2026
Merged

k-yoshimi merged 1 commit into
developfrom
fix/tr-species-profile-check-189

Conversation

@k-yoshimi

@k-yoshimi k-yoshimi commented Oct 13, 2026 •

Copy link
Copy Markdown
Owner

Fixes #189: tr_run refuses, before tr_prep, an active species whose namelist profile the solver cannot evolve, and tr_validate reports the same findings. The strict xfail on test_NSMAX_in_range is removed.

Root cause (corrects the issue text)

The issue attributed the NaN to the equilibrium import copying RNU/RTU(...,3..NSMAX) from an eqdata.TST-2 built for two species. That is not the path the tst2 fixture takes: it runs with MDLUF = 0, where tr/trprof.f90 builds RN/RT for ns = 1..NSMAX from the namelist pairs PN/PNS and PT/PTS (RNU/RTU are zero-filled and unused). Reproduced on the fixture:

NSMAX=3 mutation (tst2 fixture) after 2 steps
none (species 3 at the tr_init defaults PN=PNS=0) RN(:,3) = 0 after prep, all species' RT and WPT/BETA/TAUE = NaN** after the first step
PN[3]=0.005, PNS[3]=0.0005 finite
PN[3]=0, PNS[3]=0.0005 (5 steps) finite
PN[3]=1e-8, PNS[3]=1e-9 finite
PN[3]<0 NaN
PT[3]=0 or PTS[3]=0 (density set) NaN

So the transport step divides by the density / temperature of every active species; a species whose density is identically zero or negative, or whose temperature is non-positive at the centre or the edge, poisons the whole state. NSMAX=2 with PN[2]=PNS[2]=0 happened to fail inside tr_loop (TrlibRunError); species 3/4 produced silent NaN.

Change

  • tr/tr_api.f90: new private species_profile_violations (every MDLUF outside the UFILE modes 1/2/3, i.e. tr_prof's namelist branch; ns = 1..MIN(NSMAX, array extents)): non-finite PN/PNS/PT/PTS -> OUT_OF_RANGE; PN/PNS negative -> OUT_OF_RANGE; both zero -> OUT_OF_RANGE_AFTER_DEP; PT <= 0 / PTS <= 0 -> OUT_OF_RANGE; parameters 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 fix(fp): fp_run refuses the counts it cannot run with, before any work #242 contract: the handle stays usable, set the parameters and run again); it also refuses NSMAX > NSM (4) there, which only validate() used to report. tr_api_validate pushes the same findings as diagnostics. No physics code touched (C-ABI layer only).
  • Out of scope, filed: tr: UFILE modes (MDLUF=1/2/3) still take some species from PN/PNS/PT/PTS; the #189 pre-run check does not cover them #252 (the species that MDLUF = 1/2/3 still take from PN/PNS/PT/PTS, selected by MDNI/MODEP/RHOA inside tr_prof; no UFILE fixture to verify a mirror against) and tot: tr: parameter updates after a run bypass tr_api's prepared flag (no re-prep, no #189 pre-run check) #253 (tot_set_param("tr:...") calls tr_param_set directly, so after a first run tot mutations never clear tr_api's prepared flag: no re-prep and no pre-run check; changing that alters tot's coupled-pipeline semantics and needs its own decision).
  • Tests: test_NSMAX_in_range loses its xfail (the refusal is the tolerated "controlled failure"); a new positive test runs NSMAX=3/4 finite with the added species' profiles set; python/trlib/tests/test_species_profile_checks.py (21 cases, one per case): the refusal matrix (defaults, zero density, negative PN / PNS, zero / negative PT, zero PTS, NaN density, +Inf temperature, NSMAX=5 with all profiles set, MDLUF=4 (tr_prof's default branch), species-2 zero density now refused instead of failing in the loop), the accepted cases (profiles set, PN=0 with PNS>0, the fixture as-is, handle usable after a refusal; finiteness asserted on the scalars and every RN/RT entry), and validate() parity (codes, parameter names, -Inf, clean when profiles are set, inactive species ignored).
  • Docs: python/trlib/README.md, docs/sphinx/modules/tr/{en,ja}/parameter-setting.md (list of checks validate() performs), docs/sphinx/modules/tr/{en,ja}/mcp.md (error table: the NSMAX/PN/PT case is now invalid parameter, not calculation failed).

Default state (NSMAX=2, species 1/2 at PN=0.5, PNS=0.05, PT=1.5, PTS=0.05) and every fixture / sample input (tr_iter01, tr_m0904: NSMAX=4 with four positive values each; tr_tst2: NSMAX=2) pass the check unchanged.

Verification

Local (macOS, CI flags): the four touched trlib test files 50 passed; trlib + tr_mcp + totlib 269 passed, 1 pre-existing macOS/py3.10 ExceptionGroup failure, 18 pre-existing skips; make -C tr tr_api_check_so OK (tr_api_check_all's non-PIC link needs the gsaf graphics library, absent on macOS; CI runs it). Every refusal case fails on develop (NaN or TrlibRunError instead of TrlibParamError).

Review

In-house review + Codex (codex-rescue) + ai-review-cycle Codex on the first version: their findings (MDLUF gate shape, non-finite inputs, NSMAX > NSM at run time, RN/RT finiteness in the positive tests, stale docblock and mcp.md rows) are in this commit; the UFILE-mode subset and the tot bypass became #252 / #253. The delta was re-reviewed by Codex (ship; three LOW wording/coverage items applied).

Refs: #189, #143 (batch pre-run validation), #242 (the fp precedent).

🤖 Generated with Claude Code

https://claude.ai/code/session_01TA2KahTs3qLMZL6yjSsWwT


Note

Medium Risk
Changes pre-run validation in the C ABI entry path (not core physics), but alters when run() fails (TrlibParamError vs silent NaN or mid-loop errors) and tightens NSMAX-at-run behavior callers may have relied on.

Overview
Fixes #189 by rejecting active species whose namelist density/temperature profiles cannot be evolved, instead of letting the transport step divide by zero and poison the state with NaN.

tr/tr_api.f90 adds species_profile_violations (non-UFILE MDLUF, species 1..NSMAX): non-finite, negative, or all-zero PN/PNS, and non-positive PT/PTS, reported as PN[ns] / PT[ns] / PTS[ns]. tr_run calls this (and blocks NSMAX > NSM) before tr_prep, returning TR_ERR_INVALID with per-line stdout messages while keeping the handle usable. tr_validate emits the same rules as diagnostics.

Tests drop the test_NSMAX_in_range xfail, add a positive NSMAX=3/4 case with profiles set, and add test_species_profile_checks.py for run/validate parity. Docs (trlib README, Sphinx EN/JA parameter-setting and MCP troubleshooting) describe the new checks and retarget calculation failed guidance toward DT/equilibrium rather than PN/PT.

Reviewed by Cursor Bugbot for commit 72fb0d5. Bugbot is set up for automated code reviews on this repo. Configure here.

…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 #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; #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
@k-yoshimi

Copy link
Copy Markdown
Owner Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 72fb0d5. Configure here.

@k-yoshimi
k-yoshimi merged commit 58f1628 into develop Oct 13, 2026
4 checks passed
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 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.

1 participant