use serde::{Deserialize, Serialize};
use crate::error::GenomeError;
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Bounds {
pub min: f64,
pub max: f64,
}
impl Bounds {
pub fn new(min: f64, max: f64) -> Self {
Self::try_new(min, max).unwrap_or_else(|e| panic!("{e}"))
}
pub fn try_new(min: f64, max: f64) -> Result<Self, GenomeError> {
if matches!(
min.partial_cmp(&max),
None | Some(std::cmp::Ordering::Greater)
) {
return Err(GenomeError::InvalidStructure(format!(
"Invalid bounds: min ({min}) must be <= max ({max})"
)));
}
Ok(Self { min, max })
}
pub fn symmetric(half_width: f64) -> Self {
Self::new(-half_width, half_width)
}
pub fn unit() -> Self {
Self::new(0.0, 1.0)
}
pub fn range(&self) -> f64 {
self.max - self.min
}
pub fn center(&self) -> f64 {
(self.min + self.max) / 2.0
}
pub fn contains(&self, value: f64) -> bool {
value >= self.min && value <= self.max
}
pub fn clamp(&self, value: f64) -> f64 {
value.clamp(self.min, self.max)
}
pub fn normalize(&self, value: f64) -> f64 {
let range = self.range();
if range <= 0.0 {
return 0.5;
}
(value - self.min) / range
}
pub fn denormalize(&self, value: f64) -> f64 {
let range = self.range();
if range <= 0.0 {
return self.min;
}
self.min + value * range
}
}
impl Default for Bounds {
fn default() -> Self {
Self::symmetric(5.12) }
}
impl From<(f64, f64)> for Bounds {
fn from((min, max): (f64, f64)) -> Self {
Self::new(min, max)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MultiBounds {
pub bounds: Vec<Bounds>,
}
impl MultiBounds {
pub fn new(bounds: Vec<Bounds>) -> Self {
Self { bounds }
}
pub fn uniform(bound: Bounds, dimension: usize) -> Self {
Self {
bounds: vec![bound; dimension],
}
}
pub fn symmetric(half_width: f64, dimension: usize) -> Self {
Self::uniform(Bounds::symmetric(half_width), dimension)
}
pub fn dimension(&self) -> usize {
self.bounds.len()
}
pub fn get(&self, index: usize) -> Option<&Bounds> {
self.bounds.get(index)
}
pub fn clamp_vec(&self, values: &mut [f64]) {
for (i, value) in values.iter_mut().enumerate() {
if let Some(b) = self.bounds.get(i) {
*value = b.clamp(*value);
}
}
}
pub fn contains_vec(&self, values: &[f64]) -> bool {
values
.iter()
.enumerate()
.all(|(i, &v)| self.bounds.get(i).is_some_and(|b| b.contains(v)))
}
}
impl FromIterator<Bounds> for MultiBounds {
fn from_iter<I: IntoIterator<Item = Bounds>>(iter: I) -> Self {
Self {
bounds: iter.into_iter().collect(),
}
}
}
impl FromIterator<(f64, f64)> for MultiBounds {
fn from_iter<I: IntoIterator<Item = (f64, f64)>>(iter: I) -> Self {
Self {
bounds: iter.into_iter().map(Bounds::from).collect(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bounds_new() {
let b = Bounds::new(-5.0, 5.0);
assert_eq!(b.min, -5.0);
assert_eq!(b.max, 5.0);
}
#[test]
#[should_panic(expected = "Invalid bounds")]
fn test_bounds_invalid() {
Bounds::new(5.0, -5.0);
}
#[test]
fn test_bounds_symmetric() {
let b = Bounds::symmetric(3.0);
assert_eq!(b.min, -3.0);
assert_eq!(b.max, 3.0);
}
#[test]
fn test_bounds_unit() {
let b = Bounds::unit();
assert_eq!(b.min, 0.0);
assert_eq!(b.max, 1.0);
}
#[test]
fn test_bounds_range() {
let b = Bounds::new(-5.0, 5.0);
assert_eq!(b.range(), 10.0);
}
#[test]
fn test_bounds_center() {
let b = Bounds::new(-2.0, 6.0);
assert_eq!(b.center(), 2.0);
}
#[test]
fn test_bounds_contains() {
let b = Bounds::new(-5.0, 5.0);
assert!(b.contains(0.0));
assert!(b.contains(-5.0));
assert!(b.contains(5.0));
assert!(!b.contains(-5.1));
assert!(!b.contains(5.1));
}
#[test]
fn test_bounds_clamp() {
let b = Bounds::new(-5.0, 5.0);
assert_eq!(b.clamp(0.0), 0.0);
assert_eq!(b.clamp(-10.0), -5.0);
assert_eq!(b.clamp(10.0), 5.0);
}
#[test]
fn test_bounds_normalize() {
let b = Bounds::new(0.0, 10.0);
assert_eq!(b.normalize(0.0), 0.0);
assert_eq!(b.normalize(5.0), 0.5);
assert_eq!(b.normalize(10.0), 1.0);
}
#[test]
fn test_bounds_denormalize() {
let b = Bounds::new(0.0, 10.0);
assert_eq!(b.denormalize(0.0), 0.0);
assert_eq!(b.denormalize(0.5), 5.0);
assert_eq!(b.denormalize(1.0), 10.0);
}
#[test]
fn test_bounds_try_new_rejects_min_gt_max() {
let result = Bounds::try_new(5.0, -5.0);
assert!(result.is_err());
assert!(Bounds::try_new(-5.0, 5.0).is_ok());
assert!(Bounds::try_new(3.0, 3.0).is_ok());
}
#[test]
fn test_bounds_try_new_rejects_nan() {
assert!(Bounds::try_new(f64::NAN, 5.0).is_err());
assert!(Bounds::try_new(-5.0, f64::NAN).is_err());
assert!(Bounds::try_new(f64::NAN, f64::NAN).is_err());
}
#[test]
fn test_bounds_degenerate_normalize_denormalize() {
let b = Bounds::new(3.0, 3.0);
assert_eq!(b.range(), 0.0);
assert_eq!(b.normalize(3.0), 0.5);
assert!(b.normalize(3.0).is_finite());
assert_eq!(b.normalize(100.0), 0.5);
assert!(b.normalize(100.0).is_finite());
assert_eq!(b.denormalize(0.0), 3.0);
assert_eq!(b.denormalize(0.5), 3.0);
assert_eq!(b.denormalize(1.0), 3.0);
}
#[test]
fn test_multi_bounds_uniform() {
let mb = MultiBounds::symmetric(5.0, 3);
assert_eq!(mb.dimension(), 3);
assert_eq!(mb.get(0), Some(&Bounds::symmetric(5.0)));
assert_eq!(mb.get(1), Some(&Bounds::symmetric(5.0)));
assert_eq!(mb.get(2), Some(&Bounds::symmetric(5.0)));
assert_eq!(mb.get(3), None);
}
#[test]
fn test_multi_bounds_clamp_vec() {
let mb = MultiBounds::symmetric(5.0, 3);
let mut values = vec![-10.0, 0.0, 10.0];
mb.clamp_vec(&mut values);
assert_eq!(values, vec![-5.0, 0.0, 5.0]);
}
#[test]
fn test_multi_bounds_contains_vec() {
let mb = MultiBounds::symmetric(5.0, 3);
assert!(mb.contains_vec(&[0.0, 0.0, 0.0]));
assert!(mb.contains_vec(&[-5.0, 5.0, 0.0]));
assert!(!mb.contains_vec(&[-6.0, 0.0, 0.0]));
}
#[test]
fn test_multi_bounds_from_tuples() {
let mb: MultiBounds = vec![(0.0, 1.0), (-10.0, 10.0)].into_iter().collect();
assert_eq!(mb.dimension(), 2);
assert_eq!(mb.get(0), Some(&Bounds::new(0.0, 1.0)));
assert_eq!(mb.get(1), Some(&Bounds::new(-10.0, 10.0)));
}
}