1use ndarray::{Array1, CowArray, Ix1};
2
3use super::DataError;
4
5#[derive(Clone, Debug)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize), serde(transparent))]
14pub struct XAxis(Repr);
15
16#[cfg(feature = "serde")]
19impl<'de> serde::Deserialize<'de> for XAxis {
20 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
21 match Repr::deserialize(deserializer)? {
22 Repr::Uniform { start, step, len } => XAxis::uniform(start, step, len),
23 Repr::Explicit(values) => XAxis::explicit(values),
24 }
25 .map_err(serde::de::Error::custom)
26 }
27}
28
29#[derive(Clone, Debug)]
30#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
31enum Repr {
32 Uniform {
34 start: f64,
35 step: f64,
36 len: usize,
37 },
38 Explicit(Array1<f64>),
39}
40
41impl XAxis {
42 pub fn uniform(start: f64, step: f64, len: usize) -> Result<Self, DataError> {
46 if !start.is_finite() || !step.is_finite() || step <= 0.0 {
47 return Err(DataError::InvalidUniformAxis { start, step });
48 }
49 Ok(Self(Repr::Uniform { start, step, len }))
50 }
51
52 pub fn explicit(values: Array1<f64>) -> Result<Self, DataError> {
54 if !values.windows(2).into_iter().all(|pair| pair[1] > pair[0]) {
55 return Err(DataError::NotIncreasing);
56 }
57 Ok(Self(Repr::Explicit(values)))
58 }
59
60 pub fn indices(len: usize) -> Self {
62 Self(Repr::Uniform {
63 start: 0.0,
64 step: 1.0,
65 len,
66 })
67 }
68
69 pub fn len(&self) -> usize {
70 match &self.0 {
71 Repr::Uniform { len, .. } => *len,
72 Repr::Explicit(values) => values.len(),
73 }
74 }
75
76 pub fn is_empty(&self) -> bool {
77 self.len() == 0
78 }
79
80 pub fn as_uniform(&self) -> Option<(f64, f64)> {
82 match &self.0 {
83 Repr::Uniform { start, step, .. } => Some((*start, *step)),
84 Repr::Explicit(_) => None,
85 }
86 }
87
88 pub fn get(&self, index: usize) -> f64 {
94 match &self.0 {
95 Repr::Uniform { start, step, len } => {
96 assert!(
97 index < *len,
98 "index {index} out of bounds for axis of length {len}"
99 );
100 start + index as f64 * step
101 }
102 Repr::Explicit(values) => values[index],
103 }
104 }
105
106 pub fn iter(&self) -> impl ExactSizeIterator<Item = f64> + '_ {
107 (0..self.len()).map(move |index| match &self.0 {
108 Repr::Uniform { start, step, .. } => start + index as f64 * step,
109 Repr::Explicit(values) => values[index],
110 })
111 }
112
113 pub fn to_cow_array(&self) -> CowArray<'_, f64, Ix1> {
116 match &self.0 {
117 Repr::Uniform { .. } => CowArray::from(self.to_array()),
118 Repr::Explicit(values) => CowArray::from(values.view()),
119 }
120 }
121
122 pub fn to_array(&self) -> Array1<f64> {
124 match &self.0 {
125 Repr::Uniform { .. } => Array1::from_iter(self.iter()),
126 Repr::Explicit(values) => values.clone(),
127 }
128 }
129
130 pub fn nearest_index(&self, x: f64) -> usize {
133 let len = self.len();
134 if len == 0 || x.is_nan() {
135 return 0;
136 }
137
138 let upper = match &self.0 {
139 Repr::Uniform { start, step, .. } => {
140 let mut upper = ((x - start) / step).ceil().clamp(0.0, len as f64) as usize;
142 while upper > 0 && self.get(upper - 1) >= x {
143 upper -= 1;
144 }
145 while upper < len && self.get(upper) < x {
146 upper += 1;
147 }
148 upper
149 }
150 Repr::Explicit(values) => values
151 .as_slice()
152 .expect("owned 1-D array is contiguous")
153 .partition_point(|&value| value < x),
154 };
155
156 if upper == 0 {
157 return 0;
158 }
159 if upper == len {
160 return len - 1;
161 }
162
163 let lower = upper - 1;
164 if x - self.get(lower) <= self.get(upper) - x {
165 lower
166 } else {
167 upper
168 }
169 }
170}
171
172impl PartialEq for XAxis {
173 fn eq(&self, other: &Self) -> bool {
174 if self.len() != other.len() {
175 return false;
176 }
177 if let (
178 Repr::Uniform {
179 start: a_start,
180 step: a_step,
181 len,
182 },
183 Repr::Uniform {
184 start: b_start,
185 step: b_step,
186 ..
187 },
188 ) = (&self.0, &other.0)
189 {
190 return *len == 0 || (a_start == b_start && (*len == 1 || a_step == b_step));
192 }
193 self.iter().zip(other.iter()).all(|(a, b)| a == b)
194 }
195}
196
197impl approx::AbsDiffEq for XAxis {
198 type Epsilon = f64;
199
200 fn default_epsilon() -> Self::Epsilon {
201 f64::EPSILON
202 }
203
204 fn abs_diff_eq(&self, other: &Self, epsilon: Self::Epsilon) -> bool {
205 self.len() == other.len()
206 && self
207 .iter()
208 .zip(other.iter())
209 .all(|(a, b)| a.abs_diff_eq(&b, epsilon))
210 }
211}
212
213#[cfg(test)]
214mod tests {
215 use ndarray::array;
216
217 use super::*;
218
219 #[test]
220 fn constructors_validate_input() {
221 assert!(XAxis::uniform(0.0, 0.5, 4).is_ok());
222 assert!(XAxis::uniform(0.0, 0.0, 4).is_err());
223 assert!(XAxis::uniform(0.0, -1.0, 4).is_err());
224 assert!(XAxis::uniform(f64::NAN, 1.0, 4).is_err());
225 assert!(XAxis::uniform(0.0, f64::INFINITY, 4).is_err());
226
227 assert!(XAxis::explicit(array![0.0, 1.0, 5.0]).is_ok());
228 assert!(matches!(
229 XAxis::explicit(array![0.0, 1.0, 1.0]),
230 Err(DataError::NotIncreasing)
231 ));
232 }
233
234 #[test]
235 fn uniform_and_explicit_axes_expose_the_same_values() {
236 let uniform = XAxis::uniform(1.0, 0.25, 5).unwrap();
237 let explicit = XAxis::explicit(array![1.0, 1.25, 1.5, 1.75, 2.0]).unwrap();
238
239 assert_eq!(uniform.len(), 5);
240 assert_eq!(uniform.as_uniform(), Some((1.0, 0.25)));
241 assert_eq!(explicit.as_uniform(), None);
242 assert_eq!(uniform.get(3), 1.75);
243 assert_eq!(
244 uniform.iter().collect::<Vec<_>>(),
245 explicit.iter().collect::<Vec<_>>()
246 );
247 assert_eq!(uniform.to_array(), explicit.to_array());
248 assert_eq!(uniform.to_cow_array(), explicit.to_cow_array());
249 assert_eq!(uniform, explicit);
250 assert_eq!(
251 XAxis::indices(3),
252 XAxis::explicit(array![0.0, 1.0, 2.0]).unwrap()
253 );
254 }
255
256 #[test]
257 fn equality_compares_values() {
258 let a = XAxis::uniform(0.0, 1.0, 3).unwrap();
259 assert_ne!(a, XAxis::uniform(0.0, 2.0, 3).unwrap());
260 assert_ne!(a, XAxis::uniform(0.0, 1.0, 4).unwrap());
261 assert_eq!(
263 XAxis::uniform(2.0, 1.0, 1).unwrap(),
264 XAxis::uniform(2.0, 5.0, 1).unwrap()
265 );
266 assert_eq!(
267 XAxis::uniform(2.0, 1.0, 0).unwrap(),
268 XAxis::explicit(Array1::zeros(0)).unwrap()
269 );
270 }
271
272 #[cfg(feature = "serde")]
273 #[test]
274 fn deserialization_round_trips_and_validates() {
275 for axis in [
276 XAxis::uniform(0.5, 0.25, 4).unwrap(),
277 XAxis::explicit(array![0.0, 1.0, 5.0]).unwrap(),
278 ] {
279 let json = serde_json::to_string(&axis).unwrap();
280 let back: XAxis = serde_json::from_str(&json).unwrap();
281 assert_eq!(back, axis);
282 assert_eq!(back.as_uniform(), axis.as_uniform());
283 }
284
285 let unordered = r#"{"Explicit":{"v":1,"dim":[3],"data":[0.0,2.0,1.0]}}"#;
288 let error = serde_json::from_str::<XAxis>(unordered).unwrap_err();
289 assert!(error.to_string().contains("not increasing"), "{error}");
290
291 let zero_step = r#"{"Uniform":{"start":0.0,"step":0.0,"len":3}}"#;
292 assert!(serde_json::from_str::<XAxis>(zero_step).is_err());
293
294 let signal = crate::signal::TimeSignal::zeros(1, 3, 8.0).unwrap();
296 let json = serde_json::to_string(&signal).unwrap();
297 assert_eq!(
298 serde_json::from_str::<crate::signal::TimeSignal>(&json).unwrap(),
299 signal
300 );
301 }
302
303 #[test]
304 #[should_panic(expected = "out of bounds")]
305 fn uniform_get_panics_out_of_bounds() {
306 XAxis::uniform(0.0, 1.0, 3).unwrap().get(3);
307 }
308
309 #[test]
310 fn nearest_index_matches_linear_scan_for_both_kinds() {
311 let linear_scan = |axis: &XAxis, x: f64| {
312 axis.iter()
313 .enumerate()
314 .min_by(|(_, a), (_, b)| {
315 (a - x)
316 .abs()
317 .partial_cmp(&(b - x).abs())
318 .unwrap_or(std::cmp::Ordering::Equal)
319 })
320 .map(|(index, _)| index)
321 .unwrap()
322 };
323
324 let axes = [
325 XAxis::uniform(0.0, 0.1, 11).unwrap(),
326 XAxis::uniform(-3.0, 1.5, 7).unwrap(),
327 XAxis::explicit(array![0.0, 1.0, 2.5, 4.0, 10.0]).unwrap(),
328 ];
329 for axis in &axes {
330 let lo = axis.get(0) - 2.0;
331 let hi = axis.get(axis.len() - 1) + 2.0;
332 for i in 0..=200 {
333 let x = lo + (hi - lo) * i as f64 / 200.0;
334 assert_eq!(axis.nearest_index(x), linear_scan(axis, x), "x = {x}");
335 }
336 for i in 0..axis.len() {
337 assert_eq!(axis.nearest_index(axis.get(i)), i);
339 if i + 1 < axis.len() {
340 let mid = 0.5 * (axis.get(i) + axis.get(i + 1));
341 assert_eq!(axis.nearest_index(mid), linear_scan(axis, mid));
342 }
343 }
344 assert_eq!(axis.nearest_index(f64::NAN), 0);
345 assert_eq!(axis.nearest_index(f64::INFINITY), axis.len() - 1);
346 assert_eq!(axis.nearest_index(f64::NEG_INFINITY), 0);
347 }
348 assert_eq!(XAxis::indices(0).nearest_index(1.0), 0);
349 }
350}