1#![allow(dead_code)]
2
3use nalgebra::{DMatrix, DVector, Matrix2, Matrix3, Vector2, Vector3};
13use rand_distr::{Distribution, Normal};
14use std::f64::consts::PI;
15
16const DT: f64 = 0.1; const MAX_RANGE: f64 = 20.0; const M_DIST_TH: f64 = 4.0; const STATE_SIZE: usize = 3; const LM_SIZE: usize = 2; const Q_SIM: [[f64; 2]; 2] = [[0.2, 0.0], [0.0, (5.0 * PI / 180.0) * (5.0 * PI / 180.0)]]; const R_SIM: [[f64; 2]; 2] = [[0.3, 0.0], [0.0, (5.0 * PI / 180.0) * (5.0 * PI / 180.0)]]; fn normalize_angle(angle: f64) -> f64 {
31 let mut a = angle;
32 while a > PI {
33 a -= 2.0 * PI;
34 }
35 while a < -PI {
36 a += 2.0 * PI;
37 }
38 a
39}
40
41fn get_q_control() -> Matrix2<f64> {
43 Matrix2::new(Q_SIM[0][0], Q_SIM[0][1], Q_SIM[1][0], Q_SIM[1][1])
44}
45
46fn get_r() -> Matrix2<f64> {
48 Matrix2::new(R_SIM[0][0], R_SIM[0][1], R_SIM[1][0], R_SIM[1][1])
49}
50
51pub struct EKFSLAMState {
54 pub x: DVector<f64>,
56 pub p: DMatrix<f64>,
58 pub n_lm: usize,
60}
61
62impl EKFSLAMState {
63 pub fn new() -> Self {
65 EKFSLAMState {
66 x: DVector::zeros(STATE_SIZE),
67 p: DMatrix::identity(STATE_SIZE, STATE_SIZE),
68 n_lm: 0,
69 }
70 }
71
72 pub fn get_robot_pose(&self) -> Vector3<f64> {
74 Vector3::new(self.x[0], self.x[1], self.x[2])
75 }
76
77 pub fn get_landmark(&self, idx: usize) -> Option<Vector2<f64>> {
79 if idx < self.n_lm {
80 let lm_idx = STATE_SIZE + idx * LM_SIZE;
81 Some(Vector2::new(self.x[lm_idx], self.x[lm_idx + 1]))
82 } else {
83 None
84 }
85 }
86
87 pub fn n_landmarks(&self) -> usize {
89 self.n_lm
90 }
91}
92
93impl Default for EKFSLAMState {
94 fn default() -> Self {
95 Self::new()
96 }
97}
98
99fn motion_model(x: &Vector3<f64>, u: &Vector2<f64>) -> Vector3<f64> {
102 Vector3::new(
103 x[0] + u[0] * DT * x[2].cos(),
104 x[1] + u[0] * DT * x[2].sin(),
105 normalize_angle(x[2] + u[1] * DT),
106 )
107}
108
109fn jacob_motion(x: &Vector3<f64>, u: &Vector2<f64>) -> (Matrix3<f64>, nalgebra::Matrix3x2<f64>) {
111 let yaw = x[2];
112 let v = u[0];
113
114 let g = Matrix3::new(
116 1.0,
117 0.0,
118 -DT * v * yaw.sin(),
119 0.0,
120 1.0,
121 DT * v * yaw.cos(),
122 0.0,
123 0.0,
124 1.0,
125 );
126
127 let v_mat = nalgebra::Matrix3x2::new(DT * yaw.cos(), 0.0, DT * yaw.sin(), 0.0, 0.0, DT);
129
130 (g, v_mat)
131}
132
133fn calc_observation(robot_pose: &Vector3<f64>, landmark: &Vector2<f64>) -> Vector2<f64> {
135 let dx = landmark[0] - robot_pose[0];
136 let dy = landmark[1] - robot_pose[1];
137 let d = (dx * dx + dy * dy).sqrt();
138 let angle = normalize_angle(dy.atan2(dx) - robot_pose[2]);
139 Vector2::new(d, angle)
140}
141
142fn jacob_observation(
144 robot_pose: &Vector3<f64>,
145 landmark: &Vector2<f64>,
146) -> (nalgebra::Matrix2x3<f64>, Matrix2<f64>) {
147 let dx = landmark[0] - robot_pose[0];
148 let dy = landmark[1] - robot_pose[1];
149 let d2 = dx * dx + dy * dy;
150 let d = d2.sqrt();
151
152 let h_robot = nalgebra::Matrix2x3::new(-dx / d, -dy / d, 0.0, dy / d2, -dx / d2, -1.0);
154
155 let h_lm = Matrix2::new(dx / d, dy / d, -dy / d2, dx / d2);
157
158 (h_robot, h_lm)
159}
160
161fn ekf_slam_predict(state: &mut EKFSLAMState, u: &Vector2<f64>) {
163 let _n = state.x.len();
164
165 let robot_pose = state.get_robot_pose();
167
168 let (g, v_mat) = jacob_motion(&robot_pose, u);
170
171 let new_pose = motion_model(&robot_pose, u);
173 state.x[0] = new_pose[0];
174 state.x[1] = new_pose[1];
175 state.x[2] = new_pose[2];
176
177 let q_control = get_q_control();
179
180 let q_robot = v_mat * q_control * v_mat.transpose();
182
183 let mut p_rr = Matrix3::zeros();
195 for i in 0..STATE_SIZE {
196 for j in 0..STATE_SIZE {
197 p_rr[(i, j)] = state.p[(i, j)];
198 }
199 }
200
201 let p_rr_new = g * p_rr * g.transpose() + q_robot;
203
204 for i in 0..STATE_SIZE {
206 for j in 0..STATE_SIZE {
207 state.p[(i, j)] = p_rr_new[(i, j)];
208 }
209 }
210
211 for lm in 0..state.n_lm {
213 let lm_idx = STATE_SIZE + lm * LM_SIZE;
214
215 let mut p_rm = nalgebra::Matrix3x2::zeros();
217 for i in 0..STATE_SIZE {
218 for j in 0..LM_SIZE {
219 p_rm[(i, j)] = state.p[(i, lm_idx + j)];
220 }
221 }
222
223 let p_rm_new = g * p_rm;
225
226 for i in 0..STATE_SIZE {
228 for j in 0..LM_SIZE {
229 state.p[(i, lm_idx + j)] = p_rm_new[(i, j)];
230 state.p[(lm_idx + j, i)] = p_rm_new[(i, j)]; }
232 }
233 }
234}
235
236fn calc_innovation(
238 state: &EKFSLAMState,
239 lm_idx: usize,
240 z: &Vector2<f64>,
241) -> (Vector2<f64>, Matrix2<f64>, DMatrix<f64>) {
242 let robot_pose = state.get_robot_pose();
243 let landmark = state.get_landmark(lm_idx).unwrap();
244
245 let z_pred = calc_observation(&robot_pose, &landmark);
247
248 let y = Vector2::new(z[0] - z_pred[0], normalize_angle(z[1] - z_pred[1]));
250
251 let (h_robot, h_lm) = jacob_observation(&robot_pose, &landmark);
253
254 let n = state.x.len();
256 let mut h_full = DMatrix::zeros(2, n);
257
258 for i in 0..2 {
260 for j in 0..STATE_SIZE {
261 h_full[(i, j)] = h_robot[(i, j)];
262 }
263 }
264
265 let lm_state_idx = STATE_SIZE + lm_idx * LM_SIZE;
267 for i in 0..2 {
268 for j in 0..LM_SIZE {
269 h_full[(i, lm_state_idx + j)] = h_lm[(i, j)];
270 }
271 }
272
273 let r = get_r();
275 let s = &h_full * &state.p * h_full.transpose() + DMatrix::from_fn(2, 2, |i, j| r[(i, j)]);
276
277 (
278 y,
279 Matrix2::new(s[(0, 0)], s[(0, 1)], s[(1, 0)], s[(1, 1)]),
280 h_full,
281 )
282}
283
284fn search_correspond_landmark_id(state: &EKFSLAMState, z: &Vector2<f64>) -> Option<usize> {
286 let mut min_dist = f64::MAX;
287 let mut min_id = None;
288
289 for i in 0..state.n_lm {
290 let (y, s, _) = calc_innovation(state, i, z);
291
292 if let Some(s_inv) = s.try_inverse() {
294 let mahal = (y.transpose() * s_inv * y)[(0, 0)];
295 if mahal < min_dist {
296 min_dist = mahal;
297 min_id = Some(i);
298 }
299 }
300 }
301
302 if min_dist < M_DIST_TH * M_DIST_TH {
304 min_id
305 } else {
306 None
307 }
308}
309
310fn add_new_landmark(state: &mut EKFSLAMState, z: &Vector2<f64>) {
312 let robot_pose = state.get_robot_pose();
313
314 let lm_x = robot_pose[0] + z[0] * (robot_pose[2] + z[1]).cos();
316 let lm_y = robot_pose[1] + z[0] * (robot_pose[2] + z[1]).sin();
317
318 let old_n = state.x.len();
320 let new_n = old_n + LM_SIZE;
321
322 let mut new_x = DVector::zeros(new_n);
323 for i in 0..old_n {
324 new_x[i] = state.x[i];
325 }
326 new_x[old_n] = lm_x;
327 new_x[old_n + 1] = lm_y;
328 state.x = new_x;
329
330 let mut new_p = DMatrix::zeros(new_n, new_n);
332
333 for i in 0..old_n {
335 for j in 0..old_n {
336 new_p[(i, j)] = state.p[(i, j)];
337 }
338 }
339
340 let r = get_r();
342
343 let c = (robot_pose[2] + z[1]).cos();
345 let s = (robot_pose[2] + z[1]).sin();
346
347 let g_r = nalgebra::Matrix2x3::new(1.0, 0.0, -z[0] * s, 0.0, 1.0, z[0] * c);
349
350 let g_z = Matrix2::new(c, -z[0] * s, s, z[0] * c);
352
353 let p_rr = state.p.fixed_view::<3, 3>(0, 0);
355 let p_lm = g_r * p_rr * g_r.transpose() + g_z * r * g_z.transpose();
356
357 new_p[(old_n, old_n)] = p_lm[(0, 0)];
359 new_p[(old_n, old_n + 1)] = p_lm[(0, 1)];
360 new_p[(old_n + 1, old_n)] = p_lm[(1, 0)];
361 new_p[(old_n + 1, old_n + 1)] = p_lm[(1, 1)];
362
363 let p_rl = p_rr * g_r.transpose();
365 for i in 0..STATE_SIZE {
366 for j in 0..LM_SIZE {
367 new_p[(i, old_n + j)] = p_rl[(i, j)];
368 new_p[(old_n + j, i)] = p_rl[(i, j)];
369 }
370 }
371
372 for k in 0..state.n_lm {
374 let lm_idx = STATE_SIZE + k * LM_SIZE;
375 for i in 0..LM_SIZE {
376 for j in 0..STATE_SIZE {
377 let p_lk_r = state.p[(lm_idx + i, j)];
378 for l in 0..LM_SIZE {
379 new_p[(lm_idx + i, old_n + l)] += p_lk_r * g_r[(l, j)];
380 new_p[(old_n + l, lm_idx + i)] = new_p[(lm_idx + i, old_n + l)];
381 }
382 }
383 }
384 }
385
386 state.p = new_p;
387 state.n_lm += 1;
388}
389
390fn ekf_slam_update(state: &mut EKFSLAMState, z: &Vector2<f64>, lm_idx: usize) {
392 let (y, s, h_full) = calc_innovation(state, lm_idx, z);
393
394 let s_dmatrix = DMatrix::from_fn(2, 2, |i, j| s[(i, j)]);
396 let s_inv = s_dmatrix
397 .try_inverse()
398 .unwrap_or_else(|| DMatrix::identity(2, 2));
399 let k = &state.p * h_full.transpose() * s_inv;
400
401 let y_dvec = DVector::from_vec(vec![y[0], y[1]]);
403 state.x = &state.x + &k * y_dvec;
404
405 state.x[2] = normalize_angle(state.x[2]);
407
408 let n = state.x.len();
410 let i_kh = DMatrix::identity(n, n) - &k * h_full;
411 state.p = &i_kh * &state.p;
412
413 state.p = (&state.p + state.p.transpose()) * 0.5;
415}
416
417pub fn ekf_slam(
419 state: &mut EKFSLAMState,
420 u: &Vector2<f64>,
421 observations: &[(f64, f64)], ) {
423 ekf_slam_predict(state, u);
425
426 for (d, angle) in observations {
428 let z = Vector2::new(*d, *angle);
429
430 let lm_idx = search_correspond_landmark_id(state, &z);
432
433 match lm_idx {
434 Some(idx) => {
435 ekf_slam_update(state, &z, idx);
437 }
438 None => {
439 add_new_landmark(state, &z);
441 }
442 }
443 }
444}
445
446pub fn ekf_slam_known_correspondences(
449 state: &mut EKFSLAMState,
450 u: &Vector2<f64>,
451 observations: &[(f64, f64, usize)], n_landmarks: usize,
453) {
454 ekf_slam_predict(state, u);
456
457 if state.n_lm == 0 && !observations.is_empty() {
459 let n = STATE_SIZE + n_landmarks * LM_SIZE;
461 let mut new_x = DVector::zeros(n);
462 for i in 0..STATE_SIZE {
463 new_x[i] = state.x[i];
464 }
465 state.x = new_x;
467
468 let mut new_p = DMatrix::identity(n, n) * 1e6; for i in 0..STATE_SIZE {
471 for j in 0..STATE_SIZE {
472 new_p[(i, j)] = state.p[(i, j)];
473 }
474 }
475 state.p = new_p;
476 state.n_lm = n_landmarks;
477 }
478
479 for (d, angle, lm_id) in observations {
481 let z = Vector2::new(*d, *angle);
482
483 if *lm_id < state.n_lm {
485 let lm_idx = STATE_SIZE + lm_id * LM_SIZE;
487 if state.p[(lm_idx, lm_idx)] > 1e5 {
488 let robot_pose = state.get_robot_pose();
490 let lm_x = robot_pose[0] + z[0] * (robot_pose[2] + z[1]).cos();
491 let lm_y = robot_pose[1] + z[0] * (robot_pose[2] + z[1]).sin();
492 state.x[lm_idx] = lm_x;
493 state.x[lm_idx + 1] = lm_y;
494
495 let r = get_r();
497 let c = (robot_pose[2] + z[1]).cos();
498 let s = (robot_pose[2] + z[1]).sin();
499 let g_z = Matrix2::new(c, -z[0] * s, s, z[0] * c);
500 let p_lm = g_z * r * g_z.transpose();
501 state.p[(lm_idx, lm_idx)] = p_lm[(0, 0)] + 0.1;
502 state.p[(lm_idx, lm_idx + 1)] = p_lm[(0, 1)];
503 state.p[(lm_idx + 1, lm_idx)] = p_lm[(1, 0)];
504 state.p[(lm_idx + 1, lm_idx + 1)] = p_lm[(1, 1)] + 0.1;
505 } else {
506 ekf_slam_update(state, &z, *lm_id);
508 }
509 }
510 }
511}
512
513pub fn get_observations(x_true: &Vector3<f64>, landmarks: &[(f64, f64)]) -> Vec<(f64, f64)> {
515 let normal = Normal::new(0.0, 1.0).unwrap();
516 let r = get_r();
517 let mut z = Vec::new();
518
519 for (lx, ly) in landmarks.iter() {
520 let dx = lx - x_true[0];
521 let dy = ly - x_true[1];
522 let d = (dx * dx + dy * dy).sqrt();
523
524 if d <= MAX_RANGE {
525 let angle = normalize_angle(dy.atan2(dx) - x_true[2]);
526
527 let d_noisy = d + normal.sample(&mut rand::rng()) * r[(0, 0)].sqrt();
529 let angle_noisy = angle + normal.sample(&mut rand::rng()) * r[(1, 1)].sqrt();
530
531 z.push((d_noisy, angle_noisy));
532 }
533 }
534
535 z
536}
537
538pub fn get_observations_with_id(
540 x_true: &Vector3<f64>,
541 landmarks: &[(f64, f64)],
542) -> Vec<(f64, f64, usize)> {
543 let normal = Normal::new(0.0, 1.0).unwrap();
544 let r = get_r();
545 let mut z = Vec::new();
546
547 for (lm_id, (lx, ly)) in landmarks.iter().enumerate() {
548 let dx = lx - x_true[0];
549 let dy = ly - x_true[1];
550 let d = (dx * dx + dy * dy).sqrt();
551
552 if d <= MAX_RANGE {
553 let angle = normalize_angle(dy.atan2(dx) - x_true[2]);
554
555 let d_noisy = d + normal.sample(&mut rand::rng()) * r[(0, 0)].sqrt();
557 let angle_noisy = angle + normal.sample(&mut rand::rng()) * r[(1, 1)].sqrt();
558
559 z.push((d_noisy, angle_noisy, lm_id));
560 }
561 }
562
563 z
564}
565
566#[cfg(test)]
567mod tests {
568 use super::*;
569
570 #[test]
571 fn test_ekf_slam_state_creation() {
572 let state = EKFSLAMState::new();
573 assert_eq!(state.x.len(), STATE_SIZE);
574 assert_eq!(state.n_lm, 0);
575 }
576
577 #[test]
578 fn test_normalize_angle() {
579 assert!((normalize_angle(0.0) - 0.0).abs() < 1e-10);
580 assert!((normalize_angle(PI) - PI).abs() < 1e-10);
581 assert!((normalize_angle(2.0 * PI) - 0.0).abs() < 1e-10);
582 assert!((normalize_angle(-2.0 * PI) - 0.0).abs() < 1e-10);
583 assert!((normalize_angle(3.0 * PI).abs() - PI).abs() < 1e-10);
585 }
586
587 #[test]
588 fn test_motion_model() {
589 let x = Vector3::new(0.0, 0.0, 0.0);
590 let u = Vector2::new(1.0, 0.0); let new_x = motion_model(&x, &u);
592
593 assert!(new_x[0] > 0.0);
595 assert!(new_x[1].abs() < 1e-10);
596 assert!(new_x[2].abs() < 1e-10);
597 }
598
599 #[test]
600 fn test_add_landmark() {
601 let mut state = EKFSLAMState::new();
602 let z = Vector2::new(5.0, 0.0); add_new_landmark(&mut state, &z);
605
606 assert_eq!(state.n_lm, 1);
607 assert_eq!(state.x.len(), STATE_SIZE + LM_SIZE);
608
609 let lm = state.get_landmark(0).unwrap();
611 assert!((lm[0] - 5.0).abs() < 0.1);
612 assert!(lm[1].abs() < 0.1);
613 }
614
615 #[test]
616 fn test_ekf_slam_prediction() {
617 let mut state = EKFSLAMState::new();
618 let u = Vector2::new(1.0, 0.1);
619
620 ekf_slam_predict(&mut state, &u);
621
622 assert!(state.x[0] > 0.0);
624 assert!(state.x[2].abs() > 0.0);
625 }
626
627 #[test]
628 fn test_ekf_slam_full() {
629 let mut state = EKFSLAMState::new();
630 let u = Vector2::new(1.0, 0.0);
631 let observations = vec![(5.0, 0.0), (5.0, PI / 2.0)];
632
633 ekf_slam(&mut state, &u, &observations);
634
635 assert_eq!(state.n_lm, 2);
637 assert_eq!(state.x.len(), STATE_SIZE + 2 * LM_SIZE);
638 }
639}