feat(trlib): plot API + TOML config runner (reference)#83
Conversation
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>
|
@cursor review |
|
@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>
|
@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>
|
@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>
|
@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>
|
@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>
|
@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>
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>
|
@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>
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>
|
@cursor review |
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>
|
@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>
|
@cursor review |
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ 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.
| # 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) |
There was a problem hiding this comment.
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).
Reviewed by Cursor Bugbot for commit d5442fd. Configure here.
|
|
||
| # Cache the state once per run so multiple plots don't re-run the | ||
| # simulation backend. | ||
| state = tr.get_state() |
There was a problem hiding this comment.
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.
Reviewed by Cursor Bugbot for commit d5442fd. Configure here.


Summary
Reference implementation of the declarative plot(varname) API and TOML-driven runner described in
project_visualization_followup.mdandproject_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 matplotlibplot_sweep(param, y, *, range)— ad-hoc parameter scanplot_available()+VARIABLE_INFOregistry so callers (humans, MCP, LLMs) can probe support before requesting a figurewindow(interactive),file(returnsPath),return(returnsFigure);_in_notebook()heuristic flipsplt.showto non-blocking inside Jupytertrlib.plotraises a clearImportErrorwhen matplotlib is absent; trlib itself remains importableTrlib instance methods
Trlib.plot(varname, **kwargs)— captures state internally and forwards totrlib.plot.plotTrlib.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}; acceptspathlib.Path,str,bytes, or any file-likeapply_config(tr, cfg)replays strings -> scalars -> arrays in the order the existingtests/fixtures/*.py::applyhelpers userun_plots(tr, cfg)executes every[plot]/[[plots]]spec; supportskind = "sweep"entriestomllib(3.11+) withtomlifallbackCLI entry —
python/trlib/__main__.py(new)Exit codes:
0success,1library/calc error,2config error.TOML samples —
python/trlib/samples/iter01.toml— mirror oftest_run/inputs/tr_iter01.in(4 species, NTMAX=100)tst2.toml— mirror oftest_run/inputs/tr_tst2.in(uses sparsePA = { 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 TrStatetest_loader.py(10 tests, no .so needed) — inline string / bytes / Path / BytesIO inputs, singular[plot]table,[module].ntmaxalias, error pathstest_main.py(8 tests) —--help/--dry-run/--ntmax/--no-plotssmoke; library-dependent test gated on libtrapi.soRun:
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:
plot.py→ adjustVARIABLE_INFOfor that module's state schemaloader.pyverbatim (schema is module-agnostic) and tweak theapply_configordering if needed__main__.py, swap theTrlibimportsamples/*.tomlmirroringtest_run/inputs/*.inHard constraints honoured
trlib.pyfield changes beyond.plot()/.plot_available()--no-plotsworks without itTest plan
python3 -m unittest discover python/trlib/tests— all pass / skip cleanlypython -m trlib --helpprints usagepython -m trlib python/trlib/samples/iter01.toml --dry-runexits 0Note
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 trlibrunner that loads a config, applies scalars/arrays/strings, runs the simulation, and optionally generates plots, with--dry-run,--ntmaxoverrides,--no-plots, and defined exit codes.Introduces a matplotlib-backed plotting module (
trlib.plot) with aVARIABLE_INFOregistry,plot()output modes (window/file/return), andplot_sweep()for parameter scans, and wires it intoTrlibvia newTrlib.plot()andTrlib.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 orlibtrapi.soare unavailable).Reviewed by Cursor Bugbot for commit 6a9de89. Bugbot is set up for automated code reviews on this repo. Configure here.