Driving the solver¶
The four functions that take you from a TOML file to an advanced
network. load_case → build_network → dispatch_advance is the
explicit three-step path; run_case collapses build-and-advance into a
single call. See Overview for when to use which.
Field references live elsewhere
These functions move a Case dataclass around. Every field on it —
the [case] knobs, the per-pipe / BC / closure specs — is documented
in Run / case settings and
Closures — reference table. This page
documents the calls, not the data.
Parsing a case¶
aeolus1d.io.parsers.load_case ¶
load_case(
path: str | Path,
*,
strict_case_header: bool = False,
validate: bool = True,
) -> Case
Parse a TOML file into a Case dataclass.
Phase B10 — runs aeolus1d.io.validate.validate_case before
returning so spec-level mistakes (typos in pipe references,
pipes with no area, etc.) surface as a single aggregated
error message instead of crashing deep inside the builder
pipeline.
Phase 2026-05-26 — wraps the early parse path so the user gets
file-anchored diagnostics for: missing files, malformed TOML
syntax, unknown / mistyped keys in top-level sections, and
missing required keys. The downstream validate_case pass
already handles cross-reference issues (BC referencing a
non-existent pipe, etc.); this layer protects everything
upstream of that.
Case-header policy — any CaseHeader field omitted from [case]
fills from its schema default (a minimal [case] listing only the
handful of knobs that differ from default is the idiomatic form;
aeolus1d expand writes that minimal block). Unknown / mistyped keys
are always rejected with a fuzzy-match suggestion — that typo-safety
lives in _construct_section below and is independent of this
policy. strict_case_header=True is the opt-in lint that also
requires every field be written explicitly (the v0.9 fully-specified
form); production runners leave it False. [gas] likewise
default-fills missing fields and logs a one-shot warning naming each
fill — see :func:_warn_spec_defaults. The array sections (pipe /
junction / BC / closure specs) default-fill without that warning;
only their required keys and, per block, their unknown keys are
checked. (Corrected 2026-09-25: this claimed the warning for every
per-spec dataclass; only _construct_section emits it, and only
[case]/[gas] go through that.)
validate=False skips the validate_case call entirely and
always returns the parsed Case, even one with fatal issues —
for a caller that wants to load a broken case anyway (the GUI's
File → Open, so a case that fails validation can still be opened
and fixed in place instead of being locked out) and will run its
own validate_case pass afterward to learn what's wrong.
Production callers (aeolus1d run, aeolus1d validate) leave
this True.
Inputs
path: a TOML case file.
strict_case_header: False (default): a [case] that omits fields takes the dataclass
defaults. True: every CaseHeader field must be written (an opt-in lint; also
skipped while AEOLUS_LENIENT_CASE_HEADER=1).
validate: True (default): run validate_case before returning. False: return
whatever parsed, even a case with fatal issues (the GUI's File -> Open).
Returns:
a Case: case (CaseHeader) and gas built by _construct_section; pipes,
junctions, bcs (CSV schedules expanded, relative to the case file), turbos,
shafts, cylinders (firing phases derived), reactions, species,
controllers, crankshaft, coolant_pipes, coolant_couplings;
component_refs (location -> ref) and case_dir (the file's absolute directory).
Order: exists -> read UTF-8 -> parse TOML -> apply_case_upgrades (pops
schema_version) -> resolve_component_refs -> sections -> validate_case. Nothing
is written. DEFAULT OWNERS: every default lives on its schema dataclass; the parser
literals that restate them (species, bc, junction, cylinder, ...) agree with them, and
schema_help prose restates a few (see its contracts).
UNKNOWN KEYS: a key the dataclass does not declare in [case] or [gas] is ALWAYS
refused (with a suggestion). In every other table the parsers read named keys with
d.get and ignore the rest, so an unknown key there is refused only by validate_case:
with validate=False it is silently dropped.
Refuses:
all CaseParseError with the file set unless noted: missing file; unreadable file, a
directory, or bytes that are not UTF-8; TOML syntax error; a schema_version that is not
an integer, is below 1 or is newer than this build (section case); a component
ref that is missing, cyclic or not TOML; a wrong-typed or unknown [case]/[gas]
key; [pipe] for [[pipe]] and the other table shape errors; a missing required key
or a bad value in any table (named by file and entry); [[crankshaft]]; a bad CSV;
then, with validate=True, CaseValidationError listing every validator error.
NOT checked here, though a bad value is a run-time failure: a NaN t_end (refused by
validate_case_pre_run). A bad adaptive_atol / adaptive_rtol (under error
control) and a bad Picard band (while adaptive_picard is on) ARE refused by
validate_case (_validate_adaptive_controls).
Edges:
the smallest case and its case_dir
(test_the_smallest_case_loads_with_its_directory_and_no_refs).
missing, directory and non-UTF-8 files
(test_a_missing_file_a_directory_and_bad_bytes_name_the_path).
syntax errors (test_a_toml_syntax_error_names_the_file).
forward version, marker not reaching the header
(test_a_forward_schema_version_is_refused_and_the_marker_never_reaches_the_header).
unknown [case] key with and without validation
(test_an_unknown_case_key_is_refused_even_without_validation).
unknown key elsewhere
(test_an_unknown_key_elsewhere_is_ignored_by_the_parser_but_refused_by_the_validator).
errors name file and entry
(test_a_value_error_in_a_pipe_block_names_the_file_and_entry,
test_a_missing_required_key_in_a_table_names_the_table_and_key,
test_a_single_pipe_table_is_refused_with_the_array_spelling).
validate=False (test_validate_false_returns_a_case_the_validator_would_refuse).
the strict option (test_the_strict_header_option_requires_every_case_key).
crankshaft (test_a_crankshaft_array_and_a_bad_crankshaft_are_refused_naming_the_file).
component refs (test_a_component_reference_is_expanded_and_recorded).
CSV relative to the case file (test_a_bc_csv_path_is_resolved_against_the_case_directory).
Building the network¶
build_network materialises the parsed Case into live Pipe,
junction, and boundary-condition objects, returning a NetworkBundle
(documented under Runtime objects).
Pass a Clock if the case uses any transient boundary condition — the
helper aeolus1d.io.case._case_has_transient_bcs(case) tells you whether
one is needed.
aeolus1d.io.build.build_network ¶
build_network(
case: Case,
clock: Clock | None = None,
*,
validate: bool = True,
) -> NetworkBundle
Materialise a Case into Pipe/Junction/BC objects.
Returns a NetworkBundle (pipes_by_id, junctions,
far_bcs, controllers). Supports legacy 3-tuple unpack
pipes, junctions, bcs = build_network(case) via iter.
far_bcs is the list of (pipe, End, BCFn) triples expected by
advance_network. If the case uses any transient BC, pass a
Clock — otherwise the BC factories will refuse to build.
Validation. validate_case used to have exactly one call site —
inside the TOML text parser — so every check it performs (schedule
ordering, junction/turbo port areas, BC static params, init
plausibility, unknown keys, …) applied only to cases loaded from a
file. A Case assembled in Python and passed straight here got none
of it, which is the path notebooks, harnesses and tests all take, and
the path a newly-guarded bug is most likely to come back on.
It now runs at this boundary, which is the one every route to a live
network passes through. Errors raise CaseValidationError; warnings
print once (load_case stamps the Case, so the TOML path does not
print them twice — but the error checks DO re-run here, because
mutating a loaded Case before building is the documented way to
override a setting and can invalidate it).
Pass validate=False to skip. That exists for callers that build a
deliberately partial Case — some tests construct one field-by-field
to exercise a single builder — and is not a recommendation.
Inputs
case: a Case (from load_case or built in code). clock: the Clock
the time-dependent BCs, cylinders and user closures read t from, or None.
With None and a case that needs one (_case_has_transient_bcs) a fresh
Clock is created INSIDE the build and is not returned: BCs built on it
stay at t = 0 unless the caller advances it, so a caller that runs the
network passes its own (run_case, the CLI, optimize do; the FMU and
the CFL inspector do not). validate: True (the default) runs
validate_case first; False skips it, for deliberately partial cases and
for tests of the build-time guards.
Returns:
a NetworkBundle: pipes, junctions (junctions, turbos,
cylinders, then shafts), bcs (biased-plenum BCs first, then the
[[bc]] specs), controllers and controllers_by_id, the coolant
pipes / couplings / standalone walls, the typed mirror arrays and the
connection handshake ledger. It unpacks as the legacy
(pipes, junctions, bcs). The process-wide throttle registry is cleared
on entry and refilled by this build's throttles.
Order: validate, ghost width from the scheme, limiter, species-order check
(spec only, so its repair hint is what the user sees), pipes, BC-fed species
columns, [[reaction]] blocks, junctions, shafts, turbos, cylinders, far
BCs, end coverage, controllers, coolant system, bundle, handshake.
Refuses:
CaseValidationError from validate_case (errors; warnings print once
per case); an unknown scheme or limiter -> ValueError (the limiter used to
fall back to minmod); every refusal of the stages above, as ValueError; and,
in [case] handshake = "strict" with validation on, CaseValidationError
for a connection that loses a species or mismatches a gas model.
Edges:
a built network and its parts
(test_a_built_network_carries_pipes_bcs_a_handshake_and_the_typed_mirror).
validation on by default and skippable
(test_a_python_built_case_is_validated,
test_validation_can_be_skipped_explicitly,
test_errors_are_rechecked_even_on_an_already_validated_case,
test_warnings_are_not_printed_twice_on_the_toml_path).
a misspelt limiter
(test_a_misspelt_limiter_is_refused_not_run_as_minmod).
a clocked case builds without a caller clock
(test_a_case_with_clocked_bcs_builds_without_a_clock_from_the_caller).
the throttle registry is cleared on entry
(test_the_throttle_registry_is_cleared_on_entry).
the species-order check comes first
(test_the_species_ordering_refusal_comes_before_anything_is_built).
every bundled example builds
(test_example_builds_network_with_sane_initial_state).
Advancing: one-call path¶
run_case is the simplest driver — it builds the network, wires any
controllers into the per-step callback, and advances to
case.case.t_end, returning (pipes_by_id, junctions, t_final).
aeolus1d.io.case.run_case ¶
run_case(
case: Case,
on_step: Callable[[float], None] | None = None,
max_steps: int | None = None,
t_start: float = 0.0,
live_control: object | None = None,
) -> tuple[dict[str, Pipe], list[Component], float]
Build the network from case and advance it to case.case.t_end.
Returns (pipes_by_id, junctions, t_final). The optional on_step
callback is forwarded to the timeloop and fires once per completed
step — used by the CLI's --trace-dt sampler.
max_steps is the safety cap on solver iterations. When None
each timeloop applies its own default (10M for both single-pipe and
network); the GUI's Preferences dialog and the CLI's
--max-steps flag both flow through here when the user wants a
tighter or looser cap.
t_start defaults to 0.0 for fresh runs; the CLI's
--resume path passes the prior run's t_final so the
timeloop continues from there to case.case.t_end. Callers are
expected to seed the network state (pipes, junctions) themselves
before calling — this entry point doesn't open the prior HDF5.
Inputs
case: a Case.
on_step: called with the simulation time [s] after each completed step, or None.
max_steps: the cap on solver iterations (each timeloop's own default, 10 million, when
None).
t_start: the start time [s]; 0.0 for a fresh run, the prior t_final for a resume. The
caller seeds the network state; this function does not open a checkpoint.
live_control: a LiveControl polled by the drivers that support it, or None.
Returns:
(pipes_by_id, junctions, t_final): the built, advanced network (state advanced in
place) and the time reached [s].
Refuses:
anything validate_case_pre_run refuses (has_resume is t_start > 0), before
anything is built; whatever build_network and dispatch_advance refuse.
Edges:
a refusal happens before the build
(test_run_case_refuses_before_it_builds_anything).
t_start > 0 legalises t_end = 0; the controllers run before the caller's
on_step on every step; a clock exists only for transient BCs
(test_run_case_treats_a_positive_t_start_as_a_resume_and_chains_controller_callbacks).
a real run reaches t_end (test_run_case_runs_a_real_tube_to_t_end).
flow chart: docs/flows/case-load-build-run-write.md.
Advancing: explicit dispatch¶
When you need the built network in hand before it runs — to attach a
sampler via on_step, seed state for a --resume, or trace per step —
advance a pre-built NetworkBundle with dispatch_advance. It is the
single point that inspects the case (solver, the rk time scheme,
junction presence) and routes to the correct timeloop driver, so prefer
it over calling a driver such as advance_network directly. A
solver = "quasi_steady" case routes to the low-Mach QS driver, a
solver = "strang" case to advance_network_strang, and an implicit
rk (implicit_euler / bdf2 / radau_iia) to
advance_network_implicit; everything else takes the explicit path.
The network path is forced even without junctions whenever a pipe
carries species, catalyst, or real-gas state, or the case wires coolant
couplings / wall-thermal pairs.
aeolus1d.io.case.dispatch_advance ¶
dispatch_advance(
case: Case,
pipes: dict[str, Pipe],
junctions: list[Component],
far_bcs: FarBCs,
*,
clock: Clock | None,
on_step: Callable[[float], None] | None = None,
max_steps: int | None = None,
t_start: float = 0.0,
live_control: object | None = None,
coolant_couplings: list[tuple[Pipe, object, object]]
| None = None,
wall_thermal_pairs: list[WallThermalPair] | None = None,
soa_arrays: NetworkArrays | None = None,
) -> float
Decide which timeloop driver to call given a built network +
case knobs, and run it to case.case.t_end.
Single point of dispatch for every CLI / GUI / library entry
point that needs to advance a pre-built network — run_case
is the simplest caller (build then advance), while the CLI's
--trace-dt and resume branches need to keep the
pipes / junctions handles for tracing / seeding before calling
this. Centralising the (solver, rk, junctions) dispatch tree
here means a new driver / scheme pulls into one place rather
than the three near-duplicates we used to maintain in
cli/main.py.
Returns the final simulation time the timeloop reached.
Inputs
case: the Case. pipes: built pipes by id. junctions: built junctions and
components. far_bcs: the (pipe, end, bc) triples. clock: a Clock or None
(keyword). on_step, max_steps, t_start, live_control, coolant_couplings,
wall_thermal_pairs, soa_arrays: as for run_case (keywords).
Returns:
the final time [s]. The pipes and junctions are advanced in place; on_step runs once
per step. Order: refuse an unknown solver; build the knobs; the chem-parallel notice;
the one-shot solver banner (AEOLUS_QUIET_SETTINGS=1 silences it); then the choice:
_needs_network_path false -> the single-pipe driver; else quasi_steady ->
_advance_quasi_steady; strang -> _advance_strang; an implicit rk ->
_advance_implicit_network; otherwise _advance_explicit_network.
Refuses:
a solver outside explicit/strang/quasi_steady -> ValueError (it ran as
explicit); an unknown limiter (_DispatchKnobs.build); plus each branch's refusals.
Edges:
junction cases take the explicit network driver
(test_a_junction_case_goes_to_the_explicit_network_driver).
unknown solver (test_an_unknown_solver_name_is_refused_not_run_as_explicit).
strang and implicit routing (test_a_strang_case_passes_the_rk_and_chemistry_knobs,
test_an_implicit_rk_goes_to_the_implicit_network_driver_with_its_configs).
quasi-steady (test_a_quasi_steady_case_goes_to_the_quasi_steady_driver).
the shortcut (test_a_plain_single_pipe_takes_the_single_pipe_shortcut).
solver = "strang" with an implicit rk goes to Strang, the solver winning
(test_the_strang_solver_wins_over_an_implicit_rk).
flow chart: docs/flows/case-load-build-run-write.md.