#[derive(Clone, Debug)]
pub enum HistogramData {
RawValues(Vec<f32>),
PreBinned {
bin_edges: Vec<f32>,
counts: Vec<f32>,
},
Overlaid(Vec<HistogramSeries>),
}
#[derive(Clone, Debug)]
pub struct HistogramSeries {
pub values: Vec<f32>,
pub gate_id: u32,
}
impl HistogramData {
pub fn from_values(values: Vec<f32>) -> Self {
Self::RawValues(values)
}
pub fn pre_binned(
bin_edges: Vec<f32>,
counts: Vec<f32>,
) -> Result<Self, HistogramDataError> {
if bin_edges.len() != counts.len().saturating_add(1) {
return Err(HistogramDataError::BinCountMismatch {
edges_len: bin_edges.len(),
counts_len: counts.len(),
});
}
if bin_edges.len() < 2 {
return Err(HistogramDataError::TooFewBins);
}
Ok(Self::PreBinned { bin_edges, counts })
}
pub fn overlaid(series: Vec<(Vec<f32>, u32)>) -> Self {
Self::Overlaid(
series
.into_iter()
.map(|(values, gate_id)| HistogramSeries { values, gate_id })
.collect(),
)
}
pub fn is_overlaid(&self) -> bool {
matches!(self, Self::Overlaid(_))
}
}
#[derive(Debug, Clone)]
pub enum HistogramDataError {
BinCountMismatch { edges_len: usize, counts_len: usize },
TooFewBins,
}
impl std::fmt::Display for HistogramDataError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::BinCountMismatch { edges_len, counts_len } => {
write!(
f,
"bin_edges.len() ({}) must equal counts.len() + 1 (got {})",
edges_len, counts_len
)
}
Self::TooFewBins => write!(f, "need at least 2 bin edges"),
}
}
}
impl std::error::Error for HistogramDataError {}
#[derive(Clone, Debug)]
pub struct BinnedHistogram {
pub bin_centers: Vec<f64>,
pub counts: Vec<f64>,
}
pub fn bin_values(
values: &[f32],
num_bins: usize,
x_min: f32,
x_max: f32,
) -> Option<BinnedHistogram> {
if values.is_empty() || num_bins == 0 || x_max <= x_min {
return None;
}
let mut counts = vec![0.0f64; num_bins];
let bin_width = (x_max - x_min) as f64 / num_bins as f64;
if bin_width <= 0.0 {
return None;
}
for &v in values {
if v.is_nan() || v.is_infinite() {
continue;
}
if v < x_min || v > x_max {
continue;
}
let idx = ((v - x_min) as f64 / bin_width).floor() as usize;
let idx = idx.min(num_bins.saturating_sub(1));
counts[idx] += 1.0;
}
let bin_centers: Vec<f64> = (0..num_bins)
.map(|i| {
x_min as f64 + (i as f64 + 0.5) * bin_width
})
.collect();
Some(BinnedHistogram {
bin_centers,
counts,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_histogram_data_from_values() {
let data = HistogramData::from_values(vec![1.0, 2.0, 3.0]);
assert!(!data.is_overlaid());
}
#[test]
fn test_histogram_data_pre_binned_ok() {
let data = HistogramData::pre_binned(
vec![0.0, 1.0, 2.0, 3.0],
vec![5.0, 10.0, 7.0],
)
.unwrap();
assert!(!data.is_overlaid());
}
#[test]
fn test_histogram_data_pre_binned_mismatch() {
let result = HistogramData::pre_binned(vec![0.0, 1.0], vec![5.0, 10.0]);
assert!(result.is_err());
}
#[test]
fn test_histogram_data_overlaid() {
let data = HistogramData::overlaid(vec![
(vec![1.0, 2.0], 0),
(vec![2.0, 3.0], 1),
]);
assert!(data.is_overlaid());
}
#[test]
fn test_bin_values() {
let values = vec![0.0f32, 1.0, 1.0, 2.0, 2.0, 2.0, 3.0];
let binned = bin_values(&values, 3, 0.0, 3.0).unwrap();
assert_eq!(binned.bin_centers.len(), 3);
assert_eq!(binned.counts.len(), 3);
assert!(binned.counts[0] >= 1.0);
assert!(binned.counts[1] >= 1.0);
assert!(binned.counts[2] >= 1.0);
}
#[test]
fn test_bin_values_empty() {
assert!(bin_values(&[], 10, 0.0, 100.0).is_none());
}
}