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 self.window.len() == self.period {
93 let (ox, oy) = self.window.pop_front().expect("non-empty");
94 self.sum_x -= ox;
95 self.sum_y -= oy;
96 self.sum_xx -= ox * ox;
97 self.sum_yy -= oy * oy;
98 self.sum_xy -= ox * oy;
99 }
100 self.window.push_back((x, y));
101 self.sum_x += x;
102 self.sum_y += y;
103 self.sum_xx += x * x;
104 self.sum_yy += y * y;
105 self.sum_xy += x * y;
106 if self.window.len() < self.period {
107 return None;
108 }
109 let n = self.period as f64;
110 let mean_x = self.sum_x / n;
111 let mean_y = self.sum_y / n;
112 let var_x = (self.sum_xx / n - mean_x * mean_x).max(0.0);
113 let var_y = (self.sum_yy / n - mean_y * mean_y).max(0.0);
114 let cov = self.sum_xy / n - mean_x * mean_y;
115 let denom = (var_x * var_y).sqrt();
116 if denom == 0.0 {
117 return Some(0.0);
119 }
120 Some((cov / denom).clamp(-1.0, 1.0))
121 }
122
123 fn reset(&mut self) {
124 self.window.clear();
125 self.sum_x = 0.0;
126 self.sum_y = 0.0;
127 self.sum_xx = 0.0;
128 self.sum_yy = 0.0;
129 self.sum_xy = 0.0;
130 }
131
132 fn warmup_period(&self) -> usize {
133 self.period
134 }
135
136 fn is_ready(&self) -> bool {
137 self.window.len() == self.period
138 }
139
140 fn name(&self) -> &'static str {
141 "PearsonCorrelation"
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148 use crate::traits::BatchExt;
149 use approx::assert_relative_eq;
150
151 #[test]
152 fn rejects_period_below_two() {
153 assert!(PearsonCorrelation::new(0).is_err());
154 assert!(PearsonCorrelation::new(1).is_err());
155 assert!(PearsonCorrelation::new(2).is_ok());
156 }
157
158 #[test]
159 fn accessors_and_metadata() {
160 let p = PearsonCorrelation::new(14).unwrap();
161 assert_eq!(p.period(), 14);
162 assert_eq!(p.warmup_period(), 14);
163 assert_eq!(p.name(), "PearsonCorrelation");
164 }
165
166 #[test]
167 fn perfect_positive_is_one() {
168 let pairs: Vec<(f64, f64)> = (0..10)
169 .map(|i| (f64::from(i), 3.0 * f64::from(i) + 1.0))
170 .collect();
171 let last = PearsonCorrelation::new(5)
172 .unwrap()
173 .batch(&pairs)
174 .into_iter()
175 .flatten()
176 .last()
177 .unwrap();
178 assert_relative_eq!(last, 1.0, epsilon = 1e-9);
179 }
180
181 #[test]
182 fn perfect_negative_is_minus_one() {
183 let pairs: Vec<(f64, f64)> = (0..10)
184 .map(|i| (f64::from(i), -2.0 * f64::from(i) + 5.0))
185 .collect();
186 let last = PearsonCorrelation::new(5)
187 .unwrap()
188 .batch(&pairs)
189 .into_iter()
190 .flatten()
191 .last()
192 .unwrap();
193 assert_relative_eq!(last, -1.0, epsilon = 1e-9);
194 }
195
196 #[test]
197 fn constant_channel_yields_zero() {
198 let pairs: Vec<(f64, f64)> = (0..10).map(|i| (f64::from(i), 7.0)).collect();
199 let last = PearsonCorrelation::new(5)
200 .unwrap()
201 .batch(&pairs)
202 .into_iter()
203 .flatten()
204 .last()
205 .unwrap();
206 assert_relative_eq!(last, 0.0, epsilon = 1e-12);
207 }
208
209 #[test]
210 fn output_in_minus_one_to_one_range() {
211 let pairs: Vec<(f64, f64)> = (0..60)
212 .map(|i| {
213 let t = f64::from(i);
214 (100.0 + t.sin() * 5.0, 50.0 + (t * 0.3).cos() * 3.0)
215 })
216 .collect();
217 let mut p = PearsonCorrelation::new(20).unwrap();
218 for v in p.batch(&pairs).into_iter().flatten() {
219 assert!((-1.0..=1.0).contains(&v));
220 }
221 }
222
223 #[test]
224 fn reset_clears_state() {
225 let mut p = PearsonCorrelation::new(5).unwrap();
226 p.batch(&[(1.0, 2.0), (2.0, 4.0), (3.0, 6.0), (4.0, 8.0), (5.0, 10.0)]);
227 assert!(p.is_ready());
228 p.reset();
229 assert!(!p.is_ready());
230 assert_eq!(p.update((1.0, 1.0)), None);
231 }
232
233 #[test]
234 fn batch_equals_streaming() {
235 let pairs: Vec<(f64, f64)> = (0..60)
236 .map(|i| {
237 let t = f64::from(i);
238 (t.sin(), (t * 0.5).cos())
239 })
240 .collect();
241 let batch = PearsonCorrelation::new(14).unwrap().batch(&pairs);
242 let mut b = PearsonCorrelation::new(14).unwrap();
243 let streamed: Vec<_> = pairs.iter().map(|p| b.update(*p)).collect();
244 assert_eq!(batch, streamed);
245 }
246}