wickra_core/indicators/
pearson_correlation.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone)]
47pub struct PearsonCorrelation {
48 period: usize,
49 window: VecDeque<(f64, f64)>,
50 sum_x: f64,
51 sum_y: f64,
52 sum_xx: f64,
53 sum_yy: f64,
54 sum_xy: f64,
55}
56
57impl PearsonCorrelation {
58 pub fn new(period: usize) -> Result<Self> {
64 if period < 2 {
65 return Err(Error::InvalidPeriod {
66 message: "pearson correlation needs period >= 2",
67 });
68 }
69 Ok(Self {
70 period,
71 window: VecDeque::with_capacity(period),
72 sum_x: 0.0,
73 sum_y: 0.0,
74 sum_xx: 0.0,
75 sum_yy: 0.0,
76 sum_xy: 0.0,
77 })
78 }
79
80 pub const fn period(&self) -> usize {
82 self.period
83 }
84}
85
86impl Indicator for PearsonCorrelation {
87 type Input = (f64, f64);
88 type Output = f64;
89
90 fn update(&mut self, input: (f64, f64)) -> Option<f64> {
91 let (x, y) = input;
92 if !x.is_finite() || !y.is_finite() {
93 return None;
94 }
95 if self.window.len() == self.period {
96 let (ox, oy) = self.window.pop_front().expect("non-empty");
97 self.sum_x -= ox;
98 self.sum_y -= oy;
99 self.sum_xx -= ox * ox;
100 self.sum_yy -= oy * oy;
101 self.sum_xy -= ox * oy;
102 }
103 self.window.push_back((x, y));
104 self.sum_x += x;
105 self.sum_y += y;
106 self.sum_xx += x * x;
107 self.sum_yy += y * y;
108 self.sum_xy += x * y;
109 if self.window.len() < self.period {
110 return None;
111 }
112 let n = self.period as f64;
113 let mean_x = self.sum_x / n;
114 let mean_y = self.sum_y / n;
115 let var_x = (self.sum_xx / n - mean_x * mean_x).max(0.0);
116 let var_y = (self.sum_yy / n - mean_y * mean_y).max(0.0);
117 let cov = self.sum_xy / n - mean_x * mean_y;
118 let denom = (var_x * var_y).sqrt();
119 if denom == 0.0 {
120 return Some(0.0);
122 }
123 Some((cov / denom).clamp(-1.0, 1.0))
124 }
125
126 fn reset(&mut self) {
127 self.window.clear();
128 self.sum_x = 0.0;
129 self.sum_y = 0.0;
130 self.sum_xx = 0.0;
131 self.sum_yy = 0.0;
132 self.sum_xy = 0.0;
133 }
134
135 fn warmup_period(&self) -> usize {
136 self.period
137 }
138
139 fn is_ready(&self) -> bool {
140 self.window.len() == self.period
141 }
142
143 fn name(&self) -> &'static str {
144 "PearsonCorrelation"
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151 use crate::traits::BatchExt;
152 use approx::assert_relative_eq;
153
154 #[test]
155 fn rejects_period_below_two() {
156 assert!(PearsonCorrelation::new(0).is_err());
157 assert!(PearsonCorrelation::new(1).is_err());
158 assert!(PearsonCorrelation::new(2).is_ok());
159 }
160
161 #[test]
162 fn accessors_and_metadata() {
163 let p = PearsonCorrelation::new(14).unwrap();
164 assert_eq!(p.period(), 14);
165 assert_eq!(p.warmup_period(), 14);
166 assert_eq!(p.name(), "PearsonCorrelation");
167 }
168
169 #[test]
170 fn perfect_positive_is_one() {
171 let pairs: Vec<(f64, f64)> = (0..10)
172 .map(|i| (f64::from(i), 3.0 * f64::from(i) + 1.0))
173 .collect();
174 let last = PearsonCorrelation::new(5)
175 .unwrap()
176 .batch(&pairs)
177 .into_iter()
178 .flatten()
179 .last()
180 .unwrap();
181 assert_relative_eq!(last, 1.0, epsilon = 1e-9);
182 }
183
184 #[test]
185 fn perfect_negative_is_minus_one() {
186 let pairs: Vec<(f64, f64)> = (0..10)
187 .map(|i| (f64::from(i), -2.0 * f64::from(i) + 5.0))
188 .collect();
189 let last = PearsonCorrelation::new(5)
190 .unwrap()
191 .batch(&pairs)
192 .into_iter()
193 .flatten()
194 .last()
195 .unwrap();
196 assert_relative_eq!(last, -1.0, epsilon = 1e-9);
197 }
198
199 #[test]
200 fn constant_channel_yields_zero() {
201 let pairs: Vec<(f64, f64)> = (0..10).map(|i| (f64::from(i), 7.0)).collect();
202 let last = PearsonCorrelation::new(5)
203 .unwrap()
204 .batch(&pairs)
205 .into_iter()
206 .flatten()
207 .last()
208 .unwrap();
209 assert_relative_eq!(last, 0.0, epsilon = 1e-12);
210 }
211
212 #[test]
213 fn output_in_minus_one_to_one_range() {
214 let pairs: Vec<(f64, f64)> = (0..60)
215 .map(|i| {
216 let t = f64::from(i);
217 (100.0 + t.sin() * 5.0, 50.0 + (t * 0.3).cos() * 3.0)
218 })
219 .collect();
220 let mut p = PearsonCorrelation::new(20).unwrap();
221 for v in p.batch(&pairs).into_iter().flatten() {
222 assert!((-1.0..=1.0).contains(&v));
223 }
224 }
225
226 #[test]
227 fn reset_clears_state() {
228 let mut p = PearsonCorrelation::new(5).unwrap();
229 p.batch(&[(1.0, 2.0), (2.0, 4.0), (3.0, 6.0), (4.0, 8.0), (5.0, 10.0)]);
230 assert!(p.is_ready());
231 p.reset();
232 assert!(!p.is_ready());
233 assert_eq!(p.update((1.0, 1.0)), None);
234 }
235
236 #[test]
237 fn batch_equals_streaming() {
238 let pairs: Vec<(f64, f64)> = (0..60)
239 .map(|i| {
240 let t = f64::from(i);
241 (t.sin(), (t * 0.5).cos())
242 })
243 .collect();
244 let batch = PearsonCorrelation::new(14).unwrap().batch(&pairs);
245 let mut b = PearsonCorrelation::new(14).unwrap();
246 let streamed: Vec<_> = pairs.iter().map(|p| b.update(*p)).collect();
247 assert_eq!(batch, streamed);
248 }
249
250 #[test]
251 fn non_finite_input_returns_none() {
252 let mut p = PearsonCorrelation::new(3).unwrap();
253 assert_eq!(p.update((f64::NAN, 1.0)), None);
254 assert_eq!(p.update((1.0, f64::INFINITY)), None);
255 assert_eq!(p.update((1.0, 2.0)), None);
257 assert_eq!(p.update((2.0, 5.0)), None);
258 assert!(p.update((3.0, 7.0)).is_some());
259 }
260}