Resuming a calculation and checking compatibility#

Continue a saved calculation only when the saved state and the requested problem are compatible. Solprop checks a resume identity and completion state before proceeding. A deliberately changed physical model should instead start a new segment from a saved field.

The resume key#

configuration_hash() is a SHA-256 of the canonical JSON of the schema version, the grid, the modes, the configuration, the noise, the stepper settings, the material manifest’s identities and policies, the stepping method when the Adams integrator (SEP 13, the Adams integrator) stepped the run instead of the tableau, named with its formulation, and the region the error norm scores. A run whose field plasma integrates by the band rule (SEP 18) records the rule in its configuration as plasma_integration. The record holds the policy, K, \(\theta_b\), the taps at full precision, the certified error, and the boundary and exposure policies. A resume that would integrate the plasma differently is therefore refused. The file’s root attributes repeat it as plasma_integration, plasma_integration_half_width, plasma_integration_theta_b, plasma_integration_taps and plasma_integration_certified_error. A field-plasma run also records its held-current source equation under configuration.plasma_source, whose certificate limit and taper counts are part of the identity; the measured certificate is execution evidence under backend.settings.plasma_source, outside the hash. A resolved modal run records its transverse rule in quadrature and in the key – the family (radial_rule), the requested and realised counts, the nodes and the physical weights (SEP 19) – so a resume on another rule is refused. A run on the plain trapezoid records none of these, and a response’s integration is left out of the key when it is "trapezoid". The key also includes the absolute launch position, the scientific TDSE descriptor, the interval-cap and start-up rules, interpolation of save planes (interpolate_planes) and saving of accepted-step endpoints (step_planes) (SEP 14). It is not Python’s hash, which is salted per interpreter, so the same run digests to the same string in every process — a test runs it in a subprocess with a different PYTHONHASHSEED and compares.

For a TDSE response the identity includes the resolved atomic window, the source gate and its breakpoints, so a run never resumes under another gate or window (the edge-free gated source).

Four things are deliberately outside it: the wall-clock start time, the package versions, the git provenance and the backend record. A NumPy upgrade, an unrelated commit or a change of backend must not refuse to resume a run that is otherwise the same run.

The schema version, on the other hand, is inside it, so only a file written at the current output schema (22) can be resumed; an older file reads normally but is not continued (the schema version).

The material manifest is inside it too, deliberately conservatively: a resume is refused when any optmat model’s identity, any material policy or the optmat version differs, even where the numbers the run uses would not have moved. The refusal names the entries that changed, materials.records.Ar:gas_refractive:fit/Ar.digest or materials.optmat_version, and says that the material manifest differs. For an editable optmat installation the version string is fixed when it is installed, so it is the digests, not the version, that tell two optmat trees apart.

check_cache() is the gate:

from solprop.output import check_cache, resume

if check_cache("run.h5", metadata):        # False when there is no file
    state = resume("run.h5")               # metadata, last plane, step history
    start_from(state.plane)                # already back in the FFT convention

A file written for a different run raises OutputError listing the fields that changed — configuration.gamma, grid.n_omega, modes[0].kind — rather than saying only that a hash disagrees. Because the key covers the whole configuration, a run can never continue from the state of a run with different physics that happens to have the same shape.

To carry on in the same file, build the handler with append=True: it opens the file read-write instead of replacing it, checks that the record offered describes the run the file holds, keeps the file’s own metadata and creation time, and numbers the next plane and the next attempt where the file left off.

Completion status and terminal checkpoints#

Both standard handlers implement the optional CheckpointOutput capability. Their outcome property is a RunOutcome: running, complete, budget_exhausted or failed, together with the actual position, a stop message, cumulative logical and atomic call ledgers, and the latest execution facts. The previous_executions records retain earlier runs of the same resumed file. These records are outside the scientific configuration hash. A budget stop is unfinished; it never becomes a successful result by changing the requested end position.

An ordinary exception or a call-budget stop also writes a TerminalCheckpoint at the last available finite field. Normally this is an accepted step endpoint. A failure while processing a dense observation can instead retain that interpolated field. It is separate from the regular observations: n_saved, handler["z"], handler["field"] and the per-plane statistics keep their original meanings. Checkpoint creation uses available observations and histories and makes no new material-statistics request. An interpolated checkpoint is labelled as such: resume(path) reads it, while restarting requires the explicit Simulation.run(..., resume="interpolated") policy. Ordinary resume=True refuses it. TDSE uses landed save planes.

from solprop.output import open_file, resume

with open_file("run.h5") as stored:
    status = stored.outcome.status if stored.outcome is not None else "unrecorded"
    checkpoint = stored.terminal_checkpoint
    regular_planes = stored.n_saved

state = resume("run.h5")
restart_z = state.plane.z

Resume prefers the terminal checkpoint and computes the next scheduled plane from its absolute position. Modal amplitudes are divided by \(\sqrt{\operatorname{Re}n_{\rm eff}(\omega,z)}\) at that same position. The Adams march continues from the snapshot kept beside the checkpoint (below), and starts afresh at order one where there is none; its logical call ledger and the atomic attempt ledger remain cumulative either way, so restarting cannot replenish a budget. A checkpoint exactly on a scheduled plane whose statistics were interrupted is saved there before stepping on. Pending step history is attached once to the next regular save, or to the next terminal checkpoint if execution stops again.

Scientific changes still require a new run. Device choice, batch width and execution memory limits may change on resume and are recorded in outcome.execution; a new atomic call limit cannot be smaller than the calls already attempted. The driver validates the checkpoint and ledgers before preparing atomic resources. Resuming an already completed file returns it without preparing another backend or atomic runtime. A hard process kill cannot run exception recovery, so the file retains its last flushed state and running status rather than a manufactured completion record.

A run that keeps no field at its planes (fields="none" or "planes", output) continues only from a plane that carries a field (SEP 22). The terminal checkpoint always carries the field, whatever the run keeps at its planes: it costs one observation when the run stops, and a run stopped by an error, a signal or a call budget resumes from it. A file whose last regular plane kept no field and which holds no checkpoint — what a run killed outright leaves — is refused by name:

the run's last saved plane, at z = 1.000000e-04 m, kept no field (it was saved with
fields='none' or 'planes') and the file holds no terminal checkpoint, which always
carries the field, so there is no state to continue from: a run stopped by an error or
a signal writes one, a run killed outright does not. Rerun it, or keep the field on its
last plane

A finished field-free file resumes as finished, running nothing. A run continuing a file keeps its fields by the file’s rule, and is refused if it asks for another.

Custom handlers implementing only Output remain usable for ordinary propagation, keeping every field; a field-free run needs a handler that can hold field-free planes (retain), as both standard handlers do. TDSE requires the checkpoint capability and refuses an incompatible handler before native preparation.

handler = HDF5Output("run.h5", append=True)
handler.open(metadata)                     # refused, by field, if it is another run
handler.n_saved                            # the planes already there

To read a finished file without writing to it, open_file() opens it read-only through the same protocol: the accessors, stat_names, n_saved and metadata answer from the file, open and save are refused, and a with block closes it. That is the way solprop.processing reads a run after the driver has closed its handler.

from solprop.output import open_file

with open_file("run.h5") as stored:
    energy = stored["stats/energy"]

What a general modal run’s key holds#

A general modal guide is propagated in a basis a file must describe before a reader can say what its mode axis means, so that identity is inside the key. Two runs on guides that differ in a core radius, a declared chi3, a node rule, a table extent or the order of their channels have different digests, and a resume is refused by the field that moved — modal.guide.provider, modal.guide.regions, modal.tables, modal.channels — not by a bare hash mismatch.

A guide that declares a change of channel basis — circular partners of a degenerate pair, see general modal propagation — is in the key four times over: modal.metric.kind moves from identity to dense, modal.metric.gauge_id and modal.tables.gauge_id both gain the composed +circular(i,j), modal.channels carries handedness instead of parity, and modal.guide.sampling.basis carries the declaration itself, matrix and all. A file written in one basis therefore cannot resume in another, and the refusal names the field. The sampling’s entry is present only where a basis is declared, so a guide in its provider’s own coordinates keeps the key it would have without the feature; the two gauges are always compared, so the other direction is refused too.

The key also holds what the basis costs. modal.metric.occupied_departure is how far the interpolated basis is from orthonormal over the bins the run occupies — measured at the run’s own coarse bins, between the table’s knots, and gated against the same budget as the knot defect — and modal.metric.active_departure is the same quantity over every bin the active band admits, weighted by nothing, which is warned on rather than gated (see general modal propagation).

modal.accessors is in the record and deliberately not in the key. It is the list of statistic names the basis admits, which is a statement about what the package version that wrote the file could compute — not about what the run computed, which is what every other entry of modal says. Were it in the key, every accessor solprop gains would invalidate every general modal file already written, for a capability those files gain rather than lose. check_cache() compares the two lists as a subset instead: a file whose list lacks a name this version admits resumes, and a file naming an accessor this version does not admit is refused by that name.

Because the record holds the metric’s identity and not its arrays, solprop.processing.modal.physical_energy() refuses a file whose metric is not the identity by name: the number it could form from the stored state alone is the unweighted one, which is the answer for a different basis. The run’s own recorded physical_energy statistic carries the weighted value, and solprop.processing.modal.reconstructed_flux() reconstructs the physical flux from the guide’s reference and needs no metric at all.

Reading E and H back#

A general modal file stores channel amplitudes, not fields. solprop.processing.modal is the way back: given the run’s own reference tables, rebuilt from the guide the record names, it synthesises E(x, y, t) in V/m and H(x, y, t) in A/m at the guide’s quadrature nodes and forms what a detector would see.

from solprop.processing.modal import (
    channel_labels,
    electric_field,
    plane_nodes,
    reconstructed_flux,
)
from solprop.translate.general import general_modal_route

with open_file("run.h5") as stored:
    print([one.display_label for one in channel_labels(stored)])
    tables = general_modal_route(guide, grid, z_max=length, omega_reference=w0).tables
    field = electric_field(stored, tables, plane=-1, guide=guide)  # (n_nodes, 3, n_t)
    power = reconstructed_flux(stored, tables, plane=-1, guide=guide)  # (n_t,), W
    xy, weights = plane_nodes(stored, tables, plane=-1, guide=guide)  # m, m^2

plane_nodes() says where those values are: the nodes’ positions (x, y) in the cross-section, in the order of the node axis every field accessor returns, and the area weights the fluxes are integrated with, so that weights @ reconstructed_flux(..., density=True) is reconstructed_flux(...). On a guide whose geometry moves along z the plane is reconstructed on its own modes: the accessors solve the guide again at the plane’s stored position, on the rule that has moved there, so the nodes the fields are returned at, and the ones plane_nodes returns, are that position’s, and the flux is integrated with that position’s area weights (a guide whose geometry moves).

The tables are not stored in the file: they are tens of megabytes per run and are rebuilt from the guide in tens of milliseconds for a one-channel guide of 288 nodes (under a second if the guide itself has still to be solved, which is most of that time), and the record carries their identity instead. Every accessor checks the tables it is given against that identity, path by path, and refuses the first entry that disagrees, so a reconstruction is never made from the wrong guide.

The guide is required. A table identity records the run’s own extents and the node count and says nothing about the frequencies the provider was solved at or what each region declared, so two guides sampled at different interior knots, or declaring different chi3, build tables whose identity agrees entry for entry. guide= is checked against metadata.modal["guide"] — the sampling with its knots, and the regions — and the refusal names the path: modal.guide.sampling.omega.4, or modal.guide.regions.0.kerr.chi3. The keyword has no default on any of the six accessors that reconstruct from the tables (electric_field, magnetic_field, reconstructed_flux, instantaneous_flux, coordinate_intensity and plane_nodes): every caller has the guide, because that is what the tables were rebuilt from, and a check that closes the hole only when it is asked for is not a gate. A call that omits it raises TypeError. A record whose identity block is a subset of the rebuilt one is refused as incomplete rather than passing on the entries it happens to carry: what was not compared was not checked.

A file written in a declared channel basis needs the guide rebuilt with that same declaration, ModalGuide.from_provider(..., basis=circular_pair(...)): the sampling’s basis entry is part of the identity, and a guide in the provider’s own coordinates is refused at modal.guide.sampling.basis.

electric_field() and magnetic_field() take convention= like every other processing accessor and with the same "optics" default; the real field is convention-free, and analytic=True gives the complex signal in the convention asked for.

Beside the two fluxes, instantaneous_flux() keeps the sum-frequency term of the real fields that the cycle-averaged reconstructed_flux() drops, and coordinate_intensity() gives the flux density each channel would carry alone. That last one is not additive: the sum over channels is the physical density only where the channels do not interfere, and nothing is clipped to hide the difference. channel_amplitudes() is the inverse map, through the coefficient dual; reprojecting a field that was not synthesised from this basis creates a new initial-value problem and is not a resume path.

An ordinary modal or spatial run is refused every one of these names: it carries neither the certified metric nor the E/H reference they promise. Which names a basis admits is solprop.output.capability’s table, and it is checked at four doors: here, when the run’s statistics are admitted, in each backend’s stats, and in energy() and peak_power() (statistics).

Restoring a live-run snapshot#

Periodic snapshots preserve a running accepted endpoint while the original process continues. Use read_snapshot to inspect a complete generation and restore_snapshot(generation, new_directory) to verify and copy it into a new writable directory. Restoration checks every file’s size and SHA-256, configuration and provider identities, checkpoint position, retained history, archive UUID/session and recorded counters. Missing manifests, incomplete staging, mismatched pairs and altered files are refused before working files are published.

The returned SnapshotFiles.output and optional SnapshotFiles.tdse_archive are the arguments for the existing resume=True workflow. Reconstruct the same simulation; the normal configuration and provider compatibility rules still apply. The original archive identity, saved data, Adams history and cumulative accounting continue from the checkpoint. A fresh process adds an ordinary resume session to the restored archive. Taking a snapshot in the original process adds no session. Cumulative atomic call limits remain cumulative: restoring a snapshot does not grant a new budget.

Snapshots never claim that propagation finished. Even one taken at the requested end is a running endpoint; the normal resume workflow establishes completion. Independent resumed computations meet the repository’s accuracy-envelope policy; stored copies, history arrays and structural identifiers remain exact.