polydat_nodes/
stability.rs1#[polydat::polydat_node(category = Math, output_names(stable_value, stable))]
40fn is_stable(
41 samples: &[f64],
42 #[poly_default(0.05f64)] margin: polydat::derive_support::Const<f64>,
43 #[poly_default(8u64)] min_samples: polydat::derive_support::Const<u64>,
44) -> (f64, u64) {
45 let n = samples.len();
46 if n == 0 {
47 return (0.0, 0);
48 }
49 let median = median_of(samples);
50 if n < (*min_samples) as usize {
51 return (median, 0);
52 }
53 let mean = samples.iter().sum::<f64>() / n as f64;
54 let var = samples
55 .iter()
56 .map(|x| {
57 let d = x - mean;
58 d * d
59 })
60 .sum::<f64>()
61 / n as f64;
62 let threshold = (*margin) * median.abs().max(1.0);
63 let stable = if var.sqrt() <= threshold { 1 } else { 0 };
64 (median, stable)
65}
66
67fn median_of(samples: &[f64]) -> f64 {
70 let mut s = samples.to_vec();
71 let n = s.len();
72 let mid = n / 2;
73 let (lower, upper, _) = s.select_nth_unstable_by(mid, f64::total_cmp);
74 let upper = *upper;
75 if n % 2 == 1 {
76 upper
77 } else {
78 let below = lower.iter().copied().fold(f64::NEG_INFINITY, f64::max);
79 0.5 * (below + upper)
80 }
81}
82
83#[cfg(test)]
84mod tests {
85 use super::*;
86 use polydat::ast::{PolydatNode, SliceArc, Value};
87
88 fn window(v: &[f64]) -> Value {
89 Value::VecF64(SliceArc::from_vec(v.to_vec()))
90 }
91
92 fn eval(node: &IsStable, samples: &[f64]) -> (f64, u64) {
93 let mut out = [Value::None, Value::None];
94 node.eval(&[window(samples)], &mut out);
95 (out[0].as_f64(), out[1].as_u64())
96 }
97
98 #[test]
101 fn settles_on_a_steady_window_and_reports_the_level() {
102 let node = IsStable::new(0.05, 4);
103 let (_, stable) = eval(&node, &[0.0, 1.0, 3.0, 4.5, 5.0, 5.0, 5.0, 5.0]);
104 assert_eq!(stable, 0, "a window with the ramp in it is not steady");
105 let (level, stable) = eval(&node, &[5.0; 8]);
106 assert_eq!(stable, 1, "a steady window is stable");
107 assert!(
108 (level - 5.0).abs() < 1e-9,
109 "the level is the steady value, got {level}"
110 );
111 }
112
113 #[test]
116 fn a_trailing_outlier_does_not_move_the_level() {
117 let node = IsStable::new(0.05, 4);
118 let (level, stable) = eval(&node, &[5.0, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0, 0.0]);
119 assert!(
120 (level - 5.0).abs() < 1e-9,
121 "the median absorbs one outlier, got {level}"
122 );
123 assert_eq!(stable, 0, "one outlier breaks strict steady state");
124 }
125
126 #[test]
128 fn reports_unstable_until_min_samples() {
129 let node = IsStable::new(0.05, 4);
130 assert_eq!(eval(&node, &[5.0]).1, 0);
131 assert_eq!(eval(&node, &[5.0; 3]).1, 0);
132 assert_eq!(eval(&node, &[5.0; 4]).1, 1);
133 }
134
135 #[test]
137 fn a_near_zero_level_still_settles() {
138 let node = IsStable::new(0.05, 4);
139 let (level, stable) = eval(&node, &[0.0; 8]);
140 assert_eq!(stable, 1);
141 assert!(level.abs() < 1e-9);
142 }
143
144 #[test]
147 fn empty_and_even_windows() {
148 let node = IsStable::new(0.05, 1);
149 assert_eq!(eval(&node, &[]), (0.0, 0));
150 assert!((eval(&node, &[1.0, 4.0, 2.0, 3.0]).0 - 2.5).abs() < 1e-12);
151 }
152
153 #[test]
157 fn both_outputs_read_one_window_in_any_order() {
158 let mut k = polydat::dsl::compile::compile_polydat_kernel(
159 "extern samples: vec_f64\n(stable_value, stable) := is_stable(samples, 0.05, 4)",
160 )
161 .expect("compile");
162 k.set_input("samples", window(&[5.0; 8]))
163 .expect("write the window");
164 assert_eq!(k.pull("stable").as_u64(), 1);
165 assert!((k.pull("stable_value").as_f64() - 5.0).abs() < 1e-9);
166 assert_eq!(k.pull("stable").as_u64(), 1);
167 }
168}