1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone)]
49pub struct GrangerCausality {
50 period: usize,
51 lag: usize,
52 window: VecDeque<(f64, f64)>,
53 target: Vec<f64>,
55 restricted: Vec<f64>,
57 unrestricted: Vec<f64>,
59 xtx: Vec<f64>,
62 xty: Vec<f64>,
63 theta: Vec<f64>,
64}
65
66impl GrangerCausality {
67 pub fn new(period: usize, lag: usize) -> Result<Self> {
77 if lag < 1 {
78 return Err(Error::InvalidPeriod {
79 message: "granger causality needs lag >= 1",
80 });
81 }
82 if lag > crate::error::MAX_PERIOD {
83 return Err(Error::InvalidPeriod {
84 message: crate::error::PERIOD_ABOVE_MAX,
85 });
86 }
87 if period < 3 * lag + 2 {
88 return Err(Error::InvalidPeriod {
89 message: "granger causality needs period >= 3*lag + 2",
90 });
91 }
92 Ok(Self {
93 period,
94 lag,
95 window: VecDeque::with_capacity(period),
96 target: Vec::with_capacity(period),
97 restricted: Vec::with_capacity(period * (lag + 1)),
98 unrestricted: Vec::with_capacity(period * (2 * lag + 1)),
99 xtx: Vec::with_capacity((2 * lag + 1) * (2 * lag + 1)),
100 xty: Vec::with_capacity(2 * lag + 1),
101 theta: Vec::with_capacity(2 * lag + 1),
102 })
103 }
104
105 pub const fn period(&self) -> usize {
107 self.period
108 }
109
110 pub const fn lag(&self) -> usize {
112 self.lag
113 }
114}
115
116impl Indicator for GrangerCausality {
117 type Input = (f64, f64);
118 type Output = f64;
119
120 fn update(&mut self, input: (f64, f64)) -> Option<f64> {
121 if !input.0.is_finite() || !input.1.is_finite() {
122 return None;
123 }
124 if self.window.len() == self.period {
125 self.window.pop_front();
126 }
127 self.window.push_back(input);
128 if self.window.len() < self.period {
129 return None;
130 }
131 let lag = self.lag;
132 let num_obs = self.period - lag;
133
134 self.target.clear();
138 self.restricted.clear();
139 self.unrestricted.clear();
140 for k in 0..num_obs {
141 let now = lag + k;
142 self.target.push(self.window[now].0);
143 self.restricted.push(1.0);
144 for back in 1..=lag {
145 self.restricted.push(self.window[now - back].0);
146 }
147 let row_start = k * (lag + 1);
150 for offset in 0..=lag {
151 let value = self.restricted[row_start + offset];
152 self.unrestricted.push(value);
153 }
154 for back in 1..=lag {
155 self.unrestricted.push(self.window[now - back].1);
156 }
157 }
158
159 let Self {
160 target,
161 restricted,
162 unrestricted,
163 xtx,
164 xty,
165 theta,
166 ..
167 } = self;
168 let Some(rss_r) = ols_rss(restricted, lag + 1, target, xtx, xty, theta) else {
169 return Some(0.0);
170 };
171 let Some(rss_u) = ols_rss(unrestricted, 2 * lag + 1, target, xtx, xty, theta) else {
172 return Some(0.0);
173 };
174 let dof = (num_obs - (2 * lag + 1)) as f64;
175 let numerator = (rss_r - rss_u) / lag as f64;
176 let denominator = rss_u / dof;
177 Some((numerator / denominator).max(0.0))
178 }
179
180 fn reset(&mut self) {
181 self.window.clear();
182 self.target.clear();
183 self.restricted.clear();
184 self.unrestricted.clear();
185 self.xtx.clear();
186 self.xty.clear();
187 self.theta.clear();
188 }
189
190 #[inline]
191 fn warmup_period(&self) -> usize {
192 self.period
193 }
194
195 #[inline]
196 fn is_ready(&self) -> bool {
197 self.window.len() == self.period
198 }
199
200 #[inline]
201 fn name(&self) -> &'static str {
202 "GrangerCausality"
203 }
204}
205
206fn ols_rss(
211 rows: &[f64],
212 num_reg: usize,
213 target: &[f64],
214 xtx: &mut Vec<f64>,
215 xty: &mut Vec<f64>,
216 theta: &mut Vec<f64>,
217) -> Option<f64> {
218 xtx.clear();
219 xtx.resize(num_reg * num_reg, 0.0);
220 xty.clear();
221 xty.resize(num_reg, 0.0);
222 for (row, &observed) in rows.chunks_exact(num_reg).zip(target) {
223 for (ri, &left) in row.iter().enumerate() {
224 xty[ri] += left * observed;
225 for (ci, &right) in row.iter().enumerate() {
226 xtx[ri * num_reg + ci] += left * right;
227 }
228 }
229 }
230 solve(xtx, num_reg, xty, theta)?;
231 let mut rss = 0.0;
232 for (row, &observed) in rows.chunks_exact(num_reg).zip(target) {
233 let pred: f64 = row
234 .iter()
235 .zip(theta.iter())
236 .map(|(coeff, value)| coeff * value)
237 .sum();
238 let resid = observed - pred;
239 rss += resid * resid;
240 }
241 Some(rss)
242}
243
244fn solve(mat: &mut [f64], stride: usize, rhs: &mut [f64], sol: &mut Vec<f64>) -> Option<()> {
249 let dim = rhs.len();
250 for col in 0..dim {
251 let pivot = mat[col * stride + col];
252 if pivot.abs() < 1e-12 {
253 return None;
254 }
255 let (above, below) = mat.split_at_mut((col + 1) * stride);
258 let pivot_row = &above[col * stride..col * stride + stride];
259 for row in (col + 1)..dim {
260 let offset = (row - col - 1) * stride;
261 let target_row = &mut below[offset..offset + stride];
262 let factor = target_row[col] / pivot;
263 for (cell, &value) in target_row.iter_mut().zip(pivot_row).skip(col) {
264 *cell -= factor * value;
265 }
266 rhs[row] -= factor * rhs[col];
267 }
268 }
269 sol.clear();
270 sol.resize(dim, 0.0);
271 for row in (0..dim).rev() {
272 let known: f64 = mat[row * stride..row * stride + stride]
273 .iter()
274 .zip(sol.iter())
275 .skip(row + 1)
276 .map(|(coeff, value)| coeff * value)
277 .sum();
278 sol[row] = (rhs[row] - known) / mat[row * stride + row];
279 }
280 Some(())
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286 use crate::traits::BatchExt;
287
288 #[test]
289 fn rejects_bad_parameters() {
290 assert!(GrangerCausality::new(10, 0).is_err()); assert!(GrangerCausality::new(4, 1).is_err()); assert!(GrangerCausality::new(5, 1).is_ok());
293 }
294
295 #[test]
296 fn accessors_and_metadata() {
297 let g = GrangerCausality::new(60, 2).unwrap();
298 assert_eq!(g.period(), 60);
299 assert_eq!(g.lag(), 2);
300 assert_eq!(g.warmup_period(), 60);
301 assert_eq!(g.name(), "GrangerCausality");
302 assert!(!g.is_ready());
303 }
304
305 #[test]
306 fn warmup_returns_none() {
307 let mut g = GrangerCausality::new(5, 1).unwrap();
308 for t in 0..4 {
309 assert_eq!(g.update((f64::from(t), f64::from(t) * 0.5)), None);
310 }
311 assert!(g.update((4.0, 2.0)).is_some());
312 assert!(g.is_ready());
313 }
314
315 #[test]
316 fn b_leading_a_has_positive_statistic() {
317 let mut prev_drive = 0.0;
319 let pairs: Vec<(f64, f64)> = (0..120)
320 .map(|t| {
321 let drive = (f64::from(t) * 0.3).sin() + 0.4 * (f64::from(t) * 0.11).cos();
322 let a = 0.8 * prev_drive + 0.05 * (f64::from(t) * 0.7).sin();
323 prev_drive = drive;
324 (a, drive)
325 })
326 .collect();
327 let last = GrangerCausality::new(60, 1)
328 .unwrap()
329 .batch(&pairs)
330 .into_iter()
331 .flatten()
332 .last()
333 .unwrap();
334 assert!(last > 1.0, "F {last}");
335 }
336
337 #[test]
338 fn constant_b_is_singular_and_returns_zero() {
339 let pairs: Vec<(f64, f64)> = (0..40)
342 .map(|t| (f64::from(t) + (f64::from(t) * 0.6).sin(), 3.0))
343 .collect();
344 let last = GrangerCausality::new(20, 1)
345 .unwrap()
346 .batch(&pairs)
347 .into_iter()
348 .flatten()
349 .last()
350 .unwrap();
351 assert_eq!(last, 0.0);
352 }
353
354 #[test]
355 fn constant_a_restricted_singular_returns_zero() {
356 let pairs: Vec<(f64, f64)> = (0..40).map(|t| (5.0, (f64::from(t) * 0.4).sin())).collect();
359 let last = GrangerCausality::new(20, 1)
360 .unwrap()
361 .batch(&pairs)
362 .into_iter()
363 .flatten()
364 .last()
365 .unwrap();
366 assert_eq!(last, 0.0);
367 }
368
369 #[test]
370 fn reset_clears_state() {
371 let mut g = GrangerCausality::new(8, 1).unwrap();
372 for t in 0..12 {
373 g.update((
374 f64::from(t) + (f64::from(t) * 0.7).sin(),
375 (f64::from(t) * 0.3).cos(),
376 ));
377 }
378 assert!(g.is_ready());
379 g.reset();
380 assert!(!g.is_ready());
381 assert_eq!(g.update((1.0, 1.0)), None);
382 }
383
384 #[test]
385 fn batch_equals_streaming() {
386 let pairs: Vec<(f64, f64)> = (0..80)
387 .map(|t| {
388 let b = (f64::from(t) * 0.4).sin();
389 (
390 0.6 * (f64::from(t.max(1) - 1) * 0.4).sin() + 0.1 * f64::from(t % 3),
391 b,
392 )
393 })
394 .collect();
395 let batch = GrangerCausality::new(30, 2).unwrap().batch(&pairs);
396 let mut g = GrangerCausality::new(30, 2).unwrap();
397 let streamed: Vec<_> = pairs.iter().map(|p| g.update(*p)).collect();
398 assert_eq!(batch, streamed);
399 }
400
401 #[test]
402 fn non_finite_input_returns_none() {
403 let mut g = GrangerCausality::new(5, 1).unwrap();
404 assert_eq!(g.update((f64::NAN, 1.0)), None);
405 assert_eq!(g.update((1.0, f64::INFINITY)), None);
406 for t in 0..4 {
408 assert_eq!(g.update((f64::from(t), f64::from(t) * 0.5)), None);
409 }
410 assert!(g.update((4.0, 2.0)).is_some());
411 }
412}