checkpoint Module

Checkpoints are Fortran unformatted (stream) binary files that capture everything needed to resume a run exactly. The on-disk format is the unified format (currently version 6): the solution is gathered to rank 0 and written as a single file over the GLOBAL point count, so a checkpoint is independent of the rank count it was written under. 1D's record list is UNCHANGED since v5 (Q2d bumped only the shared header's version number): a freshly WRITTEN 1D checkpoint differs from its v5 form only in the stamped header version. That does NOT make existing v5 files readable — read_ckpt_header enforces strict version equality, so v5 files remain rejected. Contents:

  • Unified header: magic (42), version (6), dim (1 for this 1D solver).
  • Scalars: iter, t, extents (= [n_pt_global]), neq, dt.
  • Grid record: grid_type (character(len=32)) and a coordinate fingerprint — (sum(x), sum(x^2)) over the global node array. On restart, the fingerprint is compared against the configured grid; a mismatch (changed grid_type or node positions) is a fatal error.
  • The GLOBAL conserved-variable array ub(neq, n_pt_global).
  • An optional BDF2 previous-step array bdf2_ub_prev(neq, n_pt_global), preceded by a presence flag (integer 1 = present, 0 = absent).
  • (v5, F2) The Kahan t_comp compensation scalar, the halo width h (validated against the current scheme's halo width on read), and the boundary halo/ghost cells left(neq,h)/right(neq,h) — see below.

Why the halo/ghost cells: for "prescribed" BCs (dirichlet, inflow, supersonic_inlet), apply_bcs seeds the boundary ghost cells once at apply_initial_condition and never rewrites them afterwards (no_write in boundary_conditions.f90). But those cells are NOT actually constant across a run: the explicit steppers' whole-array stage arithmetic (see time_integration.f90) spans the halo too, so a mathematically-null update still perturbs the ghost value by ~1 ulp on the very first step, and that perturbed value is a stable fixed point for the rest of the run. Pre-v5, checkpoints never persisted this settled ghost, so a restart reloaded the stale IC-time value instead, feeding a 1-ulp-different ghost into the boundary reconstruction stencil on the first post-restart step — a permanent, silent divergence from an uninterrupted run. v5 closes this gap by gathering/scattering the two edge ranks' halo columns alongside the interior array (see gather_edge_ghosts_to_root/ scatter_edge_ghosts_from_root in solution_gather.F90), and also persists t_comp (previously silently reset to 0 on restart, a much smaller — sub-ulp-in-t — but real source of restart non-reproducibility on adaptive-dt runs). See doc/superpowers/2026-07-08-f2-restart-diagnosis.md for the full root-cause analysis.

MPI contract: - write_checkpoint gathers every rank's interior slab to rank 0 via gather_solution_to_root and writes the single global file only on my_rank() == 0. Write success is agreed across ranks with agree_outcome (checkpoint_io's collective wrapper over par_lor). - read_checkpoint opens the global file READ-ONLY on every rank (concurrent reads do not race), reads the global arrays with iostat= on every read, validates, then copies out this rank's slab using decomp % i_first_global. Restart success is agreed with conclude_read (one collective par_lor) so all ranks proceed or fail together (no restart deadlock). - All ranks must have consistent BDF2 allocation state (the same scheme applied uniformly across ranks); write_checkpoint additionally enforces this defensively via a collective OR (par_lor) before the collective BDF2 gather, so the file flag and the gather stay consistent on all ranks. - The v5 edge-ghost gather/scatter is a fixed collective position reached by every rank unconditionally on both write and read (mirroring the BDF2 gather's positioning), so it cannot desync ranks the way a rank-divergent early return would.

Version compatibility: this is the unified v6 format (magic 42, version 6, dim=1, extents=[n_pt_global]) — 1D's on-disk record list is unchanged from v5 (see the module-level v5 (F2) notes above); Q2d only advanced the shared header's version number, so a freshly WRITTEN 1D checkpoint differs from its v5 form only in that stamped version. This does NOT make existing v5 files readable: read_ckpt_header's version check is strict equality, so v5 — like the legacy v2/v3/v4 formats before it — is rejected cleanly with a clear error (owner-authorized break; no user-generated checkpoints exist). Everything else — the global gather, the BDF2 record, the pointer-file mechanics, and the MPI contract above — is unchanged.

File naming: <base>_N.bin where N is the (width-agnostic) iteration number. A companion text file latest_checkpoint (written by rank 0) records the path of the most recent checkpoint so that restart drivers need not track the iteration themselves.

Typical usage in the driver: @code use checkpoint, only: write_checkpoint, read_checkpoint ! --- writing --- call write_checkpoint(state, cfg%checkpoint_file, t, iter) ! --- reading --- call read_checkpoint(state, cfg%restart_file, t, iter) @endcode



Functions

private pure function grid_fingerprint(x) result(fp)

Coordinate fingerprint of the global node grid: (sum x, sum x^2). Cheap, sensitive to the node distribution, and bit-reproducible because the grid is rebuilt deterministically from the same grid_file.

Arguments

Type IntentOptional Attributes Name
real(kind=wp), intent(in) :: x(:)

Return Value real(kind=wp), (2)


Subroutines

public subroutine write_checkpoint(state, base, t, iter, is_ok, message, written_file, t_comp)

Write a checkpoint file for the current solver state.

Read more…

Arguments

Type IntentOptional Attributes Name
type(solver_state_t), intent(in) :: state

Current solver state (must have ub allocated).

character(len=*), intent(in) :: base

Base name (e.g. 'checkpoint'); file = base_NNNNNN.bin.

real(kind=wp), intent(in) :: t

Current simulation time [s].

integer, intent(in) :: iter

Current iteration number.

logical, intent(out), optional :: is_ok
character(len=*), intent(out), optional :: message
character(len=*), intent(out), optional :: written_file

Path of the file actually written (e.g. <base>_<iter>.bin), set only on success. Lets callers (e.g. the Cortex WRITE_CHECKPOINT reply) report the real on-disk path rather than reconstructing it from base.

real(kind=wp), intent(in), optional :: t_comp

Optional Kahan compensation term for t (v5, F2);

public subroutine read_checkpoint(state, fname, t, iter, is_ok, message, t_comp)

Read a checkpoint file and restore solver state.

Read more…

Arguments

Type IntentOptional Attributes Name
type(solver_state_t), intent(inout) :: state

Solver state with ub already allocated (from setup_solver).

character(len=*), intent(in) :: fname

Path to the checkpoint file (or 'latest_checkpoint').

real(kind=wp), intent(out) :: t

Restored simulation time [s].

integer, intent(out) :: iter

Restored iteration count.

logical, intent(out), optional :: is_ok
character(len=*), intent(out), optional :: message
real(kind=wp), intent(out), optional :: t_comp

Optional: restored Kahan compensation term for t