Skip to main content

scirs2_optimize/global/
multi_start.rs

1//! Multi-start strategies for global optimization
2//!
3//! These strategies run multiple optimization attempts from different
4//! starting points to increase the chance of finding the global optimum.
5
6use crate::error::OptimizeError;
7use crate::global::qmc::{halton_radical_inverse, SobolGenerator};
8use crate::unconstrained::{
9    minimize, Bounds as UnconstrainedBounds, Method as UnconstrainedMethod, OptimizeResult, Options,
10};
11use scirs2_core::ndarray::{Array1, ArrayView1};
12use scirs2_core::parallel_ops::*;
13use scirs2_core::random::prelude::SliceRandom;
14use scirs2_core::random::rngs::StdRng;
15use scirs2_core::random::{Rng, RngExt, SeedableRng};
16
17/// First 25 primes, used as Halton sequence bases (one per dimension, cycled
18/// if `ndim` exceeds the table).
19const HALTON_PRIMES: [u64; 25] = [
20    2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97,
21];
22
23/// Options for multi-start optimization
24#[derive(Debug, Clone)]
25pub struct MultiStartOptions {
26    /// Number of starting points
27    pub n_starts: usize,
28    /// Local optimization method to use
29    pub local_method: UnconstrainedMethod,
30    /// Whether to use parallel execution
31    pub parallel: bool,
32    /// Random seed for reproducibility
33    pub seed: Option<u64>,
34    /// Strategy for generating starting points
35    pub strategy: StartingPointStrategy,
36}
37
38impl Default for MultiStartOptions {
39    fn default() -> Self {
40        Self {
41            n_starts: 10,
42            local_method: UnconstrainedMethod::BFGS,
43            parallel: true,
44            seed: None,
45            strategy: StartingPointStrategy::Random,
46        }
47    }
48}
49
50/// Strategy for generating starting points
51#[derive(Debug, Clone)]
52pub enum StartingPointStrategy {
53    /// Random uniform sampling within bounds
54    Random,
55    /// Latin hypercube sampling
56    LatinHypercube,
57    /// Halton sequence
58    Halton,
59    /// Sobol sequence
60    Sobol,
61    /// Grid-based sampling
62    Grid,
63}
64
65/// Bounds for variables
66pub type Bounds = Vec<(f64, f64)>;
67
68/// Multi-start optimization solver
69pub struct MultiStart<F>
70where
71    F: Fn(&ArrayView1<f64>) -> f64 + Clone + Send + Sync,
72{
73    func: F,
74    bounds: Bounds,
75    options: MultiStartOptions,
76    ndim: usize,
77    rng: StdRng,
78}
79
80impl<F> MultiStart<F>
81where
82    F: Fn(&ArrayView1<f64>) -> f64 + Clone + Send + Sync,
83{
84    /// Create new multi-start solver
85    pub fn new(func: F, bounds: Bounds, options: MultiStartOptions) -> Self {
86        let ndim = bounds.len();
87        let seed = options
88            .seed
89            .unwrap_or_else(|| scirs2_core::random::rng().random_range(0..u64::MAX));
90        let rng = StdRng::seed_from_u64(seed);
91
92        Self {
93            func,
94            bounds,
95            options,
96            ndim,
97            rng,
98        }
99    }
100
101    /// Generate starting points based on strategy
102    fn generate_starting_points(&mut self) -> Vec<Array1<f64>> {
103        match self.options.strategy {
104            StartingPointStrategy::Random => self.generate_random_points(),
105            StartingPointStrategy::LatinHypercube => self.generate_latin_hypercube_points(),
106            StartingPointStrategy::Halton => self.generate_halton_points(),
107            StartingPointStrategy::Sobol => self.generate_sobol_points(),
108            StartingPointStrategy::Grid => self.generate_grid_points(),
109        }
110    }
111
112    /// Generate random starting points
113    fn generate_random_points(&mut self) -> Vec<Array1<f64>> {
114        let mut points = Vec::with_capacity(self.options.n_starts);
115
116        for _ in 0..self.options.n_starts {
117            let mut point = Array1::zeros(self.ndim);
118            for j in 0..self.ndim {
119                let (lb, ub) = self.bounds[j];
120                point[j] = self.rng.random_range(lb..ub);
121            }
122            points.push(point);
123        }
124
125        points
126    }
127
128    /// Generate Latin hypercube starting points
129    fn generate_latin_hypercube_points(&mut self) -> Vec<Array1<f64>> {
130        let mut points = Vec::with_capacity(self.options.n_starts);
131        let n = self.options.n_starts;
132
133        // Create segment indices for each dimension
134        for i in 0..n {
135            let mut point = Array1::zeros(self.ndim);
136
137            for j in 0..self.ndim {
138                let (lb, ub) = self.bounds[j];
139                let segment_size = (ub - lb) / n as f64;
140
141                // Random offset within segment
142                let offset = self.rng.random_range(0.0..1.0);
143                point[j] = lb + (i as f64 + offset) * segment_size;
144            }
145
146            points.push(point);
147        }
148
149        // Shuffle each dimension independently
150        for j in 0..self.ndim {
151            let mut indices: Vec<usize> = (0..n).collect();
152            indices.shuffle(&mut self.rng);
153
154            for (i, &idx) in indices.iter().enumerate() {
155                let temp = points[i][j];
156                points[i][j] = points[idx][j];
157                points[idx][j] = temp;
158            }
159        }
160
161        points
162    }
163
164    /// Generate Halton sequence starting points (a genuine low-discrepancy
165    /// sequence, one prime base per dimension; see [`crate::global::qmc`]).
166    fn generate_halton_points(&mut self) -> Vec<Array1<f64>> {
167        let mut points = Vec::with_capacity(self.options.n_starts);
168
169        for i in 0..self.options.n_starts {
170            let mut point = Array1::zeros(self.ndim);
171            for j in 0..self.ndim {
172                let base = HALTON_PRIMES[j % HALTON_PRIMES.len()];
173                let (lb, ub) = self.bounds[j];
174                point[j] = lb + halton_radical_inverse(i + 1, base) * (ub - lb);
175            }
176            points.push(point);
177        }
178
179        points
180    }
181
182    /// Generate Sobol sequence starting points (validated direction numbers
183    /// for the first [`crate::global::qmc::MAX_SOBOL_DIM`] dimensions,
184    /// Halton fallback beyond that; see [`crate::global::qmc`]).
185    fn generate_sobol_points(&mut self) -> Vec<Array1<f64>> {
186        let mut sobol_gen = SobolGenerator::new(self.ndim);
187        let mut points = Vec::with_capacity(self.options.n_starts);
188
189        for _ in 0..self.options.n_starts {
190            let sobol_point = sobol_gen.next_point();
191            let mut point = Array1::zeros(self.ndim);
192            for j in 0..self.ndim {
193                let (lb, ub) = self.bounds[j];
194                point[j] = lb + sobol_point[j] * (ub - lb);
195            }
196            points.push(point);
197        }
198
199        points
200    }
201
202    /// Generate grid-based starting points
203    fn generate_grid_points(&self) -> Vec<Array1<f64>> {
204        let points_per_dim = (self.options.n_starts as f64)
205            .powf(1.0 / self.ndim as f64)
206            .ceil() as usize;
207        let mut points = Vec::new();
208
209        // Generate grid points
210        let mut current = vec![0usize; self.ndim];
211        loop {
212            let mut point = Array1::zeros(self.ndim);
213
214            for j in 0..self.ndim {
215                let (lb, ub) = self.bounds[j];
216                let step = (ub - lb) / (points_per_dim - 1).max(1) as f64;
217                point[j] = lb + current[j] as f64 * step;
218            }
219
220            points.push(point);
221
222            // Increment grid position
223            let mut carry = true;
224            for j in current.iter_mut() {
225                if carry {
226                    *j += 1;
227                    if *j >= points_per_dim {
228                        *j = 0;
229                    } else {
230                        carry = false;
231                    }
232                }
233            }
234
235            if carry || points.len() >= self.options.n_starts {
236                break;
237            }
238        }
239
240        points.truncate(self.options.n_starts);
241        points
242    }
243
244    /// Run optimization from a single starting point
245    fn optimize_single(&self, x0: Array1<f64>) -> OptimizeResult<f64> {
246        let bounds = Some(
247            UnconstrainedBounds::from_vecs(
248                self.bounds.iter().map(|&(lb, _)| Some(lb)).collect(),
249                self.bounds.iter().map(|&(_, ub)| Some(ub)).collect(),
250            )
251            .expect("Operation failed"),
252        );
253
254        let options = Options {
255            bounds,
256            ..Default::default()
257        };
258
259        let func = self.func.clone();
260
261        minimize(
262            move |x: &ArrayView1<f64>| func(x),
263            &x0.to_vec(),
264            self.options.local_method,
265            Some(options),
266        )
267        .unwrap_or_else(|_| {
268            // Return a failed result if optimization fails
269            OptimizeResult {
270                x: x0,
271                fun: f64::INFINITY,
272                success: false,
273                ..Default::default()
274            }
275        })
276    }
277
278    /// Run the multi-start optimization
279    pub fn run(&mut self) -> OptimizeResult<f64> {
280        let starting_points = self.generate_starting_points();
281
282        let results = if self.options.parallel {
283            // Parallel execution
284            starting_points
285                .into_par_iter()
286                .map(|x0| self.optimize_single(x0))
287                .collect::<Vec<_>>()
288        } else {
289            // Sequential execution
290            starting_points
291                .into_iter()
292                .map(|x0| self.optimize_single(x0))
293                .collect::<Vec<_>>()
294        };
295
296        // Find the best result
297        let best_result = results
298            .into_iter()
299            .filter(|r| r.success)
300            .min_by(|a, b| {
301                a.fun
302                    .partial_cmp(&b.fun)
303                    .unwrap_or(std::cmp::Ordering::Equal)
304            })
305            .unwrap_or_else(|| OptimizeResult {
306                x: Array1::zeros(self.ndim),
307                fun: f64::INFINITY,
308                success: false,
309                message: "All optimization attempts failed".to_string(),
310                ..Default::default()
311            });
312
313        OptimizeResult {
314            x: best_result.x,
315            fun: best_result.fun,
316            nit: self.options.n_starts,
317            success: best_result.success,
318            message: format!(
319                "Multi-start optimization with {} starts",
320                self.options.n_starts
321            ),
322            ..Default::default()
323        }
324    }
325}
326
327/// Perform multi-start optimization
328#[allow(dead_code)]
329pub fn multi_start<F>(
330    func: F,
331    bounds: Bounds,
332    options: Option<MultiStartOptions>,
333) -> Result<OptimizeResult<f64>, OptimizeError>
334where
335    F: Fn(&ArrayView1<f64>) -> f64 + Clone + Send + Sync,
336{
337    let options = options.unwrap_or_default();
338    let mut solver = MultiStart::new(func, bounds, options);
339    Ok(solver.run())
340}