1use crate::error::{Error, Result};
4use crate::indicators::rolling_moments::ShiftedPairMoments;
5use crate::traits::Indicator;
6
7#[derive(Debug, Clone)]
46pub struct PearsonCorrelation {
47 period: usize,
48 buf: Box<[(f64, f64)]>,
51 head: usize,
52 count: usize,
54 moments: ShiftedPairMoments,
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 if period > crate::error::MAX_PERIOD {
70 return Err(Error::InvalidPeriod {
71 message: crate::error::PERIOD_ABOVE_MAX,
72 });
73 }
74 Ok(Self {
75 period,
76 buf: vec![(0.0, 0.0); period].into_boxed_slice(),
77 head: 0,
78 count: 0,
79 moments: ShiftedPairMoments::new(),
80 })
81 }
82
83 pub const fn period(&self) -> usize {
85 self.period
86 }
87}
88
89impl PearsonCorrelation {
90 pub fn batch_pairs_into(&mut self, a: &[f64], b: &[f64], out: &mut [f64]) {
97 assert!(
98 a.len() == b.len() && out.len() == a.len(),
99 "both series and the output must be equal length"
100 );
101 for ((slot, &x), &y) in out.iter_mut().zip(a).zip(b) {
102 *slot = self.update((x, y)).unwrap_or(f64::NAN);
103 }
104 }
105
106 pub fn batch_pairs_fast_into(&mut self, a: &[f64], b: &[f64], out: &mut [f64]) {
121 assert!(
122 a.len() == b.len() && out.len() == a.len(),
123 "both series and the output must be equal length"
124 );
125 let p = self.period;
126 let n = a.len();
127 if self.count != 0 || n < p || !crate::fast::in_range(a) || !crate::fast::in_range(b) {
128 self.batch_pairs_into(a, b, out);
129 return;
130 }
131 crate::fast::with_scratch(crate::fast::power_scratch_len(5, p), |scratch| {
132 wickra_simd::dispatch(crate::fast::PearsonFast {
133 a,
134 b,
135 period: p,
136 scratch,
137 out,
138 _borrow: std::marker::PhantomData,
139 });
140 });
141 self.reset();
142 for (&x, &y) in a[n - p..].iter().zip(&b[n - p..]) {
143 let _ = self.update((x, y));
144 }
145 }
146}
147
148impl Indicator for PearsonCorrelation {
149 type Input = (f64, f64);
150 type Output = f64;
151
152 #[inline]
153 fn update(&mut self, input: (f64, f64)) -> Option<f64> {
154 let (x, y) = input;
155 if !x.is_finite() || !y.is_finite() {
156 return None;
157 }
158 let slot = &mut self.buf[self.head];
160 if self.count == self.period {
161 let (ox, oy) = std::mem::replace(slot, (x, y));
162 self.moments.evict(ox, oy);
163 } else {
164 *slot = (x, y);
165 self.count += 1;
166 }
167 self.head += 1;
168 if self.head == self.period {
169 self.head = 0;
170 }
171 self.moments.push(x, y);
172 if self.moments.needs_reseed(self.period) {
173 debug_assert_eq!(self.count, self.period);
177 let (newer, older) = self.buf.split_at(self.head);
178 self.moments.reseed(older.iter().chain(newer).copied());
179 }
180 if self.count < self.period {
181 return None;
182 }
183 let var_x = self.moments.var_a(self.period);
184 let var_y = self.moments.var_b(self.period);
185 let cov = self.moments.cov(self.period);
186 let denom = (var_x * var_y).sqrt();
187 if denom == 0.0 {
188 return Some(0.0);
190 }
191 Some((cov / denom).clamp(-1.0, 1.0))
192 }
193
194 fn reset(&mut self) {
195 self.head = 0;
196 self.count = 0;
197 self.moments.reset();
198 }
199
200 #[inline]
201 fn warmup_period(&self) -> usize {
202 self.period
203 }
204
205 #[inline]
206 fn is_ready(&self) -> bool {
207 self.count == self.period
208 }
209
210 #[inline]
211 fn name(&self) -> &'static str {
212 "PearsonCorrelation"
213 }
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219 use crate::traits::BatchExt;
220 use approx::assert_relative_eq;
221
222 #[test]
223 fn rejects_period_below_two() {
224 assert!(PearsonCorrelation::new(0).is_err());
225 assert!(PearsonCorrelation::new(1).is_err());
226 assert!(PearsonCorrelation::new(2).is_ok());
227 }
228
229 #[test]
230 fn accessors_and_metadata() {
231 let p = PearsonCorrelation::new(14).unwrap();
232 assert_eq!(p.period(), 14);
233 assert_eq!(p.warmup_period(), 14);
234 assert_eq!(p.name(), "PearsonCorrelation");
235 }
236
237 #[test]
238 fn perfect_positive_is_one() {
239 let pairs: Vec<(f64, f64)> = (0..10)
240 .map(|i| (f64::from(i), 3.0 * f64::from(i) + 1.0))
241 .collect();
242 let last = PearsonCorrelation::new(5)
243 .unwrap()
244 .batch(&pairs)
245 .into_iter()
246 .flatten()
247 .last()
248 .unwrap();
249 assert_relative_eq!(last, 1.0, epsilon = 1e-9);
250 }
251
252 #[test]
253 fn perfect_negative_is_minus_one() {
254 let pairs: Vec<(f64, f64)> = (0..10)
255 .map(|i| (f64::from(i), -2.0 * f64::from(i) + 5.0))
256 .collect();
257 let last = PearsonCorrelation::new(5)
258 .unwrap()
259 .batch(&pairs)
260 .into_iter()
261 .flatten()
262 .last()
263 .unwrap();
264 assert_relative_eq!(last, -1.0, epsilon = 1e-9);
265 }
266
267 #[test]
268 fn constant_channel_yields_zero() {
269 let pairs: Vec<(f64, f64)> = (0..10).map(|i| (f64::from(i), 7.0)).collect();
270 let last = PearsonCorrelation::new(5)
271 .unwrap()
272 .batch(&pairs)
273 .into_iter()
274 .flatten()
275 .last()
276 .unwrap();
277 assert_relative_eq!(last, 0.0, epsilon = 1e-12);
278 }
279
280 #[test]
281 fn output_in_minus_one_to_one_range() {
282 let pairs: Vec<(f64, f64)> = (0..60)
283 .map(|i| {
284 let t = f64::from(i);
285 (100.0 + t.sin() * 5.0, 50.0 + (t * 0.3).cos() * 3.0)
286 })
287 .collect();
288 let mut p = PearsonCorrelation::new(20).unwrap();
289 for v in p.batch(&pairs).into_iter().flatten() {
290 assert!((-1.0..=1.0).contains(&v));
291 }
292 }
293
294 #[test]
295 fn reset_clears_state() {
296 let mut p = PearsonCorrelation::new(5).unwrap();
297 p.batch(&[(1.0, 2.0), (2.0, 4.0), (3.0, 6.0), (4.0, 8.0), (5.0, 10.0)]);
298 assert!(p.is_ready());
299 p.reset();
300 assert!(!p.is_ready());
301 assert_eq!(p.update((1.0, 1.0)), None);
302 }
303
304 #[test]
305 fn batch_equals_streaming() {
306 let pairs: Vec<(f64, f64)> = (0..60)
307 .map(|i| {
308 let t = f64::from(i);
309 (t.sin(), (t * 0.5).cos())
310 })
311 .collect();
312 let batch = PearsonCorrelation::new(14).unwrap().batch(&pairs);
313 let mut b = PearsonCorrelation::new(14).unwrap();
314 let streamed: Vec<_> = pairs.iter().map(|p| b.update(*p)).collect();
315 assert_eq!(batch, streamed);
316 }
317
318 #[test]
319 fn non_finite_input_returns_none() {
320 let mut p = PearsonCorrelation::new(3).unwrap();
321 assert_eq!(p.update((f64::NAN, 1.0)), None);
322 assert_eq!(p.update((1.0, f64::INFINITY)), None);
323 assert_eq!(p.update((1.0, 2.0)), None);
325 assert_eq!(p.update((2.0, 5.0)), None);
326 assert!(p.update((3.0, 7.0)).is_some());
327 }
328
329 #[test]
330 fn rejects_period_above_maximum() {
331 assert!(matches!(
332 PearsonCorrelation::new(crate::error::MAX_PERIOD + 1),
333 Err(Error::InvalidPeriod { .. })
334 ));
335 }
336}