wickra_core/indicators/
distance_ssd.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone)]
45pub struct DistanceSsd {
46 period: usize,
47 window: VecDeque<(f64, f64)>,
48}
49
50impl DistanceSsd {
51 pub fn new(period: usize) -> Result<Self> {
57 if period < 2 {
58 return Err(Error::InvalidPeriod {
59 message: "distance SSD needs period >= 2",
60 });
61 }
62 if period > crate::error::MAX_PERIOD {
63 return Err(Error::InvalidPeriod {
64 message: crate::error::PERIOD_ABOVE_MAX,
65 });
66 }
67 Ok(Self {
68 period,
69 window: VecDeque::with_capacity(period),
70 })
71 }
72
73 pub const fn period(&self) -> usize {
75 self.period
76 }
77}
78
79impl Indicator for DistanceSsd {
80 type Input = (f64, f64);
81 type Output = f64;
82
83 #[inline]
84 fn update(&mut self, input: (f64, f64)) -> Option<f64> {
85 if !input.0.is_finite() || !input.1.is_finite() {
86 return None;
87 }
88 if self.window.len() == self.period {
89 self.window.pop_front();
90 }
91 self.window.push_back(input);
92 if self.window.len() < self.period {
93 return None;
94 }
95 let &(a_first, b_first) = self.window.front().expect("window is full");
96 if a_first == 0.0 || b_first == 0.0 {
97 return Some(0.0);
99 }
100 let ssd = self
101 .window
102 .iter()
103 .map(|&(a, b)| {
104 let gap = a / a_first - b / b_first;
105 gap * gap
106 })
107 .sum();
108 Some(ssd)
109 }
110
111 fn reset(&mut self) {
112 self.window.clear();
113 }
114
115 #[inline]
116 fn warmup_period(&self) -> usize {
117 self.period
118 }
119
120 #[inline]
121 fn is_ready(&self) -> bool {
122 self.window.len() == self.period
123 }
124
125 #[inline]
126 fn name(&self) -> &'static str {
127 "DistanceSsd"
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134 use crate::traits::BatchExt;
135 use approx::assert_relative_eq;
136
137 #[test]
138 fn rejects_period_below_two() {
139 assert!(DistanceSsd::new(1).is_err());
140 assert!(DistanceSsd::new(2).is_ok());
141 }
142
143 #[test]
144 fn accessors_and_metadata() {
145 let d = DistanceSsd::new(20).unwrap();
146 assert_eq!(d.period(), 20);
147 assert_eq!(d.warmup_period(), 20);
148 assert_eq!(d.name(), "DistanceSsd");
149 assert!(!d.is_ready());
150 }
151
152 #[test]
153 fn warmup_returns_none() {
154 let mut d = DistanceSsd::new(3).unwrap();
155 assert_eq!(d.update((1.0, 1.0)), None);
156 assert_eq!(d.update((2.0, 2.0)), None);
157 assert!(d.update((3.0, 3.0)).is_some());
158 assert!(d.is_ready());
159 }
160
161 #[test]
162 fn identical_normalised_paths_have_zero_distance() {
163 let pairs: Vec<(f64, f64)> = (0..20)
165 .map(|t| {
166 let a = 100.0 + f64::from(t);
167 (a, 2.0 * a)
168 })
169 .collect();
170 let last = DistanceSsd::new(10)
171 .unwrap()
172 .batch(&pairs)
173 .into_iter()
174 .flatten()
175 .last()
176 .unwrap();
177 assert_relative_eq!(last, 0.0, epsilon = 1e-12);
178 }
179
180 #[test]
181 fn diverging_paths_have_positive_distance() {
182 let pairs: Vec<(f64, f64)> = (0..20)
183 .map(|t| (100.0 + f64::from(t), 100.0 + 3.0 * f64::from(t)))
184 .collect();
185 let last = DistanceSsd::new(10)
186 .unwrap()
187 .batch(&pairs)
188 .into_iter()
189 .flatten()
190 .last()
191 .unwrap();
192 assert!(last > 0.0, "ssd {last}");
193 }
194
195 #[test]
196 fn hand_computed_value() {
197 let pairs = [(1.0, 1.0), (2.0, 4.0), (3.0, 9.0)];
200 let last = DistanceSsd::new(3)
201 .unwrap()
202 .batch(&pairs)
203 .into_iter()
204 .flatten()
205 .last()
206 .unwrap();
207 assert_relative_eq!(last, 40.0, epsilon = 1e-12);
208 }
209
210 #[test]
211 fn zero_start_returns_zero() {
212 let pairs = [(0.0, 1.0), (2.0, 2.0), (3.0, 3.0)];
214 let last = DistanceSsd::new(3)
215 .unwrap()
216 .batch(&pairs)
217 .into_iter()
218 .flatten()
219 .last()
220 .unwrap();
221 assert_eq!(last, 0.0);
222 }
223
224 #[test]
225 fn reset_clears_state() {
226 let mut d = DistanceSsd::new(4).unwrap();
227 d.batch(&[(1.0, 1.0), (2.0, 2.0), (3.0, 4.0), (4.0, 5.0), (5.0, 6.0)]);
228 assert!(d.is_ready());
229 d.reset();
230 assert!(!d.is_ready());
231 assert_eq!(d.update((1.0, 1.0)), None);
232 }
233
234 #[test]
235 fn batch_equals_streaming() {
236 let pairs: Vec<(f64, f64)> = (0..60)
237 .map(|t| {
238 let a = 100.0 + f64::from(t);
239 (a, 100.0 + 1.2 * f64::from(t) + (f64::from(t) * 0.5).sin())
240 })
241 .collect();
242 let batch = DistanceSsd::new(15).unwrap().batch(&pairs);
243 let mut d = DistanceSsd::new(15).unwrap();
244 let streamed: Vec<_> = pairs.iter().map(|p| d.update(*p)).collect();
245 assert_eq!(batch, streamed);
246 }
247
248 #[test]
249 fn non_finite_input_returns_none() {
250 let mut d = DistanceSsd::new(3).unwrap();
251 assert_eq!(d.update((f64::NAN, 1.0)), None);
252 assert_eq!(d.update((1.0, f64::INFINITY)), None);
253 assert_eq!(d.update((1.0, 1.0)), None);
255 assert_eq!(d.update((2.0, 4.0)), None);
256 assert!(d.update((3.0, 9.0)).is_some());
257 }
258}