1use std::sync::Arc;
4
5use serde::{Deserialize, Deserializer, Serialize};
6
7use crate::{LadduPhysicsError, LadduPhysicsResult};
8
9#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
11pub enum FinalUpperEdge {
12 #[default]
14 Exclusive,
15 Inclusive,
17}
18
19#[derive(Clone, Debug, PartialEq, Serialize)]
21#[serde(transparent)]
22pub struct BinningAxis {
23 edges: Arc<[f64]>,
24}
25
26impl<'de> Deserialize<'de> for BinningAxis {
27 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
28 where
29 D: Deserializer<'de>,
30 {
31 let edges = Vec::<f64>::deserialize(deserializer)?;
32 Self::new(edges).map_err(serde::de::Error::custom)
33 }
34}
35
36impl BinningAxis {
37 pub fn new(edges: impl IntoIterator<Item = f64>) -> LadduPhysicsResult<Self> {
42 let edges: Vec<_> = edges.into_iter().collect();
43 if edges.len() < 2 {
44 return Err(LadduPhysicsError::invalid_length(
45 "bin axis edges",
46 "at least 2",
47 edges.len(),
48 ));
49 }
50 if edges.iter().any(|edge| !edge.is_finite())
51 || edges.windows(2).any(|pair| pair[0] >= pair[1])
52 {
53 return Err(LadduPhysicsError::invalid_relation(
54 "bin axis edges must be finite and strictly increasing",
55 ));
56 }
57 Ok(Self {
58 edges: edges.into(),
59 })
60 }
61
62 pub fn uniform(count: usize, min: f64, max: f64) -> LadduPhysicsResult<Self> {
67 if count == 0 || !min.is_finite() || !max.is_finite() || min >= max {
68 return Err(LadduPhysicsError::invalid_relation(
69 "uniform bins require a positive count and finite min < max",
70 ));
71 }
72 let width = (max - min) / count as f64;
73 Self::new((0..=count).map(|index| min + index as f64 * width))
74 }
75
76 pub fn edges(&self) -> &[f64] {
78 &self.edges
79 }
80
81 pub fn bin_count(&self) -> usize {
83 self.edges.len() - 1
84 }
85
86 pub fn index(&self, value: f64, final_upper: FinalUpperEdge) -> Option<usize> {
88 if !value.is_finite() || value < self.edges[0] || value > *self.edges.last()? {
89 return None;
90 }
91 if value == *self.edges.last()? {
92 return (final_upper == FinalUpperEdge::Inclusive).then(|| self.bin_count() - 1);
93 }
94 let upper = self.edges.partition_point(|edge| *edge <= value);
95 upper
96 .checked_sub(1)
97 .filter(|index| *index < self.bin_count())
98 }
99}
100
101pub fn bin_shape(axes: &[BinningAxis]) -> Vec<usize> {
103 axes.iter().map(BinningAxis::bin_count).collect()
104}
105
106pub fn checked_bin_count(axes: &[BinningAxis]) -> Option<usize> {
108 axes.iter()
109 .try_fold(1usize, |count, axis| count.checked_mul(axis.bin_count()))
110}
111
112pub fn flat_bin_index(
116 axes: &[BinningAxis],
117 coordinates: &[f64],
118 final_upper: FinalUpperEdge,
119) -> Option<usize> {
120 if axes.len() != coordinates.len() {
121 return None;
122 }
123 axes.iter()
124 .zip(coordinates)
125 .try_fold(0usize, |flat, (axis, value)| {
126 flat.checked_mul(axis.bin_count())?
127 .checked_add(axis.index(*value, final_upper)?)
128 })
129}
130
131pub fn flat_bin_index_for_event(
136 axes: &[BinningAxis],
137 coordinate_columns: &[Vec<f64>],
138 event: usize,
139 final_upper: FinalUpperEdge,
140) -> Option<usize> {
141 if axes.len() != coordinate_columns.len() {
142 return None;
143 }
144 axes.iter()
145 .zip(coordinate_columns)
146 .try_fold(0usize, |flat, (axis, coordinates)| {
147 flat.checked_mul(axis.bin_count())?
148 .checked_add(axis.index(*coordinates.get(event)?, final_upper)?)
149 })
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 #[test]
157 fn validates_edges_and_assigns_boundaries_explicitly() {
158 assert!(BinningAxis::new([0.0]).is_err());
159 assert!(BinningAxis::new([0.0, f64::NAN]).is_err());
160 assert!(BinningAxis::new([0.0, 0.0]).is_err());
161
162 let axis = BinningAxis::new([0.0, 1.0, 3.0]).unwrap();
163 assert_eq!(axis.index(0.0, FinalUpperEdge::Exclusive), Some(0));
164 assert_eq!(axis.index(1.0, FinalUpperEdge::Exclusive), Some(1));
165 assert_eq!(axis.index(3.0, FinalUpperEdge::Exclusive), None);
166 assert_eq!(axis.index(3.0, FinalUpperEdge::Inclusive), Some(1));
167 assert_eq!(axis.index(f64::NAN, FinalUpperEdge::Inclusive), None);
168 }
169
170 #[test]
171 fn multidimensional_assignment_is_row_major() {
172 let axes = [
173 BinningAxis::new([0.0, 1.0, 2.0]).unwrap(),
174 BinningAxis::new([10.0, 20.0, 30.0, 40.0]).unwrap(),
175 ];
176 assert_eq!(bin_shape(&axes), vec![2, 3]);
177 assert_eq!(
178 flat_bin_index(&axes, &[1.5, 25.0], FinalUpperEdge::Exclusive),
179 Some(4)
180 );
181 assert_eq!(
182 flat_bin_index(&axes, &[2.0, 40.0], FinalUpperEdge::Exclusive),
183 None
184 );
185 let columns = vec![vec![1.5], vec![25.0]];
186 assert_eq!(
187 flat_bin_index_for_event(&axes, &columns, 0, FinalUpperEdge::Exclusive),
188 Some(4)
189 );
190 }
191}