Skip to main content

fugue_evo/genome/
bounds.rs

1//! Bounds for genome values
2//!
3//! This module provides bounds types for constraining genome values.
4
5use serde::{Deserialize, Serialize};
6
7/// Bounds for a single dimension
8#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
9pub struct Bounds {
10    /// Lower bound (inclusive)
11    pub min: f64,
12    /// Upper bound (inclusive)
13    pub max: f64,
14}
15
16impl Bounds {
17    /// Create new bounds
18    ///
19    /// # Panics
20    /// Panics if min > max
21    pub fn new(min: f64, max: f64) -> Self {
22        assert!(
23            min <= max,
24            "Invalid bounds: min ({}) must be <= max ({})",
25            min,
26            max
27        );
28        Self { min, max }
29    }
30
31    /// Create symmetric bounds centered at 0
32    pub fn symmetric(half_width: f64) -> Self {
33        Self::new(-half_width, half_width)
34    }
35
36    /// Create unit bounds [0, 1]
37    pub fn unit() -> Self {
38        Self::new(0.0, 1.0)
39    }
40
41    /// Get the range (max - min)
42    pub fn range(&self) -> f64 {
43        self.max - self.min
44    }
45
46    /// Get the center point
47    pub fn center(&self) -> f64 {
48        (self.min + self.max) / 2.0
49    }
50
51    /// Check if a value is within bounds
52    pub fn contains(&self, value: f64) -> bool {
53        value >= self.min && value <= self.max
54    }
55
56    /// Clamp a value to be within bounds
57    pub fn clamp(&self, value: f64) -> f64 {
58        value.clamp(self.min, self.max)
59    }
60
61    /// Normalize a value from bounds to [0, 1]
62    pub fn normalize(&self, value: f64) -> f64 {
63        (value - self.min) / self.range()
64    }
65
66    /// Denormalize a value from [0, 1] to bounds
67    pub fn denormalize(&self, value: f64) -> f64 {
68        self.min + value * self.range()
69    }
70}
71
72impl Default for Bounds {
73    fn default() -> Self {
74        Self::symmetric(5.12) // Common default for optimization benchmarks
75    }
76}
77
78impl From<(f64, f64)> for Bounds {
79    fn from((min, max): (f64, f64)) -> Self {
80        Self::new(min, max)
81    }
82}
83
84/// Multi-dimensional bounds
85#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
86pub struct MultiBounds {
87    /// Bounds for each dimension
88    pub bounds: Vec<Bounds>,
89}
90
91impl MultiBounds {
92    /// Create new multi-dimensional bounds
93    pub fn new(bounds: Vec<Bounds>) -> Self {
94        Self { bounds }
95    }
96
97    /// Create uniform bounds for all dimensions
98    pub fn uniform(bound: Bounds, dimension: usize) -> Self {
99        Self {
100            bounds: vec![bound; dimension],
101        }
102    }
103
104    /// Create symmetric bounds for all dimensions
105    pub fn symmetric(half_width: f64, dimension: usize) -> Self {
106        Self::uniform(Bounds::symmetric(half_width), dimension)
107    }
108
109    /// Get number of dimensions
110    pub fn dimension(&self) -> usize {
111        self.bounds.len()
112    }
113
114    /// Get bounds for a specific dimension
115    pub fn get(&self, index: usize) -> Option<&Bounds> {
116        self.bounds.get(index)
117    }
118
119    /// Clamp a vector to be within bounds
120    pub fn clamp_vec(&self, values: &mut [f64]) {
121        for (i, value) in values.iter_mut().enumerate() {
122            if let Some(b) = self.bounds.get(i) {
123                *value = b.clamp(*value);
124            }
125        }
126    }
127
128    /// Check if all values are within bounds
129    pub fn contains_vec(&self, values: &[f64]) -> bool {
130        values
131            .iter()
132            .enumerate()
133            .all(|(i, &v)| self.bounds.get(i).is_some_and(|b| b.contains(v)))
134    }
135}
136
137impl FromIterator<Bounds> for MultiBounds {
138    fn from_iter<I: IntoIterator<Item = Bounds>>(iter: I) -> Self {
139        Self {
140            bounds: iter.into_iter().collect(),
141        }
142    }
143}
144
145impl FromIterator<(f64, f64)> for MultiBounds {
146    fn from_iter<I: IntoIterator<Item = (f64, f64)>>(iter: I) -> Self {
147        Self {
148            bounds: iter.into_iter().map(Bounds::from).collect(),
149        }
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    #[test]
158    fn test_bounds_new() {
159        let b = Bounds::new(-5.0, 5.0);
160        assert_eq!(b.min, -5.0);
161        assert_eq!(b.max, 5.0);
162    }
163
164    #[test]
165    #[should_panic(expected = "Invalid bounds")]
166    fn test_bounds_invalid() {
167        Bounds::new(5.0, -5.0);
168    }
169
170    #[test]
171    fn test_bounds_symmetric() {
172        let b = Bounds::symmetric(3.0);
173        assert_eq!(b.min, -3.0);
174        assert_eq!(b.max, 3.0);
175    }
176
177    #[test]
178    fn test_bounds_unit() {
179        let b = Bounds::unit();
180        assert_eq!(b.min, 0.0);
181        assert_eq!(b.max, 1.0);
182    }
183
184    #[test]
185    fn test_bounds_range() {
186        let b = Bounds::new(-5.0, 5.0);
187        assert_eq!(b.range(), 10.0);
188    }
189
190    #[test]
191    fn test_bounds_center() {
192        let b = Bounds::new(-2.0, 6.0);
193        assert_eq!(b.center(), 2.0);
194    }
195
196    #[test]
197    fn test_bounds_contains() {
198        let b = Bounds::new(-5.0, 5.0);
199        assert!(b.contains(0.0));
200        assert!(b.contains(-5.0));
201        assert!(b.contains(5.0));
202        assert!(!b.contains(-5.1));
203        assert!(!b.contains(5.1));
204    }
205
206    #[test]
207    fn test_bounds_clamp() {
208        let b = Bounds::new(-5.0, 5.0);
209        assert_eq!(b.clamp(0.0), 0.0);
210        assert_eq!(b.clamp(-10.0), -5.0);
211        assert_eq!(b.clamp(10.0), 5.0);
212    }
213
214    #[test]
215    fn test_bounds_normalize() {
216        let b = Bounds::new(0.0, 10.0);
217        assert_eq!(b.normalize(0.0), 0.0);
218        assert_eq!(b.normalize(5.0), 0.5);
219        assert_eq!(b.normalize(10.0), 1.0);
220    }
221
222    #[test]
223    fn test_bounds_denormalize() {
224        let b = Bounds::new(0.0, 10.0);
225        assert_eq!(b.denormalize(0.0), 0.0);
226        assert_eq!(b.denormalize(0.5), 5.0);
227        assert_eq!(b.denormalize(1.0), 10.0);
228    }
229
230    #[test]
231    fn test_multi_bounds_uniform() {
232        let mb = MultiBounds::symmetric(5.0, 3);
233        assert_eq!(mb.dimension(), 3);
234        assert_eq!(mb.get(0), Some(&Bounds::symmetric(5.0)));
235        assert_eq!(mb.get(1), Some(&Bounds::symmetric(5.0)));
236        assert_eq!(mb.get(2), Some(&Bounds::symmetric(5.0)));
237        assert_eq!(mb.get(3), None);
238    }
239
240    #[test]
241    fn test_multi_bounds_clamp_vec() {
242        let mb = MultiBounds::symmetric(5.0, 3);
243        let mut values = vec![-10.0, 0.0, 10.0];
244        mb.clamp_vec(&mut values);
245        assert_eq!(values, vec![-5.0, 0.0, 5.0]);
246    }
247
248    #[test]
249    fn test_multi_bounds_contains_vec() {
250        let mb = MultiBounds::symmetric(5.0, 3);
251        assert!(mb.contains_vec(&[0.0, 0.0, 0.0]));
252        assert!(mb.contains_vec(&[-5.0, 5.0, 0.0]));
253        assert!(!mb.contains_vec(&[-6.0, 0.0, 0.0]));
254    }
255
256    #[test]
257    fn test_multi_bounds_from_tuples() {
258        let mb: MultiBounds = vec![(0.0, 1.0), (-10.0, 10.0)].into_iter().collect();
259        assert_eq!(mb.dimension(), 2);
260        assert_eq!(mb.get(0), Some(&Bounds::new(0.0, 1.0)));
261        assert_eq!(mb.get(1), Some(&Bounds::new(-10.0, 10.0)));
262    }
263}