1pub fn running_median(values: &[f64], half: usize) -> Vec<f64> {
16 let mut window: Vec<f64> = Vec::with_capacity(window_len(half, values.len()));
19 let place =
20 |window: &[f64], value: f64| window.partition_point(|w| w.total_cmp(&value).is_lt());
21 for value in values.iter().take(half).copied().filter(|v| v.is_finite()) {
22 window.insert(place(&window, value), value);
23 }
24 (0..values.len())
25 .map(|index| {
26 if let Some(&value) = index.checked_add(half).and_then(|i| values.get(i))
27 && value.is_finite()
28 {
29 window.insert(place(&window, value), value);
30 }
31 if let Some(&value) = index
32 .checked_sub(half)
33 .and_then(|i| i.checked_sub(1))
34 .and_then(|i| values.get(i))
35 && value.is_finite()
36 {
37 let at = place(&window, value);
38 if window.get(at).is_some_and(|w| w.total_cmp(&value).is_eq()) {
39 window.remove(at);
40 }
41 }
42 sorted_median(&window).unwrap_or(f64::NAN)
43 })
44 .collect()
45}
46
47fn window_len(half: usize, len: usize) -> usize {
49 half.saturating_mul(2).saturating_add(1).min(len)
50}
51
52pub fn hampel(values: &[f64], half: usize, threshold: f64) -> Vec<f64> {
63 let mut window = Vec::with_capacity(window_len(half, values.len()));
64 (0..values.len())
65 .map(|index| {
66 let value = values[index];
67 let start = index.saturating_sub(half);
68 let end = index
69 .saturating_add(half)
70 .saturating_add(1)
71 .min(values.len());
72 window.clear();
73 window.extend(values[start..end].iter().copied().filter(|v| v.is_finite()));
74 let Some(middle) = median(&mut window) else {
75 return value;
76 };
77 for sample in &mut window {
78 *sample = (*sample - middle).abs();
79 }
80 let scale = 1.4826 * median(&mut window).unwrap_or(0.0);
81 if (value - middle).abs() > threshold * scale {
82 middle
83 } else {
84 value
85 }
86 })
87 .collect()
88}
89
90pub(crate) fn median(values: &mut [f64]) -> Option<f64> {
93 values.sort_by(f64::total_cmp);
94 sorted_median(values)
95}
96
97fn sorted_median(values: &[f64]) -> Option<f64> {
99 let middle = values.len() / 2;
100 match values.len() {
101 0 => None,
102 len if len % 2 == 1 => Some(values[middle]),
103 _ => Some(0.5 * (values[middle - 1] + values[middle])),
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110
111 #[test]
113 fn spikes_go_and_monotonic_runs_stay() {
114 let spiked = [0.0, 0.0, 0.0, 0.0, 9.0, 8.0, 9.0, 0.0, 0.0, 0.0, 0.0];
115 assert_eq!(running_median(&spiked, 3), [0.0; 11]);
116 let early = [0.0, 0.0, 0.0, 9.0, 8.0, 9.0, 0.0, 0.0, 0.0, 0.0];
118 assert_eq!(running_median(&early, 3)[2], 4.0);
119 let step = [0.0, 0.0, 0.0, 0.0, 5.0, 5.0, 5.0, 5.0];
120 assert_eq!(running_median(&step, 3), step);
121 let ramp: Vec<f64> = (0..20).map(|i| f64::from(i) * 1.5).collect();
122 assert_eq!(running_median(&ramp, 3)[3..17], ramp[3..17]);
123 assert!(running_median(&[f64::NAN, f64::NAN], 1)[0].is_nan());
124 assert_eq!(running_median(&[1.0, f64::NAN, 3.0], 1), [1.0, 2.0, 3.0]);
125 }
126
127 #[test]
130 fn a_peak_sample_reads_the_fall_over_half_the_half_window() {
131 let (a, dt, half) = (9.806_65, 0.05, 3_usize);
132 let trace: Vec<f64> = (-20..=20)
133 .map(|i| -0.5 * a * (f64::from(i) * dt).powi(2))
134 .collect();
135 let peak = running_median(&trace, half)[20];
136 let bound = 0.5 * a * (2.0 * dt).powi(2);
137 assert!(
138 peak <= 0.0 && -peak <= bound * (1.0 + 1e-12),
139 "{peak} against {bound}"
140 );
141 assert_eq!(peak, trace[22]);
142 }
143
144 #[test]
146 fn hampel_at_zero_is_the_running_median() {
147 let values = [3.0, 1.0, 4.0, 1.0, 5.0, 9.0, 2.0, 6.0, 5.0, 3.0, 5.0];
148 for half in 1..4 {
149 assert_eq!(hampel(&values, half, 0.0), running_median(&values, half));
150 }
151 let ramp: Vec<f64> = (0..11).map(f64::from).collect();
154 let mut spiked = ramp.clone();
155 spiked[5] = 50.0;
156 let mut expected = ramp;
157 expected[5] = 6.0;
158 assert_eq!(hampel(&spiked, 3, 4.0), expected);
159 }
160
161 #[test]
163 fn the_widest_window_is_the_whole_record() {
164 let values = [3.0, 1.0, 4.0, 1.0, 5.0];
165 assert_eq!(running_median(&values, usize::MAX), [3.0; 5]);
166 assert_eq!(hampel(&values, usize::MAX, 0.0), [3.0; 5]);
167 }
168
169 proptest::proptest! {
170 #[test]
173 fn the_sliding_median_is_each_windows_median(
174 values in proptest::collection::vec(
175 proptest::prop_oneof![
176 -5.0..5.0_f64,
177 proptest::strategy::Just(f64::NAN),
178 proptest::strategy::Just(0.0),
179 proptest::strategy::Just(-0.0),
180 proptest::strategy::Just(1.0),
181 ],
182 0..60,
183 ),
184 half in 0_usize..8,
185 ) {
186 let slid = running_median(&values, half);
187 for (index, out) in slid.iter().enumerate() {
188 let end = (index + half + 1).min(values.len());
189 let mut window: Vec<f64> = values[index.saturating_sub(half)..end]
190 .iter()
191 .copied()
192 .filter(|v| v.is_finite())
193 .collect();
194 let expected = median(&mut window).unwrap_or(f64::NAN);
195 proptest::prop_assert_eq!(out.to_bits(), expected.to_bits());
196 }
197 }
198
199 #[test]
201 fn outputs_lie_within_their_windows(
202 values in proptest::collection::vec(-1e6..1e6_f64, 0..60),
203 half in 0_usize..6,
204 ) {
205 let out = running_median(&values, half);
206 proptest::prop_assert_eq!(out.len(), values.len());
207 for (index, value) in out.iter().enumerate() {
208 let window = &values[index.saturating_sub(half)..(index + half + 1).min(values.len())];
209 let low = window.iter().copied().fold(f64::INFINITY, f64::min);
210 let high = window.iter().copied().fold(f64::NEG_INFINITY, f64::max);
211 proptest::prop_assert!(*value >= low && *value <= high);
212 }
213 }
214 }
215}