Skip to content

refactor: introduce a SolverAdapter abstraction (decouple from code_saturne) #13

Description

@florian-simvia

Problem

csauto is tightly and diffusely coupled to code_saturne: ~450 references spread across
21 modules, with no abstraction layer. Solver knowledge (command building, log/residual
parsing, file conventions, environment checks) is mixed into generic orchestration code, so
supporting another solver would mean editing ~8 modules with no single entry point.

The good news: the boundary is conceptually clean — a large part of the codebase is already
solver-agnostic and would be reused as-is.

Layer Status Modules
DOE + {placeholder}/IF templating engine, registry, config, CLI, web shell, scheduling (local/Slurm/Docker), locking, history ✅ generic template.py, registry.py, config.py, cli.py, doe.py, web
Run/GUI command building ❌ code_saturne-specific execution.py, docker.py
Output parsing (listing/run_solver.log, residuals, probes, outcome) ❌ specific logs.py, residuals.py, probes.py
File conventions (setup.xml, run.cfg, RESU/, checkpoint/) ❌ specific runner.py, template.py
Environment (code_saturne binary, image, mounts, doctor) ❌ specific maintenance.py, execution.py

Proposal

Extract a SolverAdapter interface that materializes this boundary. code_saturne becomes
one implementation (CodeSaturneAdapter), selected via config (solver = "code_saturne").

class SolverAdapter(Protocol):
    def build_run_command(self, case_dir, runtime) -> list[str]: ...
    def build_gui_command(self, case_dir, runtime) -> list[str]: ...
    def detect_outcome(self, case_dir) -> Status: ...          # log parsing
    def read_residuals(self, case_dir) -> list[Row]: ...
    def read_probes(self, case_dir) -> list[Row]: ...
    def setup_files(self) -> list[str]: ...                    # setup.xml, run.cfg…
    def resu_layout(self) -> ResuConvention: ...               # RESU/, checkpoint/
    def control(self, case_dir, action) -> None: ...           # control_file, etc.
    def doctor_checks(self) -> list[Check]: ...

Once in place, adding a solver = writing one class, with no changes to the core.

Approach (incremental, shippable at each step)

Refactor in place behind the interface — no behavior change, CodeSaturneAdapter keeps
current behavior. Suggested order (each step stays green):

  1. Define SolverAdapter protocol + CodeSaturneAdapter skeleton; wire config solver key.
  2. Move command building (execution.py/docker.py) behind build_run_command/build_gui_command.
  3. Move output parsing (logs/residuals/probes) behind detect_outcome/read_*.
  4. Move file conventions + doctor checks behind setup_files/resu_layout/doctor_checks.
  5. Route control_file actions through control().

Notes

  • This is a software-only epic (interface extraction + adapter pattern), no deep CFD needed.
  • Prerequisite / natural first client: a fake-solver test stub — it becomes the second
    adapter and unlocks integration tests without a real solver installed.
  • Scope here is the abstraction + code_saturne adapter. Shipping a second real solver
    (OpenFOAM, etc.) is a separate follow-up.

Acceptance criteria

  • SolverAdapter protocol defined; all solver-specific calls in the core go through it.
  • CodeSaturneAdapter implements it; existing behavior unchanged (tests still green).
  • solver config key selects the adapter (defaults to code_saturne).
  • No direct code_saturne/setup.xml/RESU references left in generic modules
    (registry, doe, cli, web routes).
  • A minimal fake StubAdapter used by integration tests.
  • Docs: docs/architecture.md updated with the adapter boundary.

Out of scope

A second production solver backend; per-solver UI customization; auto-detection of the solver.

Metadata

Metadata

Labels

enhancementNew feature or request

Fields

No fields configured for Feature.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions