1use 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
17const 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#[derive(Debug, Clone)]
25pub struct MultiStartOptions {
26 pub n_starts: usize,
28 pub local_method: UnconstrainedMethod,
30 pub parallel: bool,
32 pub seed: Option<u64>,
34 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#[derive(Debug, Clone)]
52pub enum StartingPointStrategy {
53 Random,
55 LatinHypercube,
57 Halton,
59 Sobol,
61 Grid,
63}
64
65pub type Bounds = Vec<(f64, f64)>;
67
68pub 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 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 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 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 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 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 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 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 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 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 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 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 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 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 OptimizeResult {
270 x: x0,
271 fun: f64::INFINITY,
272 success: false,
273 ..Default::default()
274 }
275 })
276 }
277
278 pub fn run(&mut self) -> OptimizeResult<f64> {
280 let starting_points = self.generate_starting_points();
281
282 let results = if self.options.parallel {
283 starting_points
285 .into_par_iter()
286 .map(|x0| self.optimize_single(x0))
287 .collect::<Vec<_>>()
288 } else {
289 starting_points
291 .into_iter()
292 .map(|x0| self.optimize_single(x0))
293 .collect::<Vec<_>>()
294 };
295
296 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#[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}