solprop.backends.driver#

The step loop every backend runs, with the kernels left abstract.

March, replay, landing, controller bookkeeping, history, the noise model and the seam’s set_initial/propagate_to/observe/close do not depend on how a stage is evaluated: they touch the state and StepDriver._trial_step and nothing else. Written once here, they are the same step control on every backend, so a change to docs/dev/stepper.md section 3 is made in one place and a disagreement between two backends can only be the kernels.

Every backend runs this loop, the reference included: the oracle is the passes and the per-element arithmetic, not the march, and a march written twice has to be kept identical by hand. What a backend supplies is _trial_step, _apply_mask and stats, plus _finish when it has boundary treatments of its own; two optional hooks, _begin_plane and _on_accepted_step, are where a backend scores whatever a plane or an accepted step leaves behind. The march owns the periodic statistics schedule. Its default storage hooks call stats and retain host rows; a device backend can override the three storage hooks to retain device rows until observation.

Module Attributes

LANDING_ULPS

How many ulps of slack a march allows itself when deciding it has arrived.

INITIAL_STEP_STATS_CAPACITY

Rows a plane's statistics buffer holds before it first has to grow.

Functions

reached(position, target)

Return whether a march at position has arrived at target.

refuse_general_leaves(problem, backend, *[, ...])

Refuse, by name, each schema-12 general-modal leaf a backend cannot execute.

replay_tolerance(schedule, position)

Return how far a replayed schedule's edge may sit from a target it lands on, m.

Classes

StepDriver(problem, *[, grid, modes, profiles])

The interaction-picture step loop, with the stage evaluation left to a subclass.

solprop.backends.driver.INITIAL_STEP_STATS_CAPACITY = 64#

Rows a plane’s statistics buffer holds before it first has to grow.

solprop.backends.driver.LANDING_ULPS = 8#

How many ulps of slack a march allows itself when deciding it has arrived.

The same guard solprop.stepper.schedule.landing() uses to decide that a position has already reached a target, so the march stops exactly where landing would refuse to take another step rather than one round-off-sized sliver short of it.

class solprop.backends.driver.StepDriver(problem, *, grid=None, modes=None, profiles=None)[source]#

Bases: ABC, Generic

The interaction-picture step loop, with the stage evaluation left to a subclass.

A subclass supplies _trial_step and _apply_mask and gets the whole of solprop.backends.protocol.Backend’s marching behaviour: adaptive stepping with the controller of docs/dev/stepper.md section 3, replay of a contract’s fixed schedule, landing exactly on save planes and hard kinks, the step history, and both noise models, input and modified.

The controller’s state is its own proposal, never the clipped or stretched step that was executed, and what the history and a rejection see is the norm the proposal would have scored while what decides acceptance is the error the executed step committed. Both are section 3’s, and both matter: the first costs a run hundreds of steps if it is got wrong, the second throws away short landing steps that were accurate.

Parameters:
problemsolprop.translate.problem.KernelProblem

The validated problem to step. The subclass refuses what it cannot execute before calling this constructor.

gridsolprop.grid.TimeGrid, optional

The grid the state lives on, which the statistics need and the contract does not carry.

modescollections.abc.Sequence, optional

The propagated modes, in state order, for the statistics that need them.

profilescollections.abc.Mapping, optional

The z-profiles keyed by statistic name.

Attributes:
namestr

The registry name, set by the subclass and used in every log line.

max_batchint

How many problems the backend steps at once.

propagator_default{‘tabulate’, ‘recompute’}

What this backend resolves the contract’s "auto" propagator mode to. The default suits a backend that evaluates two complex exponentials per stage; one that takes both signs from a single exp and sincos overrides it.

helperssolprop._helpers.Helpers

The implementation of the per-stage and per-step scalar evaluations – the stage tables’ Clenshaw rows and the Adams basis – which the shared driver, march and engines pass down rather than choosing one (SEP 29 section 6.6). Each backend sets its own: PLAIN for the NumPy reference, NUMBA (the default) for numba_cpu, the C++ bindings for the native backend.

available_device_memory()[source]#

Return None for host execution; device backends report free bytes.

Returns:
int or None

None for the shared host implementation.

held_observation()[source]#

Return the state the low-storage march held at its last field-free plane.

SEP 22, Q14: a low-storage step accepts in place, so a step abandoned in flight leaves a state that is neither of its ends. When the last plane kept no field, this is what the driver checkpoints instead: the state as it was observed there, with an empty step history (the plane’s own was saved with it) and its noise phase.

Returns:
Observation or None

The held plane, or None when nothing is held: another march, a plane that kept its field since, or a fresh load.

Raises:
BackendError

If the backend is closed.

helpers: Helpers = Helpers(name='numba')#
march_export_refusal()[source]#

Return why this backend’s Adams march cannot carry its history, or None.

Returns:
str or None

The reason (solprop.backends.multistep.AdamsMarch.export_refusal()), or None for a march that exports and installs its history and for the Runge-Kutta and low-storage marches, which keep none.

march_snapshot()[source]#

Return the Adams march’s state at the accepted position, for a resume.

Returns:
solprop.output.schema.MarchSnapshot or None

The snapshot, or None for the Runge-Kutta and low-storage marches and wherever the Adams march has nothing a resume could continue (solprop.backends.multistep.AdamsMarch.snapshot()).

Raises:
BackendError

If a low-storage step was abandoned in flight.

max_batch: int = 1#
metadata()[source]#

Return what this execution was, for the run’s record.

A saved run has to say what produced it: the thread configuration alone was measured to move one step by a factor of nine, so a result whose execution is not recorded is not reproducible. The base record is the name and the resolved propagator mode and, for Adams runs, cumulative logical source calls; the low-storage march’s evaluations are physical, source_evaluations, never response_calls (SEP 24 section 5); a backend with pools, plans or wisdom adds them.

Returns:
dict

JSON-serialisable scalars, keyed by name.

name: str = 'driver'#
observe(*, field=True)[source]#

Return the state at the current position, as host arrays.

The field is in the lab frame and the FFT convention (spectral_convention = "fft"): the output layer conjugates it on the way to disk and the processing accessors on the way to a user. The step history covers everything since the previous call.

With field=False (SEP 22 section 5.1) neither the field nor the noise phase crosses to the host – a device backend copies nothing – and the observation carries None for both. Nothing scans the field for finiteness then: the step loop accepts only finite error norms and a replay raises on a non-finite one. The spatial collar diagnostic, which reads the field, is not evaluated; the backend counts such planes in metadata() and logs the first. Under the low-storage march the state is held, one volume, so that a step abandoned in flight can still be checkpointed at this plane (held_observation()).

Parameters:
fieldbool, optional

Whether the field and the noise phase cross to the host. Default True.

Returns:
Observation

The field, the noise phase and the step history.

Raises:
BackendError

If the backend is closed or no initial state was loaded.

propagate_to(z)[source]#

Step the state to a position, landing on it exactly.

Adaptive by default and a replay when the contract carries a schedule. Either way the embedded estimate is computed and recorded every step, so a schedule too coarse for the tolerance it is scaled by is reported rather than silent (docs/dev/stepper.md section 8). The low-storage march has no estimate: it replays its schedule and records 0.0, “not estimated” (SEP 24 section 6.3).

Parameters:
zfloat

The save plane to reach, m, at or ahead of the current position and inside the operator table.

Returns:
StepSummary

What the call cost and achieved.

Raises:
BackendError

If the backend is closed, no initial state was loaded, a low-storage step was abandoned in flight, or z is behind the current position or outside the table.

ConvergenceError

If the controller asks for a step below the contract’s dz_min, or a replayed step’s error norm, or the low-storage march’s state, is not finite.

propagator_default: Literal['auto', 'tabulate', 'recompute'] = 'tabulate'#
restore_call_ledgers(logical, atomic=None)[source]#

Restore shared Adams call counts without restoring numerical history.

Non-TDSE backends inherit logical budget persistence while refusing atomic counts. The caller first loads the saved field, which resets Adams history; the next source evaluation charges startup against these cumulative counts.

Parameters:
logicalmapping

Persisted logical response counts; empty on ordinary RK runs.

atomicmapping or None, optional

Persisted atomic counts; absent on non-TDSE runs.

Raises:
BackendError

If this backend is handed counters it does not implement.

restore_march(snapshot)[source]#

Continue the Adams march from a snapshot at the loaded position.

Called after set_initial() at the snapshot’s position. The state is replaced by the snapshot’s own, exactly as the interrupted run held it.

Parameters:
snapshotsolprop.output.schema.MarchSnapshot

The snapshot a file holds.

Returns:
str or None

None when the history was continued, otherwise why the march starts afresh, as a resume did before snapshots were kept.

set_initial(field, noise, *, z=0.0)[source]#

Load a state and the noise realisation at a position.

The state is masked to the active band once here, as the scaffold does: the operator and the prefactor are exactly zero outside it, so out-of-band content would neither propagate nor decay and would sit in the error norm for the whole run. The traditional input noise model adds its realisation to the state here and never again; the modified model keeps it apart and adds it at every stage, so the propagating field never carries it.

A positive z is a resume: the state came back from a save plane the run already reached, so it already carries whatever the input model added at the entrance and must not be given a second realisation, and a replayed schedule continues from the edge that plane sits on. The controller starts afresh from dz_init either way (docs/dev/stepper.md section 3).

Everything else a march reads is restored too, so that a backend loaded again after a run – a SimulationRunner’s, or after a failed run – repeats a fresh backend’s run (SEP 22 section 4.7): a dense observation’s look-ahead, an Adams march’s call ledger and last-rejection flag and a low-storage march’s source-evaluation count here, and the quadrature alarm, whose value the entrance plane’s quadrature_error reads before any march, in the overrides of the backends that keep one. A resume restores its ledger afterwards (restore_call_ledgers()).

Loading is the one entry that clears a low-storage step abandoned in flight (SEP 24 section 6.2): the state it holds is replaced, and the march’s flow table is rebuilt from the next step.

Parameters:
fieldnumpy.ndarray

The state in the canonical layout, complex, shape (n_batch, n_modes, n_pol, n_omega), in the square root of watts and the FFT convention.

noisesolprop.noise.NoiseRealisation or None

The realisation to carry along the run, or None to take the one the problem was assembled with.

zfloat, optional

The position the state belongs to, m. Zero, the entrance, by default.

Raises:
BackendError

If the backend is closed, the field does not match the contract’s shape, z lies outside the operator table, or a schedule is being replayed and z is not one of its edges.

set_progress(reporter)[source]#

Attach the reporter every attempted step is reported to.

The next call to propagate_to() announces the run to it. Attaching a reporter changes nothing the propagation computes – the calls are observers, made after the step is decided – so a run with one is bitwise the run without.

Parameters:
reportersolprop.log.ProgressReporter

Where the step events go; solprop.log.NullReporter turns them off.

abstractmethod stats(spec)[source]#

Compute the requested statistics in one fused pass.

Declared here, not only on solprop.backends.protocol.Backend, because the shared step loop calls it through the default row-storage hook. A device backend shares its statistic evaluator between this method and its device row-storage hook.

Parameters:
specsolprop.stats.StatsSpec

Which statistics and which windows.

Returns:
dict

Small host arrays keyed by statistic name, each with a leading batch axis.

solprop.backends.driver.reached(position, target)[source]#

Return whether a march at position has arrived at target.

Parameters:
positionfloat

Current position, m.

targetfloat

Position being marched to, m.

Returns:
bool

True when no further step is due.

Examples

>>> from solprop.backends.driver import reached
>>> reached(0.5, 1.0), reached(1.0, 1.0)
(False, True)
solprop.backends.driver.refuse_general_leaves(problem, backend, *, admits=())[source]#

Refuse, by name, each schema-12 general-modal leaf a backend cannot execute.

The four optional leaves of the general modal route arrived at kernel schema 12 with no backend behind them. A schema check alone cannot stand in for this: a separately built backend compares against solprop’s imported constant, so a freshly built wheel would accept a problem carrying a representation it does not implement and silently run the old physical model. Each leaf is therefore named here, one refusal each, and each check is deleted only when that backend executes that exact representation.

Every backend in this repository names all four leaves and admits through the shared list in solprop.backends.numpy_ref.admission, so this refusal guards a separately built backend. A backend that executes a leaf names it in admits and checks the exact representation it supports itself: admitting a leaf is a statement about one combination of contents, not about the field being present.

Parameters:
problemsolprop.translate.problem.KernelProblem

The problem to check.

backendstr

The backend’s registry name, used in the refusal.

admitssequence of str, optional

Dotted leaf names this backend executes, for example ("transform.modal_tables",). Empty by default: a backend refuses every one until it has a kernel for it.

Raises:
BackendError

Naming the backend and the leaf it cannot execute.

solprop.backends.driver.replay_tolerance(schedule, position)[source]#

Return how far a replayed schedule’s edge may sit from a target it lands on, m.

A schedule’s edges are the running sum of its steps, and a sum of n rounded steps can drift from the position they were chosen to reach by about n ulps: FixedSchedule(np.full(160, L / 160)) ends 12 ulps short of L. The slack is therefore LANDING_ULPS or the step count, whichever is larger, in ulps of the larger of the position and the schedule’s length – still far inside the 1e-9 of the length that validate() allows an edge to miss a save plane by, so no step is ever skipped.

Parameters:
schedulesolprop.stepper.schedule.FixedSchedule

The schedule being replayed.

positionfloat

The position the edge is compared with, m.

Returns:
float

The slack, m.

Examples

>>> import numpy as np
>>> from solprop.backends.driver import replay_tolerance
>>> from solprop.stepper.schedule import FixedSchedule
>>> schedule = FixedSchedule(np.full(160, 1e-3 / 160))
>>> bool(1e-3 - schedule.z_end <= replay_tolerance(schedule, 1e-3))
True