use crate::core::scalar::ControlScalar;
use crate::navigation::dead_reckoning::NavigationError;
#[derive(Debug)]
pub struct EkfSlam2D<S: ControlScalar, const STATE_DIM: usize> {
state: [S; STATE_DIM],
covariance: [[S; STATE_DIM]; STATE_DIM],
landmark_seen: [bool; STATE_DIM], n_landmarks: usize,
q_v: S,
q_omega: S,
r_range: S,
r_bearing: S,
dt: S,
}
impl<S: ControlScalar, const STATE_DIM: usize> EkfSlam2D<S, STATE_DIM> {
pub fn new(
q_v: S,
q_omega: S,
r_range: S,
r_bearing: S,
dt: S,
) -> Result<Self, NavigationError> {
if STATE_DIM < 3 {
return Err(NavigationError::InvalidParameter);
}
if (STATE_DIM - 3) % 2 != 0 {
return Err(NavigationError::InvalidParameter);
}
if q_v <= S::ZERO || q_omega <= S::ZERO {
return Err(NavigationError::InvalidParameter);
}
if r_range <= S::ZERO || r_bearing <= S::ZERO {
return Err(NavigationError::InvalidParameter);
}
if dt <= S::ZERO {
return Err(NavigationError::InvalidParameter);
}
let n_landmarks = (STATE_DIM - 3) / 2;
let mut cov = [[S::ZERO; STATE_DIM]; STATE_DIM];
let p_vehicle = S::from_f64(0.1);
let p_landmark = S::from_f64(1e6);
cov[0][0] = p_vehicle;
cov[1][1] = p_vehicle;
cov[2][2] = p_vehicle;
for i in 0..n_landmarks {
let base = 3 + 2 * i;
cov[base][base] = p_landmark;
cov[base + 1][base + 1] = p_landmark;
}
Ok(Self {
state: [S::ZERO; STATE_DIM],
covariance: cov,
landmark_seen: [false; STATE_DIM],
n_landmarks,
q_v,
q_omega,
r_range,
r_bearing,
dt,
})
}
pub fn predict(&mut self, v: S, omega: S) -> Result<(), NavigationError> {
if !v.is_finite() || !omega.is_finite() {
return Err(NavigationError::InvalidMeasurement);
}
let theta = self.state[2];
let cos_th = S::from_f64(libm::cos(theta.to_f64()));
let sin_th = S::from_f64(libm::sin(theta.to_f64()));
self.state[0] += v * cos_th * self.dt;
self.state[1] += v * sin_th * self.dt;
self.state[2] += omega * self.dt;
let mut f = [[S::ZERO; STATE_DIM]; STATE_DIM];
for (i, row) in f.iter_mut().enumerate() {
row[i] = S::ONE;
}
f[0][2] = S::from_f64((-v.to_f64()) * theta.to_f64().sin() * self.dt.to_f64());
f[1][2] = S::from_f64(v.to_f64() * theta.to_f64().cos() * self.dt.to_f64());
let mut q = [[S::ZERO; STATE_DIM]; STATE_DIM];
let dt2 = self.dt * self.dt;
q[0][0] = self.q_v * dt2;
q[1][1] = self.q_v * dt2;
q[2][2] = self.q_omega * dt2;
self.covariance = Self::mat_triple_product_plus_q(&f, &self.covariance, &q);
Ok(())
}
pub fn update(
&mut self,
landmark_id: usize,
range: S,
bearing: S,
) -> Result<(), NavigationError> {
if landmark_id >= self.n_landmarks {
return Err(NavigationError::InvalidLandmarkId);
}
if range <= S::ZERO || !range.is_finite() || !bearing.is_finite() {
return Err(NavigationError::InvalidMeasurement);
}
let lm_base = 3 + 2 * landmark_id;
if !self.landmark_seen[landmark_id] {
let vx = self.state[0];
let vy = self.state[1];
let vth = self.state[2];
let abs_bearing = bearing + vth;
let cos_b = S::from_f64(libm::cos(abs_bearing.to_f64()));
let sin_b = S::from_f64(libm::sin(abs_bearing.to_f64()));
self.state[lm_base] = vx + range * cos_b;
self.state[lm_base + 1] = vy + range * sin_b;
self.landmark_seen[landmark_id] = true;
}
let vx = self.state[0];
let vy = self.state[1];
let vth = self.state[2];
let lx = self.state[lm_base];
let ly = self.state[lm_base + 1];
let dx = lx - vx;
let dy = ly - vy;
let r2 = dx * dx + dy * dy;
let r = S::from_f64(libm::sqrt(r2.to_f64()));
if r <= S::from_f64(1e-9) {
return Ok(());
}
let expected_range = r;
let expected_bearing = S::from_f64(libm::atan2(dy.to_f64(), dx.to_f64())) - vth;
let innov_range = range - expected_range;
let innov_bearing = Self::wrap_angle(bearing - expected_bearing);
let mut h = [[S::ZERO; STATE_DIM]; 2];
let r2_inv = S::ONE / r2;
let r_inv = S::ONE / r;
h[0][0] = -dx * r_inv; h[0][1] = -dy * r_inv;
h[1][0] = dy * r2_inv; h[1][1] = -dx * r2_inv; h[1][2] = S::from_f64(-1.0);
h[0][lm_base] = dx * r_inv; h[0][lm_base + 1] = dy * r_inv; h[1][lm_base] = -dy * r2_inv; h[1][lm_base + 1] = dx * r2_inv;
let hp = Self::mat2xn_times_nxn(&h, &self.covariance);
let hpht = Self::mat2xn_times_nx2(&hp, &h);
let s00 = hpht[0][0] + self.r_range;
let s01 = hpht[0][1];
let s10 = hpht[1][0];
let s11 = hpht[1][1] + self.r_bearing;
let det = s00 * s11 - s01 * s10;
if det.to_f64().abs() < 1e-30 {
return Err(NavigationError::SingularSystem);
}
let det_inv = S::ONE / det;
let si00 = s11 * det_inv;
let si01 = -s01 * det_inv;
let si10 = -s10 * det_inv;
let si11 = s00 * det_inv;
let pht = Self::matnxn_times_nx2_transposed(&self.covariance, &h);
let mut k = [[S::ZERO; 2]; STATE_DIM];
for (i, ki) in k.iter_mut().enumerate() {
ki[0] = pht[i][0] * si00 + pht[i][1] * si10;
ki[1] = pht[i][0] * si01 + pht[i][1] * si11;
}
for (i, si) in self.state.iter_mut().enumerate() {
*si += k[i][0] * innov_range + k[i][1] * innov_bearing;
}
let mut kh = [[S::ZERO; STATE_DIM]; STATE_DIM];
for (i, khi) in kh.iter_mut().enumerate() {
for (j, khi_j) in khi.iter_mut().enumerate() {
*khi_j = k[i][0] * h[0][j] + k[i][1] * h[1][j];
}
}
#[allow(clippy::needless_range_loop)]
let mut new_p = [[S::ZERO; STATE_DIM]; STATE_DIM];
#[allow(clippy::needless_range_loop)]
for i in 0..STATE_DIM {
for j in 0..STATE_DIM {
let mut sum = S::ZERO;
for k_idx in 0..STATE_DIM {
let i_kh = if i == k_idx {
S::ONE - kh[i][k_idx]
} else {
-kh[i][k_idx]
};
sum += i_kh * self.covariance[k_idx][j];
}
new_p[i][j] = sum;
}
}
self.covariance = new_p;
Ok(())
}
#[inline]
pub fn vehicle_pose(&self) -> [S; 3] {
[self.state[0], self.state[1], self.state[2]]
}
pub fn landmark(&self, id: usize) -> Result<[S; 2], NavigationError> {
if id >= self.n_landmarks {
return Err(NavigationError::InvalidLandmarkId);
}
let base = 3 + 2 * id;
Ok([self.state[base], self.state[base + 1]])
}
#[inline]
pub fn covariance(&self) -> &[[S; STATE_DIM]; STATE_DIM] {
&self.covariance
}
fn mat_triple_product_plus_q(
f: &[[S; STATE_DIM]; STATE_DIM],
p: &[[S; STATE_DIM]; STATE_DIM],
q: &[[S; STATE_DIM]; STATE_DIM],
) -> [[S; STATE_DIM]; STATE_DIM] {
let mut fp = [[S::ZERO; STATE_DIM]; STATE_DIM];
for i in 0..STATE_DIM {
for j in 0..STATE_DIM {
let mut s = S::ZERO;
for k in 0..STATE_DIM {
s += f[i][k] * p[k][j];
}
fp[i][j] = s;
}
}
let mut result = [[S::ZERO; STATE_DIM]; STATE_DIM];
for i in 0..STATE_DIM {
for j in 0..STATE_DIM {
let mut s = S::ZERO;
for k in 0..STATE_DIM {
s += fp[i][k] * f[j][k]; }
result[i][j] = s + q[i][j];
}
}
result
}
fn mat2xn_times_nxn(
h: &[[S; STATE_DIM]; 2],
p: &[[S; STATE_DIM]; STATE_DIM],
) -> [[S; STATE_DIM]; 2] {
let mut result = [[S::ZERO; STATE_DIM]; 2];
for i in 0..2 {
for j in 0..STATE_DIM {
let mut s = S::ZERO;
for k in 0..STATE_DIM {
s += h[i][k] * p[k][j];
}
result[i][j] = s;
}
}
result
}
fn mat2xn_times_nx2(a: &[[S; STATE_DIM]; 2], h: &[[S; STATE_DIM]; 2]) -> [[S; 2]; 2] {
let mut result = [[S::ZERO; 2]; 2];
for i in 0..2 {
for j in 0..2 {
let mut s = S::ZERO;
for k in 0..STATE_DIM {
s += a[i][k] * h[j][k]; }
result[i][j] = s;
}
}
result
}
fn matnxn_times_nx2_transposed(
p: &[[S; STATE_DIM]; STATE_DIM],
h: &[[S; STATE_DIM]; 2],
) -> [[S; 2]; STATE_DIM] {
let mut result = [[S::ZERO; 2]; STATE_DIM];
for i in 0..STATE_DIM {
for j in 0..2 {
let mut s = S::ZERO;
for k in 0..STATE_DIM {
s += p[i][k] * h[j][k]; }
result[i][j] = s;
}
}
result
}
fn wrap_angle(a: S) -> S {
let mut v = a.to_f64();
use core::f64::consts::PI;
while v > PI {
v -= 2.0 * PI;
}
while v <= -PI {
v += 2.0 * PI;
}
S::from_f64(v)
}
}
#[cfg(test)]
mod tests {
use super::*;
type Slam7 = EkfSlam2D<f64, 7>;
fn make_slam() -> Slam7 {
EkfSlam2D::<f64, 7>::new(0.01, 0.01, 0.1, 0.05, 0.1).unwrap()
}
#[test]
fn predict_changes_pose_and_grows_covariance() {
let mut slam = make_slam();
let p_before = slam.covariance[0][0];
slam.predict(1.0, 0.0).unwrap();
let pose = slam.vehicle_pose();
assert!(
pose[0] > 0.0,
"x should increase after predict: {}",
pose[0]
);
let p_after = slam.covariance[0][0];
assert!(
p_after > p_before,
"covariance should grow: {} <= {}",
p_after,
p_before
);
}
#[test]
fn update_initialises_landmark() {
let mut slam = make_slam();
slam.update(0, 5.0, 0.0).unwrap();
let lm = slam.landmark(0).unwrap();
assert!(
(lm[0] - 5.0).abs() < 1e-6,
"landmark x: expected ~5.0, got {}",
lm[0]
);
assert!(
lm[1].abs() < 1e-6,
"landmark y: expected ~0.0, got {}",
lm[1]
);
}
#[test]
fn two_updates_reduce_landmark_uncertainty() {
let mut slam = make_slam();
slam.update(0, 5.0, 0.0).unwrap();
let var0 = slam.covariance[3][3]; slam.update(0, 5.0, 0.0).unwrap();
let var1 = slam.covariance[3][3];
assert!(
var1 < var0,
"second update should reduce landmark uncertainty: {} >= {}",
var1,
var0
);
}
#[test]
fn invalid_landmark_id_returns_error() {
let mut slam = make_slam();
let result = slam.update(10, 5.0, 0.0);
assert_eq!(result.unwrap_err(), NavigationError::InvalidLandmarkId);
}
#[test]
fn invalid_state_dim_returns_error() {
let result = EkfSlam2D::<f64, 4>::new(0.01, 0.01, 0.1, 0.05, 0.1);
assert_eq!(result.unwrap_err(), NavigationError::InvalidParameter);
}
#[test]
fn landmark_converges_with_known_vehicle_pose() {
let mut slam = EkfSlam2D::<f64, 5>::new(1e-6, 1e-6, 0.01, 0.01, 0.01).unwrap();
let true_range = 5.0_f64; let true_bearing = libm::atan2(4.0, 3.0);
for _ in 0..50 {
slam.update(0, true_range, true_bearing).unwrap();
}
let lm = slam.landmark(0).unwrap();
assert!(
(lm[0] - 3.0).abs() < 0.15,
"lx should be ~3.0, got {}",
lm[0]
);
assert!(
(lm[1] - 4.0).abs() < 0.15,
"ly should be ~4.0, got {}",
lm[1]
);
}
#[test]
fn predict_nan_returns_error() {
let mut slam = make_slam();
let result = slam.predict(f64::NAN, 0.0);
assert_eq!(result.unwrap_err(), NavigationError::InvalidMeasurement);
}
#[test]
fn negative_range_returns_error() {
let mut slam = make_slam();
let result = slam.update(0, -1.0, 0.0);
assert_eq!(result.unwrap_err(), NavigationError::InvalidMeasurement);
}
#[test]
fn landmark_query_out_of_range() {
let slam = make_slam();
assert_eq!(
slam.landmark(5).unwrap_err(),
NavigationError::InvalidLandmarkId
);
}
}