1use core::time::Duration;
16
17use crate::error::{ensure_finite, KernelError, Result};
18use crate::event::SensorId;
19use crate::inline::Inline;
20use crate::matrix::Matrix;
21use crate::state::{NavigationState, StateComponent, STATE_DIM};
22use crate::time::{Instant, Utc};
23
24pub const MAX_OBSERVATION_DIM: usize = 4;
29
30#[derive(Debug, Clone, Copy, PartialEq)]
32pub struct ObservationVector {
33 elements: Inline<f64, MAX_OBSERVATION_DIM>,
34}
35
36impl ObservationVector {
37 pub const EMPTY: Self = Self {
39 elements: Inline::new(0.0),
40 };
41
42 pub fn new(elements: &[f64]) -> Result<Self> {
49 let mut inline = Inline::new(0.0);
50 for &element in elements {
51 ensure_finite("observation element", element)?;
52 inline
53 .push(element)
54 .map_err(|full| KernelError::CapacityExceeded {
55 context: "an observation",
56 needed: elements.len(),
57 capacity: full.capacity,
58 })?;
59 }
60 Ok(Self { elements: inline })
61 }
62
63 #[must_use]
65 pub fn as_slice(&self) -> &[f64] {
66 self.elements.as_slice()
67 }
68
69 #[must_use]
71 pub const fn len(&self) -> usize {
72 self.elements.len()
73 }
74
75 #[must_use]
77 pub const fn is_empty(&self) -> bool {
78 self.elements.is_empty()
79 }
80
81 #[must_use]
83 pub fn get(&self, index: usize) -> Option<f64> {
84 self.elements.get(index).copied()
85 }
86
87 #[must_use]
89 pub fn minus(&self, other: &Self) -> Option<Self> {
90 if self.len() != other.len() {
91 return None;
92 }
93 let mut elements = Inline::new(0.0);
94 for (a, b) in self.as_slice().iter().zip(other.as_slice()) {
95 elements.push(a - b).ok()?;
96 }
97 Some(Self { elements })
98 }
99}
100
101#[derive(Debug, Clone, Copy, PartialEq)]
106pub struct JacobianRow {
107 entries: [f64; STATE_DIM],
108}
109
110impl JacobianRow {
111 #[must_use]
113 pub const fn new() -> Self {
114 Self {
115 entries: [0.0; STATE_DIM],
116 }
117 }
118
119 #[must_use]
121 pub fn with(mut self, component: StateComponent, derivative: f64) -> Self {
122 if let Some(slot) = self.entries.get_mut(component.index()) {
123 *slot = derivative;
124 }
125 self
126 }
127
128 #[must_use]
130 pub fn derivative(&self, component: StateComponent) -> f64 {
131 self.entries.get(component.index()).copied().unwrap_or(0.0)
132 }
133
134 #[doc(hidden)]
139 #[must_use]
140 pub const fn entries(&self) -> &[f64; STATE_DIM] {
141 &self.entries
142 }
143}
144
145impl Default for JacobianRow {
146 fn default() -> Self {
147 Self::new()
148 }
149}
150
151#[derive(Debug, Clone, Copy, PartialEq)]
153pub struct ObservationJacobian {
154 rows: Inline<JacobianRow, MAX_OBSERVATION_DIM>,
155}
156
157impl ObservationJacobian {
158 #[must_use]
160 pub const fn new() -> Self {
161 Self {
162 rows: Inline::new(JacobianRow::new()),
163 }
164 }
165
166 pub fn with_row(mut self, row: JacobianRow) -> Result<Self> {
173 for &entry in row.entries() {
174 ensure_finite("jacobian entry", entry)?;
175 }
176 let needed = self.rows.len().saturating_add(1);
177 self.rows
178 .push(row)
179 .map_err(|full| KernelError::CapacityExceeded {
180 context: "an observation jacobian",
181 needed,
182 capacity: full.capacity,
183 })?;
184 Ok(self)
185 }
186
187 #[must_use]
189 pub const fn len(&self) -> usize {
190 self.rows.len()
191 }
192
193 #[must_use]
195 pub const fn is_empty(&self) -> bool {
196 self.rows.is_empty()
197 }
198
199 #[must_use]
201 pub fn rows(&self) -> &[JacobianRow] {
202 self.rows.as_slice()
203 }
204}
205
206impl Default for ObservationJacobian {
207 fn default() -> Self {
208 Self::new()
209 }
210}
211
212#[derive(Debug, Clone, Copy, PartialEq)]
216pub struct ObservationNoise {
217 entries: [[f64; MAX_OBSERVATION_DIM]; MAX_OBSERVATION_DIM],
218 len: usize,
219}
220
221impl ObservationNoise {
222 pub const UNIT: Self = {
224 let mut entries = [[0.0; MAX_OBSERVATION_DIM]; MAX_OBSERVATION_DIM];
225 entries[0][0] = 1.0;
226 Self { entries, len: 1 }
227 };
228
229 pub fn diagonal(variances: &[f64]) -> Result<Self> {
237 if variances.len() > MAX_OBSERVATION_DIM {
238 return Err(KernelError::CapacityExceeded {
239 context: "an observation noise",
240 needed: variances.len(),
241 capacity: MAX_OBSERVATION_DIM,
242 });
243 }
244 let mut entries = [[0.0; MAX_OBSERVATION_DIM]; MAX_OBSERVATION_DIM];
245 for (index, &variance) in variances.iter().enumerate() {
246 ensure_finite("observation variance", variance)?;
247 if variance <= 0.0 {
248 return Err(KernelError::OutOfRange {
249 parameter: "observation variance",
250 value: variance,
251 min: f64::MIN_POSITIVE,
252 max: f64::MAX,
253 });
254 }
255 if let Some(slot) = entries.get_mut(index).and_then(|row| row.get_mut(index)) {
256 *slot = variance;
257 }
258 }
259 Ok(Self {
260 entries,
261 len: variances.len(),
262 })
263 }
264
265 pub fn sigmas(sigmas: &[f64]) -> Result<Self> {
271 let mut variances = Inline::<f64, MAX_OBSERVATION_DIM>::new(0.0);
272 for &sigma in sigmas {
273 variances
274 .push(sigma * sigma)
275 .map_err(|full| KernelError::CapacityExceeded {
276 context: "an observation noise",
277 needed: sigmas.len(),
278 capacity: full.capacity,
279 })?;
280 }
281 Self::diagonal(variances.as_slice())
282 }
283
284 pub fn full(rows: &[&[f64]]) -> Result<Self> {
291 let len = rows.len();
292 if len > MAX_OBSERVATION_DIM {
293 return Err(KernelError::CapacityExceeded {
294 context: "an observation noise",
295 needed: len,
296 capacity: MAX_OBSERVATION_DIM,
297 });
298 }
299 let mut entries = [[0.0; MAX_OBSERVATION_DIM]; MAX_OBSERVATION_DIM];
300 for (i, row) in rows.iter().enumerate() {
301 if row.len() != len {
302 return Err(KernelError::NotCovariance {
303 context: "an observation noise with rows of unequal length",
304 });
305 }
306 for (j, &value) in row.iter().enumerate() {
307 ensure_finite("observation covariance", value)?;
308 if let Some(slot) = entries.get_mut(i).and_then(|row| row.get_mut(j)) {
309 *slot = value;
310 }
311 }
312 }
313 let noise = Self { entries, len };
314 let padded = Matrix::<MAX_OBSERVATION_DIM, MAX_OBSERVATION_DIM>::from_fn(|i, j| {
317 if i < len && j < len {
318 noise.get(i, j).unwrap_or(0.0)
319 } else if i == j {
320 1.0
321 } else {
322 0.0
323 }
324 });
325 if !padded.is_covariance() || (0..len).any(|i| noise.get(i, i).unwrap_or(0.0) <= 0.0) {
326 return Err(KernelError::NotCovariance {
327 context: "an observation noise that is not positive definite",
328 });
329 }
330 Ok(noise)
331 }
332
333 #[must_use]
335 pub const fn len(&self) -> usize {
336 self.len
337 }
338
339 #[must_use]
341 pub const fn is_empty(&self) -> bool {
342 self.len == 0
343 }
344
345 #[must_use]
347 pub fn get(&self, row: usize, column: usize) -> Option<f64> {
348 if row >= self.len || column >= self.len {
349 return None;
350 }
351 self.entries.get(row)?.get(column).copied()
352 }
353}
354
355#[derive(Debug, Clone, Copy, PartialEq)]
361pub struct GatingPolicy {
362 threshold: Option<f64>,
363}
364
365impl GatingPolicy {
366 #[must_use]
368 pub const fn none() -> Self {
369 Self { threshold: None }
370 }
371
372 #[must_use]
376 pub const fn reject_above(threshold: f64) -> Self {
377 Self {
378 threshold: Some(threshold),
379 }
380 }
381
382 #[must_use]
384 pub const fn threshold(&self) -> Option<f64> {
385 self.threshold
386 }
387}
388
389pub trait Observation {
395 fn sensor(&self) -> SensorId;
399
400 fn taken_at(&self) -> Instant<Utc>;
402
403 fn measured(&self) -> ObservationVector;
405
406 fn predict(&self, state: &NavigationState) -> Result<ObservationVector>;
412
413 fn jacobian(&self, state: &NavigationState) -> Result<ObservationJacobian>;
419
420 fn noise(&self) -> ObservationNoise;
422
423 fn gate(&self) -> GatingPolicy {
425 GatingPolicy::none()
426 }
427
428 fn innovation(&self, predicted: &ObservationVector) -> Result<ObservationVector> {
438 let measured = self.measured();
439 measured
440 .minus(predicted)
441 .ok_or(KernelError::BufferTooSmall {
442 needed: measured.len(),
443 found: predicted.len(),
444 })
445 }
446}
447
448#[derive(Debug, Clone, Copy, PartialEq)]
452pub struct StateJacobian {
453 matrix: Matrix<STATE_DIM, STATE_DIM>,
454}
455
456impl StateJacobian {
457 #[must_use]
459 pub fn identity() -> Self {
460 Self {
461 matrix: Matrix::identity(),
462 }
463 }
464
465 #[must_use]
467 pub fn with(mut self, row: StateComponent, column: StateComponent, derivative: f64) -> Self {
468 self.matrix.set(row.index(), column.index(), derivative);
469 self
470 }
471
472 #[must_use]
474 pub fn derivative(&self, row: StateComponent, column: StateComponent) -> f64 {
475 self.matrix.get(row.index(), column.index()).unwrap_or(0.0)
476 }
477
478 #[doc(hidden)]
483 #[must_use]
484 pub const fn matrix(&self) -> &Matrix<STATE_DIM, STATE_DIM> {
485 &self.matrix
486 }
487}
488
489#[derive(Debug, Clone, Copy, PartialEq)]
491pub struct ProcessNoise {
492 matrix: Matrix<STATE_DIM, STATE_DIM>,
493}
494
495impl ProcessNoise {
496 #[must_use]
498 pub const fn zero() -> Self {
499 Self {
500 matrix: Matrix::ZERO,
501 }
502 }
503
504 #[must_use]
506 pub fn with_variance(mut self, component: StateComponent, variance: f64) -> Self {
507 self.matrix
508 .set(component.index(), component.index(), variance);
509 self
510 }
511
512 #[must_use]
514 pub fn with_covariance(
515 mut self,
516 a: StateComponent,
517 b: StateComponent,
518 covariance: f64,
519 ) -> Self {
520 self.matrix.set(a.index(), b.index(), covariance);
521 self.matrix.set(b.index(), a.index(), covariance);
522 self
523 }
524
525 #[must_use]
527 pub fn variance(&self, component: StateComponent) -> f64 {
528 self.matrix
529 .get(component.index(), component.index())
530 .unwrap_or(0.0)
531 }
532
533 #[doc(hidden)]
538 #[must_use]
539 pub const fn matrix(&self) -> &Matrix<STATE_DIM, STATE_DIM> {
540 &self.matrix
541 }
542}
543
544pub trait ProcessModel {
546 fn propagate(&self, state: &NavigationState, over: Duration) -> Result<NavigationState>;
553
554 fn jacobian(&self, state: &NavigationState, over: Duration) -> Result<StateJacobian>;
560
561 fn noise(&self, state: &NavigationState, over: Duration) -> ProcessNoise;
568}
569
570#[cfg(test)]
571#[allow(clippy::unwrap_used, clippy::float_cmp)]
572mod tests {
573 use super::*;
574
575 #[test]
576 fn observation_vectors_are_bounded_and_finite() {
577 let vector = ObservationVector::new(&[1.0, 2.0]).unwrap();
578 assert_eq!(vector.as_slice(), &[1.0, 2.0]);
579 assert_eq!(vector.get(1), Some(2.0));
580 assert_eq!(vector.get(2), None);
581 assert!(ObservationVector::new(&[1.0; 5]).is_err());
582 assert!(ObservationVector::new(&[f64::NAN]).is_err());
583 let difference = vector
584 .minus(&ObservationVector::new(&[0.5, 0.5]).unwrap())
585 .unwrap();
586 assert_eq!(difference.as_slice(), &[0.5, 1.5]);
587 assert!(vector
588 .minus(&ObservationVector::new(&[1.0]).unwrap())
589 .is_none());
590 }
591
592 #[test]
593 fn jacobians_are_built_by_name() {
594 let row = JacobianRow::new().with(StateComponent::Heading, 2.0);
595 assert_eq!(row.derivative(StateComponent::Heading), 2.0);
596 assert_eq!(row.derivative(StateComponent::North), 0.0);
597 let jacobian = ObservationJacobian::new().with_row(row).unwrap();
598 assert_eq!(jacobian.len(), 1);
599 assert_eq!(jacobian.rows().first(), Some(&row));
600 let mut full = jacobian;
601 for _ in 1..MAX_OBSERVATION_DIM {
602 full = full.with_row(row).unwrap();
603 }
604 assert!(full.with_row(row).is_err());
605 assert!(ObservationJacobian::new()
606 .with_row(JacobianRow::new().with(StateComponent::East, f64::INFINITY))
607 .is_err());
608
609 let state_jacobian = StateJacobian::identity().with(
610 StateComponent::North,
611 StateComponent::SpeedThroughWater,
612 0.5,
613 );
614 assert_eq!(
615 state_jacobian.derivative(StateComponent::North, StateComponent::SpeedThroughWater),
616 0.5
617 );
618 assert_eq!(
619 state_jacobian.derivative(StateComponent::North, StateComponent::North),
620 1.0
621 );
622 }
623
624 #[test]
625 fn noise_must_be_positive_definite() {
626 let diagonal = ObservationNoise::sigmas(&[2.0, 3.0]).unwrap();
627 assert_eq!(diagonal.get(0, 0), Some(4.0));
628 assert_eq!(diagonal.get(1, 1), Some(9.0));
629 assert_eq!(diagonal.get(0, 1), Some(0.0));
630 assert_eq!(diagonal.get(2, 2), None);
631 assert!(ObservationNoise::diagonal(&[0.0]).is_err());
632 assert!(ObservationNoise::diagonal(&[-1.0]).is_err());
633 assert!(ObservationNoise::diagonal(&[1.0; 5]).is_err());
634 let full = ObservationNoise::full(&[&[2.0, 0.5], &[0.5, 2.0]]).unwrap();
635 assert_eq!(full.get(1, 0), Some(0.5));
636 assert!(ObservationNoise::full(&[&[1.0, 2.0], &[2.0, 1.0]]).is_err());
637 assert!(ObservationNoise::full(&[&[1.0, 0.0], &[0.0]]).is_err());
638
639 let process = ProcessNoise::zero()
640 .with_variance(StateComponent::Heading, 0.01)
641 .with_covariance(
642 StateComponent::CurrentNorth,
643 StateComponent::CurrentEast,
644 0.1,
645 );
646 assert_eq!(process.variance(StateComponent::Heading), 0.01);
647 assert_eq!(process.variance(StateComponent::North), 0.0);
648 assert_eq!(
649 process.matrix().get(
650 StateComponent::CurrentEast.index(),
651 StateComponent::CurrentNorth.index()
652 ),
653 Some(0.1)
654 );
655 }
656
657 #[test]
658 fn gating_is_optional() {
659 assert_eq!(GatingPolicy::none().threshold(), None);
660 assert_eq!(GatingPolicy::reject_above(5.99).threshold(), Some(5.99));
661 }
662
663 struct Constant(f64);
665
666 impl Observation for Constant {
667 fn sensor(&self) -> SensorId {
668 SensorId::named("constant")
669 }
670 fn taken_at(&self) -> Instant<Utc> {
671 Instant::UNIX_EPOCH
672 }
673 fn measured(&self) -> ObservationVector {
674 ObservationVector::new(&[self.0]).unwrap()
675 }
676 fn predict(&self, _: &NavigationState) -> Result<ObservationVector> {
677 ObservationVector::new(&[0.0])
678 }
679 fn jacobian(&self, _: &NavigationState) -> Result<ObservationJacobian> {
680 ObservationJacobian::new().with_row(JacobianRow::new())
681 }
682 fn noise(&self) -> ObservationNoise {
683 ObservationNoise::sigmas(&[1.0]).unwrap()
684 }
685 }
686
687 #[test]
688 fn the_default_innovation_is_the_difference() {
689 let observation = Constant(3.0);
690 let predicted = ObservationVector::new(&[1.0]).unwrap();
691 assert_eq!(
692 observation.innovation(&predicted).unwrap().as_slice(),
693 &[2.0]
694 );
695 assert!(observation
696 .innovation(&ObservationVector::new(&[1.0, 1.0]).unwrap())
697 .is_err());
698 assert_eq!(observation.gate(), GatingPolicy::none());
699 }
700}