Skip to content

fix(eq): eq_save refuses after a set of tot's eq: parameters too - #267

Open
HengyuLi-Ozaki-lab wants to merge 1 commit into
k-yoshimi:developfrom
HengyuLi-Ozaki-lab:fix/eq-save-generation-and-notes
Open

HengyuLi-Ozaki-lab wants to merge 1 commit into
k-yoshimi:developfrom
HengyuLi-Ozaki-lab:fix/eq-save-generation-and-notes

Conversation

@HengyuLi-Ozaki-lab

Copy link
Copy Markdown
Collaborator

Follow-up of the reviews of #256 and #257, with the counter you proposed on #256 (2026-10-17).

eq_save sees a set of tot's eq: parameters

eq_save's precondition was a flag that eq_api_set_param cleared. tot's eq: dispatcher calls eq_param_registry directly, so after tot_set_param("eq:RR", ...) the flag stood and eq_save wrote the parameters as they stood over fields computed for the old ones. As #259 did for tr, fp and ti:

The smaller points of the two approvals

  • eq_mcp's save tool: the MODEFW it sets stays set afterwards.
  • eq/eqfile.f90, comment only (upstream Fortran; take it out if it should not go to the lead): the length comparison depends on the run-time, and one that reports the length on disk, or -1, makes it a no-op, not a false failure.
  • test_save.py: a refused write leaves at most the limit (<=), whatever the run-time's buffer size.
  • _encode_name's docstring: 80 bytes, not 79.
  • python/_fd_isolation.py: fd 2 is repaired only when fstat says EBADF; another error means fd 2 is there, so it is left alone and the installation fails before changing anything. A host's sys.stdout whose flush raises an Exception no longer stops the installation.
  • Not taken: the os.isatty(1) exemption for a server started by hand. A host may run the server on a pseudo-terminal and parse that stream, and the exemption would put the library's output back on it; the READMEs (docs(mcp): READMEs say where Fortran unit 6 output goes under the fd isolation #264) say how to keep that output (2>server.log). Say if you want it anyway, for instance behind an environment variable.

Tests

  • test_save.py: a refused set and a string set leave the save allowed.
  • test_set_after_run.py, on the mono image (CI's mono routing step already runs the file): eq_save writes after tot_init, MODEFW=0, eq_run(0) and tot_set_param_str("eq:KNAMEQ") (so tot's string path is shown not to count), refuses after tot_set_param("eq:RR"), writes after tot_set_param("eq:MODEFW"). Also checked by hand: through libtotapi.so (not only the mono image) eq:RR makes eq_save refuse; there is one counter, in libeqapi.so's registry.
  • test_fd_isolation.py: an fd 2 that fstat cannot look at is not replaced; a failing flush does not stop the installation.
  • Each fails without its change (measured on copies, eq and the mono image rebuilt): with develop's eq_api.f90 and registry, only the eq:RR case fails; with eq_save on the flag alone, the parameter-change case and the eq:RR case; counting the file modes, four; counting a refused set, the refused-set case; counting string sets, 22 test_save.py cases, eq_mcp's real-library save case and two mono cases; each isolation case without its change.

macOS, clean build, CI's flags: 975 passed, 41 skipped; with MONO_LIB_PATH the mono tests and test_save.py: 47 passed, 4 skipped (TOTLIB_PATH not set, no /dev/full).

🤖 Generated with Claude Code

Follow-up of the reviews of k-yoshimi#256 and k-yoshimi#257, and of the maintainer's
comment on k-yoshimi#256 of 2026-10-17 (the k-yoshimi#253 counter for eq).

eq_save's precondition was a flag that eq_api_set_param cleared. tot's
"eq:" dispatcher (tot/tot_param_registry.f90) calls eq_param_registry
directly, so after tot_set_param("eq:RR", ...) the flag stood and
eq_save wrote the parameters as they stood over fields computed for the
old ones. As k-yoshimi#259 did for tr, fp and ti:

- eq_param_registry counts every successful numeric set
  (eq_param_generation()); eq_param_set is now a wrapper around the
  former body (eq_param_set_impl). Not counted: MODEFR and MODEFW (they
  say how a file is opened; the base name is taken as the setter reads
  it), and the string sets (eq_param_set_str, KNAMEQ and the other
  KNAM*), as eq_api_set_param_str already left the flag alone: the
  solve reads none of them, a load records the count itself, and
  KNAMEQ is where eq_save writes. A refused set is not counted.
- eq_api records the count when eq_run(0) or eq_run(1) succeeds, and
  eq_save refuses (EQ_ERR_INVALID, nothing touched) when the flag is
  down or the count moved. The clear in eq_api_set_param is gone.
- eq_api.h and the eqlib README: tot's eq: parameters are now seen;
  what no registry sees in the mono image (another module's eq_init,
  eq_load or eq_calc, a plcomm variable set through another module's
  registry such as ti:RR) is k-yoshimi#260.

The smaller points of the two approvals:
- eq_mcp's save tool: the MODEFW it sets stays set afterwards.
- eq/eqfile.f90, comment only: the length comparison depends on the
  run-time; one that reports the length on disk, or -1, makes it a
  no-op, not a false failure.
- test_save.py: a refused write leaves at most the limit (the run-time's
  buffer decides how much was flushed).
- _encode_name's docstring: KNAMEQ takes 80 bytes, not 79.
- python/_fd_isolation.py: fd 2 is repaired only when fstat says EBADF;
  another error means fd 2 is there, it is left alone and the
  installation fails before changing anything. A host's sys.stdout whose
  flush raises an Exception (not only OSError / ValueError) no longer
  stops the installation.
Not taken: an os.isatty(1) exemption for a server started by hand in a
terminal. A host may run the server on a pseudo-terminal and parse that
stream; the READMEs (k-yoshimi#264) say how to keep the library's output
(2>server.log).

Tests: test_save.py, a refused set and a string set leave the save
allowed; test_set_after_run.py, on the mono image (CI's mono routing
step already runs that file; the cases sit at the end of the file),
eq_save writes after tot_init, MODEFW=0, eq_run(0) and
tot_set_param_str("eq:KNAMEQ"), refuses after tot_set_param("eq:RR"),
writes after tot_set_param("eq:MODEFW"); test_fd_isolation.py, an fd 2
that fstat cannot look at is not replaced, a failing flush does not stop
the installation. The fd isolation's two points get a CHANGELOG entry
of their own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@k-yoshimi k-yoshimi left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Review through the full cycle (Codex + in-house domain, test-oracle lenses + docs/workflow review), with eq/libeqapi.so and libtotapi_mono.so rebuilt from this head (the mono image with the CI recipe) and the tests run under the CI flags.

Verdict: approve. The counter holds on every path the reviewers traced: eq_set_param and tot's eq: numeric dispatcher both go through the new eq_param_set wrapper (tot/tot_param_registry.f90:145-150), the string paths reach eq_param_set_str and are uncounted by design, eq_run(0)/(1) record the count only after success and clear the flag before the attempt, eq_init/eq_finalize clear the flag so the never-reset g_equilibrium_generation cannot admit a save after finalize → init without a run, and tot_finalize reaches eq_api_finalize. EQSAVE's record (eq/eqfile.f90:65-100) consists only of counted registry entries; EQCALC recomputes from scratch each call, so eq needs no re-prep. The MODEFR/MODEFW exclusion uses the same parse_array_subscript + TRIM compare as the setter itself, so MODEFW[1] is treated the same on both sides.

Runs: eqlib + test_fd_isolation.py + eq_mcp: 161 passed, 3 skipped. Mono image from this branch: the three new TestEqSaveAfterTotSetOnMonoImage cases pass. The test-oracle reviewer also ran that class against develop's mono image (no eq_param_generation symbol): only test_eq_save_refuses_after_a_tot_set fails there, (0, True) != (1, False), so the test discriminates and the control cases do their job. CI green at d7f60d55; the mono step runs this file with MONO_LIB_PATH.

LOW, fold in if you like (none blocks):

  1. python/eqlib/eqlib.py:317-326 — the Eq.save docstring still says "set_param of anything but MODEFR and MODEFW"; the README and eq_api.h got the tot eq: / string-set / #260 sentences, the API docstring did not. Same for eq_mcp/server.py's save docstring ("the two file modes") and the eq_mcp README save row / MODEFW description (MODEFW stays set afterwards).
  2. test_save.py:338 <= limit admits a 0-byte file; the raise and the XX EQSAVE line still pin the failure path, but 0 < size <= limit keeps the docstring's partial-write claim without depending on the runtime's buffering.
  3. test_fd_isolation.py EIO case: the old code also raises EIO (from _same_file after it had already replaced fd 2), so same_fd2=True is the discriminating assertion; a separate assert with its own message would keep that visible.
  4. _EqSaveCase "writes" cases: os.path.getsize("eq.bin") > 0 instead of exists pins the write independently of eq_api_save's own non-empty check.

Noted, no change asked: NPRINT and IDEBUG move the counter although they only steer printing and do not change the solve's result — same as the pre-diff flag through eq_set_param and as tr/fp/ti count, and the documented exclusion set is exactly the one implemented. Pre-existing and outside this diff: eq/eqfile.f90:184 EQ_READ's MODELG=15 branch calls EQDSK without IERR, so eq_run(1) could record a generation over a load that failed; and tr's run path rewrites eq's NRMAX/NTHMAX/NSUMAX through equnit::eq_parm (tr/trmetric.f90:30-41) before its eq_load, one more uncounted write for the #260 list.

The Fortran hunks are the C-ABI layer (eq_api.f90) and the registry (eq_param_registry.f90), the same shape as #259 for tr/fp/ti; the eqfile.f90 hunk is a three-line comment and can stay as it is. So no lead confirmation is needed for this one; merge on the maintainers' instruction.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants