1use nalgebra::{
5 allocator::Allocator, convert, dimension::U4, DVector, DefaultAllocator, Dyn, OMatrix,
6 RealField, SVector, U1, U8,
7};
8
9#[derive(Debug, Clone)]
30pub struct ConstantVelocityXYAHModel2<R>
31where
32 R: RealField,
33 DefaultAllocator: Allocator<U8, U8>,
34 DefaultAllocator: Allocator<U8>,
35{
36 pub mean: SVector<R, 8>,
38
39 pub std_weight_position: R,
42
43 pub std_weight_velocity: R,
46
47 pub update_factor: R,
51
52 motion_matrix: OMatrix<R, U8, U8>,
55
56 update_matrix: OMatrix<R, U4, U8>,
59
60 pub covariance: OMatrix<R, U8, U8>,
63}
64
65#[allow(dead_code)]
73pub enum GatingDistanceMetric {
74 Gaussian,
76 Mahalanobis,
78}
79
80impl<R> ConstantVelocityXYAHModel2<R>
81where
82 R: RealField + Copy,
83{
84 pub fn new(measurement: &[R; 4], update_factor: R) -> Self {
93 let ndim = 4;
94 let dt: R = convert(1.0);
95
96 let mut motion_matrix = OMatrix::<R, U8, U8>::identity();
97 for i in 0..ndim {
98 motion_matrix[(i, ndim + i)] = dt * convert(3.0);
99 }
100 let mut update_matrix = OMatrix::<R, U4, U8>::identity();
101 for i in 0..ndim {
102 update_matrix[(i, ndim + i)] = dt * convert(1.0);
103 }
104 let zero: R = convert(0.0);
105 let two: R = convert(2.0);
106 let ten: R = convert(10.0);
107 let height = measurement[3];
108
109 let mean = SVector::<R, 8>::from_row_slice(&[
110 measurement[0],
111 measurement[1],
112 measurement[2],
113 measurement[3],
114 zero,
115 zero,
116 zero,
117 zero,
118 ]);
119 let std_weight_position = convert(1.0 / 20.0);
120 let std_weight_velocity = convert(1.0 / 160.0);
121 let diag = [
122 two * std_weight_position * height,
123 two * std_weight_position * height,
124 convert(0.01),
125 two * std_weight_position * height,
126 ten * std_weight_velocity * height,
127 ten * std_weight_velocity * height,
128 convert(0.00001),
129 ten * std_weight_velocity * height,
130 ];
131 let diag = SVector::<R, 8>::from_row_slice(&diag);
132
133 let covariance = OMatrix::<R, U8, U8>::from_diagonal(&diag.component_mul(&diag));
134 Self {
135 motion_matrix,
136 update_matrix,
137 mean,
138 covariance,
139 std_weight_position,
140 std_weight_velocity,
141 update_factor,
142 }
143 }
144
145 pub fn predict(&mut self) {
152 let height = self.mean[3];
153 let diag = [
154 self.std_weight_position * height,
155 self.std_weight_position * height,
156 convert(0.01),
157 self.std_weight_position * height,
158 self.std_weight_velocity * height,
159 self.std_weight_velocity * height,
160 convert(0.00001),
161 self.std_weight_velocity * height,
162 ];
163 let diag = SVector::<R, 8>::from_row_slice(&diag);
164 let motion_cov = OMatrix::<R, U8, U8>::from_diagonal(&diag.component_mul(&diag));
165
166 let mean = (self.mean.transpose() * self.motion_matrix.transpose()).transpose();
167 let covariance =
168 self.motion_matrix * self.covariance * self.motion_matrix.transpose() + motion_cov;
169 self.mean = mean;
170 self.covariance = covariance;
171 }
172
173 pub fn project(&self) -> (OMatrix<R, U4, U1>, OMatrix<R, U4, U4>) {
180 let height = self.mean[3];
181 let diag = [
182 self.std_weight_position * height,
183 self.std_weight_position * height,
184 convert(0.01),
185 self.std_weight_position * height,
186 ];
187 let diag = SVector::<R, 4>::from_row_slice(&diag);
188 let innovation_cov = OMatrix::<R, U4, U4>::from_diagonal(&diag.component_mul(&diag));
189 let mean = self.update_matrix * self.mean;
190 let covariance =
191 self.update_matrix * self.covariance * self.update_matrix.transpose() + innovation_cov;
192 (mean, covariance)
193 }
194
195 pub fn update(&mut self, measurement: &[R; 4]) {
208 let measurement = SVector::<R, 4>::from_row_slice(&[
209 measurement[0],
210 measurement[1],
211 measurement[2],
212 measurement[3],
213 ]);
214
215 let (projected_mean, projected_cov) = self.project();
216 let cho_factor = match projected_cov.cholesky() {
217 None => return,
218 Some(v) => v,
219 };
220 let kalman_gain = cho_factor
221 .solve(&(self.covariance * self.update_matrix.transpose()).transpose())
222 .transpose();
223
224 let innovation = (measurement - projected_mean).scale(self.update_factor);
225 let diff = innovation.transpose() * kalman_gain.transpose();
228 self.mean += diff.transpose();
229 self.covariance -= kalman_gain * projected_cov * kalman_gain.transpose();
230 }
234
235 #[allow(dead_code)]
248 pub fn gating_distance(
249 &self,
250 measurements: &OMatrix<R, Dyn, U4>,
251 only_position: bool,
252 metric: GatingDistanceMetric,
253 ) -> DVector<R> {
254 let (m, cov) = self.project();
255 let ndims = if only_position { 2 } else { 4 };
256 let mean = m.transpose();
257 let mean = mean.columns_range(0..ndims);
258 let covariance = cov.view_range(0..ndims, 0..ndims);
259 let measurements = measurements.columns_range(0..ndims);
260 let mut mean_broadcast =
266 OMatrix::<R, Dyn, U4>::from_element(measurements.shape().0, convert(0.0));
267 for mut col in mean_broadcast.row_iter_mut() {
268 col.copy_from(&mean);
269 }
270 let d = measurements - mean_broadcast;
271 match metric {
272 GatingDistanceMetric::Gaussian => d.component_mul(&d).column_sum(),
273 GatingDistanceMetric::Mahalanobis => {
274 let cho_factor = match covariance.cholesky() {
275 None => return DVector::<R>::zeros(measurements.shape().0),
276 Some(v) => v,
277 };
278 let z = cho_factor.solve(&d.transpose());
279 z.component_mul(&z).row_sum_tr()
280 }
281 }
282 }
283}
284
285#[cfg(test)]
286mod tests {
287 use nalgebra::{Dyn, OMatrix, U4};
288
289 use super::{ConstantVelocityXYAHModel2, GatingDistanceMetric};
290 #[test]
291 fn filter() {
292 let mut t = ConstantVelocityXYAHModel2::new(&[0.5, 0.5, 1.0, 0.5], 0.25);
293 t.predict();
294 println!("1. t.mean={}", t.mean);
295 t.update(&[0.4, 0.5, 1.0, 0.5]);
296 t.predict();
297 println!("2. t.mean={}", t.mean);
298 t.update(&[0.3, 0.5, 1.0, 0.5]);
299 t.predict();
300 println!("3. t.mean={}", t.mean);
301 t.update(&[0.2, 0.5, 1.0, 0.5]);
302 t.predict();
303 println!("4. t.mean={}", t.mean);
304 t.update(&[0.2, 0.5, 1.0, 0.5]);
305 t.predict();
306 println!("5. t.mean={}", t.mean);
307 t.update(&[0.3, 0.5, 1.0, 0.5]);
308 t.predict();
309 println!("6. t.mean={}", t.mean);
310 t.update(&[0.4, 0.5, 1.0, 0.5]);
311 }
312
313 #[test]
314 fn gating() {
315 let mut t = ConstantVelocityXYAHModel2::new(&[0.5, 0.5, 1.0, 0.5], 0.25);
316 t.predict();
317 t.update(&[0.49, 0.5, 1.0, 0.5]);
318 t.predict();
319 t.update(&[0.48, 0.5, 1.0, 0.5]);
320 t.predict();
321 t.update(&[0.47, 0.5, 1.0, 0.5]);
322 t.predict();
323 t.update(&[0.46, 0.5, 1.0, 0.5]);
324 t.predict();
325 t.update(&[0.45, 0.5, 1.0, 0.5]);
326 t.predict();
327 t.update(&[0.44, 0.5, 1.0, 0.5]);
328 t.predict();
329 t.update(&[0.43, 0.5, 1.0, 0.5]);
330 t.predict();
331 t.update(&[0.42, 0.5, 1.0, 0.5]);
332 t.predict();
333
334 let mut measurements = OMatrix::<f32, Dyn, U4>::from_element(1, 0.0);
336 measurements.copy_from_slice(&[0.3, 0.5, 1.0, 0.5]);
337
338 let mut distances = OMatrix::<f32, Dyn, Dyn>::from_element(1, 1, 0.0);
339 for mut column in distances.column_iter_mut() {
340 let dist = t.gating_distance(&measurements, false, GatingDistanceMetric::Gaussian);
341 column.copy_from(&dist);
342 }
343 let dist = t.gating_distance(&measurements, false, GatingDistanceMetric::Mahalanobis);
344 println!("Dist(false, maha): {dist}");
345
346 let dist = t.gating_distance(&measurements, false, GatingDistanceMetric::Gaussian);
347 println!("Dist(false, gaussian): {dist}");
348 }
349
350 #[test]
351 fn test_predict_constant_velocity() {
352 let mut t = ConstantVelocityXYAHModel2::new(&[0.5, 0.5, 0.1, 2.0], 0.25);
355 t.predict();
356 t.update(&[0.5, 0.5, 0.1, 2.0]);
357
358 let x_before: f32 = t.mean[0];
360 let y_before: f32 = t.mean[1];
361
362 for _ in 0..5 {
364 t.predict();
365 }
366
367 let x_after: f32 = t.mean[0];
368 let y_after: f32 = t.mean[1];
369 let h_after: f32 = t.mean[3];
370
371 assert!(x_after.is_finite(), "x should be finite after predictions");
373 assert!(y_after.is_finite(), "y should be finite after predictions");
374 assert!(
375 h_after.is_finite(),
376 "height should be finite after predictions"
377 );
378
379 assert!(
381 (x_after - x_before).abs() < 5.0,
382 "x drift should be bounded, got delta={}",
383 (x_after - x_before).abs()
384 );
385 assert!(
386 (y_after - y_before).abs() < 5.0,
387 "y drift should be bounded, got delta={}",
388 (y_after - y_before).abs()
389 );
390 }
391
392 #[test]
393 fn test_numerical_stability_1000_cycles() {
394 let mut t = ConstantVelocityXYAHModel2::new(&[0.5, 0.5, 1.0, 0.5], 0.25);
395
396 for _ in 0..1000 {
398 t.predict();
399 }
400
401 for i in 0..8 {
403 let val: f32 = t.mean[i];
404 assert!(
405 val.is_finite(),
406 "mean[{i}] should be finite after 1000 predictions, got {val}",
407 );
408 }
409
410 for r in 0..8 {
412 for c in 0..8 {
413 let val: f32 = t.covariance[(r, c)];
414 assert!(
415 val.is_finite(),
416 "covariance[({r},{c})] should be finite after 1000 predictions, got {val}",
417 );
418 }
419 }
420 }
421
422 #[test]
423 fn test_gating_distance_edge_cases() {
424 let mut t = ConstantVelocityXYAHModel2::new(&[0.5, 0.5, 1.0, 0.5], 0.25);
425 for _ in 0..3 {
427 t.predict();
428 t.update(&[0.5, 0.5, 1.0, 0.5]);
429 }
430 t.predict();
431
432 let (projected_mean, _) = t.project();
434 let mut meas_close = OMatrix::<f32, Dyn, U4>::from_element(1, 0.0);
435 meas_close
436 .row_mut(0)
437 .copy_from_slice(projected_mean.as_slice());
438
439 let dist_close = t.gating_distance(&meas_close, false, GatingDistanceMetric::Mahalanobis);
440 assert!(
441 dist_close[0].is_finite(),
442 "Close-measurement distance should be finite"
443 );
444 assert!(
445 dist_close[0] < 1.0,
446 "Distance for exact-match measurement should be near 0, got {}",
447 dist_close[0]
448 );
449
450 let mut meas_far = OMatrix::<f32, Dyn, U4>::from_element(1, 0.0);
452 meas_far.copy_from_slice(&[10.0, 10.0, 5.0, 10.0]);
453
454 let dist_far = t.gating_distance(&meas_far, false, GatingDistanceMetric::Mahalanobis);
455 assert!(
456 dist_far[0].is_finite(),
457 "Far-measurement distance should be finite"
458 );
459 assert!(
460 dist_far[0] > dist_close[0],
461 "Far measurement should have larger distance than close one: {} vs {}",
462 dist_far[0],
463 dist_close[0]
464 );
465 }
466
467 #[test]
468 fn test_update_moves_mean_toward_measurement() {
469 let mut t = ConstantVelocityXYAHModel2::new(&[0.5, 0.5, 1.0, 0.5], 0.25);
470 t.predict();
471
472 let x_before: f32 = t.mean[0];
473 t.update(&[0.6, 0.5, 1.0, 0.5]);
475 let x_after: f32 = t.mean[0];
476
477 assert!(
478 x_after > x_before,
479 "Mean x should move toward the measurement (0.6), was {x_before}, now {x_after}"
480 );
481 assert!(
482 x_after <= 0.6,
483 "Mean x should not overshoot the measurement, got {x_after}"
484 );
485 }
486
487 #[test]
488 fn test_covariance_positive_diagonal() {
489 let t = ConstantVelocityXYAHModel2::new(&[0.5, 0.5, 1.0, 0.5], 0.25);
490
491 for i in 0..8 {
493 let val: f32 = t.covariance[(i, i)];
494 assert!(
495 val > 0.0,
496 "Covariance diagonal[{i}] should be positive, got {val}"
497 );
498 }
499 }
500
501 #[test]
502 fn test_predict_increases_uncertainty() {
503 let mut t = ConstantVelocityXYAHModel2::new(&[0.5, 0.5, 1.0, 0.5], 0.25);
504
505 let cov_before: f32 = t.covariance[(0, 0)];
506 t.predict();
507 let cov_after: f32 = t.covariance[(0, 0)];
508
509 assert!(
510 cov_after > cov_before,
511 "Predict should increase position uncertainty: {cov_before} -> {cov_after}"
512 );
513 }
514
515 #[test]
516 fn test_update_decreases_uncertainty() {
517 let mut t = ConstantVelocityXYAHModel2::new(&[0.5, 0.5, 1.0, 0.5], 0.25);
518 t.predict();
519
520 let cov_before: f32 = t.covariance[(0, 0)];
521 t.update(&[0.5, 0.5, 1.0, 0.5]);
522 let cov_after: f32 = t.covariance[(0, 0)];
523
524 assert!(
525 cov_after < cov_before,
526 "Update should decrease position uncertainty: {cov_before} -> {cov_after}"
527 );
528 }
529
530 #[test]
531 fn test_gating_distance_gaussian_vs_mahalanobis() {
532 let mut t = ConstantVelocityXYAHModel2::new(&[0.5, 0.5, 1.0, 0.5], 0.25);
533 for _ in 0..3 {
534 t.predict();
535 t.update(&[0.5, 0.5, 1.0, 0.5]);
536 }
537 t.predict();
538
539 let mut measurements = OMatrix::<f32, Dyn, U4>::from_element(1, 0.0);
540 measurements.copy_from_slice(&[0.6, 0.5, 1.0, 0.5]);
541
542 let dist_gauss = t.gating_distance(&measurements, false, GatingDistanceMetric::Gaussian);
543 let dist_maha = t.gating_distance(&measurements, false, GatingDistanceMetric::Mahalanobis);
544
545 assert!(dist_gauss[0].is_finite());
546 assert!(dist_maha[0].is_finite());
547
548 assert!(
550 dist_gauss[0] > 0.0,
551 "Gaussian distance should be > 0 for offset measurement"
552 );
553 assert!(
554 dist_maha[0] > 0.0,
555 "Mahalanobis distance should be > 0 for offset measurement"
556 );
557 }
558
559 #[test]
560 fn test_gating_distance_multiple_measurements() {
561 let mut t = ConstantVelocityXYAHModel2::new(&[0.5, 0.5, 1.0, 0.5], 0.25);
562 t.predict();
563 t.update(&[0.5, 0.5, 1.0, 0.5]);
564 t.predict();
565
566 let mut measurements = OMatrix::<f32, Dyn, U4>::from_element(2, 0.0);
568 measurements
569 .row_mut(0)
570 .copy_from_slice(&[0.5, 0.5, 1.0, 0.5]); measurements
572 .row_mut(1)
573 .copy_from_slice(&[5.0, 5.0, 1.0, 0.5]); let dists = t.gating_distance(&measurements, false, GatingDistanceMetric::Mahalanobis);
576 assert_eq!(dists.len(), 2, "Should return one distance per measurement");
577 assert!(dists[0].is_finite());
578 assert!(dists[1].is_finite());
579 assert!(
580 dists[1] > dists[0],
581 "Far measurement should have larger distance: {} vs {}",
582 dists[1],
583 dists[0]
584 );
585 }
586
587 #[test]
588 fn test_initiate_mean_matches_measurement() {
589 let measurement = [0.3, 0.7, 1.5, 2.0];
590 let t = ConstantVelocityXYAHModel2::new(&measurement, 0.25);
591
592 let x: f32 = t.mean[0];
594 let y: f32 = t.mean[1];
595 let a: f32 = t.mean[2];
596 let h: f32 = t.mean[3];
597 assert!((x - 0.3).abs() < 1e-6, "Mean x should be 0.3, got {x}");
598 assert!((y - 0.7).abs() < 1e-6, "Mean y should be 0.7, got {y}");
599 assert!((a - 1.5).abs() < 1e-6, "Mean a should be 1.5, got {a}");
600 assert!((h - 2.0).abs() < 1e-6, "Mean h should be 2.0, got {h}");
601
602 for i in 4..8 {
604 let v: f32 = t.mean[i];
605 assert!((v).abs() < 1e-6, "Velocity mean[{i}] should be 0, got {v}");
606 }
607 }
608}