1use crate::error::OptimizeError;
8use crate::unconstrained::{minimize, Bounds, Method, OptimizeResult, Options};
9use scirs2_core::ndarray::{Array1, ArrayView1};
10use scirs2_core::random::rngs::StdRng;
11use scirs2_core::random::{Rng, SeedableRng};
12
13#[allow(dead_code)]
15fn enforce_bounds_with_reflection<R: Rng>(rng: &mut R, val: f64, lb: f64, ub: f64) -> f64 {
16 if val >= lb && val <= ub {
17 val
19 } else if val < lb {
20 let excess = lb - val;
22 let range = ub - lb;
23 if excess <= range {
24 lb + excess
25 } else {
26 rng.gen_range(lb..=ub)
28 }
29 } else {
30 let excess = val - ub;
32 let range = ub - lb;
33 if excess <= range {
34 ub - excess
35 } else {
36 rng.gen_range(lb..=ub)
38 }
39 }
40}
41
42#[allow(dead_code)]
44fn validate_bounds(bounds: &[(f64, f64)]) -> Result<(), OptimizeError> {
45 for (i, &(lb, ub)) in bounds.iter().enumerate() {
46 if !lb.is_finite() || !ub.is_finite() {
47 return Err(OptimizeError::InvalidInput(format!(
48 "Bounds must be finite values. Variable {}: _bounds = ({}, {})",
49 i, lb, ub
50 )));
51 }
52 if lb >= ub {
53 return Err(OptimizeError::InvalidInput(format!(
54 "Lower bound must be less than upper bound. Variable {}: lb = {}, ub = {}",
55 i, lb, ub
56 )));
57 }
58 if (ub - lb) < 1e-12 {
59 return Err(OptimizeError::InvalidInput(format!(
60 "Bounds range is too small. Variable {}: range = {}",
61 i,
62 ub - lb
63 )));
64 }
65 }
66 Ok(())
67}
68
69#[derive(Debug, Clone)]
71pub struct BasinHoppingOptions {
72 pub niter: usize,
74 pub temperature: f64,
76 pub stepsize: f64,
78 pub niter_success: Option<usize>,
80 pub seed: Option<u64>,
82 pub minimizer_method: Method,
84 pub bounds: Option<Vec<(f64, f64)>>,
86}
87
88impl Default for BasinHoppingOptions {
89 fn default() -> Self {
90 Self {
91 niter: 100,
92 temperature: 1.0,
93 stepsize: 0.5,
94 niter_success: None,
95 seed: None,
96 minimizer_method: Method::LBFGS,
97 bounds: None,
98 }
99 }
100}
101
102pub type AcceptTest = Box<dyn Fn(f64, f64, f64) -> bool>;
104
105pub type TakeStep = Box<dyn FnMut(&Array1<f64>) -> Array1<f64>>;
107
108pub struct BasinHopping<F>
110where
111 F: Fn(&ArrayView1<f64>) -> f64 + Clone,
112{
113 func: F,
114 x0: Array1<f64>,
115 options: BasinHoppingOptions,
116 ndim: usize,
117 rng: StdRng,
118 accept_test: AcceptTest,
119 take_step: TakeStep,
120 storage: Storage,
121 nfev: usize,
122}
123
124#[derive(Debug, Clone)]
126struct Storage {
127 x: Array1<f64>,
128 fun: f64,
129 success: bool,
130}
131
132impl Storage {
133 fn new(x: Array1<f64>, fun: f64, success: bool) -> Self {
134 Self { x, fun, success }
135 }
136
137 fn update(&mut self, x: Array1<f64>, fun: f64, success: bool) -> bool {
138 if success && (fun < self.fun || !self.success) {
139 self.x = x;
140 self.fun = fun;
141 self.success = success;
142 true
143 } else {
144 false
145 }
146 }
147}
148
149impl<F> BasinHopping<F>
150where
151 F: Fn(&ArrayView1<f64>) -> f64 + Clone,
152{
153 pub fn new(
155 func: F,
156 x0: Array1<f64>,
157 options: BasinHoppingOptions,
158 accept_test: Option<AcceptTest>,
159 take_step: Option<TakeStep>,
160 ) -> Self {
161 let ndim = x0.len();
162 let seed = options
163 .seed
164 .unwrap_or_else(|| scirs2_core::random::rng().random_range(0..u64::MAX));
165 let mut rng = StdRng::seed_from_u64(seed);
166
167 let accept_test = accept_test.unwrap_or_else(|| {
169 Box::new(move |f_new: f64, f_old: f64, temp: f64| {
170 if f_new < f_old {
171 true
172 } else {
173 let delta = (f_old - f_new) / temp;
174 delta > 0.0 && scirs2_core::random::rng().random_range(0.0..1.0) < delta.exp()
175 }
176 })
177 });
178
179 let take_step = take_step.unwrap_or_else(|| {
181 let stepsize = options.stepsize;
182 let bounds = options.bounds.clone();
183 let seed = options
184 .seed
185 .unwrap_or_else(|| scirs2_core::random::rng().random_range(0..u64::MAX));
186 Box::new(move |x: &Array1<f64>| {
187 let mut local_rng = StdRng::seed_from_u64(seed + x.len() as u64);
188 let mut x_new = x.clone();
189 for i in 0..x.len() {
190 x_new[i] += local_rng.gen_range(-stepsize..stepsize);
191
192 if let Some(ref bounds) = bounds {
194 if i < bounds.len() {
195 let (lb, ub) = bounds[i];
196 x_new[i] =
197 enforce_bounds_with_reflection(&mut local_rng, x_new[i], lb, ub);
198 }
199 }
200 }
201 x_new
202 })
203 });
204
205 let mut x0_bounded = x0.clone();
207 if let Some(ref bounds) = options.bounds {
208 for (i, &(lb, ub)) in bounds.iter().enumerate() {
209 if i < x0_bounded.len() {
210 x0_bounded[i] = enforce_bounds_with_reflection(&mut rng, x0_bounded[i], lb, ub);
211 }
212 }
213 }
214
215 let initial_result = minimize(
217 func.clone(),
218 &x0_bounded.to_vec(),
219 options.minimizer_method,
220 Some(Options {
221 bounds: options.bounds.clone().map(|b| {
222 Bounds::from_vecs(
223 b.iter().map(|&(lb, _)| Some(lb)).collect(),
224 b.iter().map(|&(_, ub)| Some(ub)).collect(),
225 )
226 .unwrap()
227 }),
228 ..Default::default()
229 }),
230 )
231 .unwrap();
232
233 let storage = Storage::new(
234 initial_result.x.clone(),
235 initial_result.fun,
236 initial_result.success,
237 );
238
239 Self {
240 func,
241 x0: initial_result.x,
242 options,
243 ndim,
244 rng,
245 accept_test,
246 take_step,
247 storage,
248 nfev: initial_result.nfev,
249 }
250 }
251
252 fn step(&mut self) -> (Array1<f64>, f64, bool) {
254 let x_new = (self.take_step)(&self.x0);
256
257 let result = minimize(
259 |x| (self.func)(x),
260 &x_new.to_vec(),
261 self.options.minimizer_method,
262 Some(Options {
263 bounds: self.options.bounds.clone().map(|b| {
264 Bounds::from_vecs(
265 b.iter().map(|&(lb, _)| Some(lb)).collect(),
266 b.iter().map(|&(_, ub)| Some(ub)).collect(),
267 )
268 .unwrap()
269 }),
270 ..Default::default()
271 }),
272 )
273 .unwrap();
274
275 self.nfev += result.nfev;
276
277 let accept = (self.accept_test)(result.fun, self.storage.fun, self.temperature());
279
280 if accept {
281 self.x0 = result.x.clone();
282 }
283
284 (result.x, result.fun, result.success)
285 }
286
287 fn temperature(&self) -> f64 {
289 self.options.temperature
290 }
291
292 pub fn run(&mut self) -> OptimizeResult<f64> {
294 let mut nit = 0;
295 let mut success_counter = 0;
296 let mut message = "Maximum number of iterations reached".to_string();
297
298 for _ in 0..self.options.niter {
299 let (x, fun, success) = self.step();
300 nit += 1;
301
302 if self.storage.update(x.clone(), fun, success) {
304 success_counter = 0;
305 } else {
306 success_counter += 1;
307 }
308
309 if let Some(niter_success) = self.options.niter_success {
311 if success_counter >= niter_success {
312 message = format!("No improvement in {} iterations", niter_success);
313 break;
314 }
315 }
316 }
317
318 OptimizeResult {
319 x: self.storage.x.clone(),
320 fun: self.storage.fun,
321 nfev: self.nfev,
322 func_evals: self.nfev,
323 nit,
324 success: self.storage.success,
325 message,
326 ..Default::default()
327 }
328 }
329}
330
331#[allow(dead_code)]
333pub fn basinhopping<F>(
334 func: F,
335 x0: Array1<f64>,
336 options: Option<BasinHoppingOptions>,
337 accept_test: Option<AcceptTest>,
338 take_step: Option<TakeStep>,
339) -> Result<OptimizeResult<f64>, OptimizeError>
340where
341 F: Fn(&ArrayView1<f64>) -> f64 + Clone,
342{
343 let options = options.unwrap_or_default();
344
345 if let Some(ref bounds) = options.bounds {
347 validate_bounds(bounds)?;
348 }
349
350 let mut solver = BasinHopping::new(func, x0, options, accept_test, take_step);
351 Ok(solver.run())
352}