use std::num::Saturating;
use crate::Error;
use crate::pixel::Mono;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BinIndex {
In(usize),
Underflow,
Overflow,
Nan,
}
pub trait BinningStrategy<V: Copy> {
type Range: Copy;
fn bin_count(&self) -> usize;
fn bin_index(&self, value: V) -> BinIndex;
fn bin_range(&self, index: usize) -> (Self::Range, Self::Range);
fn validate(&self) -> Result<(), Error> {
Ok(())
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct NaturalBins;
impl BinningStrategy<u8> for NaturalBins {
type Range = u8;
#[inline]
fn bin_count(&self) -> usize {
256
}
#[inline]
fn bin_index(&self, value: u8) -> BinIndex {
BinIndex::In(value as usize)
}
#[inline]
fn bin_range(&self, index: usize) -> (u8, u8) {
assert!(
index < 256,
"NaturalBins::bin_range: index {} out of range (bin_count = 256)",
index
);
let v = index as u8;
(v, v)
}
}
impl BinningStrategy<Saturating<u8>> for NaturalBins {
type Range = Saturating<u8>;
#[inline]
fn bin_count(&self) -> usize {
256
}
#[inline]
fn bin_index(&self, value: Saturating<u8>) -> BinIndex {
BinIndex::In(value.0 as usize)
}
#[inline]
fn bin_range(&self, index: usize) -> (Saturating<u8>, Saturating<u8>) {
assert!(
index < 256,
"NaturalBins::bin_range: index {} out of range (bin_count = 256)",
index
);
let v = Saturating(index as u8);
(v, v)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct LinearBins {
pub min: f64,
pub max: f64,
pub bin_count: usize,
}
impl LinearBins {
fn validate_self(&self) -> Result<(), Error> {
if !self.min.is_finite() {
return Err(Error::InvalidBinningStrategy(format!(
"LinearBins: min is not finite ({})",
self.min
)));
}
if !self.max.is_finite() {
return Err(Error::InvalidBinningStrategy(format!(
"LinearBins: max is not finite ({})",
self.max
)));
}
if self.min >= self.max {
return Err(Error::InvalidBinningStrategy(format!(
"LinearBins: min ({}) must be strictly less than max ({})",
self.min, self.max
)));
}
if self.bin_count == 0 {
return Err(Error::InvalidBinningStrategy(
"LinearBins: bin_count must be > 0".to_string(),
));
}
Ok(())
}
#[inline]
fn linear_bin_range(&self, index: usize) -> (f64, f64) {
assert!(
index < self.bin_count,
"LinearBins::bin_range: index {} out of range (bin_count = {})",
index,
self.bin_count
);
let step = (self.max - self.min) / self.bin_count as f64;
let lower = self.min + index as f64 * step;
let upper = if index + 1 == self.bin_count {
self.max
} else {
self.min + (index + 1) as f64 * step
};
(lower, upper)
}
}
#[inline]
fn classify_linear(value: f64, min: f64, max: f64, bin_count: usize) -> BinIndex {
if value.is_nan() {
return BinIndex::Nan;
}
if value < min {
return BinIndex::Underflow;
}
if value > max {
return BinIndex::Overflow;
}
if value == max {
return BinIndex::In(bin_count - 1);
}
let t = (value - min) / (max - min);
let i = (t * bin_count as f64).floor() as usize;
BinIndex::In(i.min(bin_count - 1))
}
trait WidenF64 {
fn widen_f64(self) -> f64;
}
impl WidenF64 for f32 {
#[inline]
fn widen_f64(self) -> f64 {
self as f64
}
}
impl WidenF64 for f64 {
#[inline]
fn widen_f64(self) -> f64 {
self
}
}
impl WidenF64 for u8 {
#[inline]
fn widen_f64(self) -> f64 {
self as f64
}
}
impl WidenF64 for u16 {
#[inline]
fn widen_f64(self) -> f64 {
self as f64
}
}
impl WidenF64 for u32 {
#[inline]
fn widen_f64(self) -> f64 {
self as f64
}
}
impl WidenF64 for Saturating<u8> {
#[inline]
fn widen_f64(self) -> f64 {
self.0 as f64
}
}
impl WidenF64 for Saturating<u16> {
#[inline]
fn widen_f64(self) -> f64 {
self.0 as f64
}
}
impl WidenF64 for Saturating<u32> {
#[inline]
fn widen_f64(self) -> f64 {
self.0 as f64
}
}
impl<const BITS: usize> WidenF64 for Mono<BITS> {
#[inline]
fn widen_f64(self) -> f64 {
self.value() as f64
}
}
macro_rules! impl_linear_for {
($t:ty) => {
impl BinningStrategy<$t> for LinearBins {
type Range = f64;
#[inline]
fn bin_count(&self) -> usize {
self.bin_count
}
#[inline]
fn bin_index(&self, value: $t) -> BinIndex {
classify_linear(value.widen_f64(), self.min, self.max, self.bin_count)
}
#[inline]
fn bin_range(&self, index: usize) -> (f64, f64) {
self.linear_bin_range(index)
}
fn validate(&self) -> Result<(), Error> {
self.validate_self()
}
}
};
}
impl_linear_for!(f32);
impl_linear_for!(f64);
impl_linear_for!(u8);
impl_linear_for!(u16);
impl_linear_for!(u32);
impl_linear_for!(Saturating<u8>);
impl_linear_for!(Saturating<u16>);
impl_linear_for!(Saturating<u32>);
impl<const BITS: usize> BinningStrategy<Mono<BITS>> for LinearBins {
type Range = f64;
#[inline]
fn bin_count(&self) -> usize {
self.bin_count
}
#[inline]
fn bin_index(&self, value: Mono<BITS>) -> BinIndex {
classify_linear(value.widen_f64(), self.min, self.max, self.bin_count)
}
#[inline]
fn bin_range(&self, index: usize) -> (f64, f64) {
self.linear_bin_range(index)
}
fn validate(&self) -> Result<(), Error> {
self.validate_self()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct CustomBins {
pub edges: Vec<f64>,
}
impl CustomBins {
fn validate_self(&self) -> Result<(), Error> {
if self.edges.len() < 2 {
return Err(Error::InvalidBinningStrategy(format!(
"CustomBins: need at least 2 edges, got {}",
self.edges.len()
)));
}
for (i, &e) in self.edges.iter().enumerate() {
if !e.is_finite() {
return Err(Error::InvalidBinningStrategy(format!(
"CustomBins: edge {} is not finite ({})",
i, e
)));
}
}
for w in self.edges.windows(2) {
if w[0] >= w[1] {
return Err(Error::InvalidBinningStrategy(format!(
"CustomBins: edges must be strictly increasing, found {} >= {}",
w[0], w[1]
)));
}
}
Ok(())
}
#[inline]
fn custom_bin_range(&self, index: usize) -> (f64, f64) {
let last_bin = self.edges.len() - 1;
assert!(
index < last_bin,
"CustomBins::bin_range: index {} out of range (bin_count = {})",
index,
last_bin
);
(self.edges[index], self.edges[index + 1])
}
}
#[inline]
fn classify_custom(value: f64, edges: &[f64]) -> BinIndex {
if value.is_nan() {
return BinIndex::Nan;
}
let last = edges.len() - 1;
if value < edges[0] {
return BinIndex::Underflow;
}
if value > edges[last] {
return BinIndex::Overflow;
}
if value == edges[last] {
return BinIndex::In(last - 1);
}
let i = edges.partition_point(|&e| e <= value) - 1;
BinIndex::In(i)
}
macro_rules! impl_custom_for {
($t:ty) => {
impl BinningStrategy<$t> for CustomBins {
type Range = f64;
#[inline]
fn bin_count(&self) -> usize {
self.edges.len() - 1
}
#[inline]
fn bin_index(&self, value: $t) -> BinIndex {
classify_custom(value.widen_f64(), &self.edges)
}
#[inline]
fn bin_range(&self, index: usize) -> (f64, f64) {
self.custom_bin_range(index)
}
fn validate(&self) -> Result<(), Error> {
self.validate_self()
}
}
};
}
impl_custom_for!(f32);
impl_custom_for!(f64);
impl_custom_for!(u8);
impl_custom_for!(u16);
impl_custom_for!(u32);
impl_custom_for!(Saturating<u8>);
impl_custom_for!(Saturating<u16>);
impl_custom_for!(Saturating<u32>);
impl<const BITS: usize> BinningStrategy<Mono<BITS>> for CustomBins {
type Range = f64;
#[inline]
fn bin_count(&self) -> usize {
self.edges.len() - 1
}
#[inline]
fn bin_index(&self, value: Mono<BITS>) -> BinIndex {
classify_custom(value.widen_f64(), &self.edges)
}
#[inline]
fn bin_range(&self, index: usize) -> (f64, f64) {
self.custom_bin_range(index)
}
fn validate(&self) -> Result<(), Error> {
self.validate_self()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bin_index_variants_are_distinct() {
assert_ne!(BinIndex::In(0), BinIndex::In(1));
assert_ne!(BinIndex::In(0), BinIndex::Underflow);
assert_ne!(BinIndex::Underflow, BinIndex::Overflow);
assert_ne!(BinIndex::Overflow, BinIndex::Nan);
}
#[test]
fn natural_bins_u8_count_is_256() {
assert_eq!(
<NaturalBins as BinningStrategy<u8>>::bin_count(&NaturalBins),
256
);
}
#[test]
fn natural_bins_u8_maps_every_value_to_its_index() {
for v in 0u8..=255 {
assert_eq!(
<NaturalBins as BinningStrategy<u8>>::bin_index(&NaturalBins, v),
BinIndex::In(v as usize)
);
}
}
#[test]
fn natural_bins_u8_bin_range_matches_index() {
assert_eq!(
<NaturalBins as BinningStrategy<u8>>::bin_range(&NaturalBins, 0),
(0_u8, 0_u8)
);
assert_eq!(
<NaturalBins as BinningStrategy<u8>>::bin_range(&NaturalBins, 255),
(255_u8, 255_u8)
);
}
#[test]
#[should_panic(expected = "out of range")]
fn natural_bins_u8_bin_range_panics_at_256() {
let _ = <NaturalBins as BinningStrategy<u8>>::bin_range(&NaturalBins, 256);
}
#[test]
fn natural_bins_u8_validate_is_ok() {
assert!(<NaturalBins as BinningStrategy<u8>>::validate(&NaturalBins).is_ok());
}
#[test]
fn natural_bins_satu8_maps_every_value_to_its_index() {
for v in 0u8..=255 {
let sv = Saturating(v);
assert_eq!(
<NaturalBins as BinningStrategy<Saturating<u8>>>::bin_index(&NaturalBins, sv),
BinIndex::In(v as usize)
);
}
}
#[test]
fn natural_bins_satu8_bin_range_matches_index() {
assert_eq!(
<NaturalBins as BinningStrategy<Saturating<u8>>>::bin_range(&NaturalBins, 0),
(Saturating(0_u8), Saturating(0_u8))
);
assert_eq!(
<NaturalBins as BinningStrategy<Saturating<u8>>>::bin_range(&NaturalBins, 255),
(Saturating(255_u8), Saturating(255_u8))
);
}
#[test]
#[should_panic(expected = "out of range")]
fn natural_bins_satu8_bin_range_panics_at_256() {
let _ = <NaturalBins as BinningStrategy<Saturating<u8>>>::bin_range(&NaturalBins, 256);
}
fn lb(min: f64, max: f64, bin_count: usize) -> LinearBins {
LinearBins {
min,
max,
bin_count,
}
}
#[test]
fn linear_validate_accepts_well_formed() {
assert!(<LinearBins as BinningStrategy<f32>>::validate(&lb(0.0, 1.0, 4)).is_ok());
}
#[test]
fn linear_validate_rejects_non_finite_min() {
let err =
<LinearBins as BinningStrategy<f32>>::validate(&lb(f64::NAN, 1.0, 4)).unwrap_err();
match err {
Error::InvalidBinningStrategy(msg) => assert!(msg.contains("min")),
_ => panic!("expected InvalidBinningStrategy"),
}
}
#[test]
fn linear_validate_rejects_non_finite_max() {
let err =
<LinearBins as BinningStrategy<f32>>::validate(&lb(0.0, f64::INFINITY, 4)).unwrap_err();
match err {
Error::InvalidBinningStrategy(msg) => assert!(msg.contains("max")),
_ => panic!("expected InvalidBinningStrategy"),
}
}
#[test]
fn linear_validate_rejects_min_eq_max() {
let err = <LinearBins as BinningStrategy<f32>>::validate(&lb(1.0, 1.0, 4)).unwrap_err();
assert!(matches!(err, Error::InvalidBinningStrategy(_)));
}
#[test]
fn linear_validate_rejects_min_gt_max() {
let err = <LinearBins as BinningStrategy<f32>>::validate(&lb(2.0, 1.0, 4)).unwrap_err();
assert!(matches!(err, Error::InvalidBinningStrategy(_)));
}
#[test]
fn linear_validate_rejects_zero_bin_count() {
let err = <LinearBins as BinningStrategy<f32>>::validate(&lb(0.0, 1.0, 0)).unwrap_err();
match err {
Error::InvalidBinningStrategy(msg) => assert!(msg.contains("bin_count")),
_ => panic!("expected InvalidBinningStrategy"),
}
}
#[test]
fn linear_f32_min_maps_to_first_bin() {
let s = lb(0.0, 1.0, 4);
assert_eq!(s.bin_index(0.0_f32), BinIndex::In(0));
}
#[test]
fn linear_f32_max_maps_to_last_bin() {
let s = lb(0.0, 1.0, 4);
assert_eq!(s.bin_index(1.0_f32), BinIndex::In(3));
}
#[test]
fn linear_f32_midpoint_maps_to_expected_bin() {
let s = lb(0.0, 1.0, 4);
assert_eq!(s.bin_index(0.0_f32), BinIndex::In(0));
assert_eq!(s.bin_index(0.249_f32), BinIndex::In(0));
assert_eq!(s.bin_index(0.25_f32), BinIndex::In(1));
assert_eq!(s.bin_index(0.5_f32), BinIndex::In(2));
assert_eq!(s.bin_index(0.75_f32), BinIndex::In(3));
}
#[test]
fn linear_f32_below_range_is_underflow() {
let s = lb(0.0, 1.0, 4);
assert_eq!(s.bin_index(-0.001_f32), BinIndex::Underflow);
assert_eq!(s.bin_index(f32::NEG_INFINITY), BinIndex::Underflow);
}
#[test]
fn linear_f32_above_range_is_overflow() {
let s = lb(0.0, 1.0, 4);
assert_eq!(s.bin_index(1.001_f32), BinIndex::Overflow);
assert_eq!(s.bin_index(f32::INFINITY), BinIndex::Overflow);
}
#[test]
fn linear_f32_nan_is_nan() {
let s = lb(0.0, 1.0, 4);
assert_eq!(s.bin_index(f32::NAN), BinIndex::Nan);
}
#[test]
fn linear_f64_classification_matches_f32() {
let s = lb(0.0, 1.0, 4);
assert_eq!(s.bin_index(0.5_f64), BinIndex::In(2));
assert_eq!(s.bin_index(f64::NAN), BinIndex::Nan);
assert_eq!(s.bin_index(-1.0_f64), BinIndex::Underflow);
assert_eq!(s.bin_index(2.0_f64), BinIndex::Overflow);
}
#[test]
fn linear_u8_classification() {
let s = lb(0.0, 255.0, 4);
assert_eq!(s.bin_index(0_u8), BinIndex::In(0));
assert_eq!(s.bin_index(127_u8), BinIndex::In(1));
assert_eq!(s.bin_index(255_u8), BinIndex::In(3));
}
#[test]
fn linear_u16_classification() {
let s = lb(0.0, 65535.0, 8);
assert_eq!(s.bin_index(0_u16), BinIndex::In(0));
assert_eq!(s.bin_index(65535_u16), BinIndex::In(7));
}
#[test]
fn linear_u32_classification() {
let s = lb(0.0, 4_294_967_295.0, 4);
assert_eq!(s.bin_index(0_u32), BinIndex::In(0));
assert_eq!(s.bin_index(u32::MAX), BinIndex::In(3));
}
#[test]
fn linear_satu8_unwraps_correctly() {
let s = lb(0.0, 255.0, 4);
assert_eq!(s.bin_index(Saturating(0_u8)), BinIndex::In(0));
assert_eq!(s.bin_index(Saturating(255_u8)), BinIndex::In(3));
}
#[test]
fn linear_satu16_unwraps_correctly() {
let s = lb(0.0, 65535.0, 4);
assert_eq!(s.bin_index(Saturating(0_u16)), BinIndex::In(0));
assert_eq!(s.bin_index(Saturating(65535_u16)), BinIndex::In(3));
}
#[test]
fn linear_satu32_unwraps_correctly() {
let s = lb(0.0, 4_294_967_295.0, 4);
assert_eq!(s.bin_index(Saturating(0_u32)), BinIndex::In(0));
assert_eq!(s.bin_index(Saturating(u32::MAX)), BinIndex::In(3));
}
#[test]
fn linear_mono10_uses_value() {
let s = lb(0.0, 1023.0, 4);
assert_eq!(s.bin_index(Mono::<10>::new(0)), BinIndex::In(0));
assert_eq!(s.bin_index(Mono::<10>::new(1023)), BinIndex::In(3));
}
#[test]
fn linear_mono12_uses_value() {
let s = lb(0.0, 4095.0, 4);
assert_eq!(s.bin_index(Mono::<12>::new(0)), BinIndex::In(0));
assert_eq!(s.bin_index(Mono::<12>::new(4095)), BinIndex::In(3));
}
#[test]
fn linear_mono14_uses_value() {
let s = lb(0.0, 16383.0, 4);
assert_eq!(s.bin_index(Mono::<14>::new(0)), BinIndex::In(0));
assert_eq!(s.bin_index(Mono::<14>::new(16383)), BinIndex::In(3));
}
#[test]
fn linear_bin_range_returns_f64_edges() {
let s = lb(0.0, 1.0, 4);
let (lo, hi) = <LinearBins as BinningStrategy<f32>>::bin_range(&s, 0);
assert_eq!(lo, 0.0);
assert_eq!(hi, 0.25);
}
#[test]
fn linear_bin_range_last_pinned_to_max() {
let s = lb(0.0, 1.0, 3);
let (_, hi) = <LinearBins as BinningStrategy<f32>>::bin_range(&s, 2);
assert_eq!(hi, 1.0);
}
#[test]
#[should_panic(expected = "out of range")]
fn linear_bin_range_panics_when_out_of_range() {
let s = lb(0.0, 1.0, 4);
let _ = <LinearBins as BinningStrategy<f32>>::bin_range(&s, 4);
}
fn cb(edges: &[f64]) -> CustomBins {
CustomBins {
edges: edges.to_vec(),
}
}
#[test]
fn custom_validate_accepts_well_formed() {
assert!(<CustomBins as BinningStrategy<f32>>::validate(&cb(&[0.0, 1.0, 2.0])).is_ok());
}
#[test]
fn custom_validate_rejects_zero_or_one_edge() {
let err = <CustomBins as BinningStrategy<f32>>::validate(&cb(&[])).unwrap_err();
assert!(matches!(err, Error::InvalidBinningStrategy(_)));
let err = <CustomBins as BinningStrategy<f32>>::validate(&cb(&[1.0])).unwrap_err();
assert!(matches!(err, Error::InvalidBinningStrategy(_)));
}
#[test]
fn custom_validate_rejects_non_finite_edge() {
let err =
<CustomBins as BinningStrategy<f32>>::validate(&cb(&[0.0, f64::NAN, 1.0])).unwrap_err();
match err {
Error::InvalidBinningStrategy(msg) => assert!(msg.contains("finite")),
_ => panic!("expected InvalidBinningStrategy"),
}
}
#[test]
fn custom_validate_rejects_non_increasing_edges() {
let err =
<CustomBins as BinningStrategy<f32>>::validate(&cb(&[0.0, 1.0, 1.0])).unwrap_err();
assert!(matches!(err, Error::InvalidBinningStrategy(_)));
let err =
<CustomBins as BinningStrategy<f32>>::validate(&cb(&[0.0, 2.0, 1.0])).unwrap_err();
assert!(matches!(err, Error::InvalidBinningStrategy(_)));
}
#[test]
fn custom_lower_edge_maps_to_that_bin() {
let s = cb(&[0.0, 1.0, 2.0, 3.0]);
assert_eq!(s.bin_index(0.0_f32), BinIndex::In(0));
assert_eq!(s.bin_index(1.0_f32), BinIndex::In(1));
assert_eq!(s.bin_index(2.0_f32), BinIndex::In(2));
}
#[test]
fn custom_final_edge_maps_to_last_bin() {
let s = cb(&[0.0, 1.0, 2.0, 3.0]);
assert_eq!(s.bin_index(3.0_f32), BinIndex::In(2));
}
#[test]
fn custom_below_first_edge_is_underflow() {
let s = cb(&[0.0, 1.0, 2.0, 3.0]);
assert_eq!(s.bin_index(-0.001_f32), BinIndex::Underflow);
}
#[test]
fn custom_above_final_edge_is_overflow() {
let s = cb(&[0.0, 1.0, 2.0, 3.0]);
assert_eq!(s.bin_index(3.001_f32), BinIndex::Overflow);
}
#[test]
fn custom_nan_is_nan_for_floats() {
let s = cb(&[0.0, 1.0, 2.0]);
assert_eq!(s.bin_index(f32::NAN), BinIndex::Nan);
assert_eq!(s.bin_index(f64::NAN), BinIndex::Nan);
}
#[test]
fn custom_interior_value_is_classified_by_partition() {
let s = cb(&[0.0, 0.5, 1.0, 4.0]);
assert_eq!(s.bin_index(0.25_f64), BinIndex::In(0));
assert_eq!(s.bin_index(0.75_f64), BinIndex::In(1));
assert_eq!(s.bin_index(2.5_f64), BinIndex::In(2));
}
#[test]
fn custom_bin_count_matches_edges_minus_one() {
let s = cb(&[0.0, 1.0, 2.0, 3.0, 4.0]);
assert_eq!(<CustomBins as BinningStrategy<f32>>::bin_count(&s), 4);
}
#[test]
fn custom_bin_range_returns_adjacent_edges() {
let s = cb(&[0.0, 0.5, 1.0, 4.0]);
assert_eq!(
<CustomBins as BinningStrategy<f32>>::bin_range(&s, 0),
(0.0, 0.5)
);
assert_eq!(
<CustomBins as BinningStrategy<f32>>::bin_range(&s, 1),
(0.5, 1.0)
);
assert_eq!(
<CustomBins as BinningStrategy<f32>>::bin_range(&s, 2),
(1.0, 4.0)
);
}
#[test]
#[should_panic(expected = "out of range")]
fn custom_bin_range_panics_when_out_of_range() {
let s = cb(&[0.0, 1.0, 2.0]);
let _ = <CustomBins as BinningStrategy<f32>>::bin_range(&s, 2);
}
#[test]
fn custom_mono_channels_use_value() {
let s = cb(&[0.0, 512.0, 1024.0]);
assert_eq!(s.bin_index(Mono::<10>::new(0)), BinIndex::In(0));
assert_eq!(s.bin_index(Mono::<10>::new(600)), BinIndex::In(1));
assert_eq!(s.bin_index(Mono::<10>::new(1023)), BinIndex::In(1));
}
#[test]
fn custom_int_wrapper_channels_classify() {
let s = cb(&[0.0, 100.0, 200.0]);
assert_eq!(s.bin_index(Saturating(50_u8)), BinIndex::In(0));
assert_eq!(s.bin_index(Saturating(150_u8)), BinIndex::In(1));
assert_eq!(s.bin_index(Saturating(200_u8)), BinIndex::In(1)); assert_eq!(s.bin_index(Saturating(50_u16)), BinIndex::In(0));
assert_eq!(s.bin_index(Saturating(150_u32)), BinIndex::In(1));
}
}