wickra_core/indicators/
spearman_correlation.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone)]
55pub struct SpearmanCorrelation {
56 period: usize,
57 window: VecDeque<(f64, f64)>,
58 scratch: Vec<(f64, usize)>,
60 rx: Vec<f64>,
62 ry: Vec<f64>,
63}
64
65impl SpearmanCorrelation {
66 pub fn new(period: usize) -> Result<Self> {
71 if period < 2 {
72 return Err(Error::InvalidPeriod {
73 message: "spearman correlation needs period >= 2",
74 });
75 }
76 if period > crate::error::MAX_PERIOD {
77 return Err(Error::InvalidPeriod {
78 message: crate::error::PERIOD_ABOVE_MAX,
79 });
80 }
81 Ok(Self {
82 period,
83 window: VecDeque::with_capacity(period),
84 scratch: Vec::with_capacity(period),
85 rx: vec![0.0; period],
86 ry: vec![0.0; period],
87 })
88 }
89
90 pub const fn period(&self) -> usize {
92 self.period
93 }
94}
95
96fn rank_into(
100 values: impl Iterator<Item = f64>,
101 ranks_out: &mut [f64],
102 scratch: &mut Vec<(f64, usize)>,
103) {
104 scratch.clear();
105 for (i, v) in values.enumerate() {
106 scratch.push((v, i));
107 }
108 scratch.sort_by(|a, b| a.0.total_cmp(&b.0));
109 let n = scratch.len();
110 let mut i = 0;
111 while i < n {
112 let mut j = i + 1;
113 while j < n && scratch[j].0 == scratch[i].0 {
114 j += 1;
115 }
116 let mid = (i as f64 + 1.0 + j as f64) / 2.0;
119 for k in i..j {
120 ranks_out[scratch[k].1] = mid;
121 }
122 i = j;
123 }
124}
125
126impl Indicator for SpearmanCorrelation {
127 type Input = (f64, f64);
128 type Output = f64;
129
130 fn update(&mut self, input: (f64, f64)) -> Option<f64> {
131 if !input.0.is_finite() || !input.1.is_finite() {
132 return None;
133 }
134 if self.window.len() == self.period {
135 self.window.pop_front();
136 }
137 self.window.push_back(input);
138 if self.window.len() < self.period {
139 return None;
140 }
141 rank_into(
143 self.window.iter().map(|p| p.0),
144 &mut self.rx,
145 &mut self.scratch,
146 );
147 rank_into(
148 self.window.iter().map(|p| p.1),
149 &mut self.ry,
150 &mut self.scratch,
151 );
152 let n = self.period as f64;
156 let mut sum_x = 0.0;
157 let mut sum_y = 0.0;
158 let mut sum_xx = 0.0;
159 let mut sum_yy = 0.0;
160 let mut sum_xy = 0.0;
161 for i in 0..self.period {
162 let x = self.rx[i];
163 let y = self.ry[i];
164 sum_x += x;
165 sum_y += y;
166 sum_xx += x * x;
167 sum_yy += y * y;
168 sum_xy += x * y;
169 }
170 let mean_x = sum_x / n;
171 let mean_y = sum_y / n;
172 let var_x = (sum_xx / n - mean_x * mean_x).max(0.0);
173 let var_y = (sum_yy / n - mean_y * mean_y).max(0.0);
174 let cov = sum_xy / n - mean_x * mean_y;
175 let denom = (var_x * var_y).sqrt();
176 if denom == 0.0 {
177 return Some(0.0);
178 }
179 Some((cov / denom).clamp(-1.0, 1.0))
180 }
181
182 fn reset(&mut self) {
183 self.window.clear();
184 self.scratch.clear();
185 self.rx.iter_mut().for_each(|r| *r = 0.0);
186 self.ry.iter_mut().for_each(|r| *r = 0.0);
187 }
188
189 #[inline]
190 fn warmup_period(&self) -> usize {
191 self.period
192 }
193
194 #[inline]
195 fn is_ready(&self) -> bool {
196 self.window.len() == self.period
197 }
198
199 #[inline]
200 fn name(&self) -> &'static str {
201 "SpearmanCorrelation"
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208 use crate::traits::BatchExt;
209 use approx::assert_relative_eq;
210
211 #[test]
212 fn rejects_period_below_two() {
213 assert!(SpearmanCorrelation::new(0).is_err());
214 assert!(SpearmanCorrelation::new(1).is_err());
215 assert!(SpearmanCorrelation::new(2).is_ok());
216 }
217
218 #[test]
219 fn accessors_and_metadata() {
220 let s = SpearmanCorrelation::new(14).unwrap();
221 assert_eq!(s.period(), 14);
222 assert_eq!(s.warmup_period(), 14);
223 assert_eq!(s.name(), "SpearmanCorrelation");
224 }
225
226 #[test]
227 fn perfect_monotone_relationship_is_one() {
228 let pairs: Vec<(f64, f64)> = (1..=10)
231 .map(|i| (f64::from(i), (f64::from(i)).powi(3)))
232 .collect();
233 let last = SpearmanCorrelation::new(5)
234 .unwrap()
235 .batch(&pairs)
236 .into_iter()
237 .flatten()
238 .last()
239 .unwrap();
240 assert_relative_eq!(last, 1.0, epsilon = 1e-9);
241 }
242
243 #[test]
244 fn perfect_inverse_is_minus_one() {
245 let pairs: Vec<(f64, f64)> = (1..=10)
246 .map(|i| (f64::from(i), 1.0 / (f64::from(i))))
247 .collect();
248 let last = SpearmanCorrelation::new(5)
249 .unwrap()
250 .batch(&pairs)
251 .into_iter()
252 .flatten()
253 .last()
254 .unwrap();
255 assert_relative_eq!(last, -1.0, epsilon = 1e-9);
256 }
257
258 #[test]
259 fn constant_channel_yields_zero() {
260 let pairs: Vec<(f64, f64)> = (0..10).map(|i| (f64::from(i), 7.0)).collect();
261 let last = SpearmanCorrelation::new(5)
262 .unwrap()
263 .batch(&pairs)
264 .into_iter()
265 .flatten()
266 .last()
267 .unwrap();
268 assert_relative_eq!(last, 0.0, epsilon = 1e-12);
269 }
270
271 #[test]
272 fn output_in_minus_one_to_one_range() {
273 let pairs: Vec<(f64, f64)> = (0..60)
274 .map(|i| {
275 let t = f64::from(i);
276 (100.0 + t.sin() * 5.0, 50.0 + (t * 0.7).cos() * 3.0)
277 })
278 .collect();
279 let mut s = SpearmanCorrelation::new(20).unwrap();
280 for v in s.batch(&pairs).into_iter().flatten() {
281 assert!((-1.0..=1.0).contains(&v));
282 }
283 }
284
285 #[test]
286 fn handles_ties_via_mid_ranks() {
287 let pairs = [(1.0, 1.0), (2.0, 2.0), (3.0, 3.0), (3.0, 4.0)];
290 let last = SpearmanCorrelation::new(4)
291 .unwrap()
292 .batch(&pairs)
293 .into_iter()
294 .flatten()
295 .last()
296 .unwrap();
297 assert!(last > 0.0 && last < 1.0);
300 }
301
302 #[test]
303 fn reset_clears_state() {
304 let mut s = SpearmanCorrelation::new(5).unwrap();
305 s.batch(&[(1.0, 2.0), (2.0, 4.0), (3.0, 6.0), (4.0, 8.0), (5.0, 10.0)]);
306 assert!(s.is_ready());
307 s.reset();
308 assert!(!s.is_ready());
309 assert_eq!(s.update((1.0, 1.0)), None);
310 }
311
312 #[test]
313 fn batch_equals_streaming() {
314 let pairs: Vec<(f64, f64)> = (0..60)
315 .map(|i| {
316 let t = f64::from(i);
317 (t.sin() + (t * 0.1).cos(), (t * 0.3).cos())
318 })
319 .collect();
320 let batch = SpearmanCorrelation::new(14).unwrap().batch(&pairs);
321 let mut b = SpearmanCorrelation::new(14).unwrap();
322 let streamed: Vec<_> = pairs.iter().map(|p| b.update(*p)).collect();
323 assert_eq!(batch, streamed);
324 }
325
326 #[test]
327 fn non_finite_input_returns_none() {
328 let mut s = SpearmanCorrelation::new(2).unwrap();
329 assert_eq!(s.update((f64::NAN, 1.0)), None);
330 assert_eq!(s.update((1.0, f64::INFINITY)), None);
331 assert_eq!(s.update((1.0, 2.0)), None);
333 assert!(s.update((2.0, 5.0)).is_some());
334 }
335}