Skip to content

Reconcile bpsi/develop base into develop (bpsi↔fork merge baseline, CI-green) - #226

Open
HengyuLi-Ozaki-lab wants to merge 67 commits into
k-yoshimi:developfrom
HengyuLi-Ozaki-lab:merge/upstream-reconcile-2026-07
Open

Reconcile bpsi/develop base into develop (bpsi↔fork merge baseline, CI-green)#226
HengyuLi-Ozaki-lab wants to merge 67 commits into
k-yoshimi:developfrom
HengyuLi-Ozaki-lab:merge/upstream-reconcile-2026-07

Conversation

@HengyuLi-Ozaki-lab

Copy link
Copy Markdown

Contributing the bpsi↔fork merge baseline back to k-yoshimi/task

Cover 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.


0. TL;DR for a reviewer

This proposes bringing the Kyoto bpsi/develop base and the GitHub Phase-L modernization fork
into one reconciled, CI-green baseline. The merge is already done, resolved, and validated:

  • Merge commit 0254aa91 — 1st parent 58f1fe2e (Phase-L fork tip), 2nd parent fa9dd493
    (bpsi/develop, +23 commits). i.e. bpsi/develop merged into the fork, keeping every
    modernization and grafting bpsi's intent.
  • CI-green code baseline d1f7f9e7 — 736 passed on Python 3.11 + 3.13; three 1e-10 equivalence
    baselines regenerated on the CI compiler (gfortran-13.2), one of them for an owner-approved physics
    refinement.
  • 10 files had genuine conflicts; each was resolved as keep the fork's modernization, graft bpsi's
    intent
    (table in §3).

Upstream already incorporated (§6): this branch merges current k-yoshimi/develop in (73 commits —
mono-libtotapi routing #201/#204/#208, the Linux-canonical equivalence policy #213, the Python
fixture-parity backfill #215, and more) and adds the 57-commit bpsi baseline on top: 58 ahead / 0
behind
. Only .gitignore needed manual conflict resolution (unioned); the merge co-modifies just 5
non-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:

Line Repo Role
bpsi bpsi.nucleng.kyoto-u.ac.jp/pub/git/task Kyoto main dev — latest physics / bug-fixes
fork (Phase-L) github.com/k-yoshimi/task fork F90-ification, per-module C-ABI + libXapi.so + Python wrappers + MCP servers, trcomm split into 6 sub-modules
ats-fukuyama github.com/ats-fukuyama/task stale 2023 backup (read-only; not a merge input)

The merge brings the latest upstream base onto the modernized fork without regression. An audit
established bpsi/develop is the latest base (not a 3-way upstream merge): ats-fukuyama/develop is 119
commits behind bpsi and its "unique" commits are superseded backups. So the merge is bpsi base → the
Phase-L fork
.

7d18264e  merge-base (fork point vs k-yoshimi/develop)
   ├─ bpsi/develop        +23 commits → fa9dd493   (latest base)
   └─ Phase-L fork tip                → 58f1fe2e   (C-ABI/.so/MCP/6-module split)
        0254aa91 = merge(1st parent = fork 58f1fe2e, 2nd = bpsi base fa9dd493)  ⇒ bpsi merged INTO the fork
        + build-integration + baseline fixes → d1f7f9e7  (CI green, delivered)

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 57
ahead touch 162 files (+9,637 / −5,522). The bulk is the bpsi merge itself (the 0254aa91 merge
alone reconciles 128 files) plus the CI-greening fixes and the design/plan docs. The net intent:

  1. Graft bpsi's latest physics/bug-fixes — chiefly the eq/tr q-solver refinement (finer
    flux-surface grid, EQLOOP under-relaxation) used by the modelg=3 eq_load path.
  2. Keep every Phase-L modernization — the C-ABI libXapi.so layer, Python wrappers, MCP servers,
    and the 6-way trcomm split.
  3. Make the whole thing build and pass CI green for the first time on the sandbox.

3. Conflict resolutions (10 files)

Each conflict = keep fork modernization, graft bpsi intent:

File Resolution
eq/eqinit.f90 graft bpsi values into free-form: NTVMAX 200→400, NLPMAX 20→100
eq/eqcalc.f90 graft bpsi EQLOOP adaptive under-relaxation
eq/eqcalq.f90 graft NPRINT print + NMAX 200→400
eq/eqsub.f90 fixed-form continuations → free-form; richer diagnostic
pl/Makefile keep noeqlib recipes + take bpsi SRCS_NOEQ + plview.f90
tr/trcomm.f90 take-ours (6-module split) + idempotent DEALLOCATE_TRCOMM
tr/trcomm_param.f90 species param list 8 → 15 (add PNM/PTM/PTPR/PTPP/PUM/PUPR/PUPP)
tr/trmain.f90 take-theirs (drop double-free)
txnew/txcalv.f90 take-theirs (SAVE-workspace allocation)
wrx/wrcalpwr.f90 genuine 3-way blend: keep headless/OOB clamp + graft bpsi dx

4. How it was tested

4.1 The build had never been CI-green on the sandbox

Even the pure base develop failed — the build never reached pytest. Getting to green surfaced issues
class by class (auto-merge artifacts, a PT symbol clash between ti/wm, a pl_view module move that
broke fp at link time, a repo-name assumption, and test-env gaps). Final:

736 passed, 6 skipped, 1 xfail (pre-existing), 1 xpass — CI green on Python 3.11 + 3.13.

d1f7f9e7 carries a two-reviewer pre-push sign-off marker.

4.2 The 1e-10 equivalence gate

The project keeps per-case regression baselines compared at a 1e-10 tolerance. Three drifted on
the CI compiler (gfortran-13.2), for two distinct reasons:

  • wrx_demo / wrx_iter01 — pure compiler-version FP reordering (gf8.5 ≠ gf13.2 ≠ Mac gf15, all
    within ~1e-9). Lesson recorded: 1e-10 binary equivalence is too tight for FP-heavy modules across
    compiler versions
    ; baselines must be captured on the exact CI compiler. Regenerated on gf13.2.
  • tot_ht6m_short — a real ~1e-4 physics shift (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

make -C tr tr2 libtrapi.so tr_api_check_all
make -C tot libs && make -C tot tot_api_check_all && make -C tot tot
PYTHONPATH=python:python/mcp-servers python -m pytest python/ \
    --forked --timeout=120 --timeout-method=signal        # 736 passed on gf13.2

5. The one deliberate physics change: tot_ht6m q-solver refinement

tot_ht6m_short (integrated eq→tr coupled run) shifted by ~1e-4. Root cause: bpsi's merged
EQ-solver refinements — finer flux-surface grid (NMAX 200→400) + EQLOOP under-relaxation — on the
modelg=3 eq_load path. Signature (all 50 radial rows):

  • current profile redistributes (j up in the core, down mid-radius to −7.9e-4 @ ρ=0.74, up at the
    edge; q is its integral: down core, up outer);
  • total current conserved (AJT Δ 3.9e-5 ≪ local Δ 7.9e-4);
  • density frozen (0/50 rows); smooth, no NaN.

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 merge
commit whose parents are the delivered baseline (c6cee542) and the current k-yoshimi/develop
(4a5d977d). Result: 58 ahead / 0 behind develop.

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.

Local validation (macOS, gfortran-15):

  • make -C tr tr2 (static) builds from the merged sources; lib/mtxp/pl/eq/bpsd all compile.
  • pytest --collect-only python/trlib python/totlib collects 229 tests with no import/fixture breakage.

Deferred to this PR's Linux CI — by upstream's own policy:

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

0254aa91  merge: reconcile bpsi/develop base (23 commits) into the Phase-L fork
d1f7f9e7  CI-green code baseline (736 passed, 3 baselines regenerated on gf13.2)
c6cee542  delivered tip / merge 1st parent (= d1f7f9e7 + 8 docs-only commits)
58f1fe2e  1st parent — Phase-L fork tip (design spec + P1 plan)
fa9dd493  2nd parent — bpsi/develop base (q-solver coupling fix)

ats-fukuyama and others added 30 commits January 21, 2026 13:56
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).
…clickable GitHub links

Per user request:
1. Remove the 'Lessons learned' frame.
2. Merge the 'Status' frame into 'Repositories & current result' -> a single
   closing frame 'Repositories, links & current result' (repo table + result
   badges + 'P0 delivered/P1 underway' + Thank you), placed after 'Future plan'.
3. Add clickable GitHub links via hyperref \href (colorlinks): the three GitHub
   repos, the merge PR k-yoshimi#1, and direct links to the engineering report + the
   slides on the fork. Adds \hypersetup.
Now 15 frames; static-checked (env/brace/tikz balance, 5 \href, no bad URL char,
underscore escaping) -- no local TeX to compile.
…he deck

Per-slide (15) companion to 2026-06-30-merge-trx-beamer.tex: a short bilingual
explanation of each slide plus a natural spoken speaker script in English and
Chinese (中英对照演讲词). Drafted by a 4-agent workflow constrained to the
verified facts brief (topology SHAs, 736-passed CI, tot_ht6m physics, the
reactivity comparison, Option A), assembled + spot-verified.
…-4 merge inputs

Pre-push review (codex) on the speaker notes caught two real inaccuracies that
were also latent in the deck/report/plan:
- [HIGH] the '<=1.2% over 1-30 keV' agreement was based on a log-log spline;
  libnf actually SPL1Ds the RAW <sigma v> vs log10(T) (trx/libnf.f90:327,414).
  Re-derived: at core/fusion temps (T>=5 keV) it holds ~1-2%, but the raw-value
  spline overshoots enormously at the low-T edge (>2000% at 1.5 keV) where the
  reactivity is negligible and no fusion occurs -- immaterial to fusion power.
  Corrected the claim in the deck (Slide 12), report (5.2), plan (RECORDED
  DECISIONS), and notes (Slide 12 EN+ZH). Option A is unaffected (indeed the
  analytic MDLNF is the smoother path).
- [MED] the Motivation diagram/Slide-4 notes implied ats-fukuyama is a merge
  input; the actual merge parents are the Phase-L fork 58f1fe2 + bpsi fa9dd49.
  Relabelled the diagram node to the Phase-L fork; fixed Slide-4 notes EN+ZH.
…n consistency)

Second-reviewer (codex) pass on the correction commit flagged three items:
- [HIGH] 'the spline holds ~1-2% of SIGMAM' is ambiguous (reads as spline=1-2%
  OF SIGMAM). Reworded to 'stays within ~1-2% of SIGMAM' in report/beamer/plan.
- [MED] Beamer Goal slide labelled the GitHub line as 'ats-fukuyama'; the active
  GitHub line is the Phase-L fork kyoshimi-develop (ats-fukuyama is only its stale
  ancestor, not a merge input). Relabelled in the deck + speaker-notes Slide 3.
- [MED] Plan body still had actionable 'replace/retire MDLNF' steps contradicting
  the Option-A RECORDED DECISION. Added an OVERRIDE note + reworded Step-3 heading,
  sub-bullets, table row, and the sample commit message to 'additive, MDLNF kept'.
@k-yoshimi
k-yoshimi requested a review from Copilot July 21, 2026 21:28

Copilot AI 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.

Copilot wasn't able to review this pull request because it exceeds the maximum number of lines (20,000). Try reducing the number of changed lines and requesting a review from Copilot again.

@k-yoshimi

Copy link
Copy Markdown
Owner

Careful review completed. I found three correctness issues that should be addressed before merge:

  1. Importing eq_mcp.server or tr_mcp.server redirects process-wide fd 1 to stderr, affecting pytest and embedding applications, not only the MCP stdio subprocess.
  2. tr_mcp lifecycle handling is inconsistent: handle_finalize does not finalize/reset the Fortran backend, while run_and_get_state still invokes the SIGABRT-prone STATE.close path.
  3. EQ save can return a success message even when FWOPEN failed and no output file was created.

I opened #227 with reproduction details, required fixes, and regression-test expectations, and assigned it to @HengyuLi-Ozaki-lab.

Please also run the required GitHub Actions against the updated PR head. At review time, this PR reports no checks for fb80991.

Review outcome: changes requested pending #227.

@k-yoshimi

Copy link
Copy Markdown
Owner

Non-MCP follow-up review is complete. It found additional merge-blocking regressions outside the MCP servers, including:

  • legacy wm assigning the electron temperature to the full shared PT(NSM) array in the MODELG=1/2 path;
  • the newly added MODELG=25 path being rejected by plcheck (IERR=101, reproduced);
  • tr_eq_init not being restored across a MODELG=9 finalize/re-init lifecycle;
  • MODELG=9 q-scaling continuing after BPSD errors/non-convergence;
  • TRX NBI calculations using uninitialized species contributions when D/T/He4 is absent;
  • incomplete initialization/broadcast of the new PL PT state;
  • unconditional per-grid-point debug output in W1.

I opened a separate follow-up issue for the full evidence, affected lines, verification results, and acceptance criteria: #228

Recommendation remains do not merge yet. This issue is separate from the MCP findings tracked in #227.

@k-yoshimi

Copy link
Copy Markdown
Owner

A further non-MCP audit found six additional defects, including three new P1 merge blockers:

  • wf2dt/wfgsubn.f90 writes R into both plot coordinates and passes a three-element coordinate array to poly2D(...,4);
  • equ/equ_convert.f90 uses uninitialized n as the lower bound of its CSV comma-count loop (confirmed by gfortran -Wuninitialized);
  • the new TXNEW block solver introduces unresolved DGETRF/DGETRS symbols under the repository-default LIBLA= link configuration.

The additional P2/P3 findings cover FPX debug output inside deep NBI loops, WF2DT use of deallocated RGB arrays, and validation of the wrong WIM record index.

Full evidence, reproduction output, affected lines, and acceptance criteria were added to #228:
#228 (comment)

The recommendation remains do not merge yet pending the P1 fixes.

@k-yoshimi

Copy link
Copy Markdown
Owner

Clavius runtime verification found three additional P1 merge blockers and one regression-harness defect at the current PR head fb8099109c0e086a43fdc54180990643dca3675b:

  • Both committed TRX sample inputs set model_nnf(1)=4, but model_nnf is absent from NAMELIST /TR/. The input produces PARM INPUT ERROR and then continues with an unintended configuration.
  • With LAPACK/BLAS explicitly linked, the new TXNEW MDLPCK=2 path fails on the standard calculation at step 1 with IERR=201002 from DGETRF. No fallback is attempted for this positive factorization error, and the process still exits 0.
  • The existing EQ regression suite fails with 204 mismatches for eq_iter01 and 222 for eq_tst2, typically at relative errors of 1e-4 to 3e-4. The numerical changes have neither validated updated baselines nor a documented acceptance rationale.
  • tr_m0904 completes numerically but its comparison fails because the baseline omits the newly extracted AJRFT field.

WIM wave-dump generation/conversion and changed W1, WM/WMX, WR/WRX, FP, and OB library builds succeeded on clavius. WF2DT end-to-end plotting remains unverified because all committed GLAST inputs stop at the antenna setup error.

Full reproduction details and acceptance criteria:
#228 (comment)

Recommendation remains do not merge yet until the P1 items and regression contract are resolved.

@terasakisatoshi

Copy link
Copy Markdown

An independent re-audit at the current head fb8099109c0e086a43fdc54180990643dca3675b (six parallel subsystem reviews + warning-enabled local builds of the modules CI does not cover) found:

  • One new PR-introduced defect (P2): wf2dt/wfglib.f90:567-568 — the epsilon literal 1.-12 parses as -11.0, so the near-zero color-range fallback in convert_fmin_fmax never fires. Two-character fix (1.D-12).
  • One pre-existing defect newly exposed by this PR's eq_save C-ABI (P2): EQSAVE writes PSI/DELPSI/HJT with transposed subscripts relative to EQRTSK/the array declarations; silent save→load corruption whenever NSGMAX /= NTGMAX. Best fixed together with the PR #226 follow-up: fix MCP stdio isolation, TR lifecycle, and EQ save error reporting #227 item-3 eq_save work.

Details added to #228 (findings 18-19): #228 (comment)

Additionally, five defects in the trx fusion-reaction (nnf) path plus one wrx plotting bug initially flagged by this audit turned out to be byte-identical at merge-base 4a5d977d — i.e. pre-existing on develop, not #226 regressions. They are tracked separately in #229 so they do not block this PR.

Positive verification results: equ/ (the largest F77→F90 conversion), wf2dt/, and wim/ all compile clean to their libraries on macOS gfortran-15 with -Wall -Wextra -Wuninitialized under a CI-replica make.header; the mono-build wildcard in tot/Makefile does pick up eq_api_common.o, so the new eq_save/get_psi_rz symbols are present in libtotapi_mono.so; and the three regenerated baseline JSONs are schema-consistent with their extractors.

@terasakisatoshi

Copy link
Copy Markdown

Second audit pass, this time cross-checking the diff against every open issue (#142-#229): #228 (comment)

Summary:

Net effect: with findings 17 reassigned to #224 and #203 bug-B credited as fixed, the actual merge-gate reduces to the #227/#228 P1 items, the EQ baseline regeneration asymmetry (finding 16), and the small finding-20 fix.

@k-yoshimi

Copy link
Copy Markdown
Owner

A fifth review wave audited defect classes not covered by the earlier findings: statement-level verification of the equ/ fixed→free-form conversion against the original sources, a repository-wide impact sweep of the newly introduced shared symbols and moved APIs (including unmodified components), and CI-workflow logic. It found four additional P1 merge blockers, nine P2 defects, and ~25 smaller issues at the current PR head fb8099109c0e086a43fdc54180990643dca3675b:

  • The equ conversion dropped the single statement plasmaf%nrmax=0 in eqpl_prof (equ/equpl.f90), so BPSD profile gets run with an undefined/stale grid request — wrong pressure-profile adjustment with no diagnostic.
  • The new ../task symlink step kills both GitHub workflows on the canonical repo (ln -sfn onto the existing checkout directory fails), which is the likely root cause of this PR reporting no checks at all. The CI merge gate cannot be satisfied until this is fixed.
  • The new plcomm PT(NSM) breaks compilation of four unmodified components (trm, wmseki, wmf, wmfn — seven sites); only wm/wmsetg.f:310 had been found previously.
  • pl_view's move out of plparm leaves six un-migrated callers (dpseki ×2 compile errors; fpx and fp.anzai/fp.nuga/fp.ota link errors) — fpx is modified by this PR itself.

Notable P2s: the plasmaf-overflow this PR fixes in tr_bpsd_get is still present in tr_bpsd_put; the new TRX "Kawamura correction" drops the NSTN.EQ.0 guard (reads X(0) with excluded equations); tr_prep_ns labels species beyond NSMAX, defeating libnf's presence STOP and enabling out-of-bounds writes in tr_pnf; PUBLIC sigmav_nfb_int has no implementation anywhere in the tree; Eq.save silently truncates paths >80 chars; and test_libnf cannot fail and does not test the sigmav path trx uses.

Full evidence, line references, and acceptance criteria:

Recommendation remains do not merge yet until the P1 items across #227/#228 are resolved and the workflows run green on the PR head.

@k-yoshimi

Copy link
Copy Markdown
Owner

A fresh re-review of the unchanged head fb8099109c0e086a43fdc54180990643dca3675b found one additional P1 integrated-lifecycle regression.

The PR moves DEALLOCATE_TRCOMM from standalone trmain shutdown into reusable tr_menu. When TOT exits its TR submenu, the menu now destroys RN, RT, AJ, and QP before the integrated caller or later modules can consume them.

Fresh clavius results:

tot_demo2014_short: REGRESSION
tot_ht6m_short:    REGRESSION

Both runs calculate and reach CLOSED, but both TOT dumps contain TR_PRESENT=0, producing NRMAX/NSMAX/NT actual=0 mismatches. Cleanup ownership must remain with the standalone or embedding caller, not the reusable menu.

Full evidence and acceptance criteria were added as finding 32:
#228 (comment)

The same re-review also reconfirmed the existing EQ regression failures, TRX input error, TXNEW IERR=201002, PL zero-value handling defect, zero reported GitHub checks, and git diff --check failures. Recommendation remains do not merge yet.

@HengyuLi-Ozaki-lab

Copy link
Copy Markdown
Author

Thank you both — five review waves plus an independent re-audit, including real runs on clavius, is far more scrutiny than this PR had any right to expect, and it found real problems. I've now worked through every finding in #227/#228/#229 and re-verified each one against fb809910, bpsi fa9dd493, develop 4a5d977d, and the merge-base 7d18264e.

Below: what's ours and being fixed, one thing I need from a maintainer, two corrections with reproductions, and a proposal for how to land this.


1. What I need from a maintainer (this is the blocker)

The workflows are not failing — they were never allowed to start.

$ gh api repos/k-yoshimi/task/actions/runs?head_sha=fb8099109c0e086a43fdc54180990643dca3675b
  → 1 run: python-tests, event=pull_request, status=completed, conclusion=action_required

$ gh api repos/k-yoshimi/task/actions/runs/29195792410/jobs
  → {"total_count": 0, "jobs": []}

$ gh api repos/k-yoshimi/task/commits/fb8099.../check-runs
  → {"total_count": 0}

Zero jobs were dispatched. action_required is GitHub's "Fork pull request workflows from outside collaborators" approval gate. Could someone click "Approve and run workflows" on run 29195792410?

This also gates the EQ baseline regeneration you asked for (§4): regen-baselines.yml is on: workflow_dispatch: only, and workflow_dispatch can only be dispatched from a branch of the repo hosting the Actions tab — not from a fork PR head. So I can't produce CI-generated EQ baselines until this same gate opens.


2. Confirmed ours — being fixed

Four P1s are unambiguously mine, and one methodology failure that I think is worse than any of them.

2.1 tr/trmenu.f90:180 destroys integrated TOT state (#228 finding 32). Confirmed, and your diagnosis is exactly right. The CALL DEALLOCATE_TRCOMM line itself is bpsi-authored (fa9dd493:tr/trmenu.f90:178), but bpsi has no tot/totregress.f90git ls-tree -r fa9dd493 tot/ is 12 files with no totregress; 4a5d977d has 29. The defect exists only at the intersection of bpsi's ownership move and kyoshimi-only TOT, i.e. my merge created it. Fix: remove the menu-level call, restore it in tr/trmain.f90 before STOP. Cleanup remains owned at both other layers (tr/tr_api.f90:348 for the library path; tr/trcomm.f90:60 makes re-allocation self-healing), so nothing leaks.

2.2 MCP import mutates process-wide stdout (#227 item 1). Confirmed. eq_mcp/server.py:68-85 and tr_mcp/server.py:54-74 run dup(1)/dup2(2,1) at module scope. Not on 4a5d977d (grep -c dup2 → 0), not bpsi's (no python/ tree there). Moving it behind an explicit launcher hook.

2.3 tr_mcp lifecycle is self-contradictory (#227 item 2). Confirmed. handle_finalize marks closed without tr_finalize while handle_run_and_get_state still calls it, and eq_mcp now disagrees with tr_mcp. Picking one strategy and applying it in all three places.

2.4 eq_save reports success when nothing was written (#227 item 3). Confirmed — and worse than you framed it: our own docstring at python/eqlib/eqlib.py:303-308 documents the defect ("callers should verify file existence after the call"). Shipping a known bug with a comment instead of a guard was the wrong call. Adding the existence/size check, and propagating FWOPEN's IERR through eq_api_save.

2.5 The EQ baseline gap — you're right, and my justification was invalid.

The decision record for the tot_ht6m re-baseline argued that eq_iter01 passing on CI showed the change "is not a broad regression." That inference does not hold, and the reason is structural: the eqlib Layer-1 fixture is MODELG=3 + KNAMEQ="eqdata.ITER01", and eq/eqfile.f90:155 reads NTVMAX back out of the committed eqdata blob — overwriting the NTVMAX=400 that was just set. That test is incapable of observing the change I made. I regenerated tot_ht6m_short + the two wrx baselines and left the three eq_* baselines alone, on the strength of a green signal that was structurally blind. Your clavius run (204/222 mismatches) is the real signal.

One correction to the root-cause attribution, though: it isn't NTVMAX alone. git diff 4a5d977d fb809910 -- eq/eqinit.f90 is exactly two value changes — NTVMAX 200→400 and NLPMAX 20→100 (eqinit.f90:280, documented at :274 as "Maximum iteration number of EQ"), plus eqcalq NMAX 200→400. Three variables. The rationale document will state all three honestly rather than bisecting one.

Plan: regenerate eq_iter01/eq_tst2/eq_jt60 via regen-baselines.yml (pinned ubuntu-24.04/gfortran-13.2), regenerate the four committed eqdata fixture blobs so replayed physics matches the shipped solver, and add an assertion that the loaded NTVMAX equals the eqinit default so this blindness can't recur. Blocked on §1.

2.6 One defect nobody found: dp/dpfpin.f90:411-423 contains the same 6-line IF(ALLOCATED(FPR)) DEALLOCATE(FPR)… block twice — a silent auto-merge artifact in a file that wasn't on my conflict list. Runtime-harmless (ALLOCATED-guarded), but it's mine. I ran a duplicate-block scan across all 43 hand-authored Fortran/Makefile files in the diff; this is the only true hit.

2.7 CI never compiles equ/ trx/ txnew/ wf2dt/ wim/ wm/ — which is the structural reason ~13 of your findings survived to human review. Adding a compile-only job is the highest-leverage thing available here (see §5).


3. Two corrections, with reproductions

3.1 The ../task symlink is not why there are no checks, and it does not fail.

The mechanism claim doesn't reproduce. -n only applies when LINK_NAME is a symlink to a directory; here it's a real directory, so ln falls back to link-into-directory mode:

# Ubuntu 22.04, ln (GNU coreutils) 8.32 — the CI-equivalent toolchain
$ mkdir -p /tmp/lnprobe/work/task/task && cd /tmp/lnprobe/work/task/task
$ W=$PWD; ln -sfn "$W" "$(dirname "$W")/task"; echo "exit=$?"
exit=0
$ find /tmp/lnprobe -maxdepth 4
./work/task/task/task          ← self-referential symlink created INSIDE the checkout

# only the -T form errors, and the workflow does not use -T:
$ ln -sfnT "$W" "$(dirname "$W")/task"
ln: /tmp/lnprobe/work/task/task: cannot overwrite directory

Same result on macOS/BSD ln. And since zero jobs ran (§1), no step executed at all — a step-level bug can't be the cause of missing checks. Also, regen-baselines.yml:9 is workflow_dispatch-only with no pull_request/push trigger, so it could never contribute a PR check; "kills both workflows" doesn't hold for the second one either.

There is a small real residue nobody flagged: on a repo literally named task the step isn't the intended no-op — it leaves a stray self-referential symlink in the working tree, which would bite any future recursive step (find, tar, artifact globs). I'm fixing it with a path-equality guard, but as P3 hygiene, not as a CI unblock.

3.2 Eq.save does not silently truncate paths >80 chars (#227 item 4).

There's a hard check: eqlib.py:39 _MAX_C_STRING_VALUE_BYTES = 80:226 passes it to _encode_name:66-70 raises.

$ python3 -c "from eqlib.eqlib import _encode_name, _MAX_C_STRING_VALUE_BYTES as M; ..."
80 chars -> accepted
81 chars -> RAISES EqlibInvalidParamError: EQ C string 'xxx…' is 81 bytes; maximum is 80

Suggest closing that item as not-a-defect. (The Fortran-side 80-char limit is real and undocumented at the C ABI — worth a doc note, but nothing is silently truncated.)


4. Proposed de-scoping (evidence for each)

item evidence proposed disposition
#228 f22 — tr_bpsd_put fixed-size temp(nrmp,…) overflow subroutine body byte-identical at bpsi / 4a5d977d / head pre-existing → #229
#228 f19 (terasakisatoshi) — EQSAVE transposed PSI/DELPSI/HJT git diff 4a5d977d fb809910 -- eq/eqfile.f90 is exactly one hunk (the MODELG=25 ELSEIF); masked because eqinit:256 defaults NSGMAX=NTGMAX=32 pre-existing → #229, but the new eq_save C ABI makes it reachable from Python for the first time, so I'll add an IF(NSGMAX.NE.NTGMAX) guard + IERR here
#228 f17 — tr_m0904 baseline missing AJRFT metrics.json and extract_tr_metrics.py both byte-identical at 4a5d977d and head; python/trlib/tests/test_equivalence.py:234-246 already carries a @pytest.mark.xfail citing #224 pre-existing → #224, exactly as @terasakisatoshi reclassified it
the six trx/wrx nnf items byte-identical at 4a5d977d already in #229 — thank you for splitting these out

For the rest: of 162 changed files, 94 are byte-identical to bpsi fa9dd493 and 68 are hand-authored (43 of them Fortran/Makefile). Most remaining findings — equ/, wf2dt/, wim/, txnew/, w1/, trx/ — are in the byte-identical set: real defects in Kyoto's code that this merge is merely the courier for. I'll publish the per-file provenance table (with blob SHAs) in docs/ so review can focus on the 43 source files where my judgement was actually exercised, rather than all 162.

My proposed split for those: fix here anything that breaks the build or is unguarded UB on a default path — findings 1, 5, 8, 9, 10+15, 12, 14, 20, 21, 23, 24 (~9 small changes; note 10 and 15 must land together, since routing DGETRF/DGETRS through the LAPACK_* shim without also widening the ierr_la == -2 fallback converts a link error into a silent wrong answer that still exits 0). Catalogue and report upstream to Kyoto the rest, rather than unilaterally rewriting their code inside a merge PR.


5. How I'd like to land this

Copilot declined this diff for exceeding 20,000 lines, and that's a fair signal. But I don't think splitting #226 helps: the bulk is 0254aa91, a merge of 23 bpsi commits. Splitting it either fabricates history — destroying git blame across the F77→F90 conversions and guaranteeing conflicts at the next bpsi sync — or produces intermediate commits that don't build.

Instead I propose buying review confidence with CI rather than with smaller diffs:

  1. A small preparatory PR first: the 10 one-line plcomm PT / pl_view import fixes for trm/ wmseki/ wmf/ wmfn/ dpseki/ fp.anzai/ fp.nuga/ fp.ota/ (components Reconcile bpsi/develop base into develop (bpsi↔fork merge baseline, CI-green) #226 doesn't otherwise touch), plus a compile-only CI job for equ/ trx/ txnew/ wf2dt/ wim/ wm/. Small, independently valuable, and it makes the pipeline catch this whole class going forward.
  2. Then rebase Reconcile bpsi/develop base into develop (bpsi↔fork merge baseline, CI-green) #226 on top, with the §2 fixes, the Tier-A items, and the regenerated EQ baselines — kept as a single merge commit so the next bpsi/develop sync stays a fast-forward.

If you'd rather I fold everything into #226 instead, I'm happy to — just say which you prefer.

Sorry for the volume of work this created. The DEALLOCATE_TRCOMM regression and the EQ baseline gap in particular should have been caught on my side, and the false-negative test is the part I'm least comfortable with — I'll make sure the recurrence guard lands with the fix.

…not deallocate

Addresses k-yoshimi#228 finding 32 (P1, integrated-lifecycle regression).

The merge landed bpsi's ownership move of DEALLOCATE_TRCOMM from the standalone
program into the reusable menu. bpsi can do that safely because bpsi has no
integrated TOT regression path (`git ls-tree -r fa9dd49 tot/` = 12 files, no
totregress.f90; k-yoshimi's tree has 29 and does). On this tree, tot/totmenu.f90:48
calls tr_menu, so exiting the TR submenu destroyed RN/RT/AJ/QP before TOT could
read them.

Reproduced locally before the fix, on tot_demo2014_short (the input enters `tr`,
runs, then `x` exits the submenu — exactly the 9000 path):

    TOT_REGRESS_DUMP=1 tot/tot < test_run/inputs/tot_demo2014_short.in
    -> TR_PRESENT=0        (matches the reviewer's clavius result)

After:

    -> TR_PRESENT=1, NT=100, NRMAX=50, NSMAX=2, WPT=1.6203558665082904E+00

Fix: drop the call from tr/trmenu.f90's 9000 handler and restore it in
tr/trmain.f90 before STOP, i.e. back to the arrangement develop already had.
Cleanup stays owned at the other two layers too — tr/tr_api.f90 (tr_finalize)
for the library/MCP path, and tr/trcomm.f90's ALLOCATE_TRCOMM is self-healing on
re-entry — so removing the menu-level call leaks nothing. Verified the standalone
path still runs clean (tr2 on tr_m0904: exit 0, full dump, no double-free).

The USE list keeps only GTCPU1 + DEALLOCATE_TRCOMM; NFM/NGM/NSM/NTM are not
referenced anywhere in trmain.f90 (grep confirms zero hits).
Addresses k-yoshimi#227 item 1 (P1).

eq_mcp/server.py and tr_mcp/server.py performed the JSON-RPC fd-isolation dance
-- dup(1), dup2(2, 1), rebuild sys.stdout -- at *module scope*. Importing either
module therefore mutated the host process: pytest collection, an embedding
application, or a bare `python -c "import eq_mcp.server"` had its fd 1 redirected
to stderr and its sys.stdout replaced, irreversibly and process-wide.

The shim now lives in `_install_fd_isolation()` (idempotent, sets
_FD_ISOLATION_INSTALLED) and is called from main() immediately before
`build_server()` / `server.run()` -- i.e. only when the module actually runs as
the stdio server. The --help / --print-tools / MCP-SDK-missing paths all return
before that point, so the previous `_is_oneshot` guard is no longer needed and
the dead `_ONESHOT_FLAGS`/`_is_oneshot` pair is removed.

One knock-on: `_HAS_GFORTRAN_FLUSH` was forced False at import time whenever the
permanent redirect was active (the flush is unnecessary once fd 1 is stderr, and
_gfortran_flush_i4 SIGABRTs 10-40% of the time on this libgfortran). That
decision can no longer be made at import time, so it moved to the call site in
`_redirect_fortran_stdout_to_stderr`: `if _HAS_GFORTRAN_FLUSH and not
_FD_ISOLATION_INSTALLED`. Same behaviour for the server, correct behaviour for
an importing process.

Verification:
- New regression test python/mcp-servers/eq_mcp/tests/test_import_isolation.py
  probes both modules in fresh subprocesses. It PASSES with this change and
  FAILS on the pre-fix code (checked by stashing), so it genuinely pins the
  contract rather than just passing.
      before: fd1_unchanged=False sys.stdout_unchanged=False   (both modules)
      after : fd1_unchanged=True  sys.stdout_unchanged=True    (both modules)
- The real stdio path is unaffected: with eq/libeqapi.so built,
  test_get_psi_rz_tool.py::test_get_psi_rz_via_mcp -- which spawns the server as
  a subprocess and speaks JSON-RPC over the pipe -- passes. That test is the one
  that would break if the isolation were lost.
- Full suites: eq_mcp + tr_mcp = 77 passed, 3 skipped, 0 failed (--forked).
  (Without a built libeqapi.so the psi_rz test fails on both the pre- and
  post-fix code with a JSONDecodeError -- a missing-.so artifact, not a
  regression.)
Addresses k-yoshimi#227 item 2 (P1).

handle_finalize marked the Trlib handle closed *without* calling tr_finalize, to
dodge a SIGABRT in the Fortran cleanup path. Two consequences, both real:

- tr_init is idempotent (tr/tr_api.f90:77 "already initialized, just return
  OK"), so leaving g_initialized set made a subsequent init a no-op that did not
  restore defaults. Verified: set NSMAX=3, finalize, init -> still 3.
- handle_run_and_get_state needs a genuine reset for its documented fresh-init
  contract, so it kept calling STATE.close() -- the very tr_finalize path
  handle_finalize was avoiding. The server contradicted itself, and eq_mcp
  (plain STATE.close()) disagreed with tr_mcp.

The SIGABRT it was working around is a double DEALLOCATE, and it is already
fixed on this tree: DEALLOCATE_TRCOMM gained an idempotency guard
(`IF(.NOT.ALLOCATED(PNSS)) RETURN`, tr/trcomm.f90) that develop does not have --
`git show 4a5d977:tr/trcomm.f90` has the unguarded body. tr/tr_api.f90 itself
is unchanged vs develop, so the fix is entirely in the deallocator.

Measured on the merged tree (gfortran-15, macOS, fresh subprocess per run):

    init + close                 40/40 exit 0
    init + run(1) + close        40/40 exit 0
    init + run + close + an explicit second tr_finalize
                                 30/30 exit 0  (2nd returns TR_OK via the
                                                not-initialized guard)

So both lifecycle paths now use the honest one: handle_finalize is
`with _redirect_fortran_stdout_to_stderr(): STATE.close()`, structurally
identical to eq_mcp's. Contract restored -- set NSMAX=3, finalize, init -> 2.

New tests in python/mcp-servers/tr_mcp/tests/test_lifecycle.py cover
finalize->init reset, finalize idempotency, and run_and_get_state isolation.
The reset test FAILS on the pre-fix code (checked by stashing) and passes now.
Full eq_mcp + tr_mcp suites: 83 passed, 0 failed.

Caveat worth stating: the crash-rate measurements are macOS/gfortran-15. The
authoritative check is this PR's Linux/gfortran-13.2 CI, which is still awaiting
the fork-PR workflow approval.
Addresses k-yoshimi#227 item 3 (P1).

eqfile::EQSAVE is a bare external subroutine with no IERR out-argument: on an
FWOPEN failure (blank KNAMEQ, missing directory, permission denied) it prints a
message and RETURNs, leaving no file. eq_api_save then returned EQ_OK
unconditionally. Our own docstrings admitted this on both sides -- eq_api.f90
said "EQ_OK is returned unconditionally ... The caller is expected to verify the
file's existence", and eqlib.py said "callers should verify file existence after
the call". Shipping a known false-success with a comment instead of a guard was
the wrong call.

Fortran layer (eq/eq_api.f90) -- verify the artefact rather than trust the call:
  - blank KNAMEQ                       -> EQ_ERR_INVALID
  - no file, or a zero-length file     -> EQ_ERR_CALC_FAILED
  - otherwise                          -> EQ_OK
EQSAVE's own signature is untouched, so eqmenu.f90:98 (`CALL EQSAVE`, the
interactive path) is unaffected.

Python layer (python/eqlib/eqlib.py) -- the same check after raise_for_rc, as
defense in depth so a stale libeqapi.so built before this fix also cannot report
a success that did not happen. Both docstrings rewritten to state the contract
instead of apologising for its absence.

Verified against the rebuilt .so:
  missing directory -> EqlibCalculationFailedError (rc=3)
  blank path        -> EqlibInvalidParamError      (rc=1)
  valid path        -> OK, 45956-byte file written
Control (pre-fix eq_api.f90 + eqlib.py restored, .so rebuilt): save() to a
missing directory returned SUCCESS -- exactly the reported defect.

New regression tests in python/eqlib/tests/test_save.py cover the missing
directory and blank-path cases. Suites: eqlib + eq_mcp + tr_mcp = 133 passed,
2 skipped, 0 failed.
… 6 more components

Addresses k-yoshimi#228 findings 1, 20, 21 (P1).

The merge carries two bpsi changes to shared pl state:
  - plcomm_parm gained a bare PT(NSM) (pl/plcomm.f90:66). develop has no PT in
    that declaration block at all -- verified with `git show 4a5d977:pl/plcomm.f90`.
  - pl_view moved plparm -> the new pl/plview.f90 (develop's plparm has 3 refs;
    head has 0).

Our earlier CI-greening commits (17f21f2, 9242c12) contained this for ti, wm,
wmx and four menus, but only for the components CI builds. CI never compiles
trm/ wmf/ wmfn/ wmseki/ dpseki/, so the rest went unnoticed. All six victim
files are byte-identical to develop -- they are untouched casualties of a change
we are the courier for.

Reproduced before fixing (`make -C <dir>` from a clean worktree):
  trm     Error: Symbol 'pt' conflicts with symbol from module 'plcomm_parm'
  wmf     Error: Incompatible ranks 0 and 1 in assignment   (wmfem_vmec.f:517)
  wmfn    Error: Incompatible ranks 0 and 1 in assignment
  wmseki  Error: Incompatible ranks 0 and 1 in assignment
  dpseki  Error: Symbol 'pl_view' referenced at (1) not found in module 'plparm'

Fixes use the pattern the earlier commits established:
  - `USE plcomm,pt_pl=>PT` / `USE dpcomm,pt_dp=>PT` in trm, wmf, wmfn, wmseki.
    wmf needed BOTH routes (it imports plcomm and dpcomm; dpcomm_parm re-exports
    plcomm_parm's PT) -- the same double rename wmx/wmcomm.f90:11,24 already has.
    This restores the pre-merge meaning exactly: before bpsi added PT to
    plcomm_parm, bare PT in these legacy fixed-form files was an implicit local
    scalar, which is what the rename gives back.
  - `USE plview,ONLY: pl_view` in dpseki/dpmenu.f and dpseki/dpcont.f (the latter
    split from a combined `USE plparm,ONLY: pl_parm,pl_view`).

Separately, wm/wmsetg.f:310-311 is the silent variant of the same defect and is
the more dangerous one: `PT=(PTPR(1)+2*PTPP(1))/3.D0` is a legal Fortran
broadcast that overwrites the WHOLE shared PT(NSM) array with the electron
temperature, and `PT(1)` then reads it back. It compiles cleanly, so CI could
never have caught it -- wm builds green today. Changed to the local scalar PTAV,
matching the correct sibling at wm/wmsetg.f:185-186 (and wmvmec.f:530,
wmbooz.f:570).

After: trm, wmf, wmfn, wm, wmx, ti all compile. dpseki compiles (its remaining
link failure is a missing ../mtxp/libmtxbndmpi.o, an MPI artifact not built in
this worktree, not a code defect).

Two things found while doing this that are NOT ours, for the de-scope list:
  - fpx fails on fpcomm.f90:621 `IF(ALLOCATED(SAVPOS))` where SAVPOS is not
    declared ALLOCATABLE in that scope. fpx/fpcomm.f90 is byte-identical to
    develop, so fpx has never compiled -- pre-existing, unrelated to plcomm.
  - wmseki, after the PT fix, next fails on 'pl_wmxprf'/'wmspl_prof' not found in
    module plprof. pl/plprof.f90 is byte-identical to develop -- also pre-existing.
    wmseki additionally has no mod/ directory in the repo while 55 other mod/
    files are tracked; that packaging gap is left alone here.

Full python suite: 601 passed, 169 skipped, 0 failed.
…den its fallback

Addresses k-yoshimi#228 findings 10 and 15 (P1). They must land together -- see below.

Finding 10: the new MDLPCK=2 block-tridiagonal solver calls raw DGETRF/DGETRS
(7 sites, txnew/txexec.f90:1197-1271). Nothing in the tree defines those symbols,
and the repository default is a LAPACK-free build (make.header.org:13-14
`LAPACK = nolapack.f`, `LIBLA =`), so `make -C txnew` cannot link at all. Every
other solver in the tree goes through the LAPACK_* shim in lib/ instead -- which
had wrappers for DGBTRF/DGBTRS/DGBSV/DSBTRD/DSTEBZ/DGESV but none for
DGETRF/DGETRS. Added the two missing wrappers to lib/lapack.f (real calls) and
lib/nolapack.f (INFO=-1 stubs, matching the file's existing style), and pointed
the 7 call sites at them.

Finding 15: fixing 10 alone would have converted a loud link error into a silent
wrong answer. TXSOLV_BLOCKTRI maps a LAPACK failure to
`IER = 200000/220000 + 1000*k + info` (txexec.f90:1199,1212,1222,1229) -- always
POSITIVE -- while the caller's fallback fired only on `ierr_la == -2`
(txexec.f90:237, the "BL has non-zero far blocks" case). So with the nolapack
stubs returning INFO=-1, IER lands at 200000+1000k-1, the fallback is skipped,
and control drops into the error branch with the solution vector never solved.
The fallback now fires on any non-zero ierr_la, so a factorization failure --
from the stubs or from a genuine numerical problem -- degrades to the band
solver (LAPACK_DGBSV/GBSV) that the rest of the file already relies on. If that
also fails, the existing `if(ierr_la /= 0)` error branch still catches it.

Provenance: txnew/txexec.f90 is byte-identical to Kyoto bpsi/develop
(fa9dd49); develop (4a5d977) has no raw LAPACK calls in txnew at all. This is
Kyoto's code that our merge is the courier for, but it breaks the default build,
so it is fixed here rather than deferred.

Verified:
- `make -C txnew libtx3.a` -> exit 0 (was: "Undefined symbols ... _dgetrf_").
- `nm -gU lib/libtask.a` shows _lapack_dgetrf_ / _lapack_dgetrs_ in BOTH the
  real-LAPACK build and a `LAPACK=nolapack.f` rebuild.
- No undefined _dgetrf_/_dgetrs_ remain in the txnew objects.
Addresses k-yoshimi#228 findings 5, 8, 9, 12, 14, 18, 19, 23, 24. All ten sites are
byte-identical to Kyoto bpsi/develop (fa9dd49) -- this merge is their courier,
not their author -- and each was independently reproduced before patching.

equ/
  [9, P1]  equ_convert.f90:42 `DO n=n,LEN(line)` reads n uninitialised as its own
           loop bound. Reproduced: gfortran -Wuninitialized flags it; the same
           extract with `DO n=1,` compiles clean. Garbage > 256 => zero commas =>
           ALLOCATE(...(0)) then a read of element 1; garbage < 1 => out-of-bounds
           substring. Bound is 1, not 5: the record is `hdr,L1,...,Ln` with the
           header's comma at column 4 and no trailing comma, so counting from
           column 1 yields exactly ndata commas for ndata labels.
  [18]     equpl.f90:91 restores `plasmaf%nrmax=0`, dropped by the F77->F90
           conversion (`git show 7d18264:equ/eqpl.f`:91) while the sibling
           eqpl_get kept it at :163. NOTE FOR THE REVIEWER: this is symmetry /
           F77 fidelity, NOT the claimed P1. Against the bpsd this build pins
           (../bpsd @ 6a85985), bpsd_get_plasmaf takes its argument intent(out)
           and assigns nrmax unconditionally, so the store is provably dead --
           demonstrated with a standalone intent(out) test. It was load-bearing
           under an older intent(inout) bpsd, which is why the line existed.

wf2dt/
  [8, P1]  wfgsubn.f90: the PABS plot wrote rnode into BOTH coordinates (every
           triangle drawn at y=R, collapsing the mesh) and passed a 3-element
           vertex array to poly2D(...,4). Elements are triangles (ndelm(1:3,...)),
           and the fill loop above is DO i=1,3, so 3 is the true count and the 4th
           read was pure garbage. Now ya <- znode and poly2D(...,3).
  [12+31]  wfglib.f90: rgbf_a/rgbf_b DEALLOCATEd rgb_a/rgb_b and then read them in
           the clamp branches below (f < f_a(1) fires on any negative pabs) --
           rgbf_c already had this right and frees only the scratch array. Same
           patch also unwinds all three SPL1D failure paths, which previously
           RETURNed with the arrays allocated and INIT still 0, so the next call
           re-ALLOCATEd allocated arrays and aborted.
  [19]     wfglib.f90:567-568 `1.-12` parses as -11.0, so `ABS(fmax).LE.-11.0` is
           never true and the near-zero colour-range fallback never fired. Now
           1.D-12. (Provenance note: terasakisatoshi filed this as PR-introduced;
           it is byte-identical to fa9dd49, i.e. carried, not authored here.)

trx/
  [5, P1]  trpnb.f90:477 zero-initialises VCD3/VCT3/VCA3 before the three
           IF(NS_x.NE.0) guards. develop assigns all three unconditionally; the
           new guards leave a term undefined when a species is absent, and VC3 =
           VCD3+VCT3+VCA3 is then used as a divisor.
  [24]     trprep.f90:131 `DO NS=1,MAX(NSMAX,4)` -> `DO NS=1,NSMAX`, restoring
           develop's bound. Labelling species past NSMAX defeats libnf's presence
           STOP and enables out-of-bounds writes in tr_pnf. Applied TOGETHER with
           finding 5 by design: reverting the bound without the zero-init would
           widen finding 5's exposure.
  [23]     trexec.f90: the new "Kawamura correction" dropped the NSTN.EQ.0 guard
           every sibling branch has, reading X(0) when the equation is excluded.
           Restored, with develop's pre-Kawamura formula in the NSTN==0 arm.
  [14, P1] in/test01.in, in/test02.in: `model_nnf(1)=4` is not in NAMELIST /TR/,
           so every shipped run printed '## PARM INPUT ERROR' and then continued
           with an unintended configuration. The variable is a late-allocated
           ALLOCATABLE outside the namelist scope and no physics reads it, so
           removing the key is the correct fix rather than extending the namelist.
           Verified from trx/ (the cwd the binary needs -- running elsewhere makes
           GSFOPN fail and the menu never reaches the namelist read, which
           invalidated my first attempt at this check):
               before: PARM INPUT ERROR = 1 on both inputs
               after : PARM INPUT ERROR = 0 on both inputs

Verification: equ, trx, wf2dt, trm, wm, wmx, wmf, wmfn, tot, lib, pl, eq, tr all
compile. Python suite 601 passed / 0 failed. TOT still TR_PRESENT=1. The tr_m0904
TR_REGRESS dump is byte-identical to the pre-refactor baseline
(sha256 56d09887...cb9ad869), i.e. none of this perturbs tr numerics.

Separate observation for the PR thread, NOT patched here: develop's
trx/in/test01.in set `model_pnf=1`; bpsi's copy does not, and the merge took
bpsi's file, so the shipped sample no longer exercises the fusion model at all.
That is a test-coverage regression worth a maintainer decision rather than a
unilateral edit.
… step

Two CI changes, both prompted by PR k-yoshimi#226's review.

1. New `uncovered-modules-build` job (compile-only).

The pytest job builds tr/fp/ti/wr/wrx/eq/tot, and `make -C tot libs` adds
pl/dp/wmx/ob/adf11/adpost. equ/ trx/ txnew/ wf2dt/ wim/ wm/ trm/ wmf/ wmfn/ have
had NO CI coverage at all -- and every one of them is substantially touched by
the bpsi merge. That gap is the structural reason a batch of hard compile/link
breakages reached human review in k-yoshimi#226 instead of the pipeline: a bare PT added
to plcomm_parm colliding with local PTs (trm, wmf, wmfn, wmseki), pl_view moving
plparm->plview without migrating its callers (dpseki), and raw DGETRF/DGETRS
with no LAPACK shim (txnew). Nine directories, library targets only -- the
standalone binaries pull in graphics (gsaf) and MPI link paths this headless
runner deliberately does not provide, while compiling every source is what
catches the defect class above.

Deliberately excluded, with the reason recorded in the job comment so the
omission is auditable rather than silent: fpx, wmseki and dpseki do not build on
`develop` either (their failing files are byte-identical there), so including
them would make the job red on day one for defects this PR did not introduce.

All nine targets verified green locally before wiring them up.

2. Path-equality guard on the `../task` symlink step (both workflows).

The step's own comment claimed it was a "no-op when the repo is already named
task". It is not: when the destination is a real directory, `ln -sfn` falls back
to link-into-directory mode and creates a self-referential `task -> .` symlink
INSIDE the checkout. Reproduced on GNU coreutils 8.32 (the CI toolchain) and on
BSD ln -- both exit 0, so it is silent. Harmless for the current job graph, but a
symlink loop for any future recursive find/tar/artifact-glob step. Now guarded on
path equality, and both stale comments corrected.

Note for the record: this is NOT why PR k-yoshimi#226 reports no checks. That run
(29195792410) is `conclusion: action_required` with `{"total_count": 0, "jobs":
[]}` -- the fork-PR approval gate, so no step ran at all. regen-baselines.yml is
additionally `workflow_dispatch`-only and can never contribute a PR check.
…atch

Two independent reviews found two blockers and several real gaps. Both blockers
were in work I had already called verified, so the corrections are recorded here
rather than folded silently into the earlier commits.

BLOCKER 1 (channel A) — the new CI job could not have passed.
My "all nine targets verified green locally" was a dirty-worktree false green:
this worktree carries untracked trlib/mod/*.mod and equ objects from earlier
builds. Re-tested properly in a PRISTINE clone of HEAD, 3 of 9 failed:
  trx, trm  Cannot open module file 'cytran_mod.mod'
  equ       Cannot open module file 'aaa_mod.mod'
Root causes, both real Makefile facts rather than environment:
  - trx/ and trm/ compile against trlib's .mod files via `-I../trlib/$(MOD)`,
    and neither libtr2.a target bootstraps trlib. Only trlib/mod/dummy is
    tracked, so a fresh clone has none of them.
  - equ/'s `libequ.a: $(OBJS)` covers SRCS only, but equcomm.f90 -- which
    defines aaa_mod, USEd across SRCS -- lives in the separate SRCC/OBJC
    variable, so the library target alone can never build from clean.
Added `make -C trlib libtrm.a` and `make -C equ obj/equcomm.o` to the support
step. Re-verified in a pristine clone: 9/9 green, job exits 0.

BLOCKER 2 (channel B) — k-yoshimi#228 finding 21 was only 2 of 6 sites.
b4c07ef migrated dpseki/dpmenu.f and dpseki/dpcont.f but missed
fpx/fpmenu.f90:23, fp.anzai/fpmenu.f90:19, fp.nuga/fpmenu.f90:19 and
fp.ota/fpmenu.f90:19 -- all four still `USE plparm` and `CALL pl_view`, and
pl/plparm.f90 no longer re-exports pl_view, so they carry the identical hard
compile error the commit claimed to have swept. Now routed via `USE plview`
like fp/fpmenu.f90 already was.

Also fixed:
- [A-MED] txnew/txexec.f90: the widened fallback `ierr_la /= 0` also swallowed
  IER=-1 (BL/BX too small, :1092) and would have called LAPACK_DGBSV with the
  very dimensions just found unsatisfied. Narrowed to `== -2 .or. > 0`, so the
  dimension error keeps its loud path.
- [B-MED] eq_save's artefact check could not tell "written now" from "already
  there": channel B reproduced a false success by saving once, setting MODEFW=3
  (so FWOPEN refuses to reopen), and saving again -- the first run's file made
  both the Fortran INQUIRE and the Python os.path check pass. EQSAVE now takes a
  real IERR out-argument and propagates FWOPEN's failure; eq_api_save honours it
  before falling back to the artefact check. Both call sites updated
  (eq/eqmenu.f90:98, eq/eq_api.f90). New regression test reproduces the exact
  repeat-save scenario and asserts the earlier file is left untouched.
- [A-MED] Stale docstrings in eq_mcp/tr_mcp still said the fd shim runs "at
  startup (BEFORE any mcp/logging import)" and "at module load time", and that
  the context manager is "effectively a no-op" -- false for an importer, which
  is now exactly the case it protects. Rewritten.
- [A-MED] tr/trcomm.f90's idempotency-guard comment cited "tr_menu calls this
  unconditionally at exit" -- the call 98ba1e2 removes. Now cites trmain and
  tr_api_finalize.
- [B-LOW] "wm owns a local PT" in wmf/wmfn/wmseki was wrong: those trees have no
  declared local PT, they rely on implicit REAL*8 typing. Comment corrected.
  (Accurate as written for trm and wmx, which do declare one.)
- [A-MED] test_finalize_is_idempotent's docstring claimed to exercise the
  double-DEALLOCATE guard; _ServerState.close() nulls the handle so the second
  call never re-enters Fortran. Docstring now says what the test actually covers.

Disclosed but deliberately NOT changed here (for the PR thread):
- Restoring `DO NS=1,NSMAX` (finding 24) leaves NS_T/NS_He4 at 0 for NSMAX<4,
  which several unguarded trx/ sites would index as PZ(0)/PA(0)/PIN(NR,0).
  Upstream trm/ has always used this bound with the same half-guards, several of
  those routines hardcode species 3/4 regardless, and both committed inputs pin
  NSMAX=4 -- so NSMAX<4 was never supported. Reverting the bound would instead
  re-break libnf's presence STOP, which is the defect being fixed.
- Channel B also found pre-existing defects this batch does not claim: NS_He5
  never reset by tr_prep_ns; libnf CASE(1,12,13) vs the documented model_pnf=14;
  eq/eqfile.f90's transposed PSI/DELPSI/HJT write-vs-read; tr/trloop.f90's two
  bare `return`s; missing IOSTAT on equ_convert's READs; txmain never inspecting
  IERR. All are worth issues; none are regressions from this PR.

Verification: full python suite 602 passed / 169 skipped / 0 failed. TOT still
TR_PRESENT=1. tr_m0904 dump still byte-identical to the pre-refactor baseline
(sha256 56d09887...cb9ad869). CI job re-verified 9/9 on a pristine clone.

Review note: codex was unavailable in this environment (the CLI rejects the
default model as too new, and the account cannot select gpt-5.2/gpt-5.2-codex),
so the second channel was an independent adversarial reviewer with a different
mandate rather than the usual codex pass.
@HengyuLi-Ozaki-lab

Copy link
Copy Markdown
Author

Pushed 9 commits (head 10fb296f). This covers all of #227 and the #228 items that break a build or leave UB on a default path, plus the CI gap that let most of them reach you in the first place.

Each fix was reproduced before being written and re-verified after; where I could not reproduce a reported mechanism I say so below rather than patching on faith.


What landed

commit fixes how it was verified
98ba1e20 #228-32 tr_menu destroying integrated TOT state tot_demo2014_short: TR_PRESENT=01 (NT=100, NRMAX=50, WPT=1.6203558665082904)
0bf7e399 #227-1 import-time fd surgery new subprocess probe: fd1_unchanged False→True, sys.stdout_unchanged False→True, both servers
7e03db86 #227-2 handle_finalize not resetting the backend set NSMAX=3 → finalize → init: was still 3, now back to 2
3dc9810b + 10fb296f #227-3 eq_save false success missing dir → EqlibCalculationFailedError; blank path → EqlibInvalidParamError; valid path → 45956-byte file
b4c07ef5 #228-1, -20, -21 plcomm PT leak + pl_view move trm/wmf/wmfn/wmseki/dpseki went from hard compile errors to clean
fa7244f2 #228-10, -15 txnew DGETRF/DGETRS + fallback make -C txnew libtx3.a exit 0; nm shows the shim in both the real-LAPACK and nolapack.f builds
0b5bc72c #228-5, -8, -9, -12, -14, -18, -19, -23, -24, -31 per-item; #228-14 measured end-to-end: PARM INPUT ERROR 1 → 0 on both shipped inputs
13ea6c31 the CI coverage gap new compile-only job over the 9 directories pytest never builds

tr_m0904's TR_REGRESS_DUMP output is still byte-identical to the pre-merge baseline (sha256 56d09887…cb9ad869) after all of it, so none of this perturbs TR numerics. Full Python suite: 602 passed, 0 failed.

Two things I got wrong, and corrected

Worth stating plainly since both were in work I had already called verified.

The new CI job could not have passed. My "all nine targets green locally" was a dirty-worktree false green — this checkout had trlib/mod/*.mod left over from earlier builds. Re-tested in a pristine clone, 3 of 9 failed, for two real reasons: trx/trm compile against trlib's .mod files via -I../trlib/$(MOD) and neither libtr2.a target bootstraps trlib (only trlib/mod/dummy is tracked); and equ's libequ.a: $(OBJS) covers SRCS only, while equcomm.f90 — which defines aaa_mod, USEd across SRCS — lives in the separate SRCC/OBJC variable, so the library target alone can never build from clean. Both added to the support step; re-verified 9/9 on a fresh clone.

#228-21 was only 2 of 6 sites. I migrated dpseki/dpmenu.f and dpseki/dpcont.f and missed fpx/fpmenu.f90:23, fp.anzai/fpmenu.f90:19, fp.nuga/fpmenu.f90:19, fp.ota/fpmenu.f90:19 — all four carrying the identical error. Now swept.

And one hole in my own eq_save fix. Checking "a non-empty file exists afterwards" cannot distinguish written now from already there: save once, set MODEFW=3 so FWOPEN refuses to reopen, save again — the first run's file makes both the Fortran INQUIRE and the Python check pass. Since repeat-save-to-the-same-path is the normal MCP/optimizer workflow, every save after the first was effectively unverified. EQSAVE now takes a real IERR out-argument and propagates FWOPEN's failure; eq_api_save honours it before falling back to the artefact check. Both call sites updated; regression test reproduces exactly that sequence.

The CI gap is the part I'd most like your view on

python-tests.yml builds tr/fp/ti/wr/wrx/eq/tot, and make -C tot libs adds pl/dp/wmx/ob/adf11/adpost. That leaves equ/ trx/ txnew/ wf2dt/ wim/ wm/ trm/ wmf/ wmfn/ with no coverage at all — and they are exactly where most of #228 lives. A compile-only job over those nine is one job, and it would have caught the PT collision, the pl_view migration and the missing DGETRF shim before you ever saw them.

fpx, wmseki and dpseki are deliberately excluded, with the reason in the job comment so the omission is auditable rather than silent — they do not build on develop either (see below).

Proposed de-scoping, with evidence

Both of these surfaced while fixing the PT/pl_view class, and neither is a regression from this PR:

  • fpx fails at fpcomm.f90:621 IF(ALLOCATED(SAVPOS)) where SAVPOS is not declared ALLOCATABLE in that scope. fpx/fpcomm.f90 is byte-identical at 4a5d977d, so fpx has never compiled.
  • wmseki, once the PT leak is fixed, next fails on pl_wmxprf/wmspl_prof not found in module plprof. pl/plprof.f90 is byte-identical at 4a5d977d. (wmseki also has no mod/ directory in the repo while 55 other mod/ files are tracked — left alone here.)

Together with the items already moved to #229 and #224, and #227 item 4 (refuted earlier — eqlib.py:40 caps KNAMEQ at 80 bytes and _encode_name raises above it), that should leave the gate at the items above plus the EQ baseline contract.

Pre-existing defects worth their own issues

Found while reviewing this work; all verified NOT to be regressions from this PR, so I have not touched them:

  • trx/trprep.f90 tr_prep_ns zeroes eight species labels but never NS_He5, so it keeps pl/plinit.f90's value 7 permanently — the same "labels a slot beyond NSMAX" mechanism as PR #226 follow-up: fix non-MCP regressions in PL/TR/TRX/WM/W1 #228-24, on the one label the fix does not reach.
  • trx/libnf.f90:185 matches CASE(1,12,13) (13 is not a valid model_pnf), and the documented model_pnf=14 (libnf.f90:35) falls through to the CASE DEFAULT … STOP.
  • eq/eqfile.f90: EQSAVE writes ((PSI(NSG,NTG),NSG=1,NSGMAX),NTG=1,NTGMAX) at :57 while EQRTSK reads ((PSI(NTG,NSG),NTG=1,NTGMAX),NSG=1,NSGMAX) at :163 — silently transposed whenever NSGMAX /= NTGMAX, masked today because eqinit defaults both to 32. Now reachable from Python via the new eq_save, so worth fixing even though it is old.
  • tr/trloop.f90 has two bare if(ierr.ne.0) return where the neighbours use GOTO 9000, skipping the RIPS/RIPE restore.
  • equ/equ_convert.f90's seven READs carry no IOSTAT, and ndata=0 is unhandled.
  • txnew/txmain.f90 never inspects IERR, so a solver failure still exits 0.

Two disclosures on my own changes

#228-24. Restoring DO NS=1,NSMAX leaves NS_T/NS_He4 at 0 for NSMAX<4, which several unguarded trx/ sites would index as PZ(0)/PA(0)/PIN(NR,0). I still think restoring the bound is right — it matches develop and trm/, several of those routines hardcode species 3/4 regardless, and both committed inputs pin NSMAX=4, so NSMAX<4 was never supported — but reverting it would re-break libnf's presence STOP, which is the defect being fixed. Happy to go the other way if you prefer.

Test coverage. develop's trx/in/test01.in set model_pnf=1; bpsi's copy does not, and the merge took bpsi's file — so the shipped sample no longer exercises the fusion model at all. That felt like your call rather than a unilateral edit.

Still blocked on the same thing

The workflows have still never run. The new head has its own run — 30196568304, again conclusion: action_required with {"total_count": 0, "jobs": []} — so nothing in this push has been compiled or tested by CI either. Could someone click "Approve and run workflows"? That also unblocks the EQ baseline regeneration — regen-baselines.yml is workflow_dispatch-only and cannot be dispatched from a fork PR head.

Once CI is green I will regenerate eq_iter01/eq_tst2/eq_jt60 with the three-variable rationale (NTVMAX 200→400, NLPMAX 20→100, eqcalq NMAX 200→400), regenerate the four committed eqdata fixtures, and add the assertion that stops that false-negative recurring.

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.

6 participants