Skip to content

Merge Develop - #10

Merged
tyrrellgit merged 49 commits into
masterfrom
develop
Oct 25, 2026
Merged

tyrrellgit merged 49 commits into
masterfrom
develop

Conversation

@tyrrellgit

Copy link
Copy Markdown
Owner

Latest version merge

tyrrellgit and others added 30 commits December 27, 2025 14:42
P = (I - KH) P (I - KH)^T + K R K^T instead of (I - KH) P. Equivalent for the
optimal gain in exact arithmetic, but keeps P symmetric and positive
semi-definite under floating point rounding. Applied to LinearUpdate,
NonLinearUpdate and the Python ExtendedKalmanFilter.
State(value, *, covariance, epoch), Observation(value, *, epoch),
TransitionModel(model, *, state) and UpdateModel(model, *, transition,
measurement). Prevents silently swapping same-typed arguments. Breaking:
positional use of the trailing arguments now raises TypeError. All tests,
examples and the README are updated.
…ructor

- f, jac_f, h and jac_h now take (x, t), where t is the integer epoch: f
  advances the state from t to t + 1 and h is evaluated at the measurement
  epoch, so models may be time-varying. Autodiff still differentiates x.
- ExtendedKalmanFilter(state, *, f, jac_f, q, h, jac_h, r) is keyword-only.
- Document that returned states are live zero-copy views of the filter state.
- Tests for epoch passing, live views, Joseph-form symmetry on a badly scaled
  problem and keyword-only enforcement.
- README: Python usage notes and a roadmap (gating, UD filter, automatic
  Jacobians).
The models were const-generic (SMatrix<N, M>), so they could only be used with
sizes known at compile time and the Python bindings could not wrap them. They
are now generic over na::Dim, so one implementation serves both statically
sized (na::Const<N>) and runtime sized (na::Dyn) matrices.

- LinearTransition/Measurement/Update, NonLinearTransition/Measurement/Update
  and KalmanFilterT take N, M: na::Dim. A FilterAllocator<N, M> helper trait
  keeps the nalgebra allocator bounds readable.
- Statically sized aliases follow the existing StateN/KalmanFilterNM pattern:
  LinearTransitionN<N>, LinearUpdateNM<N, M>, NonLinearMeasurementNM<N, M>...
  plus KalmanFilterDyn and ExtendedKalmanFilterDyn. Rust call sites use them.
- The measurement update (gain + Joseph form covariance), previously copied
  in LinearUpdate and NonLinearUpdate, is shared in models::update.
- Nonlinear models run the user closures on a copy of the state instead of
  holding the state lock, so a closure that reads the state cannot deadlock.
  The closure types are now Send + Sync.
- moore_penrose_right_inverse computed the left inverse, which fails for a
  wide H; moore_penrose_inverse uses the left inverse or else the right one.
- Tests: the Dyn filters match the statically sized ones exactly.
The Python filters were reimplementations: KalmanFilter only delegated to a
Python UpdateModel (so the maths ran in Python), and ExtendedKalmanFilter had
its own copy of the EKF equations. Each Python filter is now a thin wrapper
around a Rust filter type:

  KalmanFilterDyn                        -> KalmanFilter
  ExtendedKalmanFilterDyn                -> ExtendedKalmanFilter
  KalmanFilterT<Dyn, Dyn, PyUpdateModel> -> CustomFilter

- dt.KalmanFilter(state, *, f, q, h, r) takes numpy matrices and runs in Rust.
- dt.ExtendedKalmanFilter passes the Python model functions to the Rust
  NonLinear* models as closures. An exception (or wrong shape) in a model
  function restores the filter state and is re-raised.
- The delegating filter is renamed CustomFilter. Breaking: callers of
  dt.KalmanFilter(update) must use dt.CustomFilter(update).
- State setters copy the incoming state before locking, so assigning a state
  to itself no longer deadlocks.
- The manim demo uses the Rust-computed KalmanFilter. README documents the
  three filters.
src/python had grown inconsistently (an empty linear.rs, MeasurementModel in
measurement.rs next to Observation, filters split over three files). Code is
moved, not changed:

- epoch.rs, state.rs, measurement.rs: Epoch, State, Observation, matching
  crate::epoch, crate::state and crate::measurement.
- models.rs: the Python-implemented TransitionModel, MeasurementModel and
  UpdateModel (was transition.rs, update.rs and half of measurement.rs).
- algorithms.rs: KalmanFilter, ExtendedKalmanFilter, CustomFilter (was
  kalman.rs, ekf.rs and algorithms.rs).
- callbacks.rs: Python model functions as Rust closures (was in ekf.rs).
- arrays.rs and logging.rs are unchanged; linear.rs is removed.

Tests are renamed to what they test: src/tests/test_custom_filter.rs (which
tests KalmanFilterNM) is now test_kalman.rs, and tests_py/test_algorithm.py
(which tests CustomFilter) is now test_custom_filter.py.
rory3va and others added 19 commits October 25, 2026 00:47
The model traits required methods that most filters never use, so models had
to provide them anyway. The core traits now hold only what every filter
needs, and the rest are separate capability traits:

- TransitionModel: state()
- MeasurementModel: projection() and the new noise(), the measurement noise
  covariance R
- UpdateModel: state() and apply(); jacobian() is removed (it was unused)
- TransitionJacobian, MeasurementJacobian: jacobian(), implemented by the
  linear and nonlinear models; the EKF update uses MeasurementJacobian
- InverseMeasurement: inverse(), implemented by the linear (pseudo-inverse)
  and nonlinear (with_inverse) measurements

The update models read R through noise() and H through jacobian(). The Python
model wrappers mirror the Rust traits: MeasurementModel gains noise() (calls
the Python object's noise()), and UpdateModel.jacobian is removed. Breaking
for Python models: a MeasurementModel used where R is needed must provide
noise(), and UpdateModel no longer has jacobian().
UDKalmanFilter and UDExtendedKalmanFilter hold the covariance as factors
P = U D U^T and propagate and update the factors directly, following Ramos et
al., "A summary on the UD Kalman Filter" (arXiv:2203.06105):

- initialise: udu(P) (new udu_decomposition in common::linalg)
- time update: weighted modified Gram-Schmidt on W = [F U, G], weights
  diag(D, D_Q), with Q = G D_Q G^T factorised the same way so Q need not be
  diagonal; W is kept as two N x N blocks, so static sizes stay
  allocation-free
- measurement update: scalar measurements processed sequentially with the
  modified Agee-Turner update, after decorrelating a non-diagonal R

LinearUDUpdate and NonLinearUDUpdate reuse the existing linear and nonlinear
models (no trait changes); aliases UDKalmanFilterNM/Dyn and
UDExtendedKalmanFilterNM/Dyn. The state still carries P = U D U^T after each
step; if it is changed from outside, the factors are recomputed from it.
Backward prediction is not supported.

Python: dt.UDKalmanFilter(state, *, f, q, h, r) and
dt.UDExtendedKalmanFilter(state, *, f, jac_f, q, h, jac_h, r).

Tests: match the Kalman and extended filters to rounding (non-diagonal Q,
diagonal and correlated R, several measurements, epoch gaps), stay PSD on a
badly scaled problem, recover after outside changes and Python errors, and
Dyn matches static.
…ndling.

In these tests we expect warnings and error messages to pop up in the logs, but this can make it confusion to track down real errors in the tests.  We silent these logs during exception tests with a simple helper.
@tyrrellgit tyrrellgit self-assigned this Oct 25, 2026
@tyrrellgit
tyrrellgit merged commit ff9e51b into master Oct 25, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants