Source code for vane.sysid.kalman_interface

"""Ready-to-use Kalman-filter model assembly.

A :class:`KalmanModel` bundles a state-space system with its process,
measurement, and initial covariances into the exact matrices a Kalman filter
consumes. The system can be discretized at a chosen sample time during assembly,
and the covariance dimensions are validated against the system so the resulting
model is internally consistent.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    import numpy as np
    import numpy.typing as npt

    from vane.sysid.covariance_init import CovarianceSet
    from vane.sysid.state_space import StateSpace

__all__ = ["KalmanModel", "build_kalman_model"]


[docs] @dataclass class KalmanModel: """A state-space system with Kalman covariances, ready for filtering. Parameters ---------- a : npt.NDArray[np.float64] State matrix, shape ``(n_states, n_states)``. b, c, d : npt.NDArray[np.float64] or None Input, output, and feedthrough matrices. q, r, p0 : npt.NDArray[np.float64] Process-noise, measurement-noise, and initial state covariances. dt : float or None Sample time; ``None`` for a continuous-time model. state_names, output_names : list[str] State and output names carried from the source system. """ a: npt.NDArray[np.float64] b: npt.NDArray[np.float64] | None c: npt.NDArray[np.float64] | None d: npt.NDArray[np.float64] | None q: npt.NDArray[np.float64] r: npt.NDArray[np.float64] p0: npt.NDArray[np.float64] dt: float | None = None state_names: list[str] = field(default_factory=list) output_names: list[str] = field(default_factory=list)
[docs] def build_kalman_model( state_space: StateSpace, covariances: CovarianceSet, *, dt: float | None = None, ) -> KalmanModel: """Assemble a Kalman model from a state-space system and covariances. Parameters ---------- state_space : StateSpace The continuous- or discrete-time system. covariances : CovarianceSet The process, measurement, and initial covariances. dt : float or None, optional When given and the system is continuous, discretize it at this sample time before assembly. Returns ------- KalmanModel The assembled, dimension-checked model. Raises ------ ValueError If ``dt`` is given for an already-discrete system, or the covariance dimensions do not match the system. """ if dt is not None and state_space.is_discrete: msg = ( f"state_space is already discrete (dt={state_space.dt}); cannot " f"re-discretize with dt={dt}" ) raise ValueError(msg) system = state_space.discretized(dt) if dt is not None else state_space _validate_covariance_dimensions(system, covariances) return KalmanModel( a=system.a, b=system.b, c=system.c, d=system.d, q=covariances.q, r=covariances.r, p0=covariances.p0, dt=system.dt, state_names=list(system.state_names), output_names=list(system.output_names), )
def _validate_covariance_dimensions( system: StateSpace, covariances: CovarianceSet ) -> None: """Validate that covariance shapes match the system dimensions.""" n = system.n_states if covariances.q.shape != (n, n): msg = f"Q must have shape {(n, n)}, got {covariances.q.shape}" raise ValueError(msg) if covariances.p0.shape != (n, n): msg = f"P0 must have shape {(n, n)}, got {covariances.p0.shape}" raise ValueError(msg) # Validate R against the output count unconditionally: a system without C has # zero outputs, so a non-empty R is an inconsistency rather than skipped. p = system.n_outputs if covariances.r.shape != (p, p): msg = f"R must have shape {(p, p)}, got {covariances.r.shape}" raise ValueError(msg)