|
4 | 4 | //! [`ExtendedKalmanFilter`] driven by a user [`EkfModel`]. Measurement dimension `M` must be |
5 | 5 | //! ≤ 16 (same limit as [`crate::matrix::mat_inverse_f32`]). Covariance updates use |
6 | 6 | //! `P ← (I − KH) P`; a Joseph-form update may be added later for improved numerical stability. |
| 7 | +//! |
| 8 | +//! `EkfModel::f`/`h` only see the state (plus `dt` for `f`), which doesn't fit models whose |
| 9 | +//! process or measurement equations depend on an exogenous input that isn't part of the state |
| 10 | +//! (a commanded actuation, a measured current used for an IR-drop correction, etc). For that, |
| 11 | +//! implement the `_with_input` trait methods and drive the filter with |
| 12 | +//! [`ExtendedKalmanFilter::predict_with_input`] / [`ExtendedKalmanFilter::update_with_input`]. |
| 13 | +//! Their default implementations ignore `u` and defer to `f`/`h`/the Jacobians, so existing |
| 14 | +//! [`EkfModel`] implementations keep compiling unchanged. |
7 | 15 |
|
8 | 16 | use crate::matrix::{mat_inverse_f32, MatrixInstance, MatrixInstanceMut}; |
9 | 17 | use crate::types::Status; |
@@ -417,6 +425,61 @@ pub trait EkfModel<const N: usize, const M: usize> { |
417 | 425 |
|
418 | 426 | /// Measurement Jacobian `H = ∂h/∂x` evaluated at `x` (`M×N`). |
419 | 427 | fn jacobian_h(&self, x: &[f32; N], out: &mut [[f32; N]; M]); |
| 428 | + |
| 429 | + /// Process model with an explicit exogenous input `u` (a control input, |
| 430 | + /// measured disturbance, or anything else that drives `f` but isn't |
| 431 | + /// part of the state): `out = f(x, u, dt)`. |
| 432 | + /// |
| 433 | + /// Default: ignores `u` and defers to [`EkfModel::f`], so models that |
| 434 | + /// don't need an input compile unchanged. |
| 435 | + fn f_with_input<const U: usize>( |
| 436 | + &self, |
| 437 | + x: &[f32; N], |
| 438 | + u: &[f32; U], |
| 439 | + dt: f32, |
| 440 | + out: &mut [f32; N], |
| 441 | + ) { |
| 442 | + let _ = u; |
| 443 | + self.f(x, dt, out) |
| 444 | + } |
| 445 | + |
| 446 | + /// Process Jacobian for [`EkfModel::f_with_input`], `F = ∂f/∂x` evaluated at `(x, u)`. |
| 447 | + /// |
| 448 | + /// Default: defers to [`EkfModel::jacobian_f`], which is exact whenever `u` enters `f` |
| 449 | + /// affinely (so it doesn't change the derivative with respect to `x`). |
| 450 | + fn jacobian_f_with_input<const U: usize>( |
| 451 | + &self, |
| 452 | + x: &[f32; N], |
| 453 | + u: &[f32; U], |
| 454 | + dt: f32, |
| 455 | + out: &mut [[f32; N]; N], |
| 456 | + ) { |
| 457 | + let _ = u; |
| 458 | + self.jacobian_f(x, dt, out) |
| 459 | + } |
| 460 | + |
| 461 | + /// Measurement model with an explicit exogenous input `u` (e.g. a measured current used |
| 462 | + /// for an IR-drop correction that isn't part of the state): `out = h(x, u)`. |
| 463 | + /// |
| 464 | + /// Default: ignores `u` and defers to [`EkfModel::h`]. |
| 465 | + fn h_with_input<const U: usize>(&self, x: &[f32; N], u: &[f32; U], out: &mut [f32; M]) { |
| 466 | + let _ = u; |
| 467 | + self.h(x, out) |
| 468 | + } |
| 469 | + |
| 470 | + /// Measurement Jacobian for [`EkfModel::h_with_input`], `H = ∂h/∂x` evaluated at `(x, u)`. |
| 471 | + /// |
| 472 | + /// Default: defers to [`EkfModel::jacobian_h`], which is exact whenever `u` enters `h` |
| 473 | + /// affinely. |
| 474 | + fn jacobian_h_with_input<const U: usize>( |
| 475 | + &self, |
| 476 | + x: &[f32; N], |
| 477 | + u: &[f32; U], |
| 478 | + out: &mut [[f32; N]; M], |
| 479 | + ) { |
| 480 | + let _ = u; |
| 481 | + self.jacobian_h(x, out) |
| 482 | + } |
420 | 483 | } |
421 | 484 |
|
422 | 485 | /// Extended Kalman filter with compile-time dimensions and a user [`EkfModel`]. |
@@ -476,42 +539,99 @@ impl<const N: usize, const M: usize, Model: EkfModel<N, M>> ExtendedKalmanFilter |
476 | 539 |
|
477 | 540 | let mut x_new = [0.0f32; N]; |
478 | 541 | self.model.f(&self.x, dt, &mut x_new); |
479 | | - self.x = x_new; |
480 | 542 |
|
481 | | - let mut fp = [[0.0f32; N]; N]; |
482 | | - mat_mul(&f_jac, &self.p, &mut fp); |
483 | | - let mut p_new = [[0.0f32; N]; N]; |
484 | | - mat_mul_bt(&fp, &f_jac, &mut p_new); |
485 | | - mat_add_inplace_nn(&mut p_new, &self.q); |
486 | | - self.p = p_new; |
| 543 | + ekf_predict_apply(&mut self.x, &mut self.p, &self.q, &f_jac, x_new); |
| 544 | + } |
| 545 | + |
| 546 | + /// EKF predict with an exogenous input `u`, via [`EkfModel::f_with_input`] / |
| 547 | + /// [`EkfModel::jacobian_f_with_input`]. See the [module docs](self) for when this is |
| 548 | + /// needed instead of [`ExtendedKalmanFilter::predict`]. |
| 549 | + pub fn predict_with_input<const U: usize>(&mut self, dt: f32, u: &[f32; U]) { |
| 550 | + let mut f_jac = [[0.0f32; N]; N]; |
| 551 | + self.model.jacobian_f_with_input(&self.x, u, dt, &mut f_jac); |
| 552 | + |
| 553 | + let mut x_new = [0.0f32; N]; |
| 554 | + self.model.f_with_input(&self.x, u, dt, &mut x_new); |
| 555 | + |
| 556 | + ekf_predict_apply(&mut self.x, &mut self.p, &self.q, &f_jac, x_new); |
487 | 557 | } |
488 | 558 |
|
489 | 559 | /// EKF update with measurement `z`. Linearizes `h` at the current estimate. |
490 | 560 | /// |
491 | 561 | /// On singular innovation covariance or `M > 16`, returns an error and leaves state unchanged. |
492 | 562 | pub fn update(&mut self, z: &[f32; M]) -> Status { |
493 | | - if M > 16 { |
494 | | - return Status::ArgumentError; |
495 | | - } |
496 | | - if M == 0 { |
497 | | - return Status::SizeMismatch; |
498 | | - } |
499 | | - |
500 | 563 | let mut h_jac = [[0.0f32; N]; M]; |
501 | 564 | self.model.jacobian_h(&self.x, &mut h_jac); |
502 | 565 |
|
503 | 566 | let mut hx = [0.0f32; M]; |
504 | 567 | self.model.h(&self.x, &mut hx); |
505 | 568 |
|
506 | | - // Reuse linear update with innovation z' = z - h(x) + H x so that |
507 | | - // y = z' - H x = z - h(x). |
508 | | - let mut z_equiv = [0.0f32; M]; |
509 | | - let mut hx_lin = [0.0f32; M]; |
510 | | - mat_vec_mul(&h_jac, &self.x, &mut hx_lin); |
511 | | - for i in 0..M { |
512 | | - z_equiv[i] = z[i] - hx[i] + hx_lin[i]; |
513 | | - } |
| 569 | + ekf_update_apply(&mut self.x, &mut self.p, &self.r, &h_jac, &hx, z) |
| 570 | + } |
| 571 | + |
| 572 | + /// EKF update with an exogenous input `u`, via [`EkfModel::h_with_input`] / |
| 573 | + /// [`EkfModel::jacobian_h_with_input`]. See the [module docs](self) for when this is |
| 574 | + /// needed instead of [`ExtendedKalmanFilter::update`]. |
| 575 | + /// |
| 576 | + /// On singular innovation covariance or `M > 16`, returns an error and leaves state unchanged. |
| 577 | + pub fn update_with_input<const U: usize>(&mut self, z: &[f32; M], u: &[f32; U]) -> Status { |
| 578 | + let mut h_jac = [[0.0f32; N]; M]; |
| 579 | + self.model.jacobian_h_with_input(&self.x, u, &mut h_jac); |
514 | 580 |
|
515 | | - kf_update_core(&mut self.x, &mut self.p, &self.r, &h_jac, &z_equiv) |
| 581 | + let mut hx = [0.0f32; M]; |
| 582 | + self.model.h_with_input(&self.x, u, &mut hx); |
| 583 | + |
| 584 | + ekf_update_apply(&mut self.x, &mut self.p, &self.r, &h_jac, &hx, z) |
516 | 585 | } |
517 | 586 | } |
| 587 | + |
| 588 | +/// Shared EKF predict math: `x ← x_new`, `P ← F P Fᵀ + Q`. Factored out so |
| 589 | +/// [`ExtendedKalmanFilter::predict`] and [`ExtendedKalmanFilter::predict_with_input`] (which |
| 590 | +/// differ only in how `x_new`/`f_jac` are computed) don't duplicate the covariance propagation. |
| 591 | +fn ekf_predict_apply<const N: usize>( |
| 592 | + x: &mut [f32; N], |
| 593 | + p: &mut [[f32; N]; N], |
| 594 | + q: &[[f32; N]; N], |
| 595 | + f_jac: &[[f32; N]; N], |
| 596 | + x_new: [f32; N], |
| 597 | +) { |
| 598 | + *x = x_new; |
| 599 | + |
| 600 | + let mut fp = [[0.0f32; N]; N]; |
| 601 | + mat_mul(f_jac, p, &mut fp); |
| 602 | + let mut p_new = [[0.0f32; N]; N]; |
| 603 | + mat_mul_bt(&fp, f_jac, &mut p_new); |
| 604 | + mat_add_inplace_nn(&mut p_new, q); |
| 605 | + *p = p_new; |
| 606 | +} |
| 607 | + |
| 608 | +/// Shared EKF update math: linearizes around `hx = h(x)` and reuses the linear-filter update |
| 609 | +/// core. Factored out so [`ExtendedKalmanFilter::update`] and |
| 610 | +/// [`ExtendedKalmanFilter::update_with_input`] (which differ only in how `hx`/`h_jac` are |
| 611 | +/// computed) don't duplicate the linearization. |
| 612 | +fn ekf_update_apply<const N: usize, const M: usize>( |
| 613 | + x: &mut [f32; N], |
| 614 | + p: &mut [[f32; N]; N], |
| 615 | + r: &[[f32; M]; M], |
| 616 | + h_jac: &[[f32; N]; M], |
| 617 | + hx: &[f32; M], |
| 618 | + z: &[f32; M], |
| 619 | +) -> Status { |
| 620 | + if M > 16 { |
| 621 | + return Status::ArgumentError; |
| 622 | + } |
| 623 | + if M == 0 { |
| 624 | + return Status::SizeMismatch; |
| 625 | + } |
| 626 | + |
| 627 | + // Reuse linear update with innovation z' = z - h(x) + H x so that |
| 628 | + // y = z' - H x = z - h(x). |
| 629 | + let mut z_equiv = [0.0f32; M]; |
| 630 | + let mut hx_lin = [0.0f32; M]; |
| 631 | + mat_vec_mul(h_jac, x, &mut hx_lin); |
| 632 | + for i in 0..M { |
| 633 | + z_equiv[i] = z[i] - hx[i] + hx_lin[i]; |
| 634 | + } |
| 635 | + |
| 636 | + kf_update_core(x, p, r, h_jac, &z_equiv) |
| 637 | +} |
0 commit comments