1#![forbid(unsafe_code)]
2#[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}