Reusable workflow recipes#

This page collects the repeated-geometry and explicit-axis workflows added by the consolidation milestone. The modules remain independent: the common theme is making expensive state and spatial intent explicit, not introducing one package-wide plan abstraction.

One coefficient field, several affine geometries#

Changing samples require a new interpolation prefilter. When the same sampled frame is evaluated through several affine geometries, compute that coefficient field once and share it deliberately:

from splineops.affine import AffinePlan

first = AffinePlan(frame.shape, first_matrix, first_offset, mode="mirror")
second = AffinePlan(frame.shape, second_matrix, second_offset, mode="mirror")

coefficients = first.prepare_coefficients(frame)
first_result = first.apply_coefficients(coefficients)
second_result = second.apply_coefficients(coefficients)

The tagged field rejects a plan with a different construction shape, spline degree, boundary mode, or precision. prefilter remains the raw-array, out=-capable alternative when the caller manages that provenance. Use coefficients.save("field.npz") and compatible_plan.load_coefficients("field.npz") when the field crosses a process boundary. Saving uses atomic replacement; loading validates JSON metadata and portable numeric values before rebuilding the tag. Schema-1 archives remain readable.

The plans must have the same input shape, degree, mode, and dtype. Ordinary plan(frame) remains the correct end-to-end call for one geometry. The coefficient API does not claim that changing frames can skip their mathematically required prefilter.

Batch and channel axes#

Spatial axes are selected explicitly and every remaining slice is independent:

from splineops.smoothing_splines import SmoothingSplinePlan
from splineops.differentials import DifferentialPlan

# NCHW arrays: height and width are the last two axes.
smoother = SmoothingSplinePlan(batch.shape[-2:], lamb=0.02, gamma=1.5)
smooth = smoother(batch, axes=(-2, -1))

derivatives = DifferentialPlan(batch.shape[-2:], spacing=(0.7, 0.7))
maps = derivatives(batch, spatial_axes=(-2, -1))
laplacian = derivatives(
    batch,
    gradient=False,
    hessian=False,
    laplacian=True,
    spatial_axes=(-2, -1),
).laplacian

maps.gradient follows the order supplied in spatial_axes and packed Hessian entries follow (00, 01, 11) in 2-D. A Laplacian-only request skips gradient and mixed-Hessian construction. Smoothing executes a batched real FFT; differentials use one batched per-call workspace. Structured differential destinations are validated together before execution; they may be writable strided arrays but must not overlap the source or another component. The complete affine-to-feature pipeline is recorded in Stability-soak workflows.

Multiscale batches#

Pyramids and wavelets use the same explicit convention:

from splineops.multiscale.pyramid import get_pyramid_filter, reduce_2d
from splineops.multiscale.wavelets.haar import HaarWavelets

g, _, centered = get_pyramid_filter("Spline", 3)
coarse = reduce_2d(batch, g, centered, spatial_axes=(-2, -1))

wavelet = HaarWavelets(scales=2)
coefficients = wavelet.analysis(batch, spatial_axes=(-2, -1))
reconstructed = wavelet.synthesis(coefficients, spatial_axes=(-2, -1))

Only the selected dimensions are reduced or partitioned. Wavelet divisibility requirements apply to those dimensions, not to batch or channel lengths.

Controlled denoising paths#

DenoisingPlan.solve_path carries ADMM state only within one explicit call:

from splineops.adaptive_regression_splines import DenoisingPlan

plan = DenoisingPlan(x, rho=0.5)
solutions, diagnostics = plan.solve_path(
    y,
    [0.01, 0.02, 0.05, 0.1],
    return_diagnostics=True,
)

The lambda order is preserved. Nearby values can benefit from warm starts, but a speedup is workload-dependent. Independent solve and solve_path calls do not share mutable iteration state.

Plan inspection#

Plans expose the fixed contract and known retained storage through properties such as configuration and retained_bytes. Denoising reports retained_array_bytes because SciPy’s factorization owns additional opaque storage. TensorSplineGeometryPlan.is_compatible and incompatibility_reason make geometry checks available without attempting an evaluation.

Benchmark regression checks#

The manual development workflow stores complete JSON/CSV artifacts and applies the broad policy in benchmarks/consolidation-thresholds.json. Its checks use within-run ratios and numerical errors so they remain meaningful across machines. They are regression alarms, not release performance promises.