scirs2_optimize/global/
basinhopping.rs

1//! Basin-hopping algorithm for global optimization
2//!
3//! Basin-hopping is a stochastic algorithm which attempts to find the
4//! global minimum of a function by combining random perturbations with
5//! local minimization.
6
7use 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/// Enforce bounds using reflection method for better exploration
14#[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        // Value is within bounds
18        val
19    } else if val < lb {
20        // Reflect around lower bound
21        let excess = lb - val;
22        let range = ub - lb;
23        if excess <= range {
24            lb + excess
25        } else {
26            // If reflection goes beyond upper bound, use random value in range
27            rng.gen_range(lb..=ub)
28        }
29    } else {
30        // val > ub..reflect around upper bound
31        let excess = val - ub;
32        let range = ub - lb;
33        if excess <= range {
34            ub - excess
35        } else {
36            // If reflection goes beyond lower bound, use random value in range
37            rng.gen_range(lb..=ub)
38        }
39    }
40}
41
42/// Validate bounds to ensure they are properly specified
43#[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/// Options for Basin-hopping algorithm
70#[derive(Debug, Clone)]
71pub struct BasinHoppingOptions {
72    /// Number of basin hopping iterations
73    pub niter: usize,
74    /// Temperature parameter for accept/reject criterion (higher means more permissive)
75    pub temperature: f64,
76    /// Step size for random displacement
77    pub stepsize: f64,
78    /// Number of iterations with no improvement before terminating
79    pub niter_success: Option<usize>,
80    /// Random seed for reproducibility
81    pub seed: Option<u64>,
82    /// Local minimization method to use
83    pub minimizer_method: Method,
84    /// Bounds for variables
85    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
102/// Accept test function type
103pub type AcceptTest = Box<dyn Fn(f64, f64, f64) -> bool>;
104
105/// Take step function type  
106pub type TakeStep = Box<dyn FnMut(&Array1<f64>) -> Array1<f64>>;
107
108/// Basin-hopping solver
109pub 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/// Storage for the best result found
125#[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    /// Create new Basin-hopping solver
154    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        // Default accept _test is Metropolis criterion
168        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        // Default take _step is random displacement with reflection-based bounds enforcement
180        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                    // Apply bounds if specified using reflection method
193                    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        // Apply bounds to initial x0 if specified using reflection method
206        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        // Perform initial minimization to get starting point
216        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    /// Run one iteration of basin-hopping
253    fn step(&mut self) -> (Array1<f64>, f64, bool) {
254        // Take a random step
255        let x_new = (self.take_step)(&self.x0);
256
257        // Minimize from new point
258        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        // Accept or reject the new minimum
278        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    /// Get current temperature (could be adaptive in future)
288    fn temperature(&self) -> f64 {
289        self.options.temperature
290    }
291
292    /// Run the basin-hopping algorithm
293    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            // Update storage if better solution found
303            if self.storage.update(x.clone(), fun, success) {
304                success_counter = 0;
305            } else {
306                success_counter += 1;
307            }
308
309            // Check early termination based on success iterations
310            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/// Perform global optimization using basin-hopping
332#[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    // Validate bounds if provided
346    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}