Case load, build, run, write¶
The path a case file takes from text to result: aeolus1d run case.toml
reads the TOML, turns it into a Case, builds the network, advances it with
the driver the case asks for, and writes the HDF5 result. The contract of
every function named below is in its docstring (the standard is
Function contracts and flow charts). This chart draws the
seams and says where each refusal happens; two stages have a chart of their
own and are only linked here: the build (Build a network from a
case) and resuming from a result file (Resume from a
checkpoint).
Everything between "TOML text" and "a Case" is aeolus1d.io.parsers.load_case
and is pure parsing: no pipe is created, no kernel runs. Everything a refusal
names is a file, a table and a key, as far as the stage can know them (see
"Where each refusal happens").
Load: text to a Case¶
What enters: a path, strict_case_header (False for every production caller)
and validate (True by default; the GUI's File, Open passes False so that a
broken case can still be opened and fixed). What leaves: a Case.
flowchart TD
IN["path, strict_case_header, validate"] --> EX{"file exists?"}
EX -- no --> X1["CaseParseError: case file not found"]
EX --> RD{"read as UTF-8"}
RD -- "unreadable, a directory, not UTF-8" --> X2["CaseParseError naming the path"]
RD --> TL{"tomllib.loads"}
TL -- "syntax error" --> X3["CaseParseError: TOML syntax error"]
TL --> UP{"apply_case_upgrades: pop schema_version, walk the upgrade chain"}
UP -- "newer, malformed or sub-floor version" --> X4["CaseParseError, section case"]
UP --> CR{"resolve_component_refs: expand every ref table"}
CR -- "missing, cyclic or invalid part" --> X5["CaseParseError naming the path"]
CR --> HD{"_construct_section for case, then gas"}
HD -- "wrong type, unknown key, missing key" --> X6["CaseParseError: key, line, suggestion"]
HD --> ST{"strict_case_header?"}
ST -- "yes, and a field not written" --> X7["CaseParseError listing every missing key"]
ST --> SL["_parse_section_list for each array of tables"]
SL --> BC["bc: _expand_bc_csv_schedules reads csv_path next to the case file"]
BC --> CS["_parse_crankshaft_section: parse, then derive firing phases"]
CS --> CA["Case: all sections, component_refs, case_dir"]
SL -- "not an array, a missing key, a bad value" --> X8["CaseParseError naming file and entry"]
BC -- "bad or missing csv, a column the kind ignores" --> X9["CaseParseError, section bc"]
CS -- "bad firing order or intervals" --> X10["CaseParseError, section crankshaft"]
CA --> V{"validate?"}
V -- no --> OUT["Case, as parsed"]
V -- yes --> VC{"validate_case with a TomlSourceMap for line numbers"}
VC -- "any error issue" --> X11["CaseValidationError listing every error"]
VC --> OUT
The order inside the load matters in three places. The [case] block is built
before the strict check, so a typo such as cf1 is reported with its
suggestion instead of being buried in a list of 50 "missing" keys. Component
references are expanded straight after the version upgrade and before any
section is built, because both are dict to dict transforms on the raw parse;
once expanded, the document is exactly what would have been written inline, so
no parser knows references exist. The CSV schedules are read at load, where the
case file's directory is known, so every later stage sees an ordinary inline
schedule.
One table: _parse_pipe and its blocks¶
Each [[pipe]] table goes through _parse_pipe, which hands every sub-table to
its own parser. The closure families (friction, heat, porous, catalyst, filter
sub-specs, reaction names) read through one registry-driven codec
(parse_kind_block, parse_flat_spec), so the kinds and fields a block accepts
are the registry's, not a list in the parser.
flowchart TD
P["one pipe table"] --> RQ["id, length, n_cells (a whole number) are required"]
RQ --> AP{"area_profile"}
AP -- "unknown kind, a key of another kind, a bad table" --> E1["ValueError or CaseParseError"]
AP --> IN["_parse_init: uniform, riemann, regional, mode"]
IN -- "unknown key, empty regions, a missing key" --> E2["CaseParseError naming the block"]
IN --> CL["_parse_friction, _parse_heat, _parse_porous: parse_kind_block"]
CL -- "unknown kind, a field of another kind, wrong type" --> E3["ValueError"]
CL --> CAT["_parse_catalyst: kind, then cpsi monolith resolver"]
CAT -- "cpsi beside explicit geometry, bad substrate" --> E4["ValueError"]
CAT --> SP["_parse_species, _parse_eos, _parse_filter, _parse_urea, _parse_thermal_wall"]
SP -- "unknown reaction, cubic EOS without Tc and Pc" --> E5["ValueError"]
SP --> PS["PipeSpec"]
Every ValueError in that chart reaches the user as a CaseParseError that
names the file and the entry (pipe[2]): _parse_section_list re-raises it
through _anchored, with the original text first.
Pre-flight, build, run¶
The CLI calls load_case, applies its overrides, then validate_case_pre_run
before anything is built (_load_and_preflight); run_case calls
validate_case_pre_run itself, so the check holds for the library and the GUI
subprocess too. build_network validates again by default (a case edited in
code is checked at the seam), then builds; see Build a network from a
case for what it refuses.
flowchart TD
CA["Case"] --> PR{"validate_case_pre_run"}
PR -- "NaN t_end, non-finite cfl or tolerance" --> X1["ValueError"]
PR -- "t_end <= 0 with no resume and no step cap" --> X2["ValueError: nothing to advance"]
PR -- "explicit rk with cfl > 1, bad ramp start" --> X3["ValueError: use an implicit rk"]
PR -- "bad chem knob, bs_brinkmeier channel, species contract" --> X4["ValueError naming the pipe"]
PR --> BN["build_network (own chart)"]
BN --> RN["run_case: controllers chained into on_step"]
RN --> DA["dispatch_advance"]
run_case is validate_case_pre_run, a Clock when a BC is time-dependent,
build_network, controllers chained ahead of the caller's on_step, and
dispatch_advance. The CLI's --resume, --trace-dt and controller paths
build the network first (_advance_eager) so they can seed or trace it, then
call the same dispatch_advance with the same arguments; the seeding is drawn
in Resume from a checkpoint.
dispatch_advance: which driver¶
flowchart TD
D["case, built network, far BCs, run arguments"] --> SV{"solver in explicit, strang, quasi_steady?"}
SV -- no --> X1["ValueError: solver is not one of"]
SV --> KN{"_DispatchKnobs.build: limiter name, dt knobs, ramp, pass-through dicts"}
KN -- "unknown limiter" --> X2["ValueError"]
KN --> NT["chem-parallel notice, solver banner"]
NT --> NP{"_needs_network_path: junction, species, catalyst, real gas, coolant, wall pairs, quasi_steady"}
NP -- no --> SG["_advance_single_pipe_case"]
SG -- "not one pipe, no BC on an end" --> X3["ValueError"]
SG -- "implicit rk with a CFL ramp" --> X4["NotImplementedError"]
SG --> OUT["final time"]
NP -- yes --> RS["NetworkRunSpec: the arguments every network driver takes"]
RS --> Q{"solver"}
Q -- quasi_steady --> QS["_advance_quasi_steady"]
Q -- strang --> ST["_advance_strang"]
Q -- "explicit, implicit rk" --> IM["_advance_implicit_network"]
Q -- "explicit, explicit rk" --> EX["_advance_explicit_network"]
QS -- "a surface mechanism catalyst" --> X5["ValueError"]
EX -- "a surface reaction or mechanism catalyst" --> X6["ValueError"]
QS --> OUT
ST --> OUT
IM --> OUT
EX --> OUT
A single pipe with nothing but gas takes the shortcut; anything the shortcut
cannot honour forces the network path, even with no junction. The solver
decides before the scheme: solver = "strang" with an implicit rk still runs
Strang, and quasi_steady ignores rk and cfl for stepping (the rk still
selects the CFL-ceiling branch in the pre-flight). The drivers themselves are
drawn in Explicit network step, Implicit network
step and Quasi-steady drivers;
the single-pipe loop is in Pipe time step.
Write¶
Nothing is written until the run returns. The CLI's _finish_run then prints
the summary and _write_run_outputs writes what --out and --cgns ask for.
flowchart TD
R["run returned: pipes, junctions, t_final, tracer, controllers"] --> O{"--out given?"}
O -- no --> C
O -- yes --> H["write_hdf5: truncates the file, then writes pipes, junctions, time_series, controllers"]
H --> M["build_manifest and write_manifest: out.manifest.json"]
M --> C{"--cgns given?"}
C -- yes --> G["write_cgns"]
C -- no --> END["done"]
G --> END
The HDF5 file holds the final state of every pipe (pipes/<id>), every
junction (junctions/junction_<ii>, with its kind and id), the controllers'
checkpoint state, and, when a tracer ran, the time_series group; the root
carries case_name, aeolus_version, schema_version, and t_final, written
last. A run stopped early by a live-control request or a step cap writes the
same file with t_final below t_end, which is what a later --resume
continues from. The manifest is a JSON sidecar: aeolus1d version, a hash of the
case file, the dependency set and the host. A failure part-way through
write_hdf5 leaves a partial file (it opens the target for writing before it
can fail); see its contract.
Where each refusal happens¶
| Stage | Function | Raises | What the message names |
|---|---|---|---|
| read | load_case |
CaseParseError |
the path; for a directory or non-UTF-8 bytes, the reason |
| TOML syntax | load_case |
CaseParseError |
the path and the parser's line and column |
| version | apply_case_upgrades |
CaseVersionError, re-raised as CaseParseError |
the version found and the highest this build reads |
| references | resolve_component_refs |
ComponentRefError, re-raised as CaseParseError |
the ref, every path searched, the chain on a cycle |
[case], [gas] |
_construct_section |
CaseParseError |
file, section, key, its line when unambiguous, a suggestion |
[case] completeness |
_check_case_header_strict |
CaseParseError |
every missing key (only with the opt-in strict flag) |
| each array of tables | _parse_section_list |
CaseParseError |
file, pipe[2], the missing key or the original message |
| CSV schedules | _expand_bc_csv_schedules |
CaseParseError |
file, bc[i], the csv path and what is in its directory |
| crankshaft | _parse_crankshaft_section |
CaseParseError |
file, [crankshaft], the offending list |
| whole case | validate_case |
CaseValidationError |
every error with its path and source line |
| pre-flight | validate_case_pre_run |
ValueError |
the setting, its value, the way out |
| build | build_network |
ValueError, CaseValidationError |
the pipe, junction, controller or turbo (own chart) |
| dispatch | dispatch_advance |
ValueError, NotImplementedError |
the solver, limiter, pipe or catalyst concerned |
Unknown keys and defaults¶
An unknown key is refused in two different places. In [case] and [gas] it is
ALWAYS refused at parse (_construct_section), whatever validate says. In
every other table the parsers read the keys they know with d.get and ignore
the rest, so an unknown key there (a typo such as lenght) is refused only by
validate_case; with validate=False it is silently dropped, and the field
keeps its default. A key that is a field of the spec but belongs to another kind
of a tagged block (alpha on a porous block with no kind) is refused at parse
by the codec.
Every default lives on its schema dataclass. The parsers restate many of them as
d.get(key, literal); a test per parser builds the spec from the smallest table
and compares it field by field with the dataclass, so a literal that drifts is
found. The exceptions are deliberate and stated in the contracts: the cylinder's
ignition is resolved at parse from the geometry and the combustion kind (a
compression-ignition cylinder gets knock retard 0.0, a spark-ignition one the
dataclass 2.0 and 10.0, and a key the user wrote always wins), and a [[junction]]
without init gets a placeholder pressure of 0 Pa that validate_case refuses.
The GUI's help popup (schema_help) quotes some defaults in prose; one test
compares every number it quotes with the dataclass.
Not checked at load¶
Three things a bad case can carry past load_case and validate_case_pre_run
and fail on later, listed so that nobody reads a green load as a check of them.
(A fourth, the adaptive tolerances, is closed: since 2026-10-07 adaptive_atol
and adaptive_rtol are validated at load while adaptive_error_control is on,
and the adaptive-Picard band while adaptive_picard is on; with the switch off
they are not read.) Values of nested numeric fields are taken as written:
[pipe.species] A = "abc" loads (the validator does not type-check nested
specs), and fails only for a reaction kind whose kernel reads it. An unknown
key is ignored with validate=False, as above. And t_end = inf is not
refused: with a step cap or a live stop it is a way to run until stopped.
Functions¶
src/aeolus1d/io/parsers.py::load_casesrc/aeolus1d/io/parsers.py::_construct_sectionsrc/aeolus1d/io/parsers.py::_check_primitive_typessrc/aeolus1d/io/parsers.py::_check_case_header_strictsrc/aeolus1d/io/parsers.py::_warn_spec_defaultssrc/aeolus1d/io/parsers.py::_parse_section_listsrc/aeolus1d/io/parsers.py::_anchoredsrc/aeolus1d/io/parsers.py::_parse_pipesrc/aeolus1d/io/parsers.py::_parse_initsrc/aeolus1d/io/parsers.py::_parse_frictionsrc/aeolus1d/io/parsers.py::_parse_heatsrc/aeolus1d/io/parsers.py::_parse_poroussrc/aeolus1d/io/parsers.py::_parse_catalystsrc/aeolus1d/io/parsers.py::_parse_speciessrc/aeolus1d/io/parsers.py::_parse_eossrc/aeolus1d/io/parsers.py::_parse_filtersrc/aeolus1d/io/parsers.py::_parse_ureasrc/aeolus1d/io/parsers.py::_parse_thermal_wallsrc/aeolus1d/io/parsers.py::_parse_junctionsrc/aeolus1d/io/parsers.py::_parse_bcsrc/aeolus1d/io/parsers.py::_expand_bc_csv_schedulessrc/aeolus1d/io/parsers.py::_parse_cylindersrc/aeolus1d/io/parsers.py::_parse_crankshaft_sectionsrc/aeolus1d/io/_kind_codec.py::parse_kind_blocksrc/aeolus1d/io/_kind_codec.py::parse_flat_specsrc/aeolus1d/io/case_upgrades.py::apply_case_upgradessrc/aeolus1d/io/component_library.py::resolve_component_refssrc/aeolus1d/io/source_pos.py::TomlSourceMap.from_textsrc/aeolus1d/io/validate.py::validate_casesrc/aeolus1d/io/case.py::validate_case_pre_runsrc/aeolus1d/io/case.py::run_casesrc/aeolus1d/io/case.py::dispatch_advancesrc/aeolus1d/io/case.py::_DispatchKnobs.buildsrc/aeolus1d/io/case.py::_needs_network_pathsrc/aeolus1d/io/case.py::_advance_single_pipe_casesrc/aeolus1d/io/case.py::_advance_quasi_steadysrc/aeolus1d/io/case.py::_advance_strangsrc/aeolus1d/io/case.py::_advance_implicit_networksrc/aeolus1d/io/case.py::_advance_explicit_networksrc/aeolus1d/io/build.py::build_networksrc/aeolus1d/cli/commands/run.py::_load_and_preflightsrc/aeolus1d/cli/commands/run.py::_advance_plainsrc/aeolus1d/cli/commands/run.py::_advance_eagersrc/aeolus1d/cli/commands/run.py::_finish_runsrc/aeolus1d/cli/commands/run.py::_write_run_outputssrc/aeolus1d/io/writer.py::write_hdf5src/aeolus1d/io/manifest.py::build_manifestsrc/aeolus1d/io/manifest.py::write_manifestsrc/aeolus1d/io/cgns_writer.py::write_cgnssrc/aeolus1d/io/resume.py::read_resume_state