1use nalgebra::{
18 allocator::Allocator, convert, dimension::U4, DVector, DefaultAllocator, Dyn, OMatrix,
19 RealField, SVector, U1, U8,
20};
21
22#[derive(Debug, Clone)]
43pub struct ConstantVelocityXYAHModel2<R>
44where
45 R: RealField,
46 DefaultAllocator: Allocator<U8, U8>,
47 DefaultAllocator: Allocator<U8>,
48{
49 pub mean: SVector<R, 8>,
51
52 pub std_weight_position: R,
55
56 pub std_weight_velocity: R,
59
60 pub update_factor: R,
64
65 motion_matrix: OMatrix<R, U8, U8>,
68
69 update_matrix: OMatrix<R, U4, U8>,
72
73 pub covariance: OMatrix<R, U8, U8>,
76}
77
78#[allow(dead_code)]
86pub enum GatingDistanceMetric {
87 Gaussian,
89 Mahalanobis,
91}
92
93impl<R> ConstantVelocityXYAHModel2<R>
94where
95 R: RealField + Copy,
96{
97 pub fn new(measurement: &[R; 4], update_factor: R) -> Self {
106 let ndim = 4;
107 let dt: R = convert(1.0);
108
109 let mut motion_matrix = OMatrix::<R, U8, U8>::identity();
110 for i in 0..ndim {
111 motion_matrix[(i, ndim + i)] = dt * convert(3.0);
112 }
113 let mut update_matrix = OMatrix::<R, U4, U8>::identity();
114 for i in 0..ndim {
115 update_matrix[(i, ndim + i)] = dt * convert(1.0);
116 }
117 let zero: R = convert(0.0);
118 let two: R = convert(2.0);
119 let ten: R = convert(10.0);
120 let height = measurement[3];
121
122 let mean = SVector::<R, 8>::from_row_slice(&[
123 measurement[0],
124 measurement[1],
125 measurement[2],
126 measurement[3],
127 zero,
128 zero,
129 zero,
130 zero,
131 ]);
132 let std_weight_position = convert(1.0 / 20.0);
133 let std_weight_velocity = convert(1.0 / 160.0);
134 let diag = [
135 two * std_weight_position * height,
136 two * std_weight_position * height,
137 convert(0.01),
138 two * std_weight_position * height,
139 ten * std_weight_velocity * height,
140 ten * std_weight_velocity * height,
141 convert(0.00001),
142 ten * std_weight_velocity * height,
143 ];
144 let diag = SVector::<R, 8>::from_row_slice(&diag);
145
146 let covariance = OMatrix::<R, U8, U8>::from_diagonal(&diag.component_mul(&diag));
147 Self {
148 motion_matrix,
149 update_matrix,
150 mean,
151 covariance,
152 std_weight_position,
153 std_weight_velocity,
154 update_factor,
155 }
156 }
157
158 pub fn predict(&mut self) {
165 let height = self.mean[3];
166 let diag = [
167 self.std_weight_position * height,
168 self.std_weight_position * height,
169 convert(0.01),
170 self.std_weight_position * height,
171 self.std_weight_velocity * height,
172 self.std_weight_velocity * height,
173 convert(0.00001),
174 self.std_weight_velocity * height,
175 ];
176 let diag = SVector::<R, 8>::from_row_slice(&diag);
177 let motion_cov = OMatrix::<R, U8, U8>::from_diagonal(&diag.component_mul(&diag));
178
179 let mean = (self.mean.transpose() * self.motion_matrix.transpose()).transpose();
180 let covariance =
181 self.motion_matrix * self.covariance * self.motion_matrix.transpose() + motion_cov;
182 self.mean = mean;
183 self.covariance = covariance;
184 }
185
186 pub fn project(&self) -> (OMatrix<R, U4, U1>, OMatrix<R, U4, U4>) {
193 let height = self.mean[3];
194 let diag = [
195 self.std_weight_position * height,
196 self.std_weight_position * height,
197 convert(0.01),
198 self.std_weight_position * height,
199 ];
200 let diag = SVector::<R, 4>::from_row_slice(&diag);
201 let innovation_cov = OMatrix::<R, U4, U4>::from_diagonal(&diag.component_mul(&diag));
202 let mean = self.update_matrix * self.mean;
203 let covariance =
204 self.update_matrix * self.covariance * self.update_matrix.transpose() + innovation_cov;
205 (mean, covariance)
206 }
207
208 pub fn update(&mut self, measurement: &[R; 4]) {
221 let measurement = SVector::<R, 4>::from_row_slice(&[
222 measurement[0],
223 measurement[1],
224 measurement[2],
225 measurement[3],
226 ]);
227
228 let (projected_mean, projected_cov) = self.project();
229 let cho_factor = match projected_cov.cholesky() {
230 None => return,
231 Some(v) => v,
232 };
233 let kalman_gain = cho_factor
234 .solve(&(self.covariance * self.update_matrix.transpose()).transpose())
235 .transpose();
236
237 let innovation = (measurement - projected_mean).scale(self.update_factor);
238 let diff = innovation.transpose() * kalman_gain.transpose();
241 self.mean += diff.transpose();
242 self.covariance -= kalman_gain * projected_cov * kalman_gain.transpose();
243 }
247
248 #[allow(dead_code)]
261 pub fn gating_distance(
262 &self,
263 measurements: &OMatrix<R, Dyn, U4>,
264 only_position: bool,
265 metric: GatingDistanceMetric,
266 ) -> DVector<R> {
267 let (m, cov) = self.project();
268 let ndims = if only_position { 2 } else { 4 };
269 let mean = m.transpose();
270 let mean = mean.columns_range(0..ndims);
271 let covariance = cov.view_range(0..ndims, 0..ndims);
272 let measurements = measurements.columns_range(0..ndims);
273 let mut mean_broadcast =
279 OMatrix::<R, Dyn, U4>::from_element(measurements.shape().0, convert(0.0));
280 for mut col in mean_broadcast.row_iter_mut() {
281 col.copy_from(&mean);
282 }
283 let d = measurements - mean_broadcast;
284 match metric {
285 GatingDistanceMetric::Gaussian => d.component_mul(&d).column_sum(),
286 GatingDistanceMetric::Mahalanobis => {
287 let cho_factor = match covariance.cholesky() {
288 None => return DVector::<R>::zeros(measurements.shape().0),
289 Some(v) => v,
290 };
291 let z = cho_factor.solve(&d.transpose());
292 z.component_mul(&z).row_sum_tr()
293 }
294 }
295 }
296}
297
298#[cfg(test)]
299mod tests {
300 use nalgebra::{Dyn, OMatrix, U4};
301
302 use super::{ConstantVelocityXYAHModel2, GatingDistanceMetric};
303 #[test]
304 fn filter() {
305 let mut t = ConstantVelocityXYAHModel2::new(&[0.5, 0.5, 1.0, 0.5], 0.25);
306 t.predict();
307 println!("1. t.mean={}", t.mean);
308 t.update(&[0.4, 0.5, 1.0, 0.5]);
309 t.predict();
310 println!("2. t.mean={}", t.mean);
311 t.update(&[0.3, 0.5, 1.0, 0.5]);
312 t.predict();
313 println!("3. t.mean={}", t.mean);
314 t.update(&[0.2, 0.5, 1.0, 0.5]);
315 t.predict();
316 println!("4. t.mean={}", t.mean);
317 t.update(&[0.2, 0.5, 1.0, 0.5]);
318 t.predict();
319 println!("5. t.mean={}", t.mean);
320 t.update(&[0.3, 0.5, 1.0, 0.5]);
321 t.predict();
322 println!("6. t.mean={}", t.mean);
323 t.update(&[0.4, 0.5, 1.0, 0.5]);
324 }
325
326 #[test]
327 fn gating() {
328 let mut t = ConstantVelocityXYAHModel2::new(&[0.5, 0.5, 1.0, 0.5], 0.25);
329 t.predict();
330 t.update(&[0.49, 0.5, 1.0, 0.5]);
331 t.predict();
332 t.update(&[0.48, 0.5, 1.0, 0.5]);
333 t.predict();
334 t.update(&[0.47, 0.5, 1.0, 0.5]);
335 t.predict();
336 t.update(&[0.46, 0.5, 1.0, 0.5]);
337 t.predict();
338 t.update(&[0.45, 0.5, 1.0, 0.5]);
339 t.predict();
340 t.update(&[0.44, 0.5, 1.0, 0.5]);
341 t.predict();
342 t.update(&[0.43, 0.5, 1.0, 0.5]);
343 t.predict();
344 t.update(&[0.42, 0.5, 1.0, 0.5]);
345 t.predict();
346
347 let mut measurements = OMatrix::<f32, Dyn, U4>::from_element(1, 0.0);
349 measurements.copy_from_slice(&[0.3, 0.5, 1.0, 0.5]);
350
351 let mut distances = OMatrix::<f32, Dyn, Dyn>::from_element(1, 1, 0.0);
352 for mut column in distances.column_iter_mut() {
353 let dist = t.gating_distance(&measurements, false, GatingDistanceMetric::Gaussian);
354 column.copy_from(&dist);
355 }
356 let dist = t.gating_distance(&measurements, false, GatingDistanceMetric::Mahalanobis);
357 println!("Dist(false, maha): {dist}");
358
359 let dist = t.gating_distance(&measurements, false, GatingDistanceMetric::Gaussian);
360 println!("Dist(false, gaussian): {dist}");
361 }
362
363 #[test]
364 fn test_predict_constant_velocity() {
365 let mut t = ConstantVelocityXYAHModel2::new(&[0.5, 0.5, 0.1, 2.0], 0.25);
368 t.predict();
369 t.update(&[0.5, 0.5, 0.1, 2.0]);
370
371 let x_before: f32 = t.mean[0];
373 let y_before: f32 = t.mean[1];
374
375 for _ in 0..5 {
377 t.predict();
378 }
379
380 let x_after: f32 = t.mean[0];
381 let y_after: f32 = t.mean[1];
382 let h_after: f32 = t.mean[3];
383
384 assert!(x_after.is_finite(), "x should be finite after predictions");
386 assert!(y_after.is_finite(), "y should be finite after predictions");
387 assert!(
388 h_after.is_finite(),
389 "height should be finite after predictions"
390 );
391
392 assert!(
394 (x_after - x_before).abs() < 5.0,
395 "x drift should be bounded, got delta={}",
396 (x_after - x_before).abs()
397 );
398 assert!(
399 (y_after - y_before).abs() < 5.0,
400 "y drift should be bounded, got delta={}",
401 (y_after - y_before).abs()
402 );
403 }
404
405 #[test]
406 fn test_numerical_stability_1000_cycles() {
407 let mut t = ConstantVelocityXYAHModel2::new(&[0.5, 0.5, 1.0, 0.5], 0.25);
408
409 for _ in 0..1000 {
411 t.predict();
412 }
413
414 for i in 0..8 {
416 let val: f32 = t.mean[i];
417 assert!(
418 val.is_finite(),
419 "mean[{i}] should be finite after 1000 predictions, got {val}",
420 );
421 }
422
423 for r in 0..8 {
425 for c in 0..8 {
426 let val: f32 = t.covariance[(r, c)];
427 assert!(
428 val.is_finite(),
429 "covariance[({r},{c})] should be finite after 1000 predictions, got {val}",
430 );
431 }
432 }
433 }
434
435 #[test]
436 fn test_gating_distance_edge_cases() {
437 let mut t = ConstantVelocityXYAHModel2::new(&[0.5, 0.5, 1.0, 0.5], 0.25);
438 for _ in 0..3 {
440 t.predict();
441 t.update(&[0.5, 0.5, 1.0, 0.5]);
442 }
443 t.predict();
444
445 let (projected_mean, _) = t.project();
447 let mut meas_close = OMatrix::<f32, Dyn, U4>::from_element(1, 0.0);
448 meas_close
449 .row_mut(0)
450 .copy_from_slice(projected_mean.as_slice());
451
452 let dist_close = t.gating_distance(&meas_close, false, GatingDistanceMetric::Mahalanobis);
453 assert!(
454 dist_close[0].is_finite(),
455 "Close-measurement distance should be finite"
456 );
457 assert!(
458 dist_close[0] < 1.0,
459 "Distance for exact-match measurement should be near 0, got {}",
460 dist_close[0]
461 );
462
463 let mut meas_far = OMatrix::<f32, Dyn, U4>::from_element(1, 0.0);
465 meas_far.copy_from_slice(&[10.0, 10.0, 5.0, 10.0]);
466
467 let dist_far = t.gating_distance(&meas_far, false, GatingDistanceMetric::Mahalanobis);
468 assert!(
469 dist_far[0].is_finite(),
470 "Far-measurement distance should be finite"
471 );
472 assert!(
473 dist_far[0] > dist_close[0],
474 "Far measurement should have larger distance than close one: {} vs {}",
475 dist_far[0],
476 dist_close[0]
477 );
478 }
479
480 #[test]
481 fn test_update_moves_mean_toward_measurement() {
482 let mut t = ConstantVelocityXYAHModel2::new(&[0.5, 0.5, 1.0, 0.5], 0.25);
483 t.predict();
484
485 let x_before: f32 = t.mean[0];
486 t.update(&[0.6, 0.5, 1.0, 0.5]);
488 let x_after: f32 = t.mean[0];
489
490 assert!(
491 x_after > x_before,
492 "Mean x should move toward the measurement (0.6), was {x_before}, now {x_after}"
493 );
494 assert!(
495 x_after <= 0.6,
496 "Mean x should not overshoot the measurement, got {x_after}"
497 );
498 }
499
500 #[test]
501 fn test_covariance_positive_diagonal() {
502 let t = ConstantVelocityXYAHModel2::new(&[0.5, 0.5, 1.0, 0.5], 0.25);
503
504 for i in 0..8 {
506 let val: f32 = t.covariance[(i, i)];
507 assert!(
508 val > 0.0,
509 "Covariance diagonal[{i}] should be positive, got {val}"
510 );
511 }
512 }
513
514 #[test]
515 fn test_predict_increases_uncertainty() {
516 let mut t = ConstantVelocityXYAHModel2::new(&[0.5, 0.5, 1.0, 0.5], 0.25);
517
518 let cov_before: f32 = t.covariance[(0, 0)];
519 t.predict();
520 let cov_after: f32 = t.covariance[(0, 0)];
521
522 assert!(
523 cov_after > cov_before,
524 "Predict should increase position uncertainty: {cov_before} -> {cov_after}"
525 );
526 }
527
528 #[test]
529 fn test_update_decreases_uncertainty() {
530 let mut t = ConstantVelocityXYAHModel2::new(&[0.5, 0.5, 1.0, 0.5], 0.25);
531 t.predict();
532
533 let cov_before: f32 = t.covariance[(0, 0)];
534 t.update(&[0.5, 0.5, 1.0, 0.5]);
535 let cov_after: f32 = t.covariance[(0, 0)];
536
537 assert!(
538 cov_after < cov_before,
539 "Update should decrease position uncertainty: {cov_before} -> {cov_after}"
540 );
541 }
542
543 #[test]
544 fn test_gating_distance_gaussian_vs_mahalanobis() {
545 let mut t = ConstantVelocityXYAHModel2::new(&[0.5, 0.5, 1.0, 0.5], 0.25);
546 for _ in 0..3 {
547 t.predict();
548 t.update(&[0.5, 0.5, 1.0, 0.5]);
549 }
550 t.predict();
551
552 let mut measurements = OMatrix::<f32, Dyn, U4>::from_element(1, 0.0);
553 measurements.copy_from_slice(&[0.6, 0.5, 1.0, 0.5]);
554
555 let dist_gauss = t.gating_distance(&measurements, false, GatingDistanceMetric::Gaussian);
556 let dist_maha = t.gating_distance(&measurements, false, GatingDistanceMetric::Mahalanobis);
557
558 assert!(dist_gauss[0].is_finite());
559 assert!(dist_maha[0].is_finite());
560
561 assert!(
563 dist_gauss[0] > 0.0,
564 "Gaussian distance should be > 0 for offset measurement"
565 );
566 assert!(
567 dist_maha[0] > 0.0,
568 "Mahalanobis distance should be > 0 for offset measurement"
569 );
570 }
571
572 #[test]
573 fn test_gating_distance_multiple_measurements() {
574 let mut t = ConstantVelocityXYAHModel2::new(&[0.5, 0.5, 1.0, 0.5], 0.25);
575 t.predict();
576 t.update(&[0.5, 0.5, 1.0, 0.5]);
577 t.predict();
578
579 let mut measurements = OMatrix::<f32, Dyn, U4>::from_element(2, 0.0);
581 measurements
582 .row_mut(0)
583 .copy_from_slice(&[0.5, 0.5, 1.0, 0.5]); measurements
585 .row_mut(1)
586 .copy_from_slice(&[5.0, 5.0, 1.0, 0.5]); let dists = t.gating_distance(&measurements, false, GatingDistanceMetric::Mahalanobis);
589 assert_eq!(dists.len(), 2, "Should return one distance per measurement");
590 assert!(dists[0].is_finite());
591 assert!(dists[1].is_finite());
592 assert!(
593 dists[1] > dists[0],
594 "Far measurement should have larger distance: {} vs {}",
595 dists[1],
596 dists[0]
597 );
598 }
599
600 #[test]
601 fn test_initiate_mean_matches_measurement() {
602 let measurement = [0.3, 0.7, 1.5, 2.0];
603 let t = ConstantVelocityXYAHModel2::new(&measurement, 0.25);
604
605 let x: f32 = t.mean[0];
607 let y: f32 = t.mean[1];
608 let a: f32 = t.mean[2];
609 let h: f32 = t.mean[3];
610 assert!((x - 0.3).abs() < 1e-6, "Mean x should be 0.3, got {x}");
611 assert!((y - 0.7).abs() < 1e-6, "Mean y should be 0.7, got {y}");
612 assert!((a - 1.5).abs() < 1e-6, "Mean a should be 1.5, got {a}");
613 assert!((h - 2.0).abs() < 1e-6, "Mean h should be 2.0, got {h}");
614
615 for i in 4..8 {
617 let v: f32 = t.mean[i];
618 assert!((v).abs() < 1e-6, "Velocity mean[{i}] should be 0, got {v}");
619 }
620 }
621}