Repository navigation
Reconcile bpsi/develop base into develop (bpsi↔fork merge baseline, CI-green) - #226
HengyuLi-Ozaki-lab wants to merge 81 commits into
Conversation
For web-client EQ→TR file coupling. Adds ~25 lines to eq_api.f90. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Exposes the eq_save C-ABI symbol (added in Task 1.1 / SHA eb55843) as Eq.save(path): sets KNAMEQ then calls eq_save, raising EqlibError on non-zero rc. Adds ctypes prototype in _ffi.py and a TDD integration test that verifies file existence and non-zero size. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add handle_save handler and @mcp.tool() save registration; update --print-tools list and the test_print_tools_output count from 11 → 12. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… channel Fortran's WRITE(6,...) maps to OS fd 1 (process stdout), which is the same fd the MCP stdio transport uses for JSON-RPC framing. Any Fortran diagnostic line written to fd 1 corrupts the channel and causes 'Connection closed'. Add _redirect_fortran_stdout_to_stderr() context manager to eq_mcp and tr_mcp servers: saves fd 1, dup2s fd 2 (stderr) over fd 1 before Fortran calls, then restores fd 1 afterwards. Applied to eq.run(), eq.save(), and tr.run(). Do NOT use fflush(NULL): on macOS/Python it also flushes the Python-level asyncio write buffer while fd 1 still points to stderr, sending the pending JSON response to stderr and causing Connection closed on the client side. Fortran WRITE(6,...) is line-buffered, so output reaches stderr before fd 1 is restored without an explicit flush. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previous redirect protected fd 1 during the Fortran call but didn't flush libc's stdout buffer. Pending Fortran output then leaked into the MCP pipe after restoration, corrupting JSON-RPC on subsequent calls (notably get_state after run). Now we fflush() libc's C-level stdout FILE* before restoring fd 1; this does NOT affect Python's asyncio writes because Python writes via syscalls, not via C stdout. Also extend the redirect to cover set_param, set_param_str, set_params, get_state, and finalize in both eq_mcp and tr_mcp, since those handlers also call Fortran routines that may print.
Add setvbuf(stdout, NULL, _IONBF, 0) at server startup in both eq_mcp and tr_mcp so that any Fortran WRITE(6,...) output is emitted immediately rather than staying in libc's buffer after the fd-level redirect has been torn down. Also add _gfortran_flush_i4 (Fortran-level I/O buffer flush for unit 6) inside _redirect_fortran_stdout_to_stderr() so that gfortran's own internal buffer is drained into stderr before fd 1 is restored to the JSON-RPC pipe. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…imi#194) Adds belt-and-suspenders fd-isolation at module load time, beyond the per-call redirect already in 69d3d9d4. Disables _gfortran_flush_i4 in the long-lived server path to avoid SIGABRT during teardown. Uses os._exit(0) in main() to bypass Python finalizers that race with Fortran cleanup. Local-only patch on kyoshimi-develop; will be re-evaluated after upstream macOS build support (f8f5c9e) lands and macos_stubs.f90 is dropped (which may make the SIGABRT-on-teardown class obsolete).
|
Re-verified the three #228 P1 items at head
Suggested shape, so they land in the remainder of this PR (or as a 4th small PR after the three in my previous comment): #228-2 is a one-line widening of the Status of the gate items on my side: #239 is fixed in PR #249 (comparator now compares |
wrx gate: quantified shift and a baseline-layout problemCompared the committed wrx baselines on
So the "2e-2 deposition-peak shift" is the rl peak moving by one (re-centred) bin; the rs profiles and the totals are bit-identical. That is the record I would put in front of the lead for the physics sign-off: the change in Baseline layout (blocks #249 after it merges). The regenerated |
|
Follow-up, after building
|
|
The split is up, in your order:
On your three comments of today:
|
pl/plinit.f90 documents MODELG=25 (EQDSK geometry) and EQ_READ in
eq/eqfile.f90 loads it, but plcheck refused every value outside 0..11:
XX plcheck: INVALID MODELG: MODELG= 25
with pl_parm returning 101. The PL menu's P command (mode 0) asks again
on that, so it could not be entered there; a plparm namelist (mode 1)
and a "MODELG=25" line (mode 2) set the value and report the error.
plcheck now allows 25 by name; the range itself is not widened, and the
checks of that branch (model_prof, MODELQ) still run for it.
Test: python/eqlib/tests/test_plcheck_modelg.py. No C entry point reaches
pl_parm (the main programs and the PL menu call it), so the test compiles
a 12-line driver against the PIC build of pl that the shared libraries are
linked from, with the FCFREE of make.header, and reads back pl_parm's ierr.
Six cases: 25 accepted, 5 accepted, 24, 26 and -1 refused, and
"MODELG=25,MODELQ=7" refused for MODELQ only. Without the change in
plparm.f90 the two cases with 25 fail, with the message above.
Not changed:
- MODELG 12 and 13 (documented in plinit since before this branch), 14
(a case of pl/plprof.f90) and 15 (loaded by EQ_READ) are refused by
the same range check, on develop as well.
- Nothing but EQ_READ knows 25: pl/plprof.f90, tr_set_metric,
tr_bpsd_get and the KNAMEQ check of eq_validate name 3, 5 and 8
(tr_bpsd_get also 9), and dp/dpdisp.f90 treats every MODELG above 10
like 0 and 1 (PL_PROFW3D).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…imi#228 finding 3) tr_bpsd_get adopts EQ's dpsi/dV into RDPVRHOG/RDP while the saved flag tr_eq_init (tr/trbpsd.f90) is set; tr_set_metric cleared it after the initial q-scaling and nothing set it again. Measured through libtrapi with MODELG=9, the other parameters at the tr_init defaults (macOS, gfortran 15, -O0 -fcheck=all, as everything measured below): - second tr_init .. tr_finalize lifecycle in one process: tr_run returns 0 and every q is NaN (RIPS = 1 and 3; AJT = 0 at RIPS = 1); - second tr_prep inside one lifecycle (a parameter set after a run): tr_run returns 0 and the prepared state is not the first one, AJ differs by up to 57% (RIPS = 3). By reading, the menu's second R takes the same path (R is tr_prep followed by tr_loop). tr_set_metric now sets tr_eq_init at the start of the MODELG=9 block: tr_prep is its only caller and every tr_prep builds a new initial state. tr_init and tr_finalize of the C API restore the flag as well, as k-yoshimi#228 proposes; alone they repair the lifecycle case only (measured: without the line in tr_set_metric the second-preparation test below still fails). After the change a second and a third lifecycle are bit-identical to the first, and a state prepared again inside one lifecycle has the profiles (QP, AJ, RN, RT) of the first, bit for bit. A first lifecycle is unchanged: MODELG=9 with RIPS = 1, 3 and 10 (5 steps) and RIPS = 1 with NTEQIT = 2 (6 steps), each in a process of its own, give bit-identical states before and after. Tests: python/trlib/tests/test_modelg9_init.py, four cases (finite first lifecycle near the commanded current; second lifecycle; third lifecycle; second preparation in one lifecycle). The last three fail without the change. They run on the tr_init defaults, RIPS = RIPE = 3 set by name, where the q-scaling agrees after three equilibrium solves without a warning; with RIPS = 1 it takes seven and two of the solves run out of NLPMAX. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…finding 4)
The initial q-scaling of tr_set_metric (MODELG=9) went on after every
kind of failure: eq_calc had no way to report one, the status of
tr_bpsd_get was printed, that of tr_bpsd_put ignored, the scale factor
Ip(EQ)/RIPS was divided by unchecked, 30 iterations without agreement
ended silently, and tr_eq_init was cleared on every path.
Now each of these ends tr_set_metric with ierr /= 0 (tr_prep stops,
tr_run returns TR_ERR_CALC_FAILED), the cause on unit 6 and tr_eq_init
still set:
- eq_calc fails. eq_calc (eq/equnit.f90) has an optional ierr; with it
the routine stops at the first of its four steps that returns a
status (eq_bpsd_get, EQCALC, EQCALQ, eq_bpsd_put) instead of running
EQCALQ and the put on an equilibrium that was not solved. EQLOOP
running out of NLPMAX (EQCALC's 100) is not a failure there: EQCALC
goes on after it. The line on unit 6 names the iteration of the
scaling. Called without ierr it does what it did; trx and trm call it
that way and are not touched (both were compiled against the new
interface, make -C trx libtr2.a and make -C trm libtr2.a). trn uses
the stub module equnit_mod of pl/noeqlib.f, not this routine.
- tr_bpsd_get or tr_bpsd_put returns a status.
- the scale factor is zero or not finite (ierr = -1).
- no agreement within 1e-3 after 30 iterations (ierr = -2).
RIPS <= 0 ends the loop after the first solve as before (by reading: on
the defaults the first solve fails for RIPS = 0 and -1). ierr is set to
0 at the top of tr_set_metric: for a MODELG without a branch it was
returned unassigned.
Only the first of the four was reached by the inputs tried. Measured
through libtrapi, MODELG=9, RIPS = RIPE varied, the rest at the tr_init
defaults, a build with -O0 -fcheck=all (macOS, gfortran 15), one step:
RIPS before now
0.01 0.1 0.3 0.5 0.7 0.75 0.8 process ends (*) CALC_FAILED
0.81 returns 0 CALC_FAILED
0.82 process ends (*) CALC_FAILED
0.83 returns 0 the same state
0.84 returns 0 CALC_FAILED
0.85 0.9 0.95 1 1.2 1.5 2 3 5 returns 0 the same state
10 10.5 returns 0 the same state
11 12 13 14 14.5 15 20 25 30 100 process ends (*) CALC_FAILED
(*) at a run-time check: an unallocated array in bpsd_spl1D, or index
0 of ya at eqcalq.f90:1431.
"the same state": AJT, Q0, q at the first and the last point and AJ at
the first print the same (shortest form that reads back exactly); for
RIPS = 1, 3, 10 (5 steps) and RIPS = 1 with NTEQIT = 2 (6 steps) the
whole state is bit-identical.
CALC_FAILED is EQCALC's 103 (the axis leaves the plasma) everywhere but
at 0.84, where it is EQCALQ's 103.
So two of the sampled inputs, 0.81 and 0.84, returned 0 before and fail
now. Their q-scaling went through solves that had failed (50 and 7
"XX EQAXIS" lines) and tr_run still returned 0, with AJT = 0.809 and
0.838 after the step; whether the scaling had agreed or run out of its
30 iterations the old code did not say. The same path ends the process
at 0.82. After a failure, a good run on the same handle or in a new
lifecycle is bit-identical to a first run.
The other three branches were each run once in a build with a fault
injected after the call (a status from tr_bpsd_get, from tr_bpsd_put,
Ip(EQ) = 0, NaN, Inf, a limit of 2 iterations): TR_ERR_CALC_FAILED every
time, and the good run after it bit-identical to a first run. That build
is not part of the commit. With the bpsd this branch builds against, the
two put checks cannot fire: every bpsd_put_data sets ierr = 0.
TR and EQ baseline cases (tr_iter01, tr_tst2, tr_m0904, the defaults, a
current ramp; eq_iter01, eq_tst2, the analytic default) through the
libraries: bit-identical before and after.
Tests: five cases added to python/trlib/tests/test_modelg9_init.py (an
equilibrium that cannot be solved, RIPS = 100, fails the preparation, in
a first lifecycle and in a later one; the line on unit 6 names eq_calc
inside tr_set_metric, read from a child process because the Fortran
run-time writes unit 6 out when the process ends; a good run after the
failure on the same handle equals a first run; so does the next
lifecycle). The failing runs are run(0), which prepares and takes no
step, so that an error of the transport step cannot stand in for the
preparation's. All five fail on the previous commit: in a first
lifecycle the process ends at the run-time check, in a later one
tr_run(0) returns 0 on the failed equilibrium.
Not changed, and still able to hide a failure or end the process:
- A status that EQCALC or EQCALQ do not return. MODELG=9 with RA = 1.8
(or 2.0), the rest at the defaults: EQMAGS fails inside EQCALQP
(eqcalq.f90:235, its status is not tested), EQCALC and EQCALQ return
0 up to there, and a build with run-time checks ends at
eqcalq.f90:1431, before and after this commit. EQSOLV prints a
BANDRD failure and goes on; EQCALQ overwrites the status of
EQSETS_RHO with that of EQSETS.
- tr_bpsd_get returns the status of its last bpsd_get_data only,
tr_bpsd_put that of its last bpsd_put_data, eq_bpsd_get and
eq_bpsd_put that of their last call.
- tr_loop's periodic eq_calc (NTEQIT) ignores the outcome, as do trx
and trm. With NTEQIT = 1 and a failing solve after two good steps
(BB zeroed through the module variable's symbol; NTEQIT is not in
the registry) tr_run returns 0 and takes the next steps.
- EQLOOP at NLPMAX is accepted inside the scaling (the EQ menu and
eq_run stop on EQCALC's 100): the runs that return 0 at RIPS = 0.83,
0.85, 0.9 and 1 carry 8, 5, 6 and 2 such solves.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
#228-2, -3 and -4 are on the branch, one commit each, in the shapes you gave. Two places go further than the shape, because measuring showed the shape alone did not close the finding; both are marked, so that you can take them out. #228-2 ( #228-3 ( #228-4 ( What that does, measured through
So two of the sampled inputs, 0.81 and 0.84, returned 0 before and fail now: their q-scaling went through solves that had failed (the axis leaves the plasma, status 103) and The three other branches (a status from Not changed, each seen while measuring:
Tests: |
The entries that 5565ede, 0dd157d and 6a80407 added at the top of [Unreleased] sit where develop (f41fe5b) added sections of its own since this branch last merged it. The pull request stopped merging cleanly, and its workflows do not run while it conflicts. CHANGELOG.md goes back to what 682d9af had. The three entries are kept outside the repository and are to be put back by hand, under develop's "### Fixed", when develop is next merged into this branch. No code or test changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
The push made this pull request conflict with Two tests of this run will be red, and not because of #228: |
… reported Split out of #226 at the maintainer's request: the save of an equilibrium through the library, together with its error reporting (#227 item 3), so that the contract ships with the code. What is added - eq_save() in the C ABI (eq/eq_api.f90, eq/eq_api.h): writes the equilibrium of the last eq_run to the TASK/EQ binary file named by KNAMEQ, through EQSAVE. - Eq.save(path) in eqlib: sets KNAMEQ, calls eq_save. - The `save` tool of eq_mcp (12 tools now). The file is the one MODELG=3 with KNAMEQ loads. A save that wrote nothing is an error - EQSAVE has an argument now, IERR_OUT: 0 when the file was opened and written; FWOPEN's IERR when it refused the file; 9 when the file was opened and its first record could not be written. That last case is an existing file without write permission: FWOPEN opens it for reading and returns 0, and the first WRITE used to end the program. EQSAVE returned silently on a refused open before. - eq_save returns EQ_ERR_NOT_INIT before eq_init. It returns EQ_ERR_INVALID, touching no file, when the fields do not hold the equilibrium of the present parameters: no successful eq_run since eq_init, a later solve or load that failed (it leaves the fields part-way), or eq_set_param of anything but MODEFR and MODEFW since (EQSAVE writes the parameters and counts as they stand over fields computed for the old ones). What EQSAVE writes in those states can stop the process that loads it. It returns EQ_ERR_INVALID too for a blank KNAMEQ, and for MODEFW other than 0 (overwrite an existing file) or 3 (refuse one): with 1 or 2 FWOPEN asks Y/N on unit 5 before it overwrites an existing file, with 4 to 7 it asks for the file name there, and unit 5 of a stdio server is the JSON-RPC pipe. It returns EQ_ERR_CALC_FAILED when EQSAVE reports that the file could not be opened (a directory that is missing or not writable, MODEFW=3 and an existing file) or not written, or when no non-empty file exists afterwards. Looking for the file alone is not enough: a save to a path that already holds an earlier file would pass for a success. - eq_run(1) reports a load that failed, which the precondition above rests on. EQ_READ called EQCALQ after EQDSKR had failed and handed back EQCALQ's code, and left IERR unassigned for a MODELG that is no file format: on develop the load of a missing EQDSK file (MODELG=5) ends the process at a STOP, exit status 0, in a build with run-time checks. EQ_READ now starts from IERR=0, calls EQCALQ only after a successful EQDSKR, and returns 1 for an unknown MODELG. (The product line has had these three lines since September.) - Eq.save takes a str or an os.PathLike, and refuses a path that ends in a blank (Fortran drops the blank and would write another file) or is longer than the 80 bytes of KNAMEQ, with EqlibInvalidParamError. - The other call sites of EQSAVE pass a variable now: the `S` command of the eq menu, and tr/f77/trfile.f, a file no Makefile builds, whose `CALL EQSAVE(4)` fitted neither the old routine nor the new one. - eq_mcp described MODEFR and MODEFW as "frequency-space mode switch" and "wave-frequency mode switch"; they are the file modes of FROPEN and FWOPEN, and say so now. Tests - python/eqlib/tests/test_save.py (20 cases, in a temporary directory with relative names): the saved file loads back with MODELG=3 and gives the grids and the two profiles it was saved with, exactly, and five scalars that are recomputed after the load, to 1e-9; a pathlib path and something that is no path; no run yet; a run that failed after a good one; a failed EQDSK load after a good run and a changed parameter; a parameter set after the run; the file modes set after the run; a missing directory; a blank path; 80 bytes taken and 81 refused; a path ending in a blank; a repeat save refused by MODEFW=3; a read-only file; MODEFW 1, 2, 4, 5, 6 and 7. - python/eqlib/tests/test_load_errors.py (3): the load of a missing file with MODELG 3 and 5, and with a MODELG that is no file format, is EqlibCalculationFailedError. - python/mcp-servers/eq_mcp/tests/test_save_tool.py (6): the handler, the two error mappings, the tool list, one sequence through the real library, the two descriptions. - eq/tests/c_abi/test_negative.c: eq_save before eq_init. The Makefile target that runs it does not link in a build without the graphics library (on develop neither, and CI does not run it); built against libeqapi.so directly, it passes. - With a fault put in and the library rebuilt: eq_save ignoring EQSAVE's argument fails the repeat-save and the read-only tests; EQSAVE's first WRITE without IOSTAT fails the read-only test; no MODEFW check fails the six MODEFW cases; no check for an equilibrium fails two tests; the flag not cleared when a run starts, or not cleared by a setter, fails the test of that case; the flag cleared by the file modes too fails two. - One real stdio session (initialize, init, save, run(mode=0), save, save into a missing directory): "invalid parameter: eq_save: rc=1" and no file before the run; a 45956-byte file after it; "calculation failed: eq_save: rc=3" for the missing directory. python/ with CI's flags, macOS arm64, gfortran 15: 802 passed, 35 skipped, 1 xfailed (develop a0b5840: 773 passed). The state after a run is bit-identical with develop: the two EQ fixtures that load a file and the analytic default (512 values each), and tr_tst2, tr_iter01, tr_m0904, the TR defaults and a current ramp. Not changed, and known - EQSAVE writes PSI, DELPSI and HJT as (NSG,NTG) blocks where the arrays are (NTG,NSG) and the reader reads them so: the same bytes when NSGMAX = NTGMAX (the default, 32 and 32), transposed extents otherwise (reported on #226). Older than this change, and a fix alters the file for unequal grids: left for its own pull request. - Only the first WRITE of EQSAVE has an IOSTAT: a failure later in the file (a full disk) still ends the program. - MODEFR=1 makes the load path ask for a file name on unit 5 in the same way; not touched. - eq_save's precondition is eq_api's own: set by eq_run, cleared by eq_run and eq_set_param. An equilibrium that another module computes through EQ inside the mono image does not set it, and a parameter changed by another route does not clear it: tot's `eq:` names call the registry directly, and in the mono image the other modules' setters write the shared plcomm (measured there: eq_run(0), tot_set_param("eq:NRGMAX", 65), eq_save returns EQ_OK). - EQ_READ is also reached through eq_load (tr, fp, wr, wrx), EQLOAD (ob, wm, wmx) and the L and K commands of the eq menu. tr, fp, wr, wrx, wm and wmx ask for MODELG 3, 5 or 8, where a successful load is as before (K sets MODELG=5 itself). The L command passes on whatever MODELG is set (2 as 3), and ob/obprep.f90 calls it for any: for a MODELG that is no file format they now get IERR=1. - Without the stdio isolation of the companion pull request, EQSAVE's messages on unit 6 reach the JSON-RPC pipe like the rest of EQ's output (5 of the 11 stdout lines of the session above are not JSON-RPC). - get_psi_rz, the other tool of the same group of #226, is not here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
FYI: the mcp pin from this PR ( |
No line conflict. What the merge changes for this branch without one: - k-yoshimi#233 put a scalars object into fp_mcp's state schema, zero after a set until the next run; set_param_str opens that window too, which the schema text did not say yet (next commit). - k-yoshimi#256 brought eq_mcp's save tool, which this branch's docs still called future, after k-yoshimi#226 (fixed in a later commit). - k-yoshimi#232 and k-yoshimi#257 moved fp_mcp's fd isolation into python/_fd_isolation; this branch's server.py keeps it. - k-yoshimi#261 added the layout queries fp_state_abi_version and fp_state_size to libfpapi.so; fplib refuses a library without them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With develop in this branch, a few sentences no longer hold: - eq_mcp's save tool is in develop (k-yoshimi#256), so "once k-yoshimi#226 lands" goes from the module docstring, the set_param_str tool docstring and README section 8.4. - The get_state tool docstring lists the returned keys and now names scalars too (the first LOW of the k-yoshimi#233 approval, left for this PR). - libfpapi.so exports eight C symbols since k-yoshimi#261: README section 2 now names the six entry points and the two layout queries fplib checks. - The Sphinx FP MCP pages (en, ja) still said nine tools and that the string setter is not reachable through MCP; they now list ten tools, with a set_param_str row, and point at README section 8.4. - fp_mcp/__init__.py's docstring said nine tools. - The comment above SupportedValue says that this server exposes the string setter as the set_param_str tool, as suggested on k-yoshimi#231. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
The docstring and the skip reason of test_get_psi_rz_tool.py still said "mcp>=0.9,<2", the pin this branch carried before k-yoshimi#265 moved it to develop as ">=1.2,<2" (mcp.server.fastmcp first ships in 1.2.0). They name develop's pin now; the guards themselves are unchanged. The "trivial 0.9 vs 1.2" K. Yoshimi mentioned on 2026-10-18. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
develop replaced the make.header heredoc each build job carried (with a "KEEP IN SYNC" comment) by the composite action .github/actions/provision-build-env, used by the pytest and mono-build jobs. This branch's uncovered-modules-build job still had its own copy and a comment pointing at steps that no longer exist. The copy was identical to the action's heredoc; the job now uses the action too, and the action's description names the four jobs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
eq_mcp registers 13 tools: develop's 12 (with save) and this branch's get_psi_rz. The README (--print-tools count, Tools table), the Sphinx MCP pages (en, ja: the totals, the --print-tools line, the tool catalog) and the module docstring's list of EQ-specific tools said 12 and did not list get_psi_rz (on this branch they said 11 before the merge). They now name it, with its numpy dependency. The Sphinx pages' "tr has 9" becomes 10, what tr_mcp registers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…major, for the rl binning The bpsi base (7999438) re-bins the rl deposition of wrx/wrcalpwr.f90: drl=(rlmax-rlmin)/nrlmax instead of /(nrlmax-1), and pos_nrl at the cell centres. With k-yoshimi#249's comparator, which compares arrays and arrays2 element by element and checks their shapes, this branch's wrx_demo and wrx_iter01 baselines failed the shape check (ray-major, captured before k-yoshimi#249 fixed WrxState.to_dict; the values are the same) and wrx_jt60, never regenerated here, failed on values. The three metrics.json are now the values the CI runner captured in run 38690362398 (2026-10-15, k-yoshimi#226 merged with the develop of that day, which had k-yoshimi#249; artifact equivalence-actuals-3.11-38690362398, the 3.13 one byte-identical), species-major. Nothing in wrx/ or python/wrxlib/ that changes the values moved between that develop and ea9806a. Against develop's baselines, with develop's comparator: pwr_tot and every rs quantity equal; pos_nrl moves in every bin, from +drl/2 at the first to -drl/2 at the last; pwr_nrl_nsa differs in 145, 99 and 91 of 400 bins; the bin of the rl maximum is the same, and the fitted peak value changes by -0.50/-0.35/-0.46 %. pos_pwrmax_rl_nsa_nray changes by +0.30/+2.1/+0.99 %, all of it in the fit's offset term: the formula adds that offset in metres to a bin index scaled to 0..1. The fit's vertex at the major radius moves by about half a new bin (+1.5, +10.4, +4.9 mm). docs/superpowers/decisions/2026-10-21-wrx-rl-binning-baseline.md records this for the physics sign-off (K. Yoshimi's quantification on k-yoshimi#226 of 2026-10-13, measured again, with jt60); the 2026-06-29 record, whose wrx failures were pwr_tot compiler drift, gets a note on the array change it could not see; docs/baseline-policy.md gets the new provenance of the three baselines. The 1e-10 tolerance is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
The merge. 43 files conflicted, all where
The wrx baselines ( For the sign-off,
The gate, as I read it now:
Not answered, and outside the gate.
Locally on the head (macOS, gfortran 15, clean build with the local build script's flags, CI's pytest flags): 1076 passed, 39 skipped (every equivalence test is Linux-only; the rest need the mono image, the ti fixture or /dev/full); the mono step's four files 20 passed. |
Contributing the bpsi↔fork merge baseline back to
k-yoshimi/taskCover letter for an upstream contribution — what the baseline is, what changed, and how it was tested.
Prepared 2026-07-10. Full engineering detail:
docs/2026-06-TASK-merge-and-TRX-report.md.Status on 2026-10-21 (read this first)
This description was written in July. Since then the pieces split out of this PR went into
developin their reviewed form (#256eq_save, #257 MCP fd isolation, #265 the mcp pin), anddevelop(ea9806ac) is merged into this branch (a4e91044; the merge message lists how each of the 43 conflicts was resolved). What this PR still adds on top ofdevelop: the bpsi base and its merge, the #227/#228 fixes (#228-2, -3, -4 in5565ede1,0dd157d4,6a804077; #228-16 in19a552b4),get_psi_rz(eqlib and theeq_mcptool), the CI job that builds the modules pytest does not touch, and the regenerated baselines.The wrx baselines are regenerated from CI in the species-major layout (
cc7d12b8), and the change behind them is recorded for the physics sign-off indocs/superpowers/decisions/2026-10-21-wrx-rl-binning-baseline.md: the bpsi base re-bins the rl deposition ofwrx/wrcalpwr.f90;pwr_totand every rs quantity are unchanged. §4.2 below is corrected accordingly. What is still open is listed in the comment of 2026-10-21. Where this description says "gfortran-13.2" or "gf13.2", the CI compiler was 13.3.0; 13.2 is the apt metapackage revision (docs/baseline-policy.md).0. TL;DR for a reviewer
This proposes bringing the Kyoto
bpsi/developbase and the GitHub Phase-L modernization forkinto one reconciled, CI-green baseline. The merge is already done, resolved, and validated:
0254aa91— 1st parent58f1fe2e(Phase-L fork tip), 2nd parentfa9dd493(
bpsi/develop, +23 commits). i.e.bpsi/developmerged into the fork, keeping everymodernization and grafting bpsi's intent.
d1f7f9e7— 736 passed on Python 3.11 + 3.13; three1e-10equivalencebaselines regenerated on the CI compiler (gfortran-13.2), one of them for an owner-approved physics
refinement.
intent (table in §3).
Upstream already incorporated (§6): this branch merges current
k-yoshimi/developin (73 commits —mono-
libtotapirouting #201/#204/#208, the Linux-canonical equivalence policy #213, the Pythonfixture-parity backfill #215, and more) and adds the 57-commit bpsi baseline on top: 58 ahead / 0
behind. Only
.gitignoreneeded manual conflict resolution (unioned); the merge co-modifies just 5non-Fortran files (all clean unions) and no Fortran file, so nothing upstream is dropped. Offered as
a PR, never a force-push.
1. What the baseline is
Three git lines had drifted apart:
bpsi.nucleng.kyoto-u.ac.jp/pub/git/taskgithub.com/k-yoshimi/taskforklibXapi.so+ Python wrappers + MCP servers,trcommsplit into 6 sub-modulesgithub.com/ats-fukuyama/taskThe merge brings the latest upstream base onto the modernized fork without regression. An audit
established
bpsi/developis the latest base (not a 3-way upstream merge):ats-fukuyama/developis 119commits behind bpsi and its "unique" commits are superseded backups. So the merge is bpsi base → the
Phase-L fork.
2. What the contribution changes, in aggregate
Before this merge, the baseline was 57 commits ahead / 73 behind the current
k-yoshimi/develop(this PR incorporates all 73 — see §6, now 0 behind). The 57ahead touch 162 files (+9,637 / −5,522). The bulk is the bpsi merge itself (the
0254aa91mergealone reconciles 128 files) plus the CI-greening fixes and the design/plan docs. The net intent:
eq/trq-solver refinement (finerflux-surface grid,
EQLOOPunder-relaxation) used by themodelg=3eq_loadpath.libXapi.solayer, Python wrappers, MCP servers,and the 6-way
trcommsplit.3. Conflict resolutions (10 files)
Each conflict = keep fork modernization, graft bpsi intent:
eq/eqinit.f90NTVMAX200→400,NLPMAX20→100eq/eqcalc.f90EQLOOPadaptive under-relaxationeq/eqcalq.f90NPRINTprint +NMAX200→400eq/eqsub.f90pl/Makefilenoeqlibrecipes + take bpsiSRCS_NOEQ+plview.f90tr/trcomm.f90DEALLOCATE_TRCOMMtr/trcomm_param.f90PNM/PTM/PTPR/PTPP/PUM/PUPR/PUPP)tr/trmain.f90txnew/txcalv.f90wrx/wrcalpwr.f90dx4. How it was tested
4.1 The build had never been CI-green on the sandbox
Even the pure base
developfailed — the build never reached pytest. Getting to green surfaced issuesclass by class (auto-merge artifacts, a
PTsymbol clash betweenti/wm, apl_viewmodule move thatbroke
fpat link time, a repo-name assumption, and test-env gaps). Final:d1f7f9e7carries a two-reviewer pre-push sign-off marker.4.2 The
1e-10equivalence gateThe project keeps per-case regression baselines compared at a
1e-10tolerance. Three drifted onthe CI compiler (gfortran-13.2), for two distinct reasons:
wrx_demo/wrx_iter01— pure compiler-version FP reordering (gf8.5 ≠ gf13.2 ≠ Mac gf15, allwithin ~
1e-9). Lesson recorded:1e-10binary equivalence is too tight for FP-heavy modules acrosscompiler versions; baselines must be captured on the exact CI compiler. Regenerated on gf13.2.
(Corrected 2026-10-21: that holds for the
pwr_totfailures it describes, but the comparator of the timecompared only
pwr_totfor wrx. The same merge re-bins the rl deposition ofwrx/wrcalpwr.f90; once fix(test_run): compare wrx arrays/arrays2 and fix WrxState.to_dict orientation (#239) #249compared the arrays this showed, and the three wrx baselines are regenerated from CI with it. See
docs/superpowers/decisions/2026-10-21-wrx-rl-binning-baseline.md.)tot_ht6m_short— a real physics shift, relative up to 7.9e-4 (see §5), owner-approved and re-baselined.Because the CI env couldn't regenerate these directly, an env-gated capture (
REGEN_OUTPUT_DIR)dumps each case's freshly computed metrics as a CI artifact; those gf13.2 values were committed as the
new baselines.
4.3 Reproduce
5. The one deliberate physics change:
tot_ht6mq-solver refinementtot_ht6m_short(integrated eq→tr coupled run) shifted by up to 7.9e-4 relative (corrected 2026-10-21; it said "~1e-4"). Root cause: bpsi's mergedEQ-solver refinements — finer flux-surface grid (
NMAX200→400) +EQLOOPunder-relaxation — on themodelg=3eq_loadpath. Signature (all 50 radial rows):jup in the core, down mid-radius to −7.9e-4 @ ρ=0.74, up at theedge;
qis its integral: down core, up outer);AJTΔ 3.9e-5 ≪ local Δ 7.9e-4);Reviewed and owner-approved as an intended higher-accuracy solve; the baseline was regenerated on
gf13.2 accordingly. (A truncated CI log initially mis-characterized this as core-localized; two
independent reviewers caught it against the full committed baseline — the write-up was corrected to
global redistribution.)
6. What this PR actually contains, and how it was validated
It is a merge, not a rewrite. The branch is
merge/upstream-reconcile-2026-07; its tip is a mergecommit whose parents are the delivered baseline (
c6cee542) and the currentk-yoshimi/develop(
4a5d977d). Result: 58 ahead / 0 behinddevelop.The merge is clean by construction — the two sides co-modify only 5 non-Fortran files, each a
verified clean union:
.gitignore,.github/workflows/python-tests.yml,python/eqlib/_ffi.py,python/totlib/tests/test_equivalence.py,python/wrxlib/tests/test_equivalence.py..gitignoreproduced a conflict marker (resolved as the union of both rule sets); the other fourauto-merged, and each was checked to keep both sides' additions (e.g.
_ffi.pykeeps oureq_save/psi_rzprototypes and upstream'sMONO_LIB_PATHrouting; the CI workflow keeps ournumpy/REGEN steps and upstream's
mono-buildjob).tot/, so the mergedtot(the mono-libtotapiwork, L-7b-ii Phase 2: monolithic mode + BPSD coupling activation (umbrella) #201/L-7b-ii follow-up: add Linux CI build job for libtotapi_mono.so #204/L-7b-ii Phase 2c: wrapper mono routing + activate ('eq','tr') BPSD coupling #208) is byte-identical to upstream's and known-good;and upstream's 73 commits changed no
tr/eq/pl/bpsdFortran, so those come wholly from thebaseline. There is therefore no semantic-merge risk in the Fortran.
Local validation (macOS, gfortran-15):
make -C tr tr2(static) builds from the merged sources;lib/mtxp/pl/eq/bpsdall compile.pytest --collect-only python/trlib python/totlibcollects 229 tests with no import/fixture breakage.Deferred to this PR's Linux CI — by upstream's own policy:
1e-10equivalence tests are now Linux-canonical (fplib + wrxlib equivalence tests fail 1e-10 tolerance on develop (mismatches ~1e-9) #213,@skipUnless(IS_LINUX)), so they donot run on macOS; the authoritative equivalence run is the Linux CI on this PR.
libtotapi_mono.sois a Linux-only build (L-7b-ii follow-up: add Linux CI build job for libtotapi_mono.so #204), so the mono routing/coupling tests likewise runon CI, not locally.
So the full 736-test suite (including the equivalence gate that this whole exercise protects) is
re-verified by the CI attached to this PR, against the merged tree.
7. Key commits