solprop.backends.protocol#

The seam: what a backend is, and what comes back from one.

Everything that touches the propagated state during a run is reached through this one protocol, and only the backends know how the state is laid out, transformed and stepped (docs/dev/backends.md section 1). The protocol is drawn at the level of operations on the state rather than at the array-namespace level, because an xp-style abstraction would force every backend to keep the array compiler’s structure — out-of-place transforms, separate scaling passes, materialised stages, one reduction per statistic — and that structure is precisely what a fast backend exists to change.

A backend is constructed from a validated solprop.translate.problem.KernelProblem, takes host arrays and returns host arrays; its buffers, layout and plans are private, and the state is never a parameter of Backend.propagate_to() or Backend.observe(). Selection by name and the availability rules are in solprop.backends.registry.

Sign convention. Every complex array crossing this seam, in either direction, is in the FFT convention of docs/dev/conventions.md section 2, with the phase origin at the first time sample: spectral_convention = "fft". The output layer conjugates on the way to disk and the processing accessors on the way to a user. Backends never conjugate.

Classes

Backend(*args, **kwargs)

What every execution backend provides, and nothing more.

BatchBackend(*args, **kwargs)

Advance compatible loaded members and return their separate observations.

Observation(z, field, noise_phase, ...[, ...])

The state at a save plane, on the host.

StepHistory(z, dz, error_norm, accepted, ...)

What the stepper did between two observations.

StepStats(z, step[, values])

The statistics a backend evaluated between two observations.

StepSummary(z, n_steps, n_rejected, ...)

What one call to Backend.propagate_to() cost and achieved.

class solprop.backends.protocol.Backend(*args, **kwargs)[source]#

Bases: Protocol

What every execution backend provides, and nothing more.

docs/dev/backends.md section 1. A backend is constructed from a validated solprop.translate.problem.KernelProblem; the numbers are the contract, and a backend is correct when it reproduces the numpy_ref oracle to the stated bound.

Attributes:
namestr

The registry name, for example "numpy_ref".

max_batchint

Largest batch axis this backend steps at once; 1 for a backend that steps one problem.

Examples

>>> from solprop.backends.protocol import Backend
>>> [name for name in sorted(dir(Backend)) if not name.startswith("_")][:4]
['available_device_memory', 'close', 'memory_ledger', 'metadata']
>>> [name for name in sorted(dir(Backend)) if not name.startswith("_")][4:7]
['observe', 'propagate_to', 'restore_call_ledgers']
>>> [name for name in sorted(dir(Backend)) if not name.startswith("_")][7:]
['set_initial', 'set_progress', 'stats']
available_device_memory()[source]#

Return currently free device bytes, or None for a host backend.

Returns:
int or None

Free device memory excluding live allocations and allocator reserves. Host memory is admitted independently by the controller.

close()[source]#

Release the buffers and plans. Idempotent.

max_batch: int#
memory_ledger()[source]#

Return the size of every buffer the backend holds.

Returns:
dict

Buffer name to bytes, including transform work areas. Every buffer is named even when it is empty.

metadata()[source]#

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

A saved run has to say what produced it, not only what was computed: the thread configuration alone moved one step by a factor of nine (docs/dev/backends.md section 3.4), so a result whose execution is not recorded is not reproducible. The driver writes this into solprop.output.schema.RunMetadata.backend, which is outside the resume key – a run continued on the other backend is the same run.

Returns:
dict

JSON-serialisable scalars keyed by name. backend and propagator_mode are always present; pools, plan flags and wisdom digests are a backend’s own.

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

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

Parameters:
fieldbool, optional

Whether the field and the noise phase cross to the host; False observes the step history alone (SEP 22 section 5.1).

Returns:
Observation

The field, the noise phase and the step history since the last call.

propagate_to(z)[source]#

Step the state to a position through the shared step loop.

Parameters:
zfloat

The save plane to reach, m. Steps land on it exactly.

Returns:
StepSummary

What the call cost and achieved.

restore_call_ledgers(logical, atomic=None)[source]#

Restore cumulative budgets after loading a saved state.

Parameters:
logicalmapping

Persisted logical response counters, or empty for ordinary RK runs.

atomicmapping or None, optional

Persisted provider attempts. Numerical history always restarts fresh.

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

Load a state and the noise realisation at a position, host to device, once.

Parameters:
fieldnumpy.ndarray

The state in the canonical layout, FFT convention.

noisesolprop.noise.NoiseRealisation or None

The realisation to carry along the run, or None.

zfloat, optional

The position the state belongs to, m, inside the operator table. Zero is the entrance; a positive value is a resume from a plane the run reached before, so the backend takes its position – and its place in a replayed schedule – from here. A state past the entrance already carries an input-model noise realisation, and a backend must not add one to it a second time. The step controller starts afresh either way, so an adaptive run resumed agrees with an uninterrupted one to the controller’s tolerance and not bitwise, while a replayed schedule agrees bitwise on a backend that recycles no stage and to round-off on one that does (docs/dev/stepper.md section 3).

set_progress(reporter)[source]#

Attach the reporter the step loop calls as it goes.

The driver attaches one before the first plane and never changes it mid-run. A backend that steps on the host calls step() once per attempted step, including the rejected ones; a backend whose loop is on a device may call it once per plane instead (docs/dev/backends.md section 1). Nothing a reporter does may change the propagation: a run with one is bitwise the run without.

Parameters:
reportersolprop.log.ProgressReporter

Where the step events go. solprop.log.NullReporter is the way to turn them off.

stats(spec)[source]#

Compute the requested statistics in one fused pass.

Parameters:
specsolprop.stats.StatsSpec

Which statistics and which windows.

Returns:
dict

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

class solprop.backends.protocol.BatchBackend(*args, **kwargs)[source]#

Bases: Backend, Protocol

Advance compatible loaded members and return their separate observations.

Construct through solprop.backends.registry.select_batch() after loading each member’s initial state and noise through the ordinary backend seam. Successful construction transfers ownership of those backends to the batch. Their parameter tables may differ, but grids, fixed schedules, save planes and statistics layouts must match. max_batch records the constructed count.

observe_members replaces observe for batch output: each member keeps its own error history and periodic rows. No device array crosses this interface. The common maximum error norm is diagnostic and never changes a fixed schedule.

member_metadata()[source]#

Return resolved execution policies for every batch member.

Returns:
tuple of dict

Complete per-member backend provenance, including the batch strategy.

observe_members()[source]#

Drain independently owned host observations in member order.

Returns:
tuple of Observation

Singleton field shapes and each member’s own diagnostic history.

class solprop.backends.protocol.Observation(z, field, noise_phase, step_history, n_steps, n_rejected, step_stats=<factory>)[source]#

Bases: object

The state at a save plane, on the host.

Section 9 of docs/dev/kernel-contract.md. Host arrays only: device arrays never cross the seam. The field is in the lab frame and the FFT convention (spectral_convention = "fft"), so the output layer conjugates it on the way to disk and the processing accessors on the way to a user.

Attributes:
zfloat

Position of the plane, m.

fieldnumpy.ndarray or None

The state, complex128, shape (n_batch, n_modes, n_pol, n_omega), in the square root of watts; None for an observation that asked for no field (observe(field=False), SEP 22 section 5.1).

noise_phasenumpy.ndarray or None

Im Phi(omega, z) in radians, same shape, when a noise model is active and the field is observed, so that processing can restore the shot-noise background the propagating field never carried. None otherwise.

step_historyStepHistory

What the stepper did since the previous observation.

n_stepsint

Steps taken since the previous observation, accepted and rejected together.

n_rejectedint

Rejected steps among them.

step_statsStepStats

The rows a periodic statistics subset produced between the previous observation and this one, empty unless the run asked for one. Separate from step_history because the two are sampled differently: the history is every attempt, these are every period-th accepted step.

Raises:
BackendError

If a field is not four-dimensional and finite, a noise phase is given without a field or does not match it, the step counts disagree with the history, or a statistics row points past the history it indexes.

Examples

>>> import numpy as np
>>> from solprop.backends.protocol import Observation, StepHistory
>>> history = StepHistory(np.array([1.0]), np.array([1.0]), np.array([0.3]),
...                       np.array([True]), np.array([0.02]))
>>> plane = Observation(1.0, np.zeros((1, 1, 1, 8), complex), None, history, 1, 0)
>>> plane.z, plane.field.flags.writeable
(1.0, False)
field: Complex[ndarray, 'n_batch n_modes n_pol n_omega'] | None#
n_rejected: int#
n_steps: int#
noise_phase: Float[ndarray, 'n_batch_p n_modes_p n_pol_p n_omega_p'] | None#
require_field()[source]#

Return the observed field, refusing an observation that asked for none.

Returns:
numpy.ndarray

The read-only field, shape (n_batch, n_modes, n_pol, n_omega).

Raises:
BackendError

If the observation carries no field (observe(field=False)).

Examples

>>> import numpy as np
>>> from solprop.backends.protocol import Observation, StepHistory
>>> empty = StepHistory(np.empty(0), np.empty(0), np.empty(0),
...                     np.empty(0, bool), np.empty(0))
>>> Observation(0.0, np.ones((1, 1, 1, 2), complex), None, empty, 0, 0
...             ).require_field().shape
(1, 1, 1, 2)
step_history: StepHistory#
step_stats: StepStats#
z: float#
class solprop.backends.protocol.StepHistory(z, dz, error_norm, accepted, wall_time)[source]#

Bases: object

What the stepper did between two observations.

Recorded whether the run was adaptive or replayed a fixed schedule, because the embedded error estimate costs nothing extra and is computed either way (docs/dev/stepper.md section 8): a replayed schedule too coarse for the tolerance it is scaled by is then reported rather than silent.

Attributes:
znumpy.ndarray

Position at the end of each step, m, float64, shape (n_steps,).

dznumpy.ndarray

Length of each step, m, float64, shape (n_steps,), positive.

error_normnumpy.ndarray

The scaled embedded error norm of each step, float64, shape (n_steps,). For a clipped or stretched step this is the equivalent norm of the controller’s proposal, which is what the controller’s state is (solprop.stepper.controller.PIController.equivalent_norm()).

acceptednumpy.ndarray

Whether each step was accepted, Boolean, shape (n_steps,).

wall_timenumpy.ndarray

Seconds elapsed at the end of each step, measured from the previous observation, float64, shape (n_steps,), non-decreasing.

Raises:
BackendError

If the arrays are not all the same length, a step length is not positive, or an entry is not finite.

Examples

>>> import numpy as np
>>> from solprop.backends.protocol import StepHistory
>>> history = StepHistory(np.array([0.1, 0.2]), np.array([0.1, 0.1]),
...                       np.array([0.4, 0.5]), np.array([True, True]),
...                       np.array([0.01, 0.02]))
>>> history.n_steps, history.n_rejected
(2, 0)
accepted: Bool[ndarray, 'n_accepted']#
dz: Float[ndarray, 'n_dz']#
error_norm: Float[ndarray, 'n_norm']#
property n_rejected: int[source]#

Return the number of rejected steps.

Returns:
int

How many entries of accepted are false.

Examples

>>> import numpy as np
>>> from solprop.backends.protocol import StepHistory
>>> StepHistory(np.array([0.1]), np.array([0.1]), np.array([2.0]),
...             np.array([False]), np.array([0.5])).n_rejected
1
property n_steps: int[source]#

Return the number of steps recorded, accepted and rejected together.

Returns:
int

The length of z.

Examples

>>> import numpy as np
>>> from solprop.backends.protocol import StepHistory
>>> StepHistory(*(np.zeros(0) for _ in range(3)),
...             np.zeros(0, bool), np.zeros(0)).n_steps
0
wall_time: Float[ndarray, 'n_wall']#
z: Float[ndarray, 'n_steps']#
class solprop.backends.protocol.StepStats(z, step, values=<factory>)[source]#

Bases: object

The statistics a backend evaluated between two observations.

The channel of docs/dev/kernel-contract.md section 9. A run whose solprop.stats.StatsSpec names a periodic subset gets one row every period accepted steps, evaluated inside the backend’s own step loop from the state that step just committed, and drained here at each save plane. Evaluating every statistic on every accepted step costs about a fifth of a step; a subset and a period are the two knobs that make the same information affordable.

Empty when the run asked for no periodic subset, which is the default.

Attributes:
znumpy.ndarray

Position of each row, m, float64, shape (n_rows,), increasing.

stepnumpy.ndarray

Index of the accepted step each row was taken after, into the same observation’s StepHistory, int64, shape (n_rows,), increasing. The history counts rejected attempts too, so this is not row * period.

valuescollections.abc.Mapping

The statistics, keyed by catalogue name, each with a leading row axis and then the leading batch axis every statistic crossing this seam carries: (n_rows, n_batch, *shape).

Raises:
BackendError

If the two index arrays disagree in length, are not increasing, or a value’s leading axis is not the row count.

Examples

>>> import numpy as np
>>> from solprop.backends.protocol import StepStats
>>> rows = StepStats(np.array([0.1, 0.2]), np.array([0, 1]),
...                  {"energy": np.ones((2, 1, 1, 1))})
>>> rows.n_rows, rows.names
(2, ('energy',))
>>> StepStats.empty().n_rows
0
classmethod empty()[source]#

Return the channel a run without a periodic subset carries.

Returns:
StepStats

No rows and no statistics.

property n_rows: int[source]#

Return how many rows were taken since the previous observation.

Returns:
int

The row count.

property names: tuple[str, ...][source]#

Return the statistics the channel carries, sorted.

Returns:
tuple of str

The catalogue names.

step: Int[ndarray, 'n_rows_step']#
values: Mapping[str, ndarray]#
z: Float[ndarray, 'n_rows']#
class solprop.backends.protocol.StepSummary(z, n_steps, n_rejected, max_norm, mean_norm, wall_time, met_tolerance)[source]#

Bases: object

What one call to Backend.propagate_to() cost and achieved.

A handful of host scalars, so that a driver can report progress and warn about a tolerance that was not met without pulling the whole step history across the seam every plane.

Attributes:
zfloat

Position reached, m, which is the plane that was asked for.

n_stepsint

Steps taken, accepted and rejected together.

n_rejectedint

Rejected steps among them.

max_normfloat

Largest scaled error norm over the accepted steps; 0 when none were taken.

mean_normfloat

Mean scaled error norm over the accepted steps.

wall_timefloat

Seconds the call took.

met_tolerancebool

Whether every accepted step scored at or below one. False on a replayed schedule too coarse for its tolerance, which the driver logs (stepper.md section 8).

Raises:
BackendError

If a count is negative, more steps were rejected than taken, or a norm or time is negative or not finite.

Examples

>>> from solprop.backends.protocol import StepSummary
>>> StepSummary(1.0, 42, 0, 0.7, 0.4, 0.31, True).met_tolerance
True
max_norm: float#
mean_norm: float#
met_tolerance: bool#
n_rejected: int#
n_steps: int#
wall_time: float#
z: float#