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
6805047
[dev] add docker capabailities
maxime-simvia Jul 28, 2026
0a56c50
[dev] handle binding shared floder
maxime-simvia Jul 28, 2026
dcf0bf4
[dev] add code_aster logo
maxime-simvia Jul 28, 2026
562e8db
[dev] handle docker for code_aster
maxime-simvia Jul 29, 2026
d3fdcf2
[dev] add singularity option
maxime-simvia Jul 30, 2026
d108c4b
[dev] full working version docker - singularity code_aster
maxime-simvia Jul 31, 2026
c9a2872
[proj] adding an example for code_aster
maxime-simvia Jul 31, 2026
7e923bb
[proj] add README for codeaster_cube example
maxime-simvia Jul 31, 2026
1b96452
[dev] add test and frontend adaption for code_aster
maxime-simvia Jul 31, 2026
e392c90
[dev] refomratting for ruff
maxime-simvia Jul 31, 2026
e454eb3
fix: remove leftover debug print from the singularity launch path
ulysse-bonneau-simvia Aug 3, 2026
d3f4fd3
fix: keep the docker image required in runtime selection
ulysse-bonneau-simvia Aug 3, 2026
bd5423a
fix: mount code_aster shared dirs under their own names
ulysse-bonneau-simvia Aug 3, 2026
1dca027
fix: ignore logs from previous runs in code_aster outcome detection
ulysse-bonneau-simvia Aug 3, 2026
1153746
fix: resolve the code_aster export file from the case at launch
ulysse-bonneau-simvia Aug 3, 2026
7b8d872
fix: harden code_aster container launch commands
ulysse-bonneau-simvia Aug 3, 2026
e02d132
refactor: tidy the code_aster adapter
ulysse-bonneau-simvia Aug 3, 2026
e1718dd
test: gate the code_aster docker launch test behind an explicit opt-in
ulysse-bonneau-simvia Aug 3, 2026
508cc79
docs: polish the codeaster-cube example
ulysse-bonneau-simvia Aug 3, 2026
9d9acad
chore: exclude markdown docs from ruff formatting
ulysse-bonneau-simvia Aug 3, 2026
003a6fa
chore: bump version to 0.5.0
ulysse-bonneau-simvia Aug 3, 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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/).

## [0.5.0] - 2026-08-03

Add code_aster as a second supported solver: generate, run, and monitor finite-element campaigns alongside Code_Saturne.

### Added
- code_aster solver adapter (`solver = "code_aster"`): finds the case's `.export` setup file, launches `run_aster` in docker or apptainer/singularity containers (native runtime not implemented yet), routes the solver message file to `RESU/LOGS/run_solver.log`, and derives case status from code_aster's `DIAGNOSTIC JOB` line (OK/alarm → DONE; abort, error, no-convergence, CPU/memory limits → FAILED), ignoring logs left over from previous runs when a case is relaunched
- code_aster dashboard branding: the header shows the code_aster logo for `code_aster` campaigns, and `favicon-code_aster.svg` ships in `frontend/static/` for the solver-aware favicon introduced in 0.4.1
- `examples/codeaster-cube`: a complete 9-case demo campaign (cube under triaxial traction, two mesh variants, one deliberately failing case) with template `.comm`/`.export` files and the Salome script that produced the meshes

## [0.4.1] - 2026-07-17

Deepen the solver adapter boundary (dashboard panels, timing columns, compare kinds, and error files are now adapter-driven), make `mesh_mode = "symlink"` the default with full container-runtime support, and validate DOE specs against the template.
Expand Down
2 changes: 1 addition & 1 deletion 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.4.1-blue" alt="Version" /></a>
<a href="/"><img src="https://img.shields.io/badge/version-0.5.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 Down
4 changes: 2 additions & 2 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

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

*Last updated: v0.4.1 — 2026-07-17*
*Last updated: v0.5.0 — 2026-08-03*

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

- Case generation from DOE CSV + template directory, or a generated parameter spec
- Local and Slurm job execution
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.4.1
0.5.0
6 changes: 5 additions & 1 deletion csauto/solvers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from .base import SolverAdapter, SolverAdapterBase

DEFAULT_SOLVER = "code_saturne"
_SOLVER_NAMES = ("code_saturne", "stub")
_SOLVER_NAMES = ("code_saturne", "stub", "code_aster")


def available_solvers() -> tuple[str, ...]:
Expand All @@ -37,6 +37,10 @@ def _adapter_for(normalized: str) -> SolverAdapter:
from .stub import StubAdapter

return StubAdapter()
if normalized == "code_aster":
from .code_aster import CodeAsterAdapter

return CodeAsterAdapter()
choices = ", ".join(available_solvers())
raise ValueError(f"Unknown solver: {normalized!r}. Choices: {choices}")

Expand Down
173 changes: 173 additions & 0 deletions csauto/solvers/code_aster.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
"""code_aster solver adapter.

Launches ``run_aster`` on a case's ``.export`` file inside a container
(docker or apptainer/singularity); the native runtime is not implemented
yet. Case outcome is read from the ``DIAGNOSTIC JOB`` line that code_aster
prints at the end of ``RESU/LOGS/run_solver.log``.
"""

from __future__ import annotations

import re
import shlex
from collections.abc import Mapping, Sequence
from pathlib import Path
from typing import ClassVar

from ..execution import RUNTIME_DOCKER, RUNTIME_NATIVE, RUNTIME_SINGULARITY, RuntimeSelection, shared_dir_symlink_mounts
from ..logs import _is_recent, _parse_start_time, read_tail_lines
from ..registry import STATUS_DONE, STATUS_FAILED
from .base import SolverAdapterBase

CODE_ASTER_EXPORT_EXTENSION = "export"


class CodeAsterAdapter(SolverAdapterBase):
name: ClassVar[str] = "code_aster"
native_bin_name: ClassVar[str] = "run_aster"
container_bin_name: ClassVar[str] = ""
container_root: ClassVar[str] = "/home/user"
default_docker_image: ClassVar[str] = "simvia/code_aster:17.4.0"
results_dirname: ClassVar[str] = "RESU"
logs_dirname: ClassVar[str] = "LOGS"
shared_dir_names: ClassVar[tuple[str, ...]] = (
"MESH",
"RESU",
)

def build_run_command(
self,
case_dir: Path,
nprocs: int,
nt: int,
selection: RuntimeSelection,
*,
cidfile: Path | None = None,
run_args: Sequence[str] | None = None,
cleanenv: bool = False,
env_vars: Mapping[str, str] | None = None,
tmp_name: str = "TMP",
) -> list[str]:
"""Build the container command to launch a case."""
runs_root = case_dir.parent.resolve()
container_root = self.container_root
container_case = f"{container_root}/{case_dir.name}"
export_path = self.find_setup_file(case_dir)
exportfile = export_path.name
host_tmpdir = f"{runs_root}/{case_dir.name}/{tmp_name}"
solverlogpath = f"{self.results_dirname}/{self.logs_dirname}/run_solver.log"

self._ensure_mess_entry(export_path, solverlogpath)

links = [f"{runs_root}:{container_root}"]
for name in self.shared_dir_names:
for target, readonly in shared_dir_symlink_mounts(runs_root, (name,)):
linked = f"{target}:{container_case}/{name}"
links.append(f"{linked}:ro" if readonly else linked)

rmdir = [host_tmpdir]
solver_shell = (
f"source /opt/activate.sh && run_aster {shlex.quote(exportfile)} --wrkdir {container_case}/{tmp_name}/"
)
solver_cmd = f"bash -c {shlex.quote(solver_shell)}"

if selection.runtime == RUNTIME_DOCKER:
add_cid = f"--cidfile {shlex.quote(str(cidfile))} " if cidfile else ""

bind_links = " ".join(f"-v {shlex.quote(link)}" for link in links)
cleanup = "rm -rf " + " ".join(shlex.quote(dire) for dire in rmdir)

cmd = [
"nohup",
"bash",
"-c",
f"docker run "
f"{bind_links} "
f"-w {container_case} "
f"--label csauto.case_id={case_dir.name} "
f"{add_cid}"
f"{selection.docker_image} "
f"{solver_cmd}; "
f"{cleanup}",
]
elif selection.runtime == RUNTIME_SINGULARITY:
if not selection.singularity_bin or not selection.singularity_image:
raise ValueError("Incomplete singularity configuration.")
host_apptainer = f"{runs_root}/{case_dir.name}/.apptainer_tmp"
rmdir.append(host_apptainer)

bind_links = " ".join(f"--bind {shlex.quote(link)}" for link in links)
cleanup = "rm -rf " + " ".join(shlex.quote(dire) for dire in rmdir)

cmd = [
"nohup",
"bash",
"-c",
f"mkdir -p {shlex.quote(host_apptainer)} && "
f"export APPTAINER_TMPDIR={shlex.quote(host_apptainer)} && "
f"{shlex.quote(selection.singularity_bin)} exec "
f"{bind_links} "
f"--pwd {container_case} "
f"{shlex.quote(selection.singularity_image)} "
f"{solver_cmd}; "
f"{cleanup}",
]
elif selection.runtime == RUNTIME_NATIVE:
raise NotImplementedError(f"{RUNTIME_NATIVE} must be implemented for code_aster.")
else:
if selection.runtime in ["cave", "salome_meca"]:
raise NotImplementedError(
f"{selection.runtime} could be in the roadmap of code_aster. Please contact support."
)
else:
raise NotImplementedError(f"{selection.runtime} not in the development roadmap of code_aster.")

return cmd

def run_argv(self, case_path: str | Path, nprocs: int, nt: int, run_args: Sequence[str] | None = None) -> list[str]:
"""Unused: `build_run_command` composes the full launch command itself."""
return []

@staticmethod
def _ensure_mess_entry(export_path: Path, solverlogpath: str) -> None:
"""Append the F mess entry that detect_outcome reads, unless the export already declares one."""
export_text = export_path.read_text(encoding="utf-8", errors="ignore")
if any(line.split()[:2] == ["F", "mess"] for line in export_text.splitlines()):
return
with export_path.open("a", encoding="utf-8") as f:
if export_text and not export_text.endswith("\n"):
f.write("\n")
f.write(f"F mess {solverlogpath} R 6\n")

def find_setup_file(self, template_dir: Path) -> Path:
files = list(Path(template_dir).glob(f"*.{CODE_ASTER_EXPORT_EXTENSION}"))
if len(files) == 1 and files[0].is_file():
return files[0]
raise FileNotFoundError(f".export file not found in template: {template_dir}")

def detect_outcome(self, case_dir: Path, start_time: str | None = None) -> str | None:
success_patterns = [
re.compile(r"DIAGNOSTIC JOB : OK", re.IGNORECASE),
re.compile(r"DIAGNOSTIC JOB : <A>_ALARM", re.IGNORECASE),
]
failure_patterns = [
re.compile(r"DIAGNOSTIC JOB : <F>_ABNORMAL_ABORT", re.IGNORECASE),
re.compile(r"DIAGNOSTIC JOB : <F>_SYNTAX_ERROR", re.IGNORECASE),
re.compile(r"DIAGNOSTIC JOB : <S>_MEMORY_ERROR", re.IGNORECASE),
re.compile(r"DIAGNOSTIC JOB : <S>_NO_CONVERGENCE", re.IGNORECASE),
re.compile(r"DIAGNOSTIC JOB : <S>_CPU_LIMIT", re.IGNORECASE),
re.compile(r"DIAGNOSTIC JOB : <S>_ERROR", re.IGNORECASE),
re.compile(r"DIAGNOSTIC JOB : NO_TEST_RESU", re.IGNORECASE),
re.compile(r"DIAGNOSTIC JOB : NOOK_TEST_RESU", re.IGNORECASE),
]
log_path = case_dir / self.results_dirname / self.logs_dirname / "run_solver.log"
if not log_path.is_file():
return None
if not _is_recent(log_path, _parse_start_time(start_time)):
return None
joined = "\n".join(read_tail_lines(log_path, lines=40))
if any(p.search(joined) for p in success_patterns):
return STATUS_DONE
if any(p.search(joined) for p in failure_patterns):
return STATUS_FAILED
return None
112 changes: 112 additions & 0 deletions examples/codeaster-cube/MESH/create_mesh.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
#!/usr/bin/env python

###
### This file is generated automatically by SALOME v9.15.0 with dump python functionality
###

import salome
import SMESH
from salome.geom import geomBuilder
from salome.smesh import smeshBuilder

salome.salome_init()

###
### GEOM component
###

geompy = geomBuilder.New()

Origin = geompy.MakeVertex(0, 0, 0)
OX = geompy.MakeVectorDXDYDZ(1, 0, 0)
OY = geompy.MakeVectorDXDYDZ(0, 1, 0)
OZ = geompy.MakeVectorDXDYDZ(0, 0, 1)
box = geompy.MakeBoxDXDYDZ(1, 1, 1)
x_0 = geompy.CreateGroup(box, geompy.ShapeType["FACE"])
geompy.UnionIDs(x_0, [3])
x_H = geompy.CreateGroup(box, geompy.ShapeType["FACE"])
geompy.UnionIDs(x_H, [13])
y_0 = geompy.CreateGroup(box, geompy.ShapeType["FACE"])
geompy.UnionIDs(y_0, [23])
y_H = geompy.CreateGroup(box, geompy.ShapeType["FACE"])
geompy.UnionIDs(y_H, [27])
z_0 = geompy.CreateGroup(box, geompy.ShapeType["FACE"])
geompy.UnionIDs(z_0, [31])
z_H = geompy.CreateGroup(box, geompy.ShapeType["FACE"])
geompy.UnionIDs(z_H, [33])
all_edges = geompy.CreateGroup(box, geompy.ShapeType["EDGE"])
geompy.UnionIDs(all_edges, [5, 8, 10, 12, 15, 18, 20, 22, 25, 26, 29, 30])
[x_0, x_H, y_0, y_H, z_0, z_H, all_edges] = geompy.GetExistingSubObjects(box, False)
Auto_group_for_Sub_mesh_1 = geompy.CreateGroup(box, geompy.ShapeType["FACE"])
geompy.UnionList(Auto_group_for_Sub_mesh_1, [x_0, x_H, y_0, y_H, z_0, z_H])
geompy.addToStudy(Origin, "O")
geompy.addToStudy(OX, "OX")
geompy.addToStudy(OY, "OY")
geompy.addToStudy(OZ, "OZ")
geompy.addToStudy(box, "box")
geompy.addToStudyInFather(box, x_0, "x=0")
geompy.addToStudyInFather(box, x_H, "x=H")
geompy.addToStudyInFather(box, y_0, "y=0")
geompy.addToStudyInFather(box, y_H, "y=H")
geompy.addToStudyInFather(box, z_0, "z=0")
geompy.addToStudyInFather(box, z_H, "z=H")
geompy.addToStudyInFather(box, all_edges, "all_edges")
geompy.addToStudyInFather(box, Auto_group_for_Sub_mesh_1, "Auto_group_for_Sub-mesh_1")

###
### SMESH component
###

smesh = smeshBuilder.New()

Mesh_1 = smesh.Mesh(box, "Mesh_1")
NETGEN_1D_2D_3D = Mesh_1.Tetrahedron(algo=smeshBuilder.NETGEN_1D2D3D)
NETGEN_3D_Parameters_1 = NETGEN_1D_2D_3D.Parameters()
NETGEN_3D_Parameters_1.SetMaxSize(0.173205)
NETGEN_3D_Parameters_1.SetMinSize(0.00173205)
NETGEN_3D_Parameters_1.SetSecondOrder(0)
NETGEN_3D_Parameters_1.SetOptimize(1)
NETGEN_3D_Parameters_1.SetFineness(2)
NETGEN_3D_Parameters_1.SetChordalError(-1)
NETGEN_3D_Parameters_1.SetChordalErrorEnabled(0)
NETGEN_3D_Parameters_1.SetUseSurfaceCurvature(1)
NETGEN_3D_Parameters_1.SetFuseEdges(1)
NETGEN_3D_Parameters_1.SetQuadAllowed(1)
NETGEN_3D_Parameters_1.SetCheckChartBoundary(8)
x_0_1 = Mesh_1.GroupOnGeom(x_0, "x=0", SMESH.FACE)
x_H_1 = Mesh_1.GroupOnGeom(x_H, "x=H", SMESH.FACE)
y_0_1 = Mesh_1.GroupOnGeom(y_0, "y=0", SMESH.FACE)
y_H_1 = Mesh_1.GroupOnGeom(y_H, "y=H", SMESH.FACE)
z_0_1 = Mesh_1.GroupOnGeom(z_0, "z=0", SMESH.FACE)
z_H_1 = Mesh_1.GroupOnGeom(z_H, "z=H", SMESH.FACE)
all_edges_1 = Mesh_1.GroupOnGeom(all_edges, "all_edges", SMESH.EDGE)
Quadrangle_2D = Mesh_1.Quadrangle(algo=smeshBuilder.QUADRANGLE, geom=Auto_group_for_Sub_mesh_1)
Quadrangle_Parameters_1 = Quadrangle_2D.QuadrangleParameters(smeshBuilder.QUAD_QUADRANGLE_PREF, -1, [], [])
Regular_1D = Mesh_1.Segment(geom=all_edges)
Number_of_Segments_1 = Regular_1D.NumberOfSegments(10)
isDone = Mesh_1.Compute()
Mesh_1.CheckCompute()
[x_0_1, x_H_1, y_0_1, y_H_1, z_0_1, z_H_1, all_edges_1] = Mesh_1.GetGroups()
box_1 = Mesh_1.GroupOnGeom(box, "box", SMESH.VOLUME)
Sub_mesh_1 = Quadrangle_2D.GetSubMesh()
Sub_mesh_2 = Regular_1D.GetSubMesh()


## Set names of Mesh objects
smesh.SetName(Sub_mesh_1, "Sub-mesh_1")
smesh.SetName(box_1, "box")
smesh.SetName(NETGEN_3D_Parameters_1, "NETGEN 3D Parameters_1")
smesh.SetName(y_H_1, "y=H")
smesh.SetName(z_0_1, "z=0")
smesh.SetName(Mesh_1.GetMesh(), "Mesh_1")
smesh.SetName(Quadrangle_Parameters_1, "Quadrangle Parameters_1")
smesh.SetName(x_0_1, "x=0")
smesh.SetName(x_H_1, "x=H")
smesh.SetName(Sub_mesh_2, "Sub-mesh_2")
smesh.SetName(z_H_1, "z=H")
smesh.SetName(all_edges_1, "all_edges")
smesh.SetName(Regular_1D.GetAlgorithm(), "Regular_1D")
smesh.SetName(y_0_1, "y=0")
smesh.SetName(NETGEN_1D_2D_3D.GetAlgorithm(), "NETGEN 1D-2D-3D")
smesh.SetName(Number_of_Segments_1, "Number of Segments_1")
smesh.SetName(Quadrangle_2D.GetAlgorithm(), "Quadrangle_2D")
Binary file added examples/codeaster-cube/MESH/mesh1.med
Binary file not shown.
Binary file added examples/codeaster-cube/MESH/mesh2.med
Binary file not shown.
Loading
Loading