Background
The external-code adapters under vaft.code increasingly follow a common scientific workflow:
prepare inputs
↓
run code
↓
collect outputs
The Config / Inputs / Result and prepare_* / run_* / collect_* APIs are becoming consistent, but the actual execution logic is still implemented separately inside each adapter.
For example, EFIT, CHEASE, TES, and GPEC each invoke external processes directly and handle runtime concerns independently, including:
- executable resolution
- working directory handling
- environment variables
- command-line arguments
- timeout handling
- stdout / stderr capture
- return-code handling
- future MPI or scheduler-based execution
At the same time, not every solver is an external executable. TokaMaker is currently driven in-process through its Python API.
This suggests that the scientific adapter boundary and the execution mechanism should be separated explicitly.
Goal
Allow each physics-code adapter to focus on scientific input/output translation while delegating process execution to a common execution layer.
Conceptually:
ODS / scientific configuration
↓
code adapter
prepare / run / collect
↓
execution layer
↓
local / MPI / scheduler
↓
physics code
The goal is not to unify all solver implementations, but to make the distinction explicit:
scientific adapter != execution mechanism
Proposed direction
Introduce a small execution backend protocol, for example:
class ExecutionBackend(Protocol):
def run(
self,
command,
*,
workdir,
env=None,
timeout=None,
nproc=1,
) -> ExecutionResult:
...
ExecutionResult should initially provide at least:
Additional execution metadata could be added later if useful, such as:
launcher command
process / job identifier
elapsed time
execution metadata
The first implementation only needs to support local subprocess execution:
ExecutionBackend
└── LocalBackend
Future backends could then be added without changing code-specific scientific APIs:
LocalBackend
MPIBackend
SlurmBackend
...
Relationship to CodeConfig / CodeInputs / CodeResult
vaft.code.base already defines:
CodeConfig
CodeInputs
CodeResult
CodeRunner
The execution abstraction should therefore be designed as an extension of the existing structure rather than as a separate framework.
In particular, the responsibility boundary should be clarified for runtime fields that are currently repeated across code-specific configs:
executable
workdir
args
env
timeout
Scientific and numerical configuration should remain owned by each adapter. The purpose of the execution layer is to remove duplicated execution behavior, not to eliminate code-specific configuration.
Python-native solvers
The abstraction must not assume that every solver is launched as a subprocess.
TokaMaker is currently executed in-process through a Python API. For the initial implementation, subprocess-based adapters can adopt the common execution backend while TokaMaker keeps its current execution path.
A later design can decide whether Python-native execution should also implement the same backend protocol.
Proposed scope
1. Define a common execution result and backend interface
Introduce the minimum types needed to describe a local code execution.
2. Implement a local subprocess backend
The backend should cover the common behavior currently duplicated across external-code adapters.
3. Migrate one representative adapter
Apply the backend to one relatively simple subprocess-based adapter first and use that migration to validate the interface.
There is no need to migrate every adapter in the first change.
4. Migrate remaining subprocess adapters incrementally
Once the interface is stable, move direct process-launch logic from EFIT, CHEASE, TES, GPEC, and similar adapters into the shared execution layer.
Out of scope
This issue does not require:
- changes to the Snakemake workflow structure
- changes to the ODS / OMAS data model
- unifying scientific configuration across physics codes
- replacing all adapters with one generic solver API
- changing the TokaMaker execution model
- implementing Slurm/PBS support in the first iteration
MPI and scheduler support can be added later after the abstraction is validated.
Expected benefits
- reduce duplicated process-execution code across adapters
- make timeout, environment, logging, and error-handling conventions consistent
- allow MPI or scheduler support without reimplementing it per physics code
- make adapter behavior easier to test with fake/mock execution backends
- separate scientific adapter logic from machine/runtime-specific behavior
- reduce the cost of integrating additional external physics codes
Acceptance criteria
Background
The external-code adapters under
vaft.codeincreasingly follow a common scientific workflow:The
Config / Inputs / Resultandprepare_* / run_* / collect_*APIs are becoming consistent, but the actual execution logic is still implemented separately inside each adapter.For example, EFIT, CHEASE, TES, and GPEC each invoke external processes directly and handle runtime concerns independently, including:
At the same time, not every solver is an external executable. TokaMaker is currently driven in-process through its Python API.
This suggests that the scientific adapter boundary and the execution mechanism should be separated explicitly.
Goal
Allow each physics-code adapter to focus on scientific input/output translation while delegating process execution to a common execution layer.
Conceptually:
The goal is not to unify all solver implementations, but to make the distinction explicit:
Proposed direction
Introduce a small execution backend protocol, for example:
ExecutionResultshould initially provide at least:Additional execution metadata could be added later if useful, such as:
The first implementation only needs to support local subprocess execution:
Future backends could then be added without changing code-specific scientific APIs:
Relationship to
CodeConfig / CodeInputs / CodeResultvaft.code.basealready defines:The execution abstraction should therefore be designed as an extension of the existing structure rather than as a separate framework.
In particular, the responsibility boundary should be clarified for runtime fields that are currently repeated across code-specific configs:
Scientific and numerical configuration should remain owned by each adapter. The purpose of the execution layer is to remove duplicated execution behavior, not to eliminate code-specific configuration.
Python-native solvers
The abstraction must not assume that every solver is launched as a subprocess.
TokaMaker is currently executed in-process through a Python API. For the initial implementation, subprocess-based adapters can adopt the common execution backend while TokaMaker keeps its current execution path.
A later design can decide whether Python-native execution should also implement the same backend protocol.
Proposed scope
1. Define a common execution result and backend interface
Introduce the minimum types needed to describe a local code execution.
2. Implement a local subprocess backend
The backend should cover the common behavior currently duplicated across external-code adapters.
3. Migrate one representative adapter
Apply the backend to one relatively simple subprocess-based adapter first and use that migration to validate the interface.
There is no need to migrate every adapter in the first change.
4. Migrate remaining subprocess adapters incrementally
Once the interface is stable, move direct process-launch logic from EFIT, CHEASE, TES, GPEC, and similar adapters into the shared execution layer.
Out of scope
This issue does not require:
MPI and scheduler support can be added later after the abstraction is validated.
Expected benefits
Acceptance criteria