Skip to main content

cjc_runtime/
window.rs

1//! Sliding-window functions for CJC.
2//!
3//! Provides `window_sum`, `window_mean`, `window_min`, `window_max` over
4//! arrays of numeric values. These are commonly used in time-series analysis,
5//! data science pipelines, and signal processing.
6//!
7//! # Determinism
8//!
9//! - `window_sum` and `window_mean` use Kahan summation for numerically
10//!   stable, deterministic results.
11//! - All window functions produce the same output for the same input on
12//!   every invocation.
13//!
14//! # Semantics
15//!
16//! All window functions take `(data: &[f64], window_size: usize)` and return
17//! a `Vec<f64>` of length `data.len() - window_size + 1`. That is, they
18//! produce one output per valid (full) window position.
19//!
20//! If `window_size` is 0 or greater than `data.len()`, the result is empty.
21
22use cjc_repro::KahanAccumulatorF64;
23
24// ---------------------------------------------------------------------------
25// Core window functions
26// ---------------------------------------------------------------------------
27
28/// Sliding-window sum with Kahan summation.
29///
30/// Returns a vector of length `max(0, data.len() - window_size + 1)`.
31/// Each element is the sum of the corresponding window of `window_size`
32/// consecutive elements.
33pub fn window_sum(data: &[f64], window_size: usize) -> Vec<f64> {
34    if window_size == 0 || window_size > data.len() {
35        return Vec::new();
36    }
37    let n = data.len() - window_size + 1;
38    let mut result = Vec::with_capacity(n);
39
40    for i in 0..n {
41        let mut acc = KahanAccumulatorF64::new();
42        for j in 0..window_size {
43            acc.add(data[i + j]);
44        }
45        result.push(acc.finalize());
46    }
47
48    result
49}
50
51/// Sliding-window mean with Kahan summation.
52///
53/// Returns a vector of length `max(0, data.len() - window_size + 1)`.
54/// Each element is the arithmetic mean of the corresponding window.
55pub fn window_mean(data: &[f64], window_size: usize) -> Vec<f64> {
56    if window_size == 0 || window_size > data.len() {
57        return Vec::new();
58    }
59    let n = data.len() - window_size + 1;
60    let ws = window_size as f64;
61    let mut result = Vec::with_capacity(n);
62
63    for i in 0..n {
64        let mut acc = KahanAccumulatorF64::new();
65        for j in 0..window_size {
66            acc.add(data[i + j]);
67        }
68        result.push(acc.finalize() / ws);
69    }
70
71    result
72}
73
74/// Sliding-window minimum.
75///
76/// Returns a vector of length `max(0, data.len() - window_size + 1)`.
77/// Each element is the minimum of the corresponding window.
78pub fn window_min(data: &[f64], window_size: usize) -> Vec<f64> {
79    if window_size == 0 || window_size > data.len() {
80        return Vec::new();
81    }
82    let n = data.len() - window_size + 1;
83    let mut result = Vec::with_capacity(n);
84
85    for i in 0..n {
86        let mut min_val = data[i];
87        for j in 1..window_size {
88            let v = data[i + j];
89            if v < min_val {
90                min_val = v;
91            }
92        }
93        result.push(min_val);
94    }
95
96    result
97}
98
99/// Sliding-window maximum.
100///
101/// Returns a vector of length `max(0, data.len() - window_size + 1)`.
102/// Each element is the maximum of the corresponding window.
103pub fn window_max(data: &[f64], window_size: usize) -> Vec<f64> {
104    if window_size == 0 || window_size > data.len() {
105        return Vec::new();
106    }
107    let n = data.len() - window_size + 1;
108    let mut result = Vec::with_capacity(n);
109
110    for i in 0..n {
111        let mut max_val = data[i];
112        for j in 1..window_size {
113            let v = data[i + j];
114            if v > max_val {
115                max_val = v;
116            }
117        }
118        result.push(max_val);
119    }
120
121    result
122}
123
124// ---------------------------------------------------------------------------
125// Tests
126// ---------------------------------------------------------------------------
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn test_window_sum_basic() {
134        let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
135        let result = window_sum(&data, 3);
136        assert_eq!(result, vec![6.0, 9.0, 12.0]);
137    }
138
139    #[test]
140    fn test_window_sum_full() {
141        let data = vec![1.0, 2.0, 3.0];
142        let result = window_sum(&data, 3);
143        assert_eq!(result, vec![6.0]);
144    }
145
146    #[test]
147    fn test_window_sum_single() {
148        let data = vec![1.0, 2.0, 3.0];
149        let result = window_sum(&data, 1);
150        assert_eq!(result, vec![1.0, 2.0, 3.0]);
151    }
152
153    #[test]
154    fn test_window_sum_empty_on_too_large() {
155        let data = vec![1.0, 2.0];
156        let result = window_sum(&data, 5);
157        assert!(result.is_empty());
158    }
159
160    #[test]
161    fn test_window_sum_empty_on_zero() {
162        let data = vec![1.0, 2.0, 3.0];
163        let result = window_sum(&data, 0);
164        assert!(result.is_empty());
165    }
166
167    #[test]
168    fn test_window_mean_basic() {
169        let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
170        let result = window_mean(&data, 3);
171        assert_eq!(result, vec![2.0, 3.0, 4.0]);
172    }
173
174    #[test]
175    fn test_window_min_basic() {
176        let data = vec![3.0, 1.0, 4.0, 1.0, 5.0];
177        let result = window_min(&data, 3);
178        assert_eq!(result, vec![1.0, 1.0, 1.0]);
179    }
180
181    #[test]
182    fn test_window_max_basic() {
183        let data = vec![3.0, 1.0, 4.0, 1.0, 5.0];
184        let result = window_max(&data, 3);
185        assert_eq!(result, vec![4.0, 4.0, 5.0]);
186    }
187
188    #[test]
189    fn test_window_determinism() {
190        let data: Vec<f64> = (0..100).map(|i| i as f64 * 0.1).collect();
191        let r1 = window_sum(&data, 7);
192        let r2 = window_sum(&data, 7);
193        assert_eq!(r1, r2, "window_sum must be deterministic");
194    }
195
196    #[test]
197    fn test_window_kahan_accuracy() {
198        // Kahan summation should handle many small additions without
199        // accumulating floating-point drift.
200        let n = 1000;
201        let data: Vec<f64> = vec![0.1; n];
202        let result = window_sum(&data, n);
203        assert_eq!(result.len(), 1);
204        // Naive summation of 1000 * 0.1 drifts from 100.0.
205        // Kahan should be very close to the true value.
206        let err = (result[0] - 100.0).abs();
207        assert!(
208            err < 1e-12,
209            "Kahan sum of 1000×0.1 should be close to 100.0, got {} (err={})",
210            result[0], err,
211        );
212    }
213}