scirs2_optimize/multi_fidelity/
types.rs1use crate::error::OptimizeResult;
8
9#[derive(Debug, Clone)]
20pub struct MultiFidelityConfig {
21 pub max_budget: f64,
23 pub min_budget: f64,
25 pub eta: usize,
27 pub n_initial: usize,
30}
31
32impl Default for MultiFidelityConfig {
33 fn default() -> Self {
34 Self {
35 max_budget: 81.0,
36 min_budget: 1.0,
37 eta: 3,
38 n_initial: 0, }
40 }
41}
42
43impl MultiFidelityConfig {
44 pub fn validate(&self) -> OptimizeResult<()> {
46 if self.max_budget <= 0.0 {
47 return Err(crate::error::OptimizeError::InvalidParameter(
48 "max_budget must be positive".into(),
49 ));
50 }
51 if self.min_budget <= 0.0 {
52 return Err(crate::error::OptimizeError::InvalidParameter(
53 "min_budget must be positive".into(),
54 ));
55 }
56 if self.min_budget > self.max_budget {
57 return Err(crate::error::OptimizeError::InvalidParameter(
58 "min_budget must not exceed max_budget".into(),
59 ));
60 }
61 if self.eta < 2 {
62 return Err(crate::error::OptimizeError::InvalidParameter(
63 "eta must be >= 2".into(),
64 ));
65 }
66 Ok(())
67 }
68
69 pub(crate) fn s_max(&self) -> usize {
71 let ratio = self.max_budget / self.min_budget;
72 let eta_f = self.eta as f64;
73 ratio.ln().div_euclid(eta_f.ln()) as usize
74 }
75}
76
77#[derive(Debug, Clone, Copy)]
83#[non_exhaustive]
84pub enum ConfigSampler {
85 Random,
87 LatinHypercube,
89}
90
91impl Default for ConfigSampler {
92 fn default() -> Self {
93 Self::Random
94 }
95}
96
97pub(crate) fn xorshift64(state: &mut u64) -> u64 {
99 let mut s = *state;
100 s ^= s << 13;
101 s ^= s >> 7;
102 s ^= s << 17;
103 *state = s;
104 s
105}
106
107pub(crate) fn rand_f64(state: &mut u64) -> f64 {
109 let bits = xorshift64(state);
110 (bits >> 11) as f64 / ((1u64 << 53) as f64)
111}
112
113pub(crate) fn sample_configs(
115 n: usize,
116 bounds: &[(f64, f64)],
117 sampler: &ConfigSampler,
118 rng: &mut u64,
119) -> Vec<Vec<f64>> {
120 match sampler {
121 ConfigSampler::Random => sample_random(n, bounds, rng),
122 ConfigSampler::LatinHypercube => sample_lhs(n, bounds, rng),
123 }
124}
125
126fn sample_random(n: usize, bounds: &[(f64, f64)], rng: &mut u64) -> Vec<Vec<f64>> {
127 (0..n)
128 .map(|_| {
129 bounds
130 .iter()
131 .map(|(lo, hi)| lo + rand_f64(rng) * (hi - lo))
132 .collect()
133 })
134 .collect()
135}
136
137fn sample_lhs(n: usize, bounds: &[(f64, f64)], rng: &mut u64) -> Vec<Vec<f64>> {
138 if n == 0 {
139 return Vec::new();
140 }
141 let d = bounds.len();
142 let mut configs: Vec<Vec<f64>> = vec![vec![0.0; d]; n];
144 for dim in 0..d {
145 let (lo, hi) = bounds[dim];
146 let mut indices: Vec<usize> = (0..n).collect();
147 for i in (1..n).rev() {
149 let j = (xorshift64(rng) as usize) % (i + 1);
150 indices.swap(i, j);
151 }
152 for (i, &idx) in indices.iter().enumerate() {
153 let low = lo + (hi - lo) * (idx as f64) / (n as f64);
154 let high = lo + (hi - lo) * ((idx + 1) as f64) / (n as f64);
155 configs[i][dim] = low + rand_f64(rng) * (high - low);
156 }
157 }
158 configs
159}
160
161#[derive(Debug, Clone)]
167pub struct EvaluationResult {
168 pub config_id: usize,
170 pub config: Vec<f64>,
172 pub budget: f64,
174 pub objective: f64,
176}
177
178#[derive(Debug, Clone)]
180pub struct MultiFidelityResult {
181 pub best_config: Vec<f64>,
183 pub best_objective: f64,
185 pub total_budget_used: f64,
187 pub evaluations: Vec<EvaluationResult>,
189 pub n_brackets: usize,
191}
192
193#[cfg(test)]
198mod tests {
199 use super::*;
200
201 #[test]
202 fn test_default_config_valid() {
203 let cfg = MultiFidelityConfig::default();
204 assert!(cfg.validate().is_ok());
205 assert_eq!(cfg.eta, 3);
206 assert!(cfg.max_budget > cfg.min_budget);
207 }
208
209 #[test]
210 fn test_invalid_config_eta() {
211 let cfg = MultiFidelityConfig {
212 eta: 1,
213 ..Default::default()
214 };
215 assert!(cfg.validate().is_err());
216 }
217
218 #[test]
219 fn test_invalid_config_budget() {
220 let cfg = MultiFidelityConfig {
221 min_budget: 100.0,
222 max_budget: 10.0,
223 ..Default::default()
224 };
225 assert!(cfg.validate().is_err());
226 }
227
228 #[test]
229 fn test_s_max_computation() {
230 let cfg = MultiFidelityConfig::default();
232 assert_eq!(cfg.s_max(), 4);
233 }
234
235 #[test]
236 fn test_random_sampling_bounds() {
237 let bounds = vec![(0.0, 1.0), (-5.0, 5.0)];
238 let mut rng = 42u64;
239 let configs = sample_configs(20, &bounds, &ConfigSampler::Random, &mut rng);
240 assert_eq!(configs.len(), 20);
241 for c in &configs {
242 assert_eq!(c.len(), 2);
243 assert!(c[0] >= 0.0 && c[0] <= 1.0);
244 assert!(c[1] >= -5.0 && c[1] <= 5.0);
245 }
246 }
247
248 #[test]
249 fn test_lhs_fills_space() {
250 let bounds = vec![(0.0, 1.0)];
251 let n = 10;
252 let mut rng = 123u64;
253 let configs = sample_configs(n, &bounds, &ConfigSampler::LatinHypercube, &mut rng);
254 assert_eq!(configs.len(), n);
255 let mut strata: Vec<usize> = configs
258 .iter()
259 .map(|c| (c[0] * n as f64).floor() as usize)
260 .collect();
261 strata.sort();
262 strata.dedup();
263 assert_eq!(strata.len(), n, "LHS must cover all {n} strata");
264 }
265
266 #[test]
267 fn test_lhs_empty() {
268 let bounds = vec![(0.0, 1.0)];
269 let mut rng = 1u64;
270 let configs = sample_configs(0, &bounds, &ConfigSampler::LatinHypercube, &mut rng);
271 assert!(configs.is_empty());
272 }
273
274 #[test]
275 fn test_evaluation_result_tracks_fields() {
276 let er = EvaluationResult {
277 config_id: 7,
278 config: vec![1.0, 2.0],
279 budget: 27.0,
280 objective: 0.5,
281 };
282 assert_eq!(er.config_id, 7);
283 assert!((er.budget - 27.0).abs() < f64::EPSILON);
284 }
285}