Skip to content

Measurement Models

The measurement module provides classes for defining ODE constraints and observation models used in probabilistic ODE solvers. The design separates concerns cleanly:

  • ODE classes define the dynamical system constraint
  • Constraint dataclasses define additional constraints (conservation laws, measurements)
  • Black-box models allow arbitrary user-defined measurement functions
  • Transformed models apply nonlinear state transformations with proper Jacobians

Class Hierarchy

ODE Information Classes

The module provides four ODE information classes, organized by ODE order and whether hidden states are present:

Class ODE Order Hidden States Vector Field Signature
ODEInformation 1st No vf(x, *, t) -> dx/dt
ODEInformationWithHidden 1st Yes vf(x, u, *, t) -> dx/dt
SecondOrderODEInformation 2nd No vf(x, v, *, t) -> d^2x/dt^2
SecondOrderODEInformationWithHidden 2nd Yes vf(x, v, u, *, t) -> d^2x/dt^2

Flexible Measurement Classes

Class Description
BlackBoxMeasurement User-defined g(state, t) with autodiff Jacobian
TransformedMeasurement Wraps any model with nonlinear state transformation

This separation ensures:

  • No runtime conditionals - Each class has a fixed code path, optimal for JAX JIT
  • Explicit signatures - The vector field type is clear from the class choice
  • Single responsibility - Each class handles exactly one case

Composable Constraints

Additional constraints are added via frozen dataclasses:

  • Conservation: Time-invariant linear constraints A @ x = p, passed via the ODE class's constraints= argument.
  • Measurement: Time-varying observations A @ x = z[t] at specified times. These are not measure constraints; build an observation model with prepare_observations([Measurement(...)], E0, ts) and pass it as obs_model= to the solver. Note: prepare_observations requires all stacked measurements to share the same observation times; a step where only some measurements fire raises NotImplementedError (per-dimension masking is not yet supported).

Usage Examples

First-Order ODE

from ode_filters.priors import IWP
from ode_filters.measurement import ODEInformation

prior = IWP(q=2, d=1)

def vf(x, *, t):
    return -x  # exponential decay

model = ODEInformation(vf, prior.E0, prior.E1)

Second-Order ODE (e.g., Harmonic Oscillator)

from ode_filters.priors import IWP
from ode_filters.measurement import SecondOrderODEInformation

prior = IWP(q=3, d=1)
omega = 2.0

def vf(x, v, *, t):
    return -(omega**2) * x  # harmonic oscillator

model = SecondOrderODEInformation(vf, prior.E0, prior.E1, prior.E2)

Joint State-Parameter Estimation

For estimating unknown parameters alongside the state, use JointPrior with the hidden state classes:

from ode_filters import gaussian_filter, prepare_observations
from ode_filters.priors import IWP, JointPrior
from ode_filters.measurement import SecondOrderODEInformationWithHidden, Measurement

# Prior for state x and unknown damping parameter u
prior_x = IWP(q=2, d=1)
prior_u = IWP(q=2, d=1)
prior_joint = JointPrior(prior_x, prior_u)

# Damped oscillator with unknown damping
def vf(x, v, u, *, t):
    omega = 1.0
    return -(omega**2) * x - u * v

model = SecondOrderODEInformationWithHidden(
    vf,
    E0=prior_joint.E0_x,        # extracts x
    E1=prior_joint.E1,          # extracts dx/dt
    E2=prior_joint.E2,          # extracts d^2x/dt^2
    E0_hidden=prior_joint.E0_hidden,  # extracts u
)

# Data observations go through obs_model, not the measure constraints.
# Observe position x only; align observation times to the filter grid `ts`.
A_obs = prior_joint.E0_x[:1, :]  # observe x only
meas = Measurement(A=A_obs, z=observations, z_t=obs_times, noise=0.01)
obs_model = prepare_observations([meas], prior_joint.E0_x, ts)

result = gaussian_filter(
    mu_0, P_0_sqr, prior_joint, model, tspan, N, obs_model=obs_model
)

Adding Conservation Laws

from ode_filters.measurement import ODEInformation, Conservation
import jax.numpy as np

# Energy conservation: x1 + x2 = 1
cons = Conservation(A=np.array([[1.0, 1.0]]), p=np.array([1.0]))

model = ODEInformation(vf, E0, E1, constraints=[cons])

Black-Box Measurement Models

For cases where the standard ODE structure doesn't fit, use BlackBoxMeasurement to define an arbitrary differentiable measurement function. The Jacobian is computed automatically via JAX autodiff.

Example:

from ode_filters.measurement import BlackBoxMeasurement
import jax.numpy as np

# Custom nonlinear observation: observe squared position and velocity
def custom_g(state, *, t):
    x, v = state[0], state[1]
    return np.array([x**2, v])

model = BlackBoxMeasurement(
    g_func=custom_g,
    state_dim=6,      # full state dimension (e.g., q=2, d=2 -> D=6)
    obs_dim=2,        # observation dimension
    noise=0.01        # measurement noise variance
)

# Use like any other measurement model
H, c = model.linearize(state, t=0.0)
R = model.get_noise(t=0.0)

Transformed Measurement Models

TransformedMeasurement wraps any existing measurement model with a nonlinear state transformation sigma(state). The Jacobian of the mean is composed correctly via the chain rule: J_total = J_g(sigma(state)) @ J_sigma(state). Note, however, that only the mean/linearization is transformed: the measurement-noise covariance R is delegated to and taken unchanged from the base model (see get_noise), and is not propagated through sigma. R must therefore already be expressed in the base model's post-transform coordinates.

Use cases:

  • Nonlinear coordinate transformations (e.g., polar to Cartesian)
  • Applying constraints like softmax normalization
  • Feature extraction before measurement

Example with autodiff Jacobian:

from ode_filters.measurement import ODEInformation, TransformedMeasurement
from ode_filters.priors import IWP
import jax
import jax.numpy as np

# Base ODE model
def vf(x, *, t):
    return -x

prior = IWP(q=2, d=2)
base_model = ODEInformation(vf, prior.E0, prior.E1)

# Apply softmax to ensure state components sum to 1
def softmax_transform(state):
    x = state[:2]  # extract position components
    x_normalized = jax.nn.softmax(x)
    return state.at[:2].set(x_normalized)

model = TransformedMeasurement(base_model, softmax_transform)

# Jacobian includes chain rule automatically
H, c = model.linearize(state, t=0.0)

Example with explicit Jacobian:

For performance-critical applications, you can provide a custom Jacobian:

def sigma_jacobian(state):
    # Custom Jacobian implementation
    return jax.jacfwd(softmax_transform)(state)

model = TransformedMeasurement(
    base_model,
    softmax_transform,
    use_autodiff_jacobian=False,
    sigma_jacobian=sigma_jacobian
)

API Reference

Measurement model utilities for ODE filtering.

Modules:

Name Description
measurement_models

Classes:

Name Description
BlackBoxMeasurement

Black-box measurement model with autodiff Jacobian computation.

Conservation

Conservation constraint: A @ x = p (always active).

Measurement

Time-varying linear measurement: A @ x = z[t] (active only at specified times).

ODEInformation

First-order ODE measurement model: dx/dt = f(x, t).

ODEInformationWithHidden

First-order ODE with hidden states: dx/dt = f(x, u, t).

ObsModel

Pre-computed observation data for sequential filtering.

SecondOrderODEInformation

Second-order ODE measurement model: d^2x/dt^2 = f(x, v, t).

SecondOrderODEInformationWithHidden

Second-order ODE with hidden states: d^2x/dt^2 = f(x, v, u, t).

TransformedMeasurement

Wrapper that applies a nonlinear state transformation before measurement.

Functions:

Name Description
ODEconservation

Create first-order ODE model with conservation constraint.

SecondOrderODEconservation

Create second-order ODE model with conservation constraint.

prepare_observations

Build an :class:ObsModel from :class:Measurement objects.

Classes

BlackBoxMeasurement

BlackBoxMeasurement(
    g_func: Callable[..., Array],
    state_dim: int,
    obs_dim: int,
    noise: float | ArrayLike = 0.0,
)

Black-box measurement model with autodiff Jacobian computation.

Allows users to define an arbitrary measurement function g(state, *, t) and automatically computes the Jacobian via JAX autodiff.

Parameters:

Name Type Description Default
g_func Callable[..., Array]

Measurement function g(state, *, t) -> observation. Must be a differentiable function compatible with JAX.

required
state_dim int

Dimension of the state vector.

required
obs_dim int

Dimension of the observation vector.

required
noise float | ArrayLike

Measurement noise (scalar, 1D diagonal, or 2D covariance matrix). Default is 0.0 (no noise).

0.0
Example

def custom_g(state, , t): ... # Nonlinear observation: squared position + velocity ... return jnp.array([state[0]*2, state[1]]) measure = BlackBoxMeasurement(custom_g, state_dim=4, obs_dim=2)

Methods:

Name Description
g

Evaluate the measurement function.

get_noise

Return the square root of the measurement noise covariance.

jacobian_g

Compute Jacobian of the measurement function via autodiff.

linearize

Linearize the measurement model around the given state.

Attributes:

Name Type Description
R Array

Measurement noise covariance matrix.

Attributes
R property writable
R: Array

Measurement noise covariance matrix.

Functions
g
g(state: Array, *, t: ArrayLike) -> Array

Evaluate the measurement function.

Parameters:

Name Type Description Default
state Array

State vector of length state_dim.

required
t ArrayLike

Current time.

required

Returns:

Type Description
Array

Observation vector of length obs_dim.

get_noise
get_noise(*, t: ArrayLike) -> Array

Return the square root of the measurement noise covariance.

Returns an upper-triangular matrix L such that L.T @ L ≈ R.

Parameters:

Name Type Description Default
t ArrayLike

Current time (unused, included for API consistency).

required

Returns:

Type Description
Array

Upper-triangular square root of the noise covariance matrix.

jacobian_g
jacobian_g(state: Array, *, t: ArrayLike) -> Array

Compute Jacobian of the measurement function via autodiff.

Parameters:

Name Type Description Default
state Array

State vector of length state_dim.

required
t ArrayLike

Current time.

required

Returns:

Type Description
Array

Jacobian matrix of shape (obs_dim, state_dim).

linearize
linearize(
    state: Array, *, t: ArrayLike
) -> tuple[Array, Array]

Linearize the measurement model around the given state.

Parameters:

Name Type Description Default
state Array

State vector to linearize around.

required
t ArrayLike

Current time.

required

Returns:

Type Description
Array

Tuple of (H_t, c_t) where:

Array
  • H_t is the Jacobian matrix (shape [obs_dim, state_dim])
tuple[Array, Array]
  • c_t is the constant term (observation offset)

Conservation dataclass

Conservation(A: Array, p: Array, full_state: bool = False)

Conservation constraint: A @ x = p (always active).

Parameters:

Name Type Description Default
A Array

Constraint matrix (shape [k, d] or [k, state_dim] if full_state=True).

required
p Array

Target values (shape [k]).

required
full_state bool

If True, A operates on the full state X instead of x = E0 @ X.

False

Methods:

Name Description
jacobian

Return Jacobian (constant): A.

residual

Compute residual: A @ x - p.

Attributes:

Name Type Description
dim int

Dimension of constraint output.

Attributes
dim property
dim: int

Dimension of constraint output.

Functions
jacobian
jacobian() -> Array

Return Jacobian (constant): A.

residual
residual(x: Array) -> Array

Compute residual: A @ x - p.

Measurement dataclass

Measurement(
    A: Array,
    z: Array,
    z_t: Array,
    noise: float | Array = DEFAULT_MEASUREMENT_NOISE,
    full_state: bool = False,
)

Time-varying linear measurement: A @ x = z[t] (active only at specified times).

This constraint is only active when the filter's current time matches one of the measurement times in z_t. Time matching uses tolerance-based comparison (not exact equality) to handle floating-point discrepancies.

Important

When creating time grids, use the same linspace implementation (preferably jax.numpy.linspace) for both measurement times (z_t) and filter time steps. NumPy and JAX linspace can produce slightly different values (~1e-8 differences) which may cause measurements to be missed with exact comparison.

Parameters:

Name Type Description Default
A Array

Measurement matrix (shape [k, d] or [k, state_dim] if full_state=True).

required
z Array

Measurement values (shape [n, k]).

required
z_t Array

Measurement times (shape [n]). Should use jax.numpy.linspace for consistency with the filter's internal time grid.

required
noise float | Array

Measurement noise variance (scalar or [k] or [k, k]).

DEFAULT_MEASUREMENT_NOISE
full_state bool

If True, A operates on the full state X instead of x = E0 @ X.

False

Methods:

Name Description
find_index

Find measurement index for time t, or None if not found.

get_noise_matrix

Get noise covariance matrix.

jacobian

Return Jacobian (constant): A, or None if no measurement at t.

residual

Compute residual: A @ x - z[t], or None if no measurement at t.

Attributes:

Name Type Description
dim int

Dimension of measurement output.

Attributes
dim property
dim: int

Dimension of measurement output.

Functions
find_index
find_index(
    t: float,
    rtol: float = MEASUREMENT_TIME_RTOL,
    atol: float = MEASUREMENT_TIME_ATOL,
) -> int | None

Find measurement index for time t, or None if not found.

Uses binary search with tolerance for robust floating-point comparison. Times are assumed to be sorted.

Note: Default tolerances are set to handle discrepancies between NumPy and JAX linspace implementations, which can differ by ~1e-8 for typical time grids.

get_noise_matrix
get_noise_matrix() -> Array

Get noise covariance matrix.

jacobian
jacobian(t: float) -> Array | None

Return Jacobian (constant): A, or None if no measurement at t.

residual
residual(x: Array, t: float) -> Array | None

Compute residual: A @ x - z[t], or None if no measurement at t.

ODEInformation

ODEInformation(
    vf: Callable[..., Array],
    E0: ArrayLike,
    E1: ArrayLike,
    constraints: list[Conservation] | None = None,
)

Bases: BaseODEInformation

First-order ODE measurement model: dx/dt = f(x, t).

Parameters:

Name Type Description Default
vf Callable[..., Array]

Vector field function vf(x, *, t) -> dx/dt.

required
E0 ArrayLike

State extraction matrix (shape [d, D]).

required
E1 ArrayLike

First derivative extraction matrix (shape [d, D]).

required
constraints list[Conservation] | None

Optional list of Conservation constraints. (Data observations are not measure constraints -- pass them as an obs_model built with prepare_observations; a Measurement here raises TypeError.)

None

ODEInformationWithHidden

ODEInformationWithHidden(
    vf: Callable[[Array, Array], Array],
    E0: ArrayLike,
    E1: ArrayLike,
    E0_hidden: ArrayLike,
    constraints: list[Conservation] | None = None,
)

Bases: BaseODEInformation

First-order ODE with hidden states: dx/dt = f(x, u, t).

For joint state-parameter estimation where u is a hidden parameter that appears in the dynamics but evolves according to its own prior.

Parameters:

Name Type Description Default
vf Callable[[Array, Array], Array]

Vector field function vf(x, u, *, t) -> dx/dt.

required
E0 ArrayLike

State extraction matrix for x (shape [d_x, D]).

required
E1 ArrayLike

First derivative extraction matrix (shape [d_x, D]).

required
E0_hidden ArrayLike

Hidden state extraction matrix for u (shape [d_u, D]).

required
constraints list[Conservation] | None

Optional list of Conservation constraints. (Data observations are not measure constraints -- pass them as an obs_model built with prepare_observations; a Measurement here raises TypeError.)

None

ObsModel

Bases: NamedTuple

Pre-computed observation data for sequential filtering.

Describes linear, time-invariant observations where the observation matrix H and noise covariance are constant across time and only the measurement values vary per step. Built by :func:prepare_observations from a list of :class:Measurement objects and a time grid.

Attributes:

Name Type Description
H Array

Observation Jacobian (constant), shape [obs_dim, state_dim].

R_sqr Array

Square-root noise covariance for active observations (constant), shape [obs_dim, obs_dim].

c_seq Array

Observation offsets per step, shape [N, obs_dim]. Equal to -z[idx] when the observation is active, zero otherwise.

mask Array

Boolean mask for active dimensions, shape [N, obs_dim].

SecondOrderODEInformation

SecondOrderODEInformation(
    vf: Callable[[Array, Array], Array],
    E0: ArrayLike,
    E1: ArrayLike,
    E2: ArrayLike,
    constraints: list[Conservation] | None = None,
)

Bases: BaseODEInformation

Second-order ODE measurement model: d^2x/dt^2 = f(x, v, t).

Parameters:

Name Type Description Default
vf Callable[[Array, Array], Array]

Vector field function vf(x, v, *, t) -> d^2x/dt^2.

required
E0 ArrayLike

State extraction matrix (shape [d, D]).

required
E1 ArrayLike

First derivative extraction matrix (shape [d, D]).

required
E2 ArrayLike

Second derivative extraction matrix (shape [d, D]).

required
constraints list[Conservation] | None

Optional list of Conservation constraints. (Data observations are not measure constraints -- pass them as an obs_model built with prepare_observations; a Measurement here raises TypeError.)

None

SecondOrderODEInformationWithHidden

SecondOrderODEInformationWithHidden(
    vf: Callable[[Array, Array, Array], Array],
    E0: ArrayLike,
    E1: ArrayLike,
    E2: ArrayLike,
    E0_hidden: ArrayLike,
    constraints: list[Conservation] | None = None,
)

Bases: BaseODEInformation

Second-order ODE with hidden states: d^2x/dt^2 = f(x, v, u, t).

For joint state-parameter estimation where u is a hidden parameter that appears in the dynamics but evolves according to its own prior.

Parameters:

Name Type Description Default
vf Callable[[Array, Array, Array], Array]

Vector field function vf(x, v, u, *, t) -> d^2x/dt^2.

required
E0 ArrayLike

State extraction matrix for x (shape [d_x, D]).

required
E1 ArrayLike

First derivative extraction matrix (shape [d_x, D]).

required
E2 ArrayLike

Second derivative extraction matrix (shape [d_x, D]).

required
E0_hidden ArrayLike

Hidden state extraction matrix for u (shape [d_u, D]).

required
constraints list[Conservation] | None

Optional list of Conservation constraints. (Data observations are not measure constraints -- pass them as an obs_model built with prepare_observations; a Measurement here raises TypeError.)

None

TransformedMeasurement

TransformedMeasurement(
    base_model: BaseODEInformation | BlackBoxMeasurement,
    sigma: Callable[[Array], Array],
    use_autodiff_jacobian: bool = True,
    sigma_jacobian: Callable[[Array], Array] | None = None,
)

Wrapper that applies a nonlinear state transformation before measurement.

Given a base measurement model and a transformation sigma(state), this class computes g(sigma(state)) with proper chain-rule Jacobian: J_total = J_g(sigma(state)) @ J_sigma(state)

This is useful for: - Nonlinear coordinate transformations - Feature extraction before measurement - Applying learned transformations to the state

Parameters:

Name Type Description Default
base_model BaseODEInformation | BlackBoxMeasurement

Base measurement model with g, jacobian_g, get_noise, linearize.

required
sigma Callable[[Array], Array]

State transformation function sigma(state) -> transformed_state. Must be differentiable and compatible with JAX.

required
use_autodiff_jacobian bool

If True (default), compute J_sigma via autodiff. If False, expect sigma_jacobian to be provided.

True
sigma_jacobian Callable[[Array], Array] | None

Optional explicit Jacobian function for sigma. If provided and use_autodiff_jacobian=False, this will be used instead of autodiff.

None
Example
Apply softmax transformation to state before ODE measurement

def softmax_transform(state): ... # Transform first 3 components via softmax ... x = state[:3] ... x_soft = jax.nn.softmax(x) ... return state.at[:3].set(x_soft) base = ODEInformation(vf, E0, E1) transformed = TransformedMeasurement(base, softmax_transform)

Methods:

Name Description
g

Evaluate the measurement function on transformed state.

get_noise

Return the square root of the noise covariance (delegated to base model).

jacobian_g

Compute Jacobian with chain rule: J_g(sigma(state)) @ J_sigma(state).

linearize

Linearize the transformed measurement model.

Attributes:

Name Type Description
R Array

Measurement noise covariance matrix (delegated to base model).

Attributes
R property writable
R: Array

Measurement noise covariance matrix (delegated to base model).

Functions
g
g(state: Array, *, t: ArrayLike) -> Array

Evaluate the measurement function on transformed state.

Computes g(sigma(state), t).

Parameters:

Name Type Description Default
state Array

Original state vector.

required
t ArrayLike

Current time.

required

Returns:

Type Description
Array

Observation vector.

get_noise
get_noise(*, t: ArrayLike) -> Array

Return the square root of the noise covariance (delegated to base model).

Parameters:

Name Type Description Default
t ArrayLike

Current time.

required

Returns:

Type Description
Array

Upper-triangular square root of the noise covariance matrix.

jacobian_g
jacobian_g(state: Array, *, t: ArrayLike) -> Array

Compute Jacobian with chain rule: J_g(sigma(state)) @ J_sigma(state).

Parameters:

Name Type Description Default
state Array

Original state vector.

required
t ArrayLike

Current time.

required

Returns:

Type Description
Array

Jacobian matrix of the composed measurement model.

linearize
linearize(
    state: Array, *, t: ArrayLike
) -> tuple[Array, Array]

Linearize the transformed measurement model.

Parameters:

Name Type Description Default
state Array

Original state vector to linearize around.

required
t ArrayLike

Current time.

required

Returns:

Type Description
Array

Tuple of (H_t, c_t) where:

Array
  • H_t is the Jacobian of the composed model
tuple[Array, Array]
  • c_t is the constant term (observation offset)

Functions

ODEconservation

ODEconservation(
    vf: Callable,
    E0: ArrayLike,
    E1: ArrayLike,
    A: Array,
    p: Array,
) -> ODEInformation

Create first-order ODE model with conservation constraint.

Parameters:

Name Type Description Default
vf Callable

Vector field function vf(x, *, t) -> dx/dt.

required
E0 ArrayLike

State extraction matrix.

required
E1 ArrayLike

Derivative extraction matrix.

required
A Array

Conservation constraint matrix (shape [k, d]).

required
p Array

Conservation target values (shape [k]).

required

Returns:

Type Description
ODEInformation

ODEInformation with conservation constraint.

SecondOrderODEconservation

SecondOrderODEconservation(
    vf: Callable,
    E0: ArrayLike,
    E1: ArrayLike,
    E2: ArrayLike,
    A: Array,
    p: Array,
) -> SecondOrderODEInformation

Create second-order ODE model with conservation constraint.

Parameters:

Name Type Description Default
vf Callable

Vector field function vf(x, v, *, t) -> d^2x/dt^2.

required
E0 ArrayLike

State extraction matrix.

required
E1 ArrayLike

First derivative extraction matrix.

required
E2 ArrayLike

Second derivative extraction matrix.

required
A Array

Conservation constraint matrix (shape [k, d]).

required
p Array

Conservation target values (shape [k]).

required

Returns:

Type Description
SecondOrderODEInformation

SecondOrderODEInformation with conservation constraint.

prepare_observations

prepare_observations(
    observations: list[Measurement],
    E0: ArrayLike,
    ts: ArrayLike,
) -> ObsModel | None

Build an :class:ObsModel from :class:Measurement objects.

Assumes linear, time-invariant observation matrices (A and noise are constant across time; only the measurement values z vary).

Parameters:

Name Type Description Default
observations list[Measurement]

List of Measurement constraints.

required
E0 ArrayLike

State extraction matrix, shape [d, state_dim].

required
ts ArrayLike

Time grid of shape [N+1] (includes initial time t0). The filter uses ts[1:] for the N filter steps.

required

Returns:

Name Type Description
An ObsModel | None

class:ObsModel, or None when observations is empty.