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 Ok(Self {
63 period,
64 window: VecDeque::with_capacity(period),
65 })
66 }
67
68 pub const fn period(&self) -> usize {
70 self.period
71 }
72}
73
74impl Indicator for DistanceSsd {
75 type Input = (f64, f64);
76 type Output = f64;
77
78 fn update(&mut self, input: (f64, f64)) -> Option<f64> {
79 if self.window.len() == self.period {
80 self.window.pop_front();
81 }
82 self.window.push_back(input);
83 if self.window.len() < self.period {
84 return None;
85 }
86 let &(a_first, b_first) = self.window.front().expect("window is full");
87 if a_first == 0.0 || b_first == 0.0 {
88 return Some(0.0);
90 }
91 let ssd = self
92 .window
93 .iter()
94 .map(|&(a, b)| {
95 let gap = a / a_first - b / b_first;
96 gap * gap
97 })
98 .sum();
99 Some(ssd)
100 }
101
102 fn reset(&mut self) {
103 self.window.clear();
104 }
105
106 fn warmup_period(&self) -> usize {
107 self.period
108 }
109
110 fn is_ready(&self) -> bool {
111 self.window.len() == self.period
112 }
113
114 fn name(&self) -> &'static str {
115 "DistanceSsd"
116 }
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122 use crate::traits::BatchExt;
123 use approx::assert_relative_eq;
124
125 #[test]
126 fn rejects_period_below_two() {
127 assert!(DistanceSsd::new(1).is_err());
128 assert!(DistanceSsd::new(2).is_ok());
129 }
130
131 #[test]
132 fn accessors_and_metadata() {
133 let d = DistanceSsd::new(20).unwrap();
134 assert_eq!(d.period(), 20);
135 assert_eq!(d.warmup_period(), 20);
136 assert_eq!(d.name(), "DistanceSsd");
137 assert!(!d.is_ready());
138 }
139
140 #[test]
141 fn warmup_returns_none() {
142 let mut d = DistanceSsd::new(3).unwrap();
143 assert_eq!(d.update((1.0, 1.0)), None);
144 assert_eq!(d.update((2.0, 2.0)), None);
145 assert!(d.update((3.0, 3.0)).is_some());
146 assert!(d.is_ready());
147 }
148
149 #[test]
150 fn identical_normalised_paths_have_zero_distance() {
151 let pairs: Vec<(f64, f64)> = (0..20)
153 .map(|t| {
154 let a = 100.0 + f64::from(t);
155 (a, 2.0 * a)
156 })
157 .collect();
158 let last = DistanceSsd::new(10)
159 .unwrap()
160 .batch(&pairs)
161 .into_iter()
162 .flatten()
163 .last()
164 .unwrap();
165 assert_relative_eq!(last, 0.0, epsilon = 1e-12);
166 }
167
168 #[test]
169 fn diverging_paths_have_positive_distance() {
170 let pairs: Vec<(f64, f64)> = (0..20)
171 .map(|t| (100.0 + f64::from(t), 100.0 + 3.0 * f64::from(t)))
172 .collect();
173 let last = DistanceSsd::new(10)
174 .unwrap()
175 .batch(&pairs)
176 .into_iter()
177 .flatten()
178 .last()
179 .unwrap();
180 assert!(last > 0.0, "ssd {last}");
181 }
182
183 #[test]
184 fn hand_computed_value() {
185 let pairs = [(1.0, 1.0), (2.0, 4.0), (3.0, 9.0)];
188 let last = DistanceSsd::new(3)
189 .unwrap()
190 .batch(&pairs)
191 .into_iter()
192 .flatten()
193 .last()
194 .unwrap();
195 assert_relative_eq!(last, 40.0, epsilon = 1e-12);
196 }
197
198 #[test]
199 fn zero_start_returns_zero() {
200 let pairs = [(0.0, 1.0), (2.0, 2.0), (3.0, 3.0)];
202 let last = DistanceSsd::new(3)
203 .unwrap()
204 .batch(&pairs)
205 .into_iter()
206 .flatten()
207 .last()
208 .unwrap();
209 assert_eq!(last, 0.0);
210 }
211
212 #[test]
213 fn reset_clears_state() {
214 let mut d = DistanceSsd::new(4).unwrap();
215 d.batch(&[(1.0, 1.0), (2.0, 2.0), (3.0, 4.0), (4.0, 5.0), (5.0, 6.0)]);
216 assert!(d.is_ready());
217 d.reset();
218 assert!(!d.is_ready());
219 assert_eq!(d.update((1.0, 1.0)), None);
220 }
221
222 #[test]
223 fn batch_equals_streaming() {
224 let pairs: Vec<(f64, f64)> = (0..60)
225 .map(|t| {
226 let a = 100.0 + f64::from(t);
227 (a, 100.0 + 1.2 * f64::from(t) + (f64::from(t) * 0.5).sin())
228 })
229 .collect();
230 let batch = DistanceSsd::new(15).unwrap().batch(&pairs);
231 let mut d = DistanceSsd::new(15).unwrap();
232 let streamed: Vec<_> = pairs.iter().map(|p| d.update(*p)).collect();
233 assert_eq!(batch, streamed);
234 }
235}