Skip to content

feat(trlib): plot API + TOML config runner (reference)#83

Open
k-yoshimi wants to merge 15 commits into
developfrom
feature/trlib-plot-toml-runner
Open

feat(trlib): plot API + TOML config runner (reference)#83
k-yoshimi wants to merge 15 commits into
developfrom
feature/trlib-plot-toml-runner

Conversation

@k-yoshimi
Copy link
Copy Markdown
Owner

@k-yoshimi k-yoshimi commented Apr 18, 2026

Summary

Reference implementation of the declarative plot(varname) API and TOML-driven runner described in project_visualization_followup.md and project_toml_sample_runner.md. trlib is the most mature Python wrapper (L-0..L-7 merged) so it lands first; ti / wr / wrx / fp / eq will copy this pattern after their L-7 + MCP work.

What's new

Plot API — python/trlib/plot.py (new)

  • plot(varname, *, output, format, path, title, overlay, state) — 1D / species-stacked 2D radial profiles via matplotlib
  • plot_sweep(param, y, *, range) — ad-hoc parameter scan
  • plot_available() + VARIABLE_INFO registry so callers (humans, MCP, LLMs) can probe support before requesting a figure
  • Output modes: window (interactive), file (returns Path), return (returns Figure); _in_notebook() heuristic flips plt.show to non-blocking inside Jupyter
  • matplotlib stays an optional dependency — importing trlib.plot raises a clear ImportError when matplotlib is absent; trlib itself remains importable

Trlib instance methods

  • Trlib.plot(varname, **kwargs) — captures state internally and forwards to trlib.plot.plot
  • Trlib.plot_available() — staticmethod so callers can probe without instantiating (no libtrapi.so load required)

TOML config loader — python/trlib/loader.py (new)

  • load_config(path_or_stream) returns {module, scalars, arrays, strings, plots, raw}; accepts pathlib.Path, str, bytes, or any file-like
  • apply_config(tr, cfg) replays strings -> scalars -> arrays in the order the existing tests/fixtures/*.py::apply helpers use
  • run_plots(tr, cfg) executes every [plot] / [[plots]] spec; supports kind = "sweep" entries
  • Uses stdlib tomllib (3.11+) with tomli fallback

CLI entry — python/trlib/__main__.py (new)

python -m trlib python/trlib/samples/iter01.toml          # full run + plots
python -m trlib python/trlib/samples/iter01.toml --dry-run  # parse only
python -m trlib python/trlib/samples/tst2.toml --ntmax 5    # NTMAX override
python -m trlib python/trlib/samples/iter01.toml --no-plots # skip matplotlib

Exit codes: 0 success, 1 library/calc error, 2 config error.

TOML samples — python/trlib/samples/

  • iter01.toml — mirror of test_run/inputs/tr_iter01.in (4 species, NTMAX=100)
  • tst2.toml — mirror of test_run/inputs/tr_tst2.in (uses sparse PA = { 2 = 1.0 } form)

Tests

  • test_plot.py (10 tests, skip if matplotlib absent) — VARIABLE_INFO schema, plot_available ordering, all 3 output modes round-trip with hand-built TrState
  • test_loader.py (10 tests, no .so needed) — inline string / bytes / Path / BytesIO inputs, singular [plot] table, [module].ntmax alias, error paths
  • test_main.py (8 tests) — --help / --dry-run / --ntmax / --no-plots smoke; library-dependent test gated on libtrapi.so

Run: python3 -m unittest discover python/trlib/tests -> 56 tests, 14 skipped (lib-dependent), 0 failures.

Docs

README gains TOML schema + plot section in です・ます調 with sample invocations.

Reference pattern for other modules

When ti / wr / wrx / fp / eq follow:

  1. Copy plot.py → adjust VARIABLE_INFO for that module's state schema
  2. Copy loader.py verbatim (schema is module-agnostic) and tweak the apply_config ordering if needed
  3. Copy __main__.py, swap the Trlib import
  4. Add samples/*.toml mirroring test_run/inputs/*.in
  5. Mirror the three test files

Hard constraints honoured

  • No Fortran / Makefile / other Python lib changes
  • No core trlib.py field changes beyond .plot() / .plot_available()
  • matplotlib remains optional; --no-plots works without it
  • No --admin / no self-merge

Test plan

  • CI: python3 -m unittest discover python/trlib/tests — all pass / skip cleanly
  • python -m trlib --help prints usage
  • python -m trlib python/trlib/samples/iter01.toml --dry-run exits 0
  • When libtrapi.so present: full run produces plots under ./plots/

Note

Medium Risk
Adds a new CLI entrypoint and optional matplotlib plotting layer, plus configuration parsing that can affect how simulations are parameterized and executed. Risk is moderated by --dry-run/library gating and unit tests, but it touches execution flow and introduces new user-facing I/O paths.

Overview
Adds a TOML-driven python -m trlib runner that loads a config, applies scalars/arrays/strings, runs the simulation, and optionally generates plots, with --dry-run, --ntmax overrides, --no-plots, and defined exit codes.

Introduces a matplotlib-backed plotting module (trlib.plot) with a VARIABLE_INFO registry, plot() output modes (window/file/return), and plot_sweep() for parameter scans, and wires it into Trlib via new Trlib.plot() and Trlib.plot_available() helpers.

Ships sample configs (iter01.toml, tst2.toml), expands README with TOML/plot documentation, and adds unit tests covering TOML parsing/application, CLI smoke paths, and plotting (skipped when matplotlib or libtrapi.so are unavailable).

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

k-yoshimi and others added 5 commits April 19, 2026 07:42
Reference implementation of the declarative visualization layer
described in project_visualization_followup.md. trlib.plot exposes:

- plot(varname, *, output=..., format=..., path=..., ...) for 1D
  and species-stacked 2D radial profiles
- plot_sweep(param, y, *, range=...) for ad-hoc parameter scans
- plot_available() / VARIABLE_INFO metadata so MCP tools and human
  callers can probe support before requesting a figure

Trlib gains two thin convenience methods (.plot / .plot_available)
that capture state internally and forward to trlib.plot. matplotlib
stays an optional dependency: importing trlib.plot raises a clear
ImportError when matplotlib is missing, but trlib itself remains
usable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements the schema described in project_toml_sample_runner.md.
trlib.loader.load_config() parses TOML into a structured dict
(module/scalars/arrays/strings/plots); apply_config() replays the
data against a live Trlib instance; run_plots() executes [plot] /
[[plots]] specs via trlib.plot.

trlib.__main__ wires the pipeline together for
`python -m trlib <config.toml>` with --dry-run / --ntmax / --no-plots
flags. Exit codes follow the spec: 0 success, 1 library error, 2
config error.

tomllib (stdlib 3.11+) is used directly with a tomli fallback for
older runtimes. matplotlib stays optional: --no-plots lets the
runner work without it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirror test_run/inputs/tr_iter01.in and tr_tst2.in as TOML configs
that exercise the new python -m trlib runner end-to-end. Each
sample defines [scalars] / [arrays] / [strings] matching the
namelist case and a [[plots]] section that writes profile PNGs
under ./plots/ when matplotlib + libtrapi.so are available.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- test_plot.py: skips if matplotlib absent; verifies VARIABLE_INFO
  schema, plot_available() ordering, and round-trips through the
  three output modes (window/file/return) using a hand-built TrState.
- test_loader.py: pure parsing tests with inline TOML strings, file
  paths, and BytesIO streams; checks the singular [plot] table form,
  the [module].ntmax -> NTMAX scalar alias, and that apply_config
  pushes scalars/arrays/strings in the documented order.
- test_main.py: --help / --dry-run / --ntmax / --no-plots smoke
  tests that never touch libtrapi.so; library smoke test gated on
  the .so being built.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds two new sections (です・ます調) at the bottom of the README:

- "TOML config 実行方法" — schema overview, exit codes, sample
  invocations of python -m trlib (--dry-run / --ntmax / --no-plots).
- "プロット" — output modes (window/file/return), plot_available()
  introspection, and a note that matplotlib stays optional.

Refines the "no graphics" limitations bullet to point at trlib.plot
as the Python-side alternative to PGPlot/GSAF.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@k-yoshimi
Copy link
Copy Markdown
Owner Author

@cursor review

Comment thread python/trlib/plot.py Outdated
@k-yoshimi
Copy link
Copy Markdown
Owner Author

@cursor review

Bugbot HIGH on PR #83: plot_sweep had a 'range' parameter that
shadowed the builtin, so the list-comprehension `range(n)` raised
TypeError on every call.

Add 'sweep_range' as the canonical kwarg, keep 'range' as a
backward-compat alias so existing TOML configs ([[plots]] kind="sweep"
with range=[...]) continue to work. Use builtins.range explicitly to
avoid future shadowing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread python/trlib/plot.py
Comment thread python/trlib/plot.py Outdated
@k-yoshimi
Copy link
Copy Markdown
Owner Author

@cursor review

…cast)

Bugbot on PR #83:
- MED (plot.py:384): plot_sweep opens a new `with Trlib()` while the
  outer caller's Trlib was still alive, double-finalizing globals.
  Split run_plots into two phases: state-dependent plots inside the
  outer with-block (use live tr.get_state()), sweep plots in a new
  run_sweep_plots() called AFTER the outer Trlib exits. __main__.py
  updated to invoke both in order.
- LOW (plot.py:373): n from TOML may be float (range = [1.0,5.0,10.0]);
  Python's range() requires int. Cast n = int(n) before use; also
  coerce start/stop to float for safe arithmetic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread python/trlib/plot.py
Comment thread python/trlib/plot.py Outdated
@k-yoshimi
Copy link
Copy Markdown
Owner Author

@cursor review

Bugbot on PR #83:
- MED (plot.py:392): plot_sweep iterated all sample points within ONE
  Trlib(), so each tr.run inherited the end-state of the previous
  sample (cumulative ntmax > 0) and init-time scalars (WPT/AJT/Q0)
  froze at the first sample (ntmax = 0). Move `with Trlib()` INSIDE
  the loop so every sample is an independent fresh init+set+run cycle.
- LOW (plot.py:56): _PROFILE_XAXIS was assigned but never referenced
  (label is fetched per-variable from VARIABLE_INFO[var].xaxis instead).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread python/trlib/loader.py
Comment thread python/trlib/loader.py
Comment thread python/trlib/trlib.py Outdated
@k-yoshimi
Copy link
Copy Markdown
Owner Author

@cursor review

Bugbot on PR #83:
- MED (loader.py:233): run_plots skipped only entries without `variable`,
  so a sweep entry that also carried `variable` would be processed both
  as a regular plot AND again in run_sweep_plots. Add explicit
  `kind == "sweep"` guard at the top of the loop.
- LOW (loader.py:281): _run_sweep_spec accepted an always-None tr
  parameter. Remove it; doc clarifies plot_sweep owns its lifecycle.
- LOW (trlib.py:169): kwargs.pop("state", None) or self.get_state()
  swallowed any falsy explicit state. Switch to `is None` check.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread python/trlib/plot.py
Comment thread python/trlib/plot.py Outdated
@k-yoshimi
Copy link
Copy Markdown
Owner Author

@cursor review

Bugbot LOW x2 on PR #83:
- (plot.py:229) _draw_profile set xlabel="rg" unconditionally, including
  for scalar bar charts where "rg" is meaningless. Only emit the rg
  default for profile_1d/2d; scalars get blank xlabel.
- (plot.py:300) When user requested an alias (e.g. RNT → canonical RN),
  legend labels showed the canonical name. Plumb display_name through
  _draw_profile so user-facing labels match what the user asked for.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread python/trlib/loader.py
Comment thread python/trlib/plot.py Outdated
@k-yoshimi
Copy link
Copy Markdown
Owner Author

@cursor review

…arams

Bugbot on PR #83:
- LOW (plot.py:319): output="file" without explicit path used the
  canonical name (e.g. RN.png) for an aliased varname (e.g. RNT). Use
  the user-requested varname so the saved file matches their request.
- MED (loader.py:287): _run_sweep_spec used _plot_kwargs which only
  extracts {output, format, path, title, overlay}, silently dropping
  sweep-specific ntmax/base_params from TOML. Add explicit forwarding
  for both when the user supplied them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread python/trlib/plot.py
k-yoshimi added a commit that referenced this pull request Apr 19, 2026
Layer 1 equivalence (eq_iter01/eq_tst2 vs L-0, tol 1e-10, EQ_RUN_OK
gated), Layer 2 C ABI test_negative (pre-init, unknown name,
malformed PSIB[abc] / bare PSIB, double-finalize, post-finalize,
re-init), Layer 4 3x3 RR/BB sweep with re-init per sample (mirror
trlib PR #83). Adds eq_api_check_all bundling smoke + run_so +
negative; appends 5 eqlib_* rows to test_definitions.conf.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@k-yoshimi k-yoshimi mentioned this pull request Apr 19, 2026
6 tasks
@k-yoshimi
Copy link
Copy Markdown
Owner Author

@cursor review

Bugbot MED on PR #83: plot_sweep silently fell through to the window
branch for invalid output values (e.g. user typo "bogus" in TOML).
plot() already validates output up front; mirror that.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread python/trlib/plot.py
k-yoshimi added a commit that referenced this pull request Apr 19, 2026
Add the 4-layer test suite for the tot orchestrator library, mirroring
the eqlib / trlib L-6 work. tot-specific twist: every parameter name
carries an <ns>: namespace prefix (eq:RR, tr:DT, fp:NSMAX, ...) because
the orchestrator parameter space is the union of the per-module
registries.

- python/totlib/tests/fixtures/{__init__,tot_demo2014_params,tot_ht6m_params}.py:
  namespaced PARAMS replaying the standalone tot_demo2014_short and
  tot_ht6m_short namelists through libtotapi.so (eq:RR=8.5/0.65,
  tr:DT=0.0001, etc.).

- python/totlib/tests/test_equivalence.py (Layer 1): diff a Tot()
  replay vs test_run/baselines/tot_*/metrics.json at tol=1e-10.
  Triple-skip-gated on libtotapi.so, totlib importability, and
  TOT_RUN_OK=1 (the L-3/L-4/L-5 stubs return TOT_ERR_NOT_IMPL).

- python/totlib/tests/test_sweep.py (Layer 4): 3x3 eq:RR x eq:BB
  grid with a fresh `with Tot()` per sample (re-init fix from trlib
  PR #83 / eqlib L-6); verifies nrmax / nsmax stay invariant across
  cells. Same TOT_RUN_OK gating.

- tot/tests/c_abi/test_negative.c (Layer 2): pre-init NOT_IMPL
  contract, unknown-namespace / missing-prefix / empty-prefix
  rejection (TOT_ERR_INVALID), per-module unknown-bare reject for
  every prefix, double-finalize safety, post-finalize dispatcher
  invariance.

- tot/Makefile: new tot_api_check_all umbrella target wraps
  tot_api_check + tot_api_check_so + test_negative for one-shot CI
  invocation. Mirrors eq_api_check_all in eq/Makefile.

- test_run/test_definitions.conf: append five totlib_* rows
  (c_abi / wrapper / ffi / equivalence / sweep) following the existing
  per-module L-6 row shape.

Verification:
- python3 -m unittest discover python/totlib/tests -v -> 47 tests, 24
  skipped (pass-or-skip clean; equivalence + sweep gated on so/RUN_OK).
- bash -n test_run/run_tests.sh -> rc=0.
- ./test_run/run_tests.sh -l | grep totlib_ -> 5 rows.

No edits to totlib core (totlib.py / state.py / _ffi.py / errors.py).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@k-yoshimi
Copy link
Copy Markdown
Owner Author

@cursor review

@k-yoshimi k-yoshimi mentioned this pull request Apr 19, 2026
5 tasks
Bugbot MED on PR #83: plot_sweep accepted profile variables (RN/RNT/AJ)
because they appear in VARIABLE_INFO, but the per-sample lookup uses
state.scalars[y] — profile keys aren't there, so y=0.0 silently for
every sample, producing a flat-zero plot.

Validate y is kind="scalar" up front and list valid choices in error.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread python/trlib/__main__.py
Comment thread python/trlib/plot.py
@k-yoshimi
Copy link
Copy Markdown
Owner Author

@cursor review

Bugbot on PR #83:
- LOW (plot.py:43): HAS_MATPLOTLIB was set to True after import success
  but the except block re-raises ImportError — module is unimportable
  if matplotlib is missing, so the flag can NEVER be False. Remove
  both the assignment and the export.
- MED (__main__.py:154): KeyError/ValueError/TypeError from plot or
  sweep config (e.g., unknown variable, missing range key) were caught
  by the outer "except Exception" and reported as _EXIT_LIB (1) instead
  of _EXIT_CONFIG (2). Add explicit (KeyError, ValueError, TypeError)
  handlers in both plot and sweep blocks to return _EXIT_CONFIG.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread python/trlib/loader.py Outdated
Comment thread python/trlib/plot.py Outdated
@k-yoshimi
Copy link
Copy Markdown
Owner Author

@cursor review

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit d5442fd. Configure here.

Comment thread python/trlib/loader.py
# An actual path does not contain '\n' or a top-level '=' sign or
# square brackets. This is a small heuristic; callers that want
# precise behaviour can wrap inline text in ``io.BytesIO``.
return ("\n" in text) or ("=" in text and len(text) > 40) or ("[" in text and "]" in text and "=" in text)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Heuristic misclassifies long file paths as inline TOML

Low Severity

The _looks_like_toml heuristic treats any string containing = and longer than 40 characters as inline TOML content rather than a file path. A legitimate file path like /home/user/configs/long_project_name_config/param=default.toml would match the condition "=" in text and len(text) > 40, causing _loads() to attempt TOML parsing of the path string itself. This produces a confusing TOMLDecodeError instead of loading the intended file. The os.PathLike branch is unaffected (only str/bytes inputs hit this heuristic).

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d5442fd. Configure here.

Comment thread python/trlib/loader.py

# Cache the state once per run so multiple plots don't re-run the
# simulation backend.
state = tr.get_state()
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Unnecessary state copy when all plots are sweeps

Low Severity

run_plots unconditionally calls tr.get_state() before iterating over plot specs, even when every spec has kind == "sweep" and will be skipped. The state copy involves marshalling Fortran arrays through ctypes. Moving the tr.get_state() call to just before the first non-sweep spec is actually used would avoid the unnecessary work in sweep-only configurations.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d5442fd. Configure here.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant