Skip to content

Resume from a checkpoint

Every result file aeolus1d run --out result.h5 writes is also a checkpoint. aeolus1d run case.toml --resume result.h5 reads it back, seeds a freshly built network from it, sets the clock to the file's t_final, and continues to the case's t_end. This chart follows that path: what the file holds, which function reads what, what is restored, where a file or a case is refused, and what is not compared (that part matters most). The contracts of every function below are in their docstrings (the standard is Function contracts and flow charts).

What resume does not do. It does not re-read the case from the file: the case TOML is loaded and built exactly as for a fresh run, and the checkpoint only overwrites state in the built network. So the case and the checkpoint are two independent inputs, and the code compares them only by array shape and by id (table below).

What the file holds

write_hdf5 writes the final state under pipes/<id> and junctions/junction_NN, the loop state under controllers/<id>, and, when the run was traced, time_series/. t_final is written last, so an interrupted flush leaves the clock behind the state, never ahead of it; the time series commits the same way, with t created last. A fresh write opens the file "w", which truncates it first and is not staged in a .partial file: a failure part-way leaves a truncated result, and when --out is the file being resumed from, the only checkpoint is gone with it. The reader therefore never sees a half-advanced clock, but it can see a file that is not a checkpoint at all (no pipes, no t_final).

flowchart TD
    R["run: state at t_final"] --> W{"write_hdf5"}
    W --> P["pipes/id: U (N,3), Y_*, T_solid, surface/*, filter/*, wall/*, coolant/*; also rho u p T for plots"]
    W --> J["junctions/junction_NN: p T m E_internal, m_fuel, x_burnt, burn_progress, m_species, actuators, omega, housing_T, kind, id, n_ports"]
    W --> C["controllers/id: kind and checkpoint_state()"]
    W --> TS["time_series/ when traced: t and per-pipe, per-junction series"]
    P --> F["root attrs: case_name, schema_version, then t_final LAST"]
    J --> F
    C --> F
    TS --> F

Reading the file

read_resume_state is the only reader. It opens the file, finite-checks and size-caps every array it will seed from, and returns a ResumeState. Every refusal is a ResumeError that names the file; the CLI catches that one type, logs resume failed: ... and exits 1, before the network is built.

flowchart TD
    A["--resume prior.h5"] --> O{"h5py can open it?"}
    O -- "missing, truncated, not HDF5" --> X1["ResumeError: cannot be opened as an HDF5 file"]
    O --> G{"has a pipes group and a finite t_final?"}
    G -- "no pipes group" --> X2["ResumeError"]
    G -- "t_final missing, NaN or Inf" --> X3["ResumeError"]
    G --> V{"_read_schema_version"}
    V -- "newer than this install, or below the minimum" --> X4["ResumeError"]
    V -- "attribute missing" --> N1["read as v1, a note"]
    V -- "older, with a registered step" --> N2["a note per upgrade step"]
    V --> PP{"_read_pipe_checkpoint for each pipe"}
    PP -- "no U dataset" --> X5["ResumeError"]
    PP -- "a seeding array is NaN or Inf, or above 50 million elements" --> X6["ResumeError naming the dataset"]
    PP --> JJ{"_read_junction_checkpoint for each junction group"}
    JJ -- "p, T, m or E_internal missing, or not finite" --> X7["ResumeError naming the group"]
    JJ --> CC{"_read_controller_checkpoints"}
    CC -- "a value is not finite" --> X8["ResumeError naming the controller"]
    CC --> TT{"time_series with a t axis?"}
    TT -- "group without t: an interrupted first flush" --> N3["skipped, a note: the trace starts at the resume time"]
    TT -- "no group" --> N4["empty series: the trace starts at the resume time"]
    TT -- yes --> RS["_read_prior_series: size-capped, not finite-checked"]
    RS --> OUT["ResumeState"]
    N3 --> OUT
    N4 --> OUT

Only the seeding state is finite-checked. The time series is plot data: a sample that went non-finite must not block a resume, so it is read as it is. The saved rho, u, p and T are never read back for seeding: U is the state and the rest is derived, so a file from before 2026-09-28 (no T) resumes exactly like a new one. In a trace, a prior run without T gives NaN for those samples after the merge.

Seeding the built network

_build_eager_network builds the network from the case (initial conditions applied), then calls the three seed functions in order: pipes, junctions, controllers (after the junctions, because a turbine actuator is restored by both, to the same value), and finally sets clock.t to t_final for a case that has a clock (cylinders and transient boundary conditions need one: the crank angle is omega t, so a resumed engine continues at the right angle). A mismatch never raises here: it becomes a note, a UserWarning, or nothing.

flowchart TD
    S["ResumeState + a network built from the case"] --> PS{"seed_pipes_from_prior, per live pipe"}
    PS -- "id not in the file" --> K1["kept initial state, a note"]
    PS -- "cell count changed" --> K2["whole pipe kept, a note"]
    PS --> U["U copied bit for bit; ghost cells untouched"]
    U --> SP{"species by name and shape"}
    SP -- "missing or other shape" --> K3["that species kept, a note"]
    U --> OPT{"T_solid, surface, DPF, wall, coolant: shape matches?"}
    OPT -- "no, or absent" --> K4["kept, NO note except wall and coolant when seeded"]
    S --> JS{"seed_junctions_from_prior, per live junction"}
    JS --> M{"matched by id, else by list position"}
    M -- "no checkpoint" --> K5["kept initial state, a note"]
    M --> KD{"kind and n_ports equal the live component's?"}
    KD -- "no" --> W1["UserWarning and a note; initial state kept"]
    KD --> JV["set_state(p, T), then m and E_internal exactly, fuel, burnt fraction, species inventory REPLACED, actuators, shaft omega, housing T"]
    S --> CS{"seed_controllers_from_prior, per live controller"}
    CS -- "file has no controllers group" --> K6["loop re-primes from the case's initial actuator value, a note"]
    CS -- "id not in the file" --> K7["starts fresh, a note"]
    CS -- "checkpoint written by another class" --> W2["UserWarning and a note; initial state kept"]
    CS --> CR["restore_state: restored and not-restored keys listed in a note"]
    U --> CK["clock.t = t_final"]
    JV --> CK
    CR --> CK
    CK --> RUN["dispatch_advance from t_start = t_final to t_end"]

What must match, and what happens when it does not

Between checkpoint and case Checked? On a mismatch
file is an aeolus1d result yes ResumeError, exit 1
schema version yes newer or too old: ResumeError; older: notes
t_final is finite yes ResumeError
case_name no (read, only informational) resumes silently into a different case
t_final below t_end no t_final >= t_end takes no step and rewrites the file at the same state
pipe ids by id missing from file: kept, note; extra in file: ignored, no note
pipe cell count yes pipe kept at its initial state, note
pipe length, area, gamma, R_gas, EOS, species list, closures no silent: U is conservative, so a changed gamma changes the resumed p and T (a case at gamma 1.3 resumed from a 1.4 file gives p = 75 kPa for a saved 100 kPa)
species by name and shape yes that species kept, note
T_solid, surface, DPF state shapes yes kept, no note
wall and coolant shapes yes kept; a note only when seeded
junction identity (kind, n_ports, id) yes, when the file has them warning and note, initial state kept; old files skip the check and match by position
junction count and order by id, else position missing: kept, note
controller id and class yes missing: fresh, note; other class: warning, note
--out equals --resume no allowed; the manifest records the digest of the file that was READ as resume_sha256 (taken in _read_resume, before anything is written; until 2026-10-05 it was taken after the write and so named the NEW file); a failed write loses the checkpoint, because write_hdf5 truncates its target first

What is not restored

The combustion sub-state of the predictive diesel model (the per-cycle Wiebe rebuild, the knock integral, the adaptive timing retard), the Helmholtz per-port neck velocity (relaxes within milliseconds), and the turbo surge-damping memory. A file with no controllers group (written before 2026-09-29) resumes with each loop re-primed.

Functions

  • src/aeolus1d/cli/commands/run.py::_cmd_run
  • src/aeolus1d/cli/commands/run.py::_read_resume
  • src/aeolus1d/cli/commands/run.py::_build_eager_network
  • src/aeolus1d/cli/commands/run.py::_build_tracer
  • src/aeolus1d/cli/commands/run.py::_advance_eager
  • src/aeolus1d/io/writer.py::write_hdf5
  • src/aeolus1d/io/writer.py::_emit_pipe_group
  • src/aeolus1d/io/writer.py::_emit_junction_group
  • src/aeolus1d/io/writer.py::_emit_controllers_group
  • src/aeolus1d/io/writer.py::_component_kind_and_id
  • src/aeolus1d/io/snapshot.py::pipe_state_dict
  • src/aeolus1d/io/snapshot.py::junction_state_dict
  • src/aeolus1d/io/resume.py::read_resume_state
  • src/aeolus1d/io/resume.py::_read_schema_version
  • src/aeolus1d/io/resume.py::_read_pipe_checkpoint
  • src/aeolus1d/io/resume.py::_read_junction_checkpoint
  • src/aeolus1d/io/resume.py::_read_controller_checkpoints
  • src/aeolus1d/io/resume.py::_read_prior_series
  • src/aeolus1d/io/resume.py::_checked_read
  • src/aeolus1d/io/resume.py::seed_pipes_from_prior
  • src/aeolus1d/io/resume.py::seed_junctions_from_prior
  • src/aeolus1d/io/resume.py::seed_controllers_from_prior
  • src/aeolus1d/io/tracer.py::Tracer.preload_from_prior
  • src/aeolus1d/io/tracer.py::Tracer._pad_to_live_keys