Skip to content

Bounded forward parameter sensitivities

solve_sensitivities integrates the smooth deterministic augmented equations S' = (df/dy) @ S + df/dtheta, with S = dy/dtheta. It reuses the existing fixed Euler, Heun, RK4 or DOPRI5 stage equations. This is a numerical approximation to continuous sensitivities; mesh refinement remains necessary.

The engine accepts the structural DerivativeArtifact protocol and imports no compiler or statistical package. The optional op_system.symbolic_derivatives artifact supplies ordered state, parameter and scalar-measurement matrices from one retained source IR. Its own bounded supported-operation contract applies. The caller must bind that artifact to the same RHS and coordinate definitions.

import numpy as np
from op_system import compile_spec, symbolic_derivatives
from op_engine import (
    SensitivityInitial,
    SensitivityOptions,
    SensitivitySystem,
    solve_sensitivities,
)
from op_engine.core_solver import RunConfig

compiled = compile_spec({
    "kind": "expr",
    "state": ["x", "z"],
    "equations": {"x": "-rate*x", "z": "-rate*z"},
    "observables": {"measurement": {"expr": "gain*(x+2*z)"}},
})
system = SensitivitySystem(
    compiled.eval_fn,
    symbolic_derivatives(compiled),
    initial_parameters=("amplitude",),
)
assert system.parameter_names == ("rate", "gain", "amplitude")
solution = solve_sensitivities(
    system,
    {"rate": 0.7, "gain": 1.5, "amplitude": 2.0},
    SensitivityInitial(
        state=np.array([2.0, 4.0]),
        parameter_derivatives=np.array([[0., 0., 1.], [0., 0., 2.]]),
    ),
    np.array([0., 0.5, 1.]),
    options=SensitivityOptions(
        solver=RunConfig(method="rk4", fixed_max_step=0.0625),
    ),
)
assert solution.times == (1.0,)
assert solution.parameter_derivatives.shape == (1, 2, 3)
assert np.allclose(
    solution.observable_derivatives,
    np.exp(-0.7) * np.array([[[-15., 10., 7.5]]]),
    rtol=2e-6,
)

Initial-state derivatives are required. Supply an explicit zero matrix for a parameter-independent initial state. Parameters used only by initialization are appended in initial_parameters order, receive zero RHS/direct-measurement columns, and enter through the supplied initial derivatives. Measurement-only parameters retain their compiler order and direct measurement terms.

For N states, P complete parameters, M scalar measurements and T retained times, the returned native arrays have shapes (T,N), (T,N,P) and (T,M,P). The measured sensitivity is dg/dy @ S + dg/dtheta. A state Jacobian alone does not supply it. Observation noise, prior curvature, experimental design,+nuisance treatment and Fisher definitions belong to inference consumers.

Memory and execution scope

The augmented state has N*(1+P) entries. Final-only output is the default; store_history=True retains requested output times, not internal stages. Default bounds are 16,384 augmented entries and 64 MiB of returned numerical arrays. They do not bound temporary work, compiler memory, retained AD graphs or whole-process RSS. The derivative producer may additionally allocate bounded dense matrices at every stage.

State, initial derivatives, native scalar parameters and produced matrices must retain namespace and dtype. NumPy, JAX and CPU Torch float32/64 are qualified. For JAX, close over a static system/mesh/options and return the needed native array fields from the transformed function; the full provenance dataclass is host metadata, not a registered JAX pytree. fixed_loop="scan" and optional checkpointing reuse the existing engine adapter. Independent solves can be batched with native vmap. CUDA requires its own device qualification.

The stored mesh records the solver's actual time dtype. Tolerance settings do not define accuracy for these fixed methods; declare the mesh and maximum step, then check refinement. Adaptive acceptance, forcing jumps, events, history dependence, stochastic models, operator/implicit solves and automatic gauge transformations require separately qualified sensitivity methods. Unsupported solver configurations fail explicitly.

Independent checks and cost comparison

Required native backend checks cover independent stability polynomials for all four methods, shared/local states, initial-only parameters, dtype retention, same-mesh finite differences, refinement, JAX JIT/vmap/checkpointing and native AD. A separate mandatory compiler lane checks a coupled two-rate transfer with transitive aliases and a direct measurement-gain term against a closed form.

With a compiler source that includes the derivative API, run each mode in a fresh process:

python examples/forward_sensitivities.py --backend jax --mode augmented --steps 64
python examples/forward_sensitivities.py --backend jax --mode ad --steps 64

Both modes return the full (3,4) derivative of two final states and one measurement with respect to two rates, gain and initial amplitude, on exactly the same fixed RK4 mesh and float64 precision. The augmented system has ten entries. The comparison uses native forward AD for JAX, native reverse AD for Torch, and explicitly labelled central finite differences for NumPy. First-call and synchronized warm times are separate; no universal cost or stability advantage is assumed.

The raw local receipt retains all 12 fresh-process backend/mesh/mode cases, including Jacobians, source hashes, continuous-reference errors and whole-worker RSS. All six paired derivative matrices agree within rtol=atol=1e-8. On this shared one-core CPU run, the 64-step warm augmented/AD times were 31/37 microseconds for JAX and 50/112 ms for Torch; first calls were approximately 0.69/0.64 s and 0.08/0.11 s respectively. Other local test work was active, each case has only one worker, and methods differentiate this particular small output. These advisory measurements do not select a general sensitivity method or establish a portable speedup. The continuous-reference tolerance scales with fourth-order mesh error; a shared fixed-mesh derivative and a continuous derivative have different error targets.