Skip to main content

use_search_space/
lib.rs

1#![forbid(unsafe_code)]
2//! Primitive search-space helpers for optimization.
3//!
4//! `RangeSpace` supports both ascending and descending ranges while requiring a
5//! positive, finite step size.
6//!
7//! # Examples
8//!
9//! ```rust
10//! use use_search_space::{linspace, RangeSpace};
11//!
12//! let ascending = RangeSpace {
13//!     start: 0.0,
14//!     end: 2.0,
15//!     step: 1.0,
16//! };
17//! assert_eq!(ascending.values().unwrap(), vec![0.0, 1.0, 2.0]);
18//! assert_eq!(linspace(0.0, 1.0, 3).unwrap(), vec![0.0, 0.5, 1.0]);
19//! ```
20
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub enum SearchSpaceError {
23    NonFiniteInput,
24    InvalidStep,
25    InvalidPointCount,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq)]
29pub struct RangeSpace {
30    pub start: f64,
31    pub end: f64,
32    pub step: f64,
33}
34
35impl RangeSpace {
36    pub fn values(&self) -> Result<Vec<f64>, SearchSpaceError> {
37        if !self.start.is_finite() || !self.end.is_finite() || !self.step.is_finite() {
38            return Err(SearchSpaceError::NonFiniteInput);
39        }
40
41        if self.step <= 0.0 {
42            return Err(SearchSpaceError::InvalidStep);
43        }
44
45        if self.start == self.end {
46            return Ok(vec![self.start]);
47        }
48
49        let increasing = self.start < self.end;
50        let tolerance = self.step * 1.0e-12;
51        let mut values = Vec::new();
52        let mut current = self.start;
53
54        loop {
55            if increasing {
56                if current > self.end + tolerance {
57                    break;
58                }
59            } else if current < self.end - tolerance {
60                break;
61            }
62
63            let value = if (current - self.end).abs() <= tolerance {
64                self.end
65            } else {
66                current
67            };
68            values.push(value);
69
70            current = if increasing {
71                current + self.step
72            } else {
73                current - self.step
74            };
75
76            if !current.is_finite() {
77                return Err(SearchSpaceError::NonFiniteInput);
78            }
79        }
80
81        Ok(values)
82    }
83}
84
85pub fn linspace(start: f64, end: f64, points: usize) -> Result<Vec<f64>, SearchSpaceError> {
86    if !start.is_finite() || !end.is_finite() {
87        return Err(SearchSpaceError::NonFiniteInput);
88    }
89
90    if points == 0 {
91        return Err(SearchSpaceError::InvalidPointCount);
92    }
93
94    if points == 1 {
95        return Ok(vec![start]);
96    }
97
98    let step = (end - start) / (points - 1) as f64;
99    let mut values = Vec::with_capacity(points);
100    for index in 0..points {
101        if index + 1 == points {
102            values.push(end);
103        } else {
104            values.push(start + step * index as f64);
105        }
106    }
107
108    Ok(values)
109}
110
111#[cfg(test)]
112mod tests {
113    use super::{RangeSpace, SearchSpaceError, linspace};
114
115    fn approx_eq(left: &[f64], right: &[f64]) {
116        assert_eq!(left.len(), right.len());
117        for (left_value, right_value) in left.iter().zip(right.iter()) {
118            assert!(
119                (left_value - right_value).abs() < 1.0e-10,
120                "left={left_value}, right={right_value}"
121            );
122        }
123    }
124
125    #[test]
126    fn generates_ascending_values() {
127        let space = RangeSpace {
128            start: 0.0,
129            end: 2.0,
130            step: 1.0,
131        };
132
133        assert_eq!(space.values().unwrap(), vec![0.0, 1.0, 2.0]);
134    }
135
136    #[test]
137    fn generates_descending_values() {
138        let space = RangeSpace {
139            start: 3.0,
140            end: 0.0,
141            step: 1.0,
142        };
143
144        assert_eq!(space.values().unwrap(), vec![3.0, 2.0, 1.0, 0.0]);
145    }
146
147    #[test]
148    fn handles_single_point_ranges() {
149        let space = RangeSpace {
150            start: 2.0,
151            end: 2.0,
152            step: 0.5,
153        };
154
155        assert_eq!(space.values().unwrap(), vec![2.0]);
156    }
157
158    #[test]
159    fn rejects_invalid_search_spaces() {
160        assert_eq!(
161            RangeSpace {
162                start: 0.0,
163                end: 1.0,
164                step: 0.0,
165            }
166            .values(),
167            Err(SearchSpaceError::InvalidStep)
168        );
169        assert_eq!(
170            RangeSpace {
171                start: 0.0,
172                end: f64::INFINITY,
173                step: 1.0,
174            }
175            .values(),
176            Err(SearchSpaceError::NonFiniteInput)
177        );
178        assert_eq!(
179            linspace(0.0, 1.0, 0),
180            Err(SearchSpaceError::InvalidPointCount)
181        );
182    }
183
184    #[test]
185    fn builds_linspace_values() {
186        approx_eq(
187            &linspace(0.0, 1.0, 5).unwrap(),
188            &[0.0, 0.25, 0.5, 0.75, 1.0],
189        );
190        approx_eq(&linspace(3.0, 1.0, 3).unwrap(), &[3.0, 2.0, 1.0]);
191        assert_eq!(linspace(9.0, 5.0, 1).unwrap(), vec![9.0]);
192    }
193}