Run control API¶
FoamNordic describes a workload with immutable declarations and returns a non-blocking Run. The source case is copied before any preparation command.
flowchart LR
A[Case and programme declarations] --> B[Longship.compile]
B --> C[Isolated case preparation]
C --> D[Local or Slurm launch]
D --> E[Runtime.Run]
E --> F[Runtime.Result]
F --> G[Postprocess.Case]
API index¶
| API | Purpose | Returns |
|---|---|---|
fno.OpenFOAM.setup(...) |
Set the default OpenFOAM shell environment | None |
fno.OpenFOAM.Case(...) |
Declare a source case, run location and OpenFOAM environment | OpenFOAM.Case |
case.initialize(...) |
Declare mesh validation or generation for the isolated run | The same Case |
fno.Operator.model(...) |
Use an executable FNOM model | Operator |
fno.Operator.table(...) |
Use a regular-grid FNOM table | Operator |
fno.Operator.function(...) |
Use a Python function or solver-native symbolic graph | Operator |
fno.Closure(...) |
Bind an operator to a solver-side closure adapter | Closure |
closure.update(...) |
Append an immutable solver-mediated update stage | New Closure |
fno.Transform(...) |
Bind an operator to a generic field-update stage | Transform |
fno.Observe(...) |
Request bounded field summaries | Observe |
fno.Longship(...) |
Assemble one complete workload | Longship |
longship.launch(...) |
Prepare and launch the workload | Runtime.Run |
Case¶
from pathlib import Path
import foamnordic as fno
fno.OpenFOAM.setup(
of_cmd="openfoam/2512",
shell="bash",
)
case = fno.OpenFOAM.Case(
name="lidDrivenCavity",
case_dir=Path("cases/lidDrivenCavity"),
run_dir=Path("output"),
application="pimpleFoam",
)
case.initialize(ranks=1, mesh="blockMesh", validate_mesh=True)
OpenFOAM.setup(of_cmd="openfoam/2512") resolves the module shorthand to module load openfoam/2512 inside generated shell commands. A Case may override either setting. mesh=None uses an existing mesh. If 0/ is absent, 0.orig/ is copied into the isolated case.
OpenFOAM.setup¶
fno.OpenFOAM.setup(*, of_cmd=None, shell="bash")
| Parameter | Type | Default | Allowed values or constraint |
|---|---|---|---|
of_cmd |
str \| None |
None |
Module shorthand, shell initialisation command, OpenFOAM.app wrapper, or None for commands already on PATH |
shell |
str |
"bash" |
Executable resolving to bash or zsh |
OpenFOAM.Case¶
fno.OpenFOAM.Case(*, name=None, case_dir, run_dir, of_cmd=None,
shell=None, application="pimpleFoam", ranks=1,
integration=None)
| Parameter | Type | Default | Allowed values or constraint |
|---|---|---|---|
name |
str \| None |
None |
Non-empty run label; inferred from case_dir when omitted |
case_dir |
path | required | Existing source case; must differ from run_dir |
run_dir |
path | required | Parent output location for isolated runs |
of_cmd |
str \| None |
configured default | Module name, shell initialisation command, OpenFOAM.app wrapper or commands already on PATH |
shell |
str \| None |
configured default | Executable resolving to bash or zsh |
application |
str |
"pimpleFoam" |
Non-empty OpenFOAM executable name or path |
ranks |
int |
1 |
Positive integer |
integration |
OpenFOAM.DictionaryTemplate \| None |
None |
Optional out-of-tree adapter dictionary template |
case.initialize(*, ranks=None, mesh=None, validate_mesh=False)
| Parameter | Type | Default | Allowed values or constraint |
|---|---|---|---|
ranks |
int \| None |
None |
Positive integer; retains the constructor value when omitted |
mesh |
str \| None |
None |
None or "blockMesh" |
validate_mesh |
bool |
False |
Runs checkMesh during isolated preparation when true |
Parallel runs use scotch when decomposeParDict is absent. Existing dictionaries are copied and numberOfSubdomains is set to ranks. simple or hierarchical survives only when its n factors multiply to ranks; otherwise FoamNordic reports the mismatch and uses scotch. The source case stays unchanged.
Case inspection¶
| Member | Type or signature | Meaning |
|---|---|---|
initial_directory |
Path |
0/ when present, otherwise 0.orig/ |
fields |
Mapping[str, OpenFOAM.Field] |
Lazily decoded initial fields |
boundary_names |
tuple[str, ...] |
Sorted unique initial-field patch names |
field(name) |
OpenFOAM.Field |
Return one field or raise an error listing the available names |
Each OpenFOAM.Field record exposes name, field_class, dimensions, internal_value, boundary_names and path. Native OpenFOAM dictionary directives are expanded through the selected toolchain before decoding.
Custom integration dictionary¶
fno.OpenFOAM.DictionaryTemplate(source, destination, variables={})
| Parameter | Type | Default | Constraint |
|---|---|---|---|
source |
path | required | Existing template resolved by the case preparation stage |
destination |
path | required | Relative path that remains inside the isolated case |
variables |
Mapping[str, str] |
{} |
Non-empty variable names mapped to rendered string values |
Pass the declaration as Case(integration=...) for an out-of-tree solver adapter. FoamNordic supplies its runtime variables and combines them with variables; the adapter retains ownership of solver-specific call sites and equations.
Models and field programs¶
transform = fno.Transform(
name="predictVelocity",
operator=fno.Operator.model("velocity.fnom"),
inputs={"pressure": fno.Field("p")},
outputs={"velocity": fno.Field("U")},
at="time_step_start",
)
closure = fno.Closure(
name="nutFjord",
operator=fno.Operator.model("eddy-viscosity.fnom"),
inputs={
"velocity_grad": fno.Field.grad("U"),
"delta": fno.Field.delta(),
},
outputs={"nut": fno.Field("nut")},
)
Transform is a generic stored-field operation. Closure requires a solver-side equation adapter. FNOM selects ONNX, compiled, Joblib, Equinox, or a native table without changing this interface.
Use Field.runtime(name, kind=...) for adapter-created fields absent from the source case. Supported kinds are scalar, vector, spherical_tensor, symm_tensor and tensor. Use Field(name) for stored fields so FoamNordic validates their layout.
closure.update() returns a new declaration with another solver-mediated stage. Each stage has its own resident worker. The integration template owns call order and places @FOAMNORDIC_UPDATE_0_BODY@ at the first update call site.
closure = fno.Closure(
name="progressRateFjord",
operator=fno.Operator.model("reaction-rate.fnom"),
inputs={
"c": fno.Field("c"),
"c_var": fno.Field("cVar"),
"T": fno.Field("T"),
},
outputs={"omega_c": fno.Field("omega_c")},
)
closure = closure.update(
name="thermochemicalState",
operator=fno.Operator.table("manifold.fnom"),
inputs={
"c": fno.Field("c"),
"c_var": fno.Field("cVar"),
},
outputs={
"H2": fno.Field("H2"),
"O2": fno.Field("O2"),
"H2O": fno.Field("H2O"),
"N2": fno.Field("N2"),
},
)
Operator.table() requires a lookup-table FNOM manifest. The update list defines worker ownership and lifetime; the solver adapter advances equations between stages.
Pure mathematical functions can use Operator.function; logical inputs become keyword arguments and returned mapping keys are logical outputs.
Operators and field programmes¶
fno.Operator.model(path)
fno.Operator.table(path)
fno.Operator.function(function)
| Constructor | Argument | Constraint | Execution |
|---|---|---|---|
model |
.fnom path |
Manifest must describe an executable model | Native connector or resident ModelHost selected by the artefact |
table |
.fnom path |
Manifest must describe a regular-grid table | Native multilinear lookup |
function |
callable | Logical inputs must match callable parameters | Native OpenFOAM graph when fully supported; otherwise resident Python |
fno.Closure(name, operator=None, inputs=None, outputs=None, key=None, *, artifact=None)
fno.Transform(name, operator, inputs=None, outputs=None,
at="time_step_start", key=None)
| Parameter | Applies to | Default | Allowed values or constraint |
|---|---|---|---|
name |
Closure |
required | Non-empty adapter name; names must be unique within a Longship |
name |
Transform |
required | Non-empty OpenFOAM word; names must be unique within a Longship |
operator |
Both | required¹ | Operator; legacy artifact= remains accepted by Closure |
inputs |
Both | None |
Mapping from logical model names to FieldExpression; inferred for recognised FNOM contracts when omitted |
outputs |
Both | None |
Mapping from logical model names to mutable OpenFOAM fields; inferred for recognised FNOM contracts when omitted |
key |
Both | None |
Random.Key; deterministic global key 42 when omitted |
at |
Transform |
"time_step_start" |
time_step_start, outer_corrector, pressure_corrected, time_step_end |
¹ Use artifact= only for compatibility; new code should always pass an Operator.
Local launch¶
longship = fno.Longship(
case=case,
transforms=(transform,),
numerics="strict", # optional; default is convenient
)
run = longship.launch(start_timeout=900, profile_every=0)
result = run.stop(timeout=3600, progress=True)
result.summary(style="compact")
With no scheduler, rank one runs directly and multiple ranks use the MPI launcher associated with the case environment.
Slurm launch¶
scheduler = fno.Slurm(
account="project_0000000",
partition="small",
time="00:30:00",
openfoam=fno.Slurm.openfoam(
nodes=1, ntasks=16, cpus_per_task=1, mem_per_cpu="2G"
),
model=fno.Slurm.model(cpus_per_task=4, mem_per_cpu="2G"),
)
case.initialize(ranks=16, mesh=None, validate_mesh=True)
run = fno.Longship(
case=case, scheduler=scheduler, closures=(closure,)
).launch(start_timeout=900)
Solver and model resources are separate. Coupled jobs place a resident model worker on every solver node and fail together inside one allocation.
Slurm¶
fno.Slurm(account=None, partition=None, time=None, *, openfoam, model=None)
fno.Slurm.openfoam(*, nodes, ntasks, cpus_per_task=1, mem_per_cpu=None)
fno.Slurm.model(
*,
cpus_per_task=1,
mem_per_cpu=None,
gres_model=None,
gres_count=None,
)
| Parameter | Applies to | Default | Constraint |
|---|---|---|---|
account |
Slurm |
None |
Non-empty Slurm account without whitespace; omission uses the cluster default account |
partition |
Slurm |
None |
Non-empty Slurm partition without whitespace; omission delegates selection to the cluster |
time |
Slurm |
required | Non-empty Slurm time limit without whitespace |
openfoam |
Slurm |
required | Value returned by Slurm.openfoam() |
model |
Slurm |
None |
Value returned by Slurm.model(); one ModelHost task is placed on each OpenFOAM node |
nodes |
Slurm.openfoam |
required | Positive integer |
ntasks |
Slurm.openfoam |
required | Positive integer divisible by nodes |
cpus_per_task |
Both resource declarations | 1 |
Positive integer |
mem_per_cpu |
Both resource declarations | None |
Non-empty Slurm memory value without whitespace, such as "2G" |
gres_model |
Model resources | None |
GPU model requested for each ModelHost node, such as "gh200" |
gres_count |
Model resources | None |
Positive GPU count per ModelHost node; declared together with gres_model |
Declaring model memory requires mem_per_cpu on the OpenFOAM resources as well, because both steps share the allocation request. Omitting model retains the default one-CPU ModelHost resources without emitting an explicit model resource section.
Omitted account and partition produce no corresponding #SBATCH directives. Cluster defaults apply; supply these values when the cluster requires them.
Model GRES resources are reserved on every OpenFOAM node and assigned only to the ModelHost step. For example, gres_model="gh200", gres_count=1 renders --gres=gpu:gh200:1 for the allocation and ModelHost launch.
Placement¶
fno.Runtime.Attached(model_cpus_per_node=None, data_path="auto")
| Parameter | Type | Default | Allowed values or constraint |
|---|---|---|---|
model_cpus_per_node |
int \| None |
None |
Positive integer, or automatic selection when omitted |
data_path |
str |
"auto" |
auto, shm, uds, ucx or tcp |
Attached placement starts one ModelHost per solver node. auto selects a compatible available transport; an unavailable explicit transport raises a launch error.
Observations¶
Observe requests bounded native summaries at a declared interval. Observation records are monitoring data and never gate solver progress. Stored fields and full scientific comparison belong to Result.postprocess.
fno.Observe(summaries, interval=1)
| Parameter | Type | Default | Allowed values or constraint |
|---|---|---|---|
summaries |
Mapping[str, tuple[str, ...]] |
required | At least one field; reductions are min, max, mean and l2 |
interval |
int |
1 |
Positive number of completed field-programme exchanges |
Observation records¶
for record in run.observe(poll_interval=0.1, progress=False):
print(record.exchange_index, record.time, record.summary["U"].l2)
| Type | Members |
|---|---|
Runtime.ObservationRecord |
exchange_index, time, summary, timing |
Runtime.FieldSummary |
minimum, maximum, mean, l2, count; unrequested values are None |
Runtime.ObservationTiming |
model_wait, evaluate in seconds |
Runtime.ObservationStream |
Iterable stream with path, poll_interval, expected_sources and close() |
Multi-rank records are merged by exchange index. ObservationStream.close() stops only local consumption; it neither cancels the workload nor deletes retained records.
Longship¶
fno.Longship(case, closures=(), transforms=(), observations=(),
placement=fno.Runtime.Attached(), scheduler=None, name=None,
verbose=False, numerics="convenient")
| Parameter | Type | Default | Allowed values or constraint |
|---|---|---|---|
case |
OpenFOAM.Case |
required | Case declaration owned by this workload |
closures |
tuple[Closure, ...] |
() |
Solver-adapter programmes |
transforms |
tuple[Transform, ...] |
() |
Generic field programmes |
observations |
tuple[Observe, ...] |
() |
Read-only summaries |
placement |
Runtime.Attached |
automatic | One ModelHost per solver node; data_path is auto, shm, uds, ucx or tcp |
scheduler |
Slurm \| None |
None |
Local execution when omitted |
name |
str \| None |
None |
Inferred from case.name when omitted |
verbose |
bool |
False |
Display declaration and launch information |
numerics |
str |
"convenient" |
convenient may add missing derived-field schemes to the copied case; strict rejects them |
longship.launch(*, start_timeout=None, readiness_timeout=120.0,
receive_memory_bytes=None, profile_every=0,
termination_grace=30.0, orphan_timeout=30.0, verbose=None)
| Parameter | Type | Default | Meaning |
|---|---|---|---|
start_timeout |
float \| None |
None |
Positive seconds to wait for scheduler start; unlimited when omitted |
readiness_timeout |
float |
120.0 |
Positive seconds allowed for ModelHost readiness |
receive_memory_bytes |
int \| None |
None |
Positive shared pre-receive memory credit; automatically budgeted when omitted |
profile_every |
int |
0 |
Profile every N exchanges; 0 disables profiling |
termination_grace |
float |
30.0 |
Positive seconds allowed for coordinated child shutdown |
orphan_timeout |
float |
30.0 |
Non-negative seconds before cancelling after owner loss; 0 requests immediate cancellation |
verbose |
bool \| None |
None |
Overrides Longship.verbose; None inherits it |
Run ownership¶
run.statusreads current state.run.stop()waits for normal completion.run.stop(force=True)requests bounded cancellation.run.detach()allows the workload to outlive Python ownership.result.artifactsexposes the isolated case, logs, observations, and Slurm files.
Closing an owning Python process cancels its active workload after the orphan timeout. A detached run must be managed through its scheduler or supervisor.
Runtime.Run¶
| Member | Signature or type | Behaviour |
|---|---|---|
status |
Runtime.RunStatus |
Non-blocking current state |
summary |
summary(style="compact", *, display=True) |
Return current local or Slurm metadata; styles are short, compact, long and expanded |
observe |
observe(*, poll_interval=0.1, progress=False) |
Return a streaming iterator of merged observation records |
stop |
stop(*, force=False, timeout=None, progress=False) |
Wait for completion or request bounded cancellation; always returns the same terminal Result |
detach |
detach() |
Release Python ownership and return the same Run |
RunStatus values are running, succeeded, failed and cancelled. A non-forced stop() does not cancel at timeout; it raises TimeoutError while the workload remains owned and running.
Runtime.Result¶
| Member | Type or signature | Meaning |
|---|---|---|
status |
Runtime.RunStatus |
Terminal lifecycle state |
success |
bool |
True only for succeeded |
exit_code |
int |
Supervisor exit code |
elapsed_seconds |
float |
Measured wall-clock duration |
work_dir |
Path |
Generated run root |
case |
Path |
Isolated OpenFOAM case |
logs |
Path |
Run log directory |
artifacts |
Runtime.ResultArtifacts |
Stable case, log, observation and Slurm paths |
postprocess |
Postprocess.Case |
Durable field reader for this result |
job_id |
str \| None |
Slurm job identifier, or None for a local run |
plan_digest |
str \| None |
Digest of the compiled execution plan |
summary() |
summary(style="compact", *, display=True) |
Return a RunSummary; styles are short, compact, long and expanded |
Reproducibility¶
The run marker stores the compiled plan and digest. It includes resources, placement, stages, bindings, random keys, and numerics policy. Convenient numerics records actual dictionary insertions in the run directory.