Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 116 additions & 3 deletions python/trlib/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,9 +184,12 @@ interactive menu — those live in `tr/tr2` only.
- **Single instance per process.** TR backend uses COMMON blocks.
Two concurrent `Trlib()` instances share state; the second
`tr_init` resets globals.
- **No graphics, no MPI, no OpenMP API.** Graphics symbols exist but
are not reachable from the 5 exported entry points; the loader uses
`RTLD_LAZY` so dangling graphics references never resolve.
- **No PGPlot/GSAF graphics, no MPI, no OpenMP API.** Fortran-side
graphics symbols exist but are not reachable from the 5 exported
entry points; the loader uses `RTLD_LAZY` so dangling graphics
references never resolve. Python-side visualization is provided
separately via `trlib.plot` (matplotlib backend, optional dependency)
— see the Plot section below.
- **String parameters not yet wired** (e.g. `KNAMEQ`, `KNAMTR`). See
`docs/superpowers/specs/2026-04-17-tr-library-design.md` §4.3.
- **Unregistered namelist keys** — any name missing from
Expand Down Expand Up @@ -226,6 +229,116 @@ top-level license. Bug reports and PRs are welcome; please keep
wrapper changes minimal — the C ABI is the stable layer, so new
parameters should be added to the Fortran registry first.

## TOML config 実行方法

`python -m trlib <config.toml>` でパラメータ設定 + 計算 + プロットを 1
コマンドで実行できます。CLI 版の namelist 入力 (`./tr < tr_iter01.in`)
を Python 側に置き換えるための入口です。

サンプル config はリポジトリ内に同梱しています:

- `python/trlib/samples/iter01.toml` — `test_run/inputs/tr_iter01.in` を TOML 化したもの
- `python/trlib/samples/tst2.toml` — `test_run/inputs/tr_tst2.in` を TOML 化したもの

```bash
# 計算 + プロットを一気に実行 (libtrapi.so + matplotlib が必要)
python -m trlib python/trlib/samples/iter01.toml

# 設定だけ確認 (ライブラリ未ビルドでも OK)
python -m trlib python/trlib/samples/iter01.toml --dry-run

# NTMAX を上書き
python -m trlib python/trlib/samples/tst2.toml --ntmax 5

# プロットだけスキップ (matplotlib が無い環境向け)
python -m trlib python/trlib/samples/iter01.toml --no-plots
```

### TOML スキーマ

```toml
[module]
name = "tr"
ntmax = 100 # NTMAX scalar への alias

[scalars]
RR = 3.0
NSMAX = 4

[arrays]
PN = [0.7, 0.315, 0.315, 0.035] # 1-origin リスト
# PA = { 2 = 1.0 } # sparse dict 形式 (PA(1) は default)

[strings]
KNAMEQ = "eqdata.ITER"

[[plots]] # 配列 of tables で複数プロット
variable = "RNT"
output = "file" # window | file | return
format = "png"
path = "./plots/rnt.png"
title = "温度密度プロファイル"
```

### Exit code

| code | 意味 |
|---|---|
| 0 | 正常終了 |
| 1 | ライブラリ / 計算エラー |
| 2 | config エラー (未存在ファイル / TOML 構文エラー / matplotlib 不足) |

## プロット

`trlib.plot` は :mod:`matplotlib` をオプション依存とする可視化レイヤー
です。`trlib` 本体は matplotlib なしでも import できますが、`plot()` を
呼ぶと `ImportError` になります。

### 対話的に呼ぶ

```bash
python -c "from trlib import Trlib; \
tr = Trlib(); tr.run(0); tr.plot('RNT'); tr.close()"
```

### 利用可能な変数を確認する

```python
from trlib import Trlib
print(Trlib.plot_available()) # ['AJ', 'ALI', 'BETA0', ..., 'WPT']
```

`VARIABLE_INFO` 辞書 (in `trlib/plot.py`) に登録された変数のみが描画でき
ます。新しい変数を追加するときはこの dict にエントリを足してください。

### 出力モード

| `output` | 用途 | 戻り値 |
|---|---|---|
| `"window"` (default) | CLI / インタラクティブ表示 | `None` |
| `"file"` | バッチ / CI で画像保存 | `pathlib.Path` |
| `"return"` | notebook embed / 後処理 | `matplotlib.figure.Figure` |

### サンプル

```python
from trlib import Trlib

with Trlib() as tr:
tr.set_params(RR=8.5, RA=2.0, BB=5.3, NSMAX=2, DT=0.1)
tr.run(50)
tr.plot("RNT", output="file", path="./rnt.png") # PNG 保存
fig = tr.plot("AJ", output="return") # Figure 受け取り
```

### matplotlib が無い環境での挙動

`pip install matplotlib` を行わずに `trlib.plot` を import / `.plot()` を
呼ぶと、明示的な `ImportError` が発生します。`__main__` では plot
セクションが無い限り matplotlib を import しないため、`--no-plots` を
付ければ matplotlib 未インストール環境でも `python -m trlib` を実行でき
ます。

## See also

- `docs/superpowers/specs/2026-04-17-tr-library-design.md` — full
Expand Down
170 changes: 170 additions & 0 deletions python/trlib/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
"""``python -m trlib <config.toml>`` — TOML-driven runner.

Pipeline::

init → apply_config → run(ntmax) → get_state → run_plots → finalize

Exit codes:

* 0 — success
* 1 — library / calculation error
* 2 — config error (missing file, malformed TOML, unknown variable)

Flags:

* ``--dry-run`` — parse and validate the TOML, print a summary, but
skip every libtrapi.so call. Useful for CI and quick verification.
* ``--ntmax N`` — override ``[module].ntmax`` / ``[scalars].NTMAX``.
* ``--no-plots`` — apply scalars/arrays and run, but skip every plot
spec. Handy when matplotlib is not installed in the runner env.
* ``--help`` — argparse-generated usage.
"""
from __future__ import annotations

import argparse
import sys
from pathlib import Path
from typing import List, Optional, Sequence

from .loader import apply_config, load_config, run_plots, run_sweep_plots


_EXIT_OK = 0
_EXIT_LIB = 1
_EXIT_CONFIG = 2


def build_parser() -> argparse.ArgumentParser:
"""Return the argparse parser used by :func:`main`."""
parser = argparse.ArgumentParser(
prog="python -m trlib",
description=(
"Run a TASK/TR simulation defined by a TOML config file and "
"optionally render plots. See python/trlib/samples/ for "
"example configurations."
),
)
parser.add_argument(
"config", type=Path,
help="Path to a TOML config file.",
)
parser.add_argument(
"--dry-run", action="store_true",
help="Parse the config and print a summary; skip every library call.",
)
parser.add_argument(
"--ntmax", type=int, default=None,
help="Override the NTMAX scalar from the config.",
)
parser.add_argument(
"--no-plots", action="store_true",
help="Skip [plot] / [[plots]] execution even if the config defines them.",
)
return parser


def _summarise(cfg: dict) -> str:
"""Return a short human-readable summary of a parsed config."""
lines = []
module = cfg.get("module") or {}
lines.append(f"module: {module.get('name', '<unnamed>')}")
ntmax = cfg.get("scalars", {}).get("NTMAX")
if ntmax is not None:
lines.append(f"NTMAX: {ntmax}")
lines.append(f"scalars: {len(cfg.get('scalars', {}))} keys")
lines.append(f"arrays: {len(cfg.get('arrays', {}))} keys")
lines.append(f"strings: {len(cfg.get('strings', {}))} keys")
lines.append(f"plots: {len(cfg.get('plots', []))} specs")
return "\n".join(lines)


def main(argv: Optional[Sequence[str]] = None) -> int:
"""Program entry point. Returns a process exit code."""
parser = build_parser()
args = parser.parse_args(argv)

# --- Config load ---------------------------------------------------
if not args.config.exists():
print(f"[trlib] config not found: {args.config}", file=sys.stderr)
return _EXIT_CONFIG
try:
cfg = load_config(args.config)
except Exception as exc:
print(f"[trlib] failed to parse {args.config}: {exc}", file=sys.stderr)
return _EXIT_CONFIG

# --- CLI overrides -------------------------------------------------
if args.ntmax is not None:
cfg.setdefault("scalars", {})["NTMAX"] = int(args.ntmax)
if args.no_plots:
cfg["plots"] = []

# --- Summary + dry run --------------------------------------------
print(f"[trlib] loaded {args.config}")
print(_summarise(cfg))
if args.dry_run:
print("[trlib] --dry-run: skipping library calls")
return _EXIT_OK

ntmax = int(cfg.get("scalars", {}).get("NTMAX", 0))

# --- Live run ------------------------------------------------------
# Lazy import: importing ``Trlib`` triggers _ffi.load_library() which
# probes libtrapi.so. Doing this only inside the live branch keeps
# --dry-run functional on systems where the .so is not built.
try:
from . import Trlib
except Exception as exc: # pragma: no cover - extremely unusual
print(f"[trlib] cannot import Trlib: {exc}", file=sys.stderr)
return _EXIT_LIB

try:
# State-dependent plots run inside the with-block (need live tr).
with Trlib() as tr:
apply_config(tr, cfg)
tr.run(ntmax=ntmax)
state = tr.get_state()
print(
f"[trlib] run complete: NT={state.nt} NRMAX={state.nrmax} "
f"NSMAX={state.nsmax}"
)
if cfg.get("plots"):
try:
results = run_plots(tr, cfg)
except ImportError as exc:
print(f"[trlib] plot backend unavailable: {exc}",
file=sys.stderr)
return _EXIT_CONFIG
except (KeyError, ValueError, TypeError) as exc:
# User-config errors (unknown variable, bad output=, etc.)
print(f"[trlib] plot config error: {exc}",
file=sys.stderr)
return _EXIT_CONFIG
for name, descriptor in results:
print(f"[trlib] plot {name} -> {descriptor}")
# Sweep plots run AFTER the outer Trlib closes — each sweep
# sample needs its own tr_init/tr_run/tr_finalize cycle and
# would collide with the still-live outer instance.
if cfg.get("plots"):
try:
sweep_results = run_sweep_plots(cfg)
except ImportError as exc:
print(f"[trlib] plot backend unavailable: {exc}",
file=sys.stderr)
return _EXIT_CONFIG
except (KeyError, ValueError, TypeError) as exc:
# User-config errors in sweep spec (missing range, unknown y, etc.)
print(f"[trlib] sweep config error: {exc}",
file=sys.stderr)
return _EXIT_CONFIG
for name, descriptor in sweep_results:
print(f"[trlib] plot {name} -> {descriptor}")
except Exception as exc:
print(f"[trlib] library error: {exc}", file=sys.stderr)
return _EXIT_LIB
Comment thread
cursor[bot] marked this conversation as resolved.

return _EXIT_OK


if __name__ == "__main__":
sys.exit(main())
Loading