Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
30937de
refactor: let each solver adapter declare its dashboard tabs
ulysse-bonneau-simvia Oct 7, 2026
c6839b1
refactor: wire the API layer to adapter-declared log files
ulysse-bonneau-simvia Oct 7, 2026
10bf090
refactor: move setup.xml and run.cfg discovery into the code_saturne …
ulysse-bonneau-simvia Oct 7, 2026
a1335ac
refactor: let the code_saturne adapter declare its results layout
ulysse-bonneau-simvia Oct 7, 2026
e18fc34
refactor: split logs.py into a generic engine and code_saturne vocabu…
ulysse-bonneau-simvia Oct 7, 2026
35ff700
test: scan every generic module for solver literals
ulysse-bonneau-simvia Oct 7, 2026
c427131
refactor: let adapters describe their solver instead of csauto assumi…
ulysse-bonneau-simvia Oct 7, 2026
3985ee3
test: add an adapter conformance suite and real docker runs of both s…
ulysse-bonneau-simvia Oct 7, 2026
a017ec6
fix: finalize RUNNING records with nothing left to watch, and tail th…
ulysse-bonneau-simvia Oct 7, 2026
8fc7bfa
feat: drive the dashboard from the solver adapter
ulysse-bonneau-simvia Oct 7, 2026
576ecce
fix: unescape \{name} in files without placeholders, keep failed rows…
ulysse-bonneau-simvia Oct 7, 2026
93ae6ab
docs: describe the adapter-driven design for users and adapter authors
ulysse-bonneau-simvia Oct 7, 2026
f9b8057
fix: harden run liveness, container mounts, Clean and file access
ulysse-bonneau-simvia Oct 7, 2026
23967b6
feat: report the solver in telemetry serve pings
ulysse-bonneau-simvia Oct 8, 2026
373a800
fix: finalize reaped runs on macOS, keep cleaned code_aster cases PRE…
ulysse-bonneau-simvia Oct 8, 2026
b9be9f4
feat: start docker solvers through the image's own entrypoint when it…
ulysse-bonneau-simvia Oct 8, 2026
ddbfd6d
fix: start code_aster images that have no /opt/activate.sh, and corre…
ulysse-bonneau-simvia Oct 8, 2026
15de5e2
fix: report an invalid csauto.toml as a one-line error instead of a t…
ulysse-bonneau-simvia Oct 8, 2026
e32a995
chore: bump version to 0.6.0
ulysse-bonneau-simvia Oct 8, 2026
7687307
fix: keep the plot panels usable when a case has no results
florian-simvia Oct 8, 2026
db44a4f
fix: clear the dashboard's svelte-check errors and make the check gat…
ulysse-bonneau-simvia Oct 8, 2026
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
1 change: 0 additions & 1 deletion .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ jobs:
- name: Svelte check
working-directory: frontend
run: pnpm check
continue-on-error: true

- name: Prettier check
working-directory: frontend
Expand Down
62 changes: 58 additions & 4 deletions CHANGELOG.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ By participating in this project, you agree to abide by our [Code of Conduct](CO

### Adding a New Solver

Merge requests adding support for new solvers are gladly welcome! csauto's solver-specific logic (commands, log parsing, file conventions, dashboard columns and panels) lives behind a single `SolverAdapter` class, so supporting a new solver means writing one adapter — no changes to the orchestration core or the frontend. Follow the step-by-step guide in [docs/adding-a-solver.md](docs/adding-a-solver.md), and use the built-in `stub` adapter and `tests/unit/test_solver_boundary.py` as references for the expected shape.
Merge requests adding support for new solvers are gladly welcome! csauto knows a solver through one `SolverAdapter` class that says how to start it, where its files are, and how to read its output. Adding a solver means writing that class, registering its name in `csauto/solvers/__init__.py`, and adding a sample finished case to `tests/unit/test_adapter_conformance.py`, which checks the adapter against it. Follow the step-by-step guide in [docs/adding-a-solver.md](docs/adding-a-solver.md); the built-in `stub` adapter (`csauto/solvers/stub.py`) is the smallest complete example.

### Improving Documentation

Expand Down
48 changes: 28 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<p align="center">
<a href="https://simvia-tech.github.io/csauto/"><img src="https://img.shields.io/badge/website-landing%20page-1057C8" alt="Website" /></a>
<a href="/"><img src="https://img.shields.io/badge/version-0.5.0-blue" alt="Version" /></a>
<a href="/"><img src="https://img.shields.io/badge/version-0.6.0-blue" alt="Version" /></a>
<a href="https://github.com/simvia-tech/csauto/actions/workflows/pr.yml"><img src="https://github.com/simvia-tech/csauto/actions/workflows/pr.yml/badge.svg" alt="CI-CD" /></a>
<a href="./LICENSE"><img src="https://img.shields.io/badge/license-GPL%203.0-green" alt="License" /></a>
</p>
Expand All @@ -13,13 +13,13 @@ This project is in early development. Expect breaking changes until v1.0.

## Description

**csauto** automates [code_saturne](https://www.code-saturne.org/) CFD simulation campaigns.
**csauto** automates [code_saturne](https://www.code-saturne.org/) CFD and [code_aster](https://www.code-aster.org/) finite-element simulation campaigns.
It offers:

- Generation of tens or hundreds of cases from one template + one CSV, in seconds
- Local and Slurm job execution with status tracking
- A web dashboard for monitoring residuals, probes, logs, and job status in real time
- Restart from checkpoint, live case steering (stop/extend/checkpoint without killing), input file comparison, and cleanup utilities
- A web dashboard for monitoring job status, logs, and errors in real time, plus residuals, probes, and timings for code_saturne
- Input file comparison and cleanup utilities, plus restart from checkpoint and live case steering (stop/extend/checkpoint without killing) for code_saturne

No manual case duplication. No directory juggling. Just simulations.

Expand All @@ -37,7 +37,7 @@ bash -c "$(curl -fsSL https://raw.githubusercontent.com/simvia-tech/csauto/main/
source ~/.bashrc # or: source ~/.zshrc
```

To update an existing installation, re-run the same command — the script detects the existing install, pulls the latest changes, and reinstalls.
To update an existing installation, re-run the same command: the script detects the existing install, pulls the latest changes, and reinstalls.

<details>
<summary>Manual installation (for contributors or custom locations)</summary>
Expand Down Expand Up @@ -72,13 +72,13 @@ csauto --help

### Prerequisites

You need at least one code_saturne runtime:
You need at least one runtime for your solver:

| Runtime | Requirement |
|---------------|--------------------------------------------------|
| `native` | `code_saturne` binary in PATH or set via config |
| `native` | The solver's executable (`code_saturne`, or `run_aster` for code_aster) in PATH or set via `saturne_bin` |
| `singularity` | Apptainer/Singularity + a `.sif` image |
| `docker` | Docker engine + image |
| `docker` | Docker engine; the image defaults to the solver's own (`simvia/code_saturne`, `simvia/code_aster:17.4.0`) |

## Usage

Expand All @@ -98,7 +98,7 @@ csauto doctor RUNS
csauto serve RUNS --host 127.0.0.1 --port 8000
```

Open [http://127.0.0.1:8000](http://127.0.0.1:8000), then launch runs from the **Status** panel: `Ctrl+A` → **Run Selected**.
Open [http://127.0.0.1:8000](http://127.0.0.1:8000), then launch runs from the **Status** panel: `Ctrl+A` → **Run**.

See [docs/doe-format.md](docs/doe-format.md) for the `doe.csv` format and the `csauto doe` generator (factorial/LHS/Sobol/CCD).

Expand All @@ -113,27 +113,30 @@ my-campaign/
│ └── setup.xml ← uses {u_inlet}, {turbulence_model}, etc.
└── RUNS/ ← generated by csauto prepare
├── registry.json
├── campaign.json ← the solver the campaign was prepared for
├── case0001/
└── case0002/
```

The solver defaults to code_saturne. For code_aster, set `solver = "code_aster"` in `csauto.toml` and see [examples/codeaster-cube](examples/codeaster-cube/README.md). Later commands on `RUNS` use the solver recorded in `RUNS/campaign.json`, from any directory.

### CLI commands

The web UI (`csauto serve`) is the primary interface for runtime monitoring and operations — launching, steering, killing, restarting, comparing, and inspecting cases. The CLI covers initial setup (`prepare`, `doctor`), data export (`residuals`, `perf`), and offers terminal alternatives for common actions.
The web UI (`csauto serve`) is the primary interface for runtime monitoring and operations: launching, steering, killing, restarting, comparing, and inspecting cases. The CLI covers initial setup (`prepare`, `doctor`), data export (`residuals`, `perf`), and offers terminal alternatives for common actions.

| Command | Description | UI equivalent |
|-------------|---------------------------------------------------|----------------------------|
| `prepare` | Generate cases from a DOE CSV and a template | — (CLI only) |
| `doe` | Generate a `doe.csv` from a parameter spec (factorial/LHS/Sobol/CCD) | — (CLI only) |
| `doctor` | Validate runtime environment and configuration | — (CLI only) |
| `serve` | Start the web monitoring dashboard | — (starts the UI) |
| `run` | Launch cases locally or on Slurm (`--n`, `--nt` required) | Status → Run Selected |
| `prepare` | Generate cases from a DOE CSV and a template | CLI only |
| `doe` | Generate a `doe.csv` from a parameter spec (factorial/LHS/Sobol/CCD) | CLI only |
| `doctor` | Validate runtime environment and configuration | CLI only |
| `serve` | Start the web monitoring dashboard | Starts the UI |
| `run` | Launch cases locally or on Slurm (`--n`, `--nt` required) | Status → Run |
| `status` | Show case status in the terminal | Status panel |
| `tail` | Stream a case log file (like `tail -f`) | Log Tail panel |
| `control` | Steer a running case: stop/extend/checkpoint/flush | Status → Stop / More ▾ menu |
| `control` | Send a live control action to a running case (code_saturne: stop, extend, checkpoint, flush) | Status → Control menu |
| `residuals` | Export residuals data and/or SVG plot | Residuals Plot panel |
| `perf` | Extract performance metrics from logs | Timing Snapshot panel |
| `cleanup` | Remove old RESU dirs, logs (`--prune-resu`, etc.) | Status → Clean Selected |
| `perf` | Export the solver's timing metrics | Timing Snapshot panel |
| `cleanup` | Remove old run folders, truncate logs (`--prune-resu`, etc.) | Status → Clean |

The web UI also provides features beyond the CLI: kill running cases, edit case files, add notes, mark convergence, compare runs side-by-side, plot probes/profiles, and scan logs for recent errors.

Expand All @@ -153,6 +156,7 @@ csauto collects anonymous usage statistics to help us understand how the tool is
- csauto version
- Timezone offset
- Runtime type (docker, singularity, or native)
- Solver of the served campaign (code_saturne or code_aster)
- Event type (install or serve session)

**When it is sent:**
Expand Down Expand Up @@ -188,12 +192,13 @@ To re-enable: `csauto enable-telemetry`
- [HTTP API](docs/api.md)
- [Task cookbook](docs/task-cookbook.md)
- [Limitations](docs/limitations.md)
- [Adding a new solver](docs/adding-a-solver.md)

## Contributing

Please check out our [CONTRIBUTING.md](./CONTRIBUTING.md) file if you want to contribute to the project.

Merge requests adding support for new solvers are gladly welcome: all solver-specific logic lives behind a single adapter class, so a new solver means writing one adapter with no changes to the core or the UI — see [docs/adding-a-solver.md](docs/adding-a-solver.md) for the step-by-step guide.
Merge requests adding support for new solvers are gladly welcome. csauto knows a solver through one adapter class that says how to start it, where its files are, and how to read its output: [docs/adding-a-solver.md](docs/adding-a-solver.md) walks through writing, registering, and testing one.

Thank you to all our contributors :

Expand All @@ -216,6 +221,9 @@ pytest tests/web -q

# Run a single test
pytest tests/unit/test_foo.py::test_bar -q

# Real runs of the shipped examples in docker (opt-in, needs the solver images)
CSAUTO_DOCKER_TESTS=1 pytest tests/integration/test_docker_solvers.py -q
```

### Frontend development
Expand All @@ -242,7 +250,7 @@ See [architecture](docs/architecture.md) for an overview of the codebase.

## See Also

- [code_saturne](https://www.code-saturne.org/) — the open-source CFD solver csauto automates
- [code_saturne](https://www.code-saturne.org/) and [code_aster](https://www.code-aster.org/): the open-source solvers csauto automates
- [Simvia's website](https://simvia.tech)

## License
Expand Down
6 changes: 3 additions & 3 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,22 @@

This roadmap is a declaration of intent, not a contractual engagement. It is updated at each minor or major release.

*Last updated: v0.5.0 — 2026-08-03*
*Last updated: v0.6.0 — 2026-10-08*

## Current Capabilities (v0.5.0)
## Current Capabilities (v0.6.0)

- Case generation from DOE CSV + template directory, or a generated parameter spec
- Local and Slurm job execution
- Web monitoring dashboard (residuals, probes, logs, status)
- Restart from checkpoint, live case steering (stop/extend/checkpoint), input file comparison, cleanup
- Support for native, Docker, and Singularity runtimes
- code_saturne and code_aster solvers, each described by one adapter class that the CLI and dashboard follow

## Short Term

- Add total line count indicator in the log tail panel
- Show timing snapshots for previous restarts
- Show log tail history for previous restarts
- Avoid copying the mesh folder into the calculation directory to reduce disk space usage

## Medium Term

Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.5.0
0.6.0
95 changes: 48 additions & 47 deletions csauto/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,15 @@
tail_log,
)
from .maintenance import cleanup_runs, run_doctor
from .registry import read_campaign_solver
from .residuals import (
collect_residuals,
plot_residuals,
)
from .runner import refresh_status, run_cases
from .serve_commands import add_serve_subcommands, dispatch_serve_command
from .solvers import get_solver_adapter
from .warn import error, flush_warnings
from .solvers import available_solvers, get_solver_adapter
from .warn import error, flush_warnings, warn


def serve_fastapi(*args: Any, **kwargs: Any) -> Any:
Expand Down Expand Up @@ -114,9 +115,8 @@ def parse_arguments(
dest="mesh_mode",
choices=["copy", "symlink"],
default=config.mesh_mode,
help="How to place the solver's shared dirs (meshes, postprocessing) into output_root: 'copy' (default) "
"or 'symlink'. 'symlink' avoids duplicating large meshes but is not supported with the docker/singularity "
"runtimes unless the mesh lives inside output_root already.",
help="How to place the solver's shared dirs (meshes, postprocessing) into output_root: 'symlink' "
"(default, no duplication; containers get the symlink targets mounted) or 'copy'.",
)
prepare_parser.add_argument(
"--strict",
Expand Down Expand Up @@ -201,7 +201,7 @@ def parse_arguments(
"--docker-image",
dest="docker_image",
default=config.docker_image,
help="Docker image to use (default from csauto.toml).",
help="Docker image to use (default: docker_image in csauto.toml, else the solver's own image).",
)
run_parser.add_argument(
"--saturne-bin",
Expand Down Expand Up @@ -271,7 +271,7 @@ def _add_export_args(parser: argparse.ArgumentParser) -> None:
help="Residual columns to plot (e.g. density velocity).",
)

perf_parser = subparsers.add_parser("perf", help="Export performance info from performance.log for selected cases.")
perf_parser = subparsers.add_parser("perf", help="Export the solver's performance metrics for selected cases.")
_add_export_args(perf_parser)

tail_parser = subparsers.add_parser("tail", help="Follow a case log file (like tail -f).")
Expand All @@ -280,8 +280,8 @@ def _add_export_args(parser: argparse.ArgumentParser) -> None:
tail_parser.add_argument(
"--file",
dest="file_name",
default="listing",
help="File to follow (listing, run_solver.log, run_status.running, csauto.stdout, ...).",
default=None,
help="File to follow (default: the solver's main log, e.g. csauto.stdout or a path relative to the case).",
)
tail_parser.add_argument(
"-n",
Expand All @@ -298,32 +298,14 @@ def _add_export_args(parser: argparse.ArgumentParser) -> None:
)

control_parser = subparsers.add_parser(
"control", help="Send a live control directive to a running case (stop/extend/checkpoint/flush)."
"control",
help="Send a live control action to a running case (the actions depend on the solver; "
"csauto doctor lists them).",
)
control_parser.add_argument("runs_dir", type=Path, help="Directory containing generated cases")
control_parser.add_argument("case", help="Case name (caseXXXX)")
control_action_group = control_parser.add_mutually_exclusive_group(required=True)
control_action_group.add_argument(
"--stop",
action="store_true",
help="Graceful stop: finish the current time step, checkpoint, and exit (no restart needed).",
)
control_action_group.add_argument(
"--extend",
type=int,
metavar="N",
help="Extend the run by N additional time steps beyond its current progress.",
)
control_action_group.add_argument(
"--checkpoint",
action="store_true",
help="Request a checkpoint at the next time step.",
)
control_action_group.add_argument(
"--flush",
action="store_true",
help="Flush logs and time plots at the next time step.",
)
control_parser.add_argument("action", help="Action name (csauto doctor lists the solver's actions)")
control_parser.add_argument("value", nargs="?", type=float, help="Value, for actions that take one")

add_serve_subcommands(subparsers, config)

Expand Down Expand Up @@ -421,14 +403,38 @@ def _print_doctor(items: Sequence[object]) -> bool:
return failed


def _use_campaign_solver(runs_dir: Path | None, config: Config) -> bool:
"""Switch `config` to the solver the campaign in `runs_dir` was prepared for.

Returns True when that replaced the configured solver: the configuration's
solver-specific settings (docker image, solver executable, apptainer image)
then belong to another solver and are dropped.
"""
recorded = read_campaign_solver(runs_dir) if runs_dir is not None else None
if not recorded or recorded == config.solver:
return False
if recorded not in available_solvers():
raise ValueError(f"{runs_dir}/campaign.json names an unknown solver: {recorded!r}")
if config.path is not None:
warn(
f"{runs_dir} was prepared for {recorded}: ignoring solver, docker_image, saturne_bin "
f"and singularity_image from {config.path}"
)
config.solver = recorded
config.docker_image = config.saturne_bin = config.singularity_image = None
return True


def main(argv: Sequence[str] | None = None) -> int:
argv_list = list(argv or sys.argv[1:])
config_path = _preparse_config(argv_list)
config = load_config(config_path)
parser, args = parse_arguments(argv_list, config)
adapter = get_solver_adapter(config.solver)

try:
config = load_config(_preparse_config(argv_list))
parser, args = parse_arguments(argv_list, config)
# Commands on an existing campaign use the solver it was prepared for, so
# they work from any directory; serve reads it from config.solver.
if _use_campaign_solver(getattr(args, "runs_dir", None), config):
parser, args = parse_arguments(argv_list, config) # drop the other solver's defaults
adapter = get_solver_adapter(config.solver)
if args.command is None:
parser.print_help()
return 0
Expand Down Expand Up @@ -527,18 +533,10 @@ def main(argv: Sequence[str] | None = None) -> int:
adapter=adapter,
)
elif args.command == "control":
if args.stop:
control_action, control_value = "stop", None
elif args.extend is not None:
control_action, control_value = "extend", args.extend
elif args.checkpoint:
control_action, control_value = "checkpoint", None
else:
control_action, control_value = "flush", None
details = control_case(
args.runs_dir, args.case, control_action, value=control_value, source="cli", adapter=adapter
args.runs_dir, args.case, args.action, value=args.value, source="cli", adapter=adapter
)
print(f"control: {control_action} -> {details}")
print(f"control: {args.action} -> {details}")
elif dispatch_serve_command(
args,
config,
Expand All @@ -565,6 +563,7 @@ def main(argv: Sequence[str] | None = None) -> int:
if not (args.prune_resu or args.max_log_mb > 0 or args.clear_cid or args.clear_pyc):
print("No action specified. Use --prune-resu/--max-log-mb/--clear-cid/--clear-pyc.")
return 0
refresh_status(args.runs_dir, adapter=adapter) # finalize runs that ended, so they are not skipped
report = cleanup_runs(
args.runs_dir,
prune_resu=args.prune_resu,
Expand All @@ -584,6 +583,8 @@ def main(argv: Sequence[str] | None = None) -> int:
print(f"{prefix}.csauto.cid removed: {report.cid_removed}")
if args.clear_pyc:
print(f"{prefix}__pycache__ removed: {report.pycache_removed}")
if report.skipped_active:
print(f"Skipped running or queued cases: {', '.join(report.skipped_active)}")
elif args.command == "completion":
from .completion import generate_bash, generate_zsh

Expand Down
Loading
Loading