1#[derive(Clone, Copy, Debug, PartialEq)]
7pub struct Edge {
8 pub at: f64,
10 pub rising: bool,
12}
13
14#[must_use]
26pub fn edges(x: &[f64], y: &[f64], low: f64, high: f64) -> Vec<Edge> {
27 let length = x.len().min(y.len());
28 if length < 2 {
29 return Vec::new();
30 }
31
32 let lowest = y[..length].iter().copied().fold(f64::INFINITY, f64::min);
33 let highest = y[..length]
34 .iter()
35 .copied()
36 .fold(f64::NEG_INFINITY, f64::max);
37 let range = highest - lowest;
38 if !range.is_finite() || range < f64::EPSILON {
39 return Vec::new();
40 }
41
42 let (low, high) = (lowest + range * low, lowest + range * high);
43 let mut state = level(y[0], low, high);
44 let mut found = Vec::new();
45
46 for index in 1..length {
47 let (previous, current) = (y[index - 1], y[index]);
48
49 match state {
50 Level::Beneath if current > high => {
51 found.push(Edge {
52 at: crossed(x, index, high, previous, current),
53 rising: true,
54 });
55 state = Level::Above;
56 }
57 Level::Above if current < low => {
58 found.push(Edge {
59 at: crossed(x, index, low, previous, current),
60 rising: false,
61 });
62 state = Level::Beneath;
63 }
64 Level::Between => state = level(current, low, high),
65 Level::Beneath | Level::Above => {}
66 }
67 }
68
69 found
70}
71
72#[must_use]
78pub fn crossing(x: &[f64], y: &[f64], target: f64) -> Option<f64> {
79 let length = x.len().min(y.len());
80
81 (1..length).find_map(|index| {
82 let (previous, current) = (y[index - 1], y[index]);
83 let straddles =
84 (previous <= target && target <= current) || (current <= target && target <= previous);
85
86 straddles.then(|| crossed(x, index, target, previous, current))
87 })
88}
89
90#[derive(Clone, Copy)]
92enum Level {
93 Beneath,
94 Between,
95 Above,
96}
97
98fn level(value: f64, low: f64, high: f64) -> Level {
100 if value > high {
101 Level::Above
102 } else if value < low {
103 Level::Beneath
104 } else {
105 Level::Between
106 }
107}
108
109fn crossed(x: &[f64], index: usize, target: f64, previous: f64, current: f64) -> f64 {
111 let (before, after) = (x[index - 1], x[index]);
112 let rise = current - previous;
113
114 if rise.abs() < f64::EPSILON {
115 return before;
116 }
117
118 before + (target - previous) / rise * (after - before)
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 fn axis(count: usize) -> Vec<f64> {
127 (0..count).map(|index| index as f64).collect()
128 }
129
130 #[test]
131 fn a_square_wave_makes_one_edge_per_transition() {
132 let y = vec![0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0];
133 let found = edges(&axis(8), &y, 0.1, 0.9);
134
135 assert_eq!(found.len(), 3);
136 assert!(found[0].rising);
137 assert!(!found[1].rising);
138 assert!(found[2].rising);
139 }
140
141 #[test]
142 fn an_edge_is_placed_between_the_samples_that_straddle_it() {
143 let found = edges(&axis(3), &[0.0, 2.0, 0.0], 0.25, 0.5);
145
146 assert!((found[0].at - 0.5).abs() < 1e-12, "rose at {}", found[0].at);
147 assert!(
148 (found[1].at - 1.75).abs() < 1e-12,
149 "fell at {}",
150 found[1].at
151 );
152 }
153
154 #[test]
155 fn hysteresis_refuses_to_trigger_twice_on_one_transition() {
156 let y = vec![0.0, 0.0, 1.0, 0.6, 1.0, 1.0];
159 let found = edges(&axis(6), &y, 0.1, 0.9);
160
161 assert_eq!(found.len(), 1);
162 assert!(found[0].rising);
163 }
164
165 #[test]
166 fn a_constant_waveform_has_no_edges() {
167 assert!(edges(&axis(10), &[5.0; 10], 0.1, 0.9).is_empty());
168 assert!(edges(&[], &[], 0.1, 0.9).is_empty());
169 assert!(edges(&[0.0], &[1.0], 0.1, 0.9).is_empty());
170 }
171
172 #[test]
173 fn a_crossing_is_the_first_one_in_either_direction() {
174 let y = vec![0.0, 1.0, 2.0, 1.0, 0.0];
175
176 assert!(crossing(&axis(5), &y, 1.5).is_some_and(|at| (at - 1.5).abs() < 1e-12));
177 assert!(crossing(&axis(5), &y, 0.5).is_some_and(|at| (at - 0.5).abs() < 1e-12));
178 }
179
180 #[test]
181 fn a_value_the_waveform_never_reaches_is_never_crossed() {
182 assert!(crossing(&axis(5), &[0.0, 1.0, 2.0, 1.0, 0.0], 5.0).is_none());
183 assert!(crossing(&[], &[], 1.0).is_none());
184 }
185
186 #[test]
187 fn a_flat_pair_straddling_the_target_answers_the_earlier_coordinate() {
188 assert!(crossing(&axis(3), &[1.0, 1.0, 2.0], 1.0).is_some_and(|at| at.abs() < 1e-12));
189 }
190}