Estimators¶
shinro.estimators
State estimation algorithms for reconstructing system state from measurements.
Provides discrete-time state estimators that combine dynamics models with sensor measurements. All estimators implement the StateEstimator ABC.
Available estimators: KalmanFilter — Optimal stochastic filter (predict-update cycle) ExtendedKalmanFilter — Nonlinear EKF (local linearization of f, h) UnscentedKF — Nonlinear UKF (deterministic sigma-point propagation) ComplementaryFilter — Rate + absolute-angle fusion (no model, no noise stats) LuenbergerObserver — Deterministic observer with fixed gain
KalmanFilter¶
Bases: StateEstimator
Discrete-time linear Kalman filter for optimal state estimation.
Implements the predict-update cycle for a system of the form:
Tracks the posterior state estimate \(\hat{x}\) and error covariance \(P\) through the standard Kalman filter equations.
Uses flat vectors \((n,)\) throughout (not column \((n, 1)\)), matching the rest of the control stack.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
A
|
State transition matrix (n_x, n_x). |
required | |
B
|
Control input matrix (n_x, n_u). |
required | |
Q
|
Process noise covariance (n_x, n_x). |
required | |
R
|
Measurement noise covariance (n_y, n_y). |
required | |
C
|
Any | None
|
Observation matrix (n_y, n_x). Defaults to identity. |
None
|
D
|
Any | None
|
Feedthrough matrix (n_y, n_u). Defaults to zeros. |
None
|
x0
|
Any | None
|
Initial state estimate (n_x,). Defaults to zeros. |
None
|
backend
|
ArrayBackend | None
|
Array backend. Defaults to NumpyBackend. |
None
|
Source code in src/shinro/estimators/kalman_filter.py
estimate
¶
Run one predict-update cycle and return the posterior state estimate.
Implements the standard Kalman filter equations:
-
Predict: \(x_{\text{pred}} = A \hat{x} + B u\) \(P_{\text{pred}} = A P A^T + Q\)
-
Update: \(K = P_{\text{pred}} C^T (C P_{\text{pred}} C^T + R)^{-1}\) \(\hat{x} = x_{\text{pred}} + K (y - C x_{\text{pred}} - D u)\) \(P = (I - K C) P_{\text{pred}}\)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
measurement
|
Observation vector (n_y,) from sensors. |
required | |
control_input
|
Control vector (n_u,) applied at this step. |
required |
Returns:
| Type | Description |
|---|---|
|
Posterior state estimate \(\hat{x}\) (n_x,). |
Source code in src/shinro/estimators/kalman_filter.py
measure
¶
reset
¶
Reset the filter to its initial state.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x0
|
Any | None
|
Initial state estimate (n_x,). Defaults to zeros. |
None
|
Source code in src/shinro/estimators/kalman_filter.py
from_config
classmethod
¶
from_config(config, backend: ArrayBackend | None = None)
Create a Kalman filter from a TOML config dict or KalmanFilterConfig.
Config fields
process_noise: Diagonal Q weights (n_x,) or full Q matrix (n_x, n_x). measurement_noise: Diagonal R weights (n_y,) or full R matrix (n_y, n_y). dt: Time step — used to set B = dt * I unless B_dynamics is given. A_dynamics: Optional full A matrix (n_x, n_x). Defaults to I. B_dynamics: Optional full B matrix (n_x, n_u). Defaults to dt * I. C: Optional full C matrix (n_y, n_x). Defaults to I. D: Optional full D matrix (n_y, n_u). Defaults to zeros.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
config
|
TOML config dict or KalmanFilterConfig. |
required | |
backend
|
ArrayBackend | None
|
Array backend. Defaults to NumpyBackend. |
None
|
Returns:
| Type | Description |
|---|---|
|
KalmanFilter instance. |
Source code in src/shinro/estimators/kalman_filter.py
ExtendedKalmanFilter¶
Bases: StateEstimator
Extended Kalman filter for nonlinear state estimation.
Estimates the state of a continuous-time nonlinear system
by linearizing \(f\) and \(h\) about the current estimate and running the standard Kalman predict-update cycle on the local model.
Conventions. Everything is a flat vector — state \((n_x,)\),
control \((n_u,)\), measurement \((n_y,)\) — matching the rest of
the control stack. dynamics_fn(x, u) -> dx/dt is continuous-time:
the filter Euler-discretizes it at dt, so the discrete process
Jacobian is \(F = I + dt \cdot \partial f/\partial x\). The control
Jacobian \(\partial f/\partial u\) is never needed — the EKF assumes
the applied input is known exactly, so it does not enter the covariance.
Predict (local linearization about \(\hat{x}\)):
Update (linearization about \(x^-\)):
The two Jacobians are computed by the backend's numerical Jacobian
(shinro.utils.array_backend.ArrayBackend.jacobian — central finite
differences on numpy, autograd on torch; on the tracing backend it lowers as
graph nodes via shinro.codegen.trace_backend.TraceBackend.jacobian),
so dynamics_fn and measurement_fn are the only model inputs the filter
needs. A linear measurement model may be given as measurement_matrix
instead, which yields the exact H = C with no differentiation.
A model callable may accept an optional bk (as
shinro.components.Plant.dynamics does). The filter passes its own
backend when the signature accepts one, which is what lets a traced filter
route the plant's ops through its TraceBackend; plain two-argument
callables keep working.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dynamics_fn
|
Callable[..., Any] | None
|
Continuous-time process model |
required |
measurement_fn
|
Callable[..., Any] | None
|
Measurement model |
required |
dt
|
float
|
Integration / sample time step in seconds. |
required |
Q
|
Any
|
Process noise covariance (n_x, n_x). |
required |
R
|
Any
|
Measurement noise covariance (n_y, n_y). |
required |
x0
|
Any | None
|
Initial state estimate (n_x,). Defaults to zeros. |
None
|
backend
|
ArrayBackend | None
|
Array backend. Defaults to NumpyBackend. |
None
|
measurement_matrix
|
Any | None
|
Optional linear measurement matrix |
None
|
Source code in src/shinro/estimators/extended_kf.py
measure
¶
Expected measurement h(x) for a true state (the linear C or the callable/plant model).
estimate
¶
Run one predict-update cycle and return the posterior state estimate.
See the class docstring for the equations. The Jacobians are the local
linearization; the covariance update uses the same simple
\((I - KH)P^-\) form as
shinro.estimators.kalman_filter.KalmanFilter (not the Joseph form).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
measurement
|
Observation vector \(y\) (n_y,). |
required | |
control_input
|
Control vector \(u\) applied at this step (n_u,). |
required |
Returns:
| Type | Description |
|---|---|
|
Posterior state estimate \(\hat{x}\) (n_x,). |
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If the model is unset — |
Source code in src/shinro/estimators/extended_kf.py
reset
¶
Reset the filter to its initial state.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x0
|
Any | None
|
Initial state estimate (n_x,). Defaults to zeros. |
None
|
Source code in src/shinro/estimators/extended_kf.py
attach_plant
¶
Inject the plant's process model, and its sensor if it exposes one.
The compiled/served path hands the EKF the plant so its dynamics_fn
is the plant's model — written with the rank helpers and an optional
bk, so it traces and lowers — instead of a hand-wired lambda. When
the plant also overrides shinro.components.Plant.measurement,
and the filter has neither a measurement_fn nor a linear C, that
sensor becomes h too, derived from the same plant so the live and
lowered measurement models cannot drift.
Each side is filled only when it is unset, so an explicit injection
(or a measurement_matrix) always wins. A plant with no sensor model
leaves h to the config's measurement_matrix.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
plant
|
Any
|
The plant whose |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the plant exposes no dynamics and none was injected. |
Source code in src/shinro/estimators/extended_kf.py
from_config
classmethod
¶
from_config(config, backend: ArrayBackend | None = None)
Create an extended Kalman filter from a TOML config dict or ExtendedKalmanFilterConfig.
Config fields
process_noise: Diagonal Q weights (n_x,) or full Q matrix (n_x, n_x).
measurement_noise: Diagonal R weights (n_y,) or full R matrix (n_y, n_y).
dt: Time step. Required at runtime; injected from the plant in
scenario builds.
measurement_matrix: Optional linear measurement matrix C (n_y, n_x);
when set, h(x) = C x and H = C exactly, with no callable.
initial_state: Optional initial state estimate (n_x,). Defaults to zeros.
The dynamics_fn / measurement_fn callables cannot be serialized
to TOML. They are created as None and must be injected after
construction:
.. code-block:: python
ekf = ExtendedKalmanFilter.from_config(config)
ekf.dynamics_fn = my_f
ekf.measurement_fn = my_h
A config may instead carry a linear measurement_matrix (C), in which
case measurement_fn is never needed — the compiled path takes this route,
since a plant has no measurement callable to inject.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
config
|
TOML config dict or ExtendedKalmanFilterConfig. |
required | |
backend
|
ArrayBackend | None
|
Array backend. Defaults to NumpyBackend. |
None
|
Returns:
| Type | Description |
|---|---|
|
ExtendedKalmanFilter instance. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/shinro/estimators/extended_kf.py
UnscentedKF¶
Bases: StateEstimator
Source code in src/shinro/estimators/unscented_kf.py
measure
¶
Expected measurement h(x) for a true state (the linear C or the callable/plant model).
reset
¶
Reset the filter to its initial state.
Clears the posterior estimate and covariance back to their construction
values (\(\hat{x}=0\), \(P=0.1\,I\)) so the filter can be reused
on a new run. Mirrors ExtendedKalmanFilter.reset.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x0
|
Any | None
|
Initial state estimate (n_x,). Defaults to zeros. |
None
|
Source code in src/shinro/estimators/unscented_kf.py
attach_plant
¶
Inject the plant's process model, and its sensor if it exposes one.
The compiled/served path hands the UKF the plant so its dynamics_fn
is the plant's model — written with the rank helpers and an optional
bk, so it traces and lowers — instead of a hand-wired lambda. When
the plant also overrides shinro.components.Plant.measurement,
and the filter has neither a measurement_fn nor a linear C, that
sensor becomes h too, derived from the same plant so the live and
lowered measurement models cannot drift.
Each side is filled only when it is unset, so an explicit injection
(or a measurement_matrix) always wins. A plant with no sensor model
leaves h to the config's measurement_matrix.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
plant
|
Any
|
The plant whose |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the plant exposes no dynamics and none was injected. |
Source code in src/shinro/estimators/unscented_kf.py
from_config
classmethod
¶
from_config(config, backend: ArrayBackend | None = None)
Create an unscented Kalman filter from a TOML config dict or UnscentedKFConfig.
The dynamics_fn / measurement_fn callables cannot be serialized
to TOML: they are created as None and must be injected after
construction (ukf.dynamics_fn = ...), or filled from a plant. A
config may instead carry a linear measurement_matrix (C), in which
case measurement_fn is never needed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
config
|
TOML config dict or UnscentedKFConfig. |
required | |
backend
|
ArrayBackend | None
|
Array backend. Defaults to NumpyBackend. |
None
|
Returns:
| Type | Description |
|---|---|
|
UnscentedKF instance. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/shinro/estimators/unscented_kf.py
ComplementaryFilter¶
Bases: StateEstimator
Complementary filter for rate + absolute-angle sensor fusion.
Estimates an angle \(\theta\) from a rate \(\omega\) (good at high frequency) and an absolute measurement \(\theta_a\) (good at low frequency). The discrete form is
where \(\alpha \in [0, 1]\) is the blend weight on the integrated rate. It is the discrete equivalent of the continuous blend \(\hat{\theta} = \frac{1}{1+\tau s}\theta_a + \frac{\tau s}{1+\tau s}\frac{\omega}{s}\) with \(\alpha = \tau / (\tau + \Delta t)\): the rate is high-passed (its drift is rejected), the absolute is low-passed (its noise is rejected).
Measurement layout. The framework passes one measurement vector; it is
the concatenation of the per-axis rate followed by the per-axis absolute,
[\omega_0 .. \omega_{N-1}, \theta_{a,0} .. \theta_{a,N-1}]
(length 2 * channels). Everything is flat (N,), matching the EKF/UKF.
State. The estimate \(\hat{\theta}\) (channels,) is the only
recurrent state; self.x_hat carries it across ticks (the composed graph
threads it as state_x_hat).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
alpha
|
float
|
Blend weight on the integrated rate, in |
required |
channels
|
int
|
Number of independent angles fused (the state dimension n_x). |
required |
dt
|
float
|
Sample time in seconds. |
required |
x0
|
Any | None
|
Initial angle estimate (channels,). Defaults to zeros. |
None
|
backend
|
ArrayBackend | None
|
Array backend. Defaults to NumpyBackend. |
None
|
Source code in src/shinro/estimators/complementary_filter.py
estimate
¶
Run one fuse step and return the posterior angle estimate.
Splits the measurement into its rate and absolute halves, integrates the
rate over dt, and blends:
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
measurement
|
Concatenated |
required | |
control_input
|
Unused — a complementary filter has no control input, but the estimator contract requires the argument. |
required |
Returns:
| Type | Description |
|---|---|
|
Posterior angle estimate \(\hat{\theta}\) (n_x,). |
Source code in src/shinro/estimators/complementary_filter.py
reset
¶
Reset the filter to its initial state.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x0
|
Any | None
|
Initial angle estimate (n_x,). Defaults to zeros. |
None
|
Source code in src/shinro/estimators/complementary_filter.py
from_config
classmethod
¶
from_config(config, backend: ArrayBackend | None = None)
Create a complementary filter from a TOML config dict or ComplementaryFilterConfig.
Config fields
alpha: Blend weight on the integrated rate, in [0, 1].
channels: Number of fused axes (n_x); the measurement is 2 * channels.
dt: Time step — required at runtime; injected from the plant in
scenario builds.
initial_state: Optional initial angle estimate (n_x,). Defaults to zeros.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
config
|
TOML config dict or ComplementaryFilterConfig. |
required | |
backend
|
ArrayBackend | None
|
Array backend. Defaults to NumpyBackend. |
None
|
Returns:
| Type | Description |
|---|---|
|
ComplementaryFilter instance. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in src/shinro/estimators/complementary_filter.py
LuenbergerObserver¶
Bases: StateEstimator
Luenberger observer for deterministic linear state estimation.
Implements the discrete-time observer dynamics:
where L is the observer gain chosen to place the eigenvalues of \((A - LC)\) inside the unit circle for stable estimation.
Unlike the Kalman filter, the Luenberger observer uses a fixed gain and does not assume noise statistics. No matrix inverses are needed at runtime — just three matrix-vector multiplies.
Uses flat vectors \((n,)\) throughout (not column \((n, 1)\)), matching the rest of the control stack.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
A
|
State transition matrix (n_x, n_x). |
required | |
B
|
Control input matrix (n_x, n_u). |
required | |
observer_gain
|
Observer gain matrix L (n_x, n_y). Must place eigenvalues of (A - LC) inside the unit circle. |
required | |
C
|
Any | None
|
Output matrix (n_y, n_x). Defaults to identity. |
None
|
D
|
Any | None
|
Feedthrough matrix (n_y, n_u). Defaults to zeros. |
None
|
x0
|
Any | None
|
Initial state estimate (n_x,). Defaults to zeros. |
None
|
backend
|
ArrayBackend | None
|
Array backend. Defaults to NumpyBackend. |
None
|
Source code in src/shinro/estimators/luenberger_observer.py
estimate
¶
Perform one step of state estimation.
Computes the predicted state from the dynamics, calculates the innovation (measurement residual), and corrects the prediction using the observer gain:
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
measurement
|
Output measurement \(y_k\) (n_y,). |
required | |
control_input
|
Control input \(u_k\) (n_u,). |
required |
Returns:
| Type | Description |
|---|---|
|
Updated state estimate \(\hat{x}_{k+1}\) (n_x,). |
Source code in src/shinro/estimators/luenberger_observer.py
measure
¶
reset
¶
Reset the observer to its initial state.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x0
|
Any | None
|
Initial state estimate (n_x,). Defaults to zeros. |
None
|
Source code in src/shinro/estimators/luenberger_observer.py
from_config
classmethod
¶
from_config(config, backend: ArrayBackend | None = None)
Create a Luenberger observer from a TOML config dict or LuenbergerObserverConfig.
Config fields
observer_gain: Diagonal gain weights (n_x,) or full gain matrix (n_x, n_y). dt: Time step — used to set B = dt * I unless B_dynamics is given. A_dynamics: Optional full A matrix (n_x, n_x). Defaults to I. B_dynamics: Optional full B matrix (n_x, n_u). Defaults to dt * I. C: Optional full C matrix (n_y, n_x). Defaults to I. D: Optional full D matrix (n_y, n_u). Defaults to zeros.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
config
|
TOML config dict or LuenbergerObserverConfig. |
required | |
backend
|
ArrayBackend | None
|
Array backend. Defaults to NumpyBackend. |
None
|
Returns:
| Type | Description |
|---|---|
|
LuenbergerObserver instance. |