scirs2_optimize/parallel/
mod.rs1use scirs2_core::ndarray::{Array1, ArrayView1};
25use scirs2_core::parallel_ops::*;
26
27#[cfg(feature = "parallel")]
29use scirs2_core::parallel_ops::{IntoParallelIterator, ParallelIterator};
30
31#[derive(Debug, Clone)]
33pub struct ParallelOptions {
34 pub num_workers: Option<usize>,
36
37 pub min_parallel_size: usize,
39
40 pub chunk_size: usize,
42
43 pub parallel_evaluations: bool,
45
46 pub parallel_gradient: bool,
48}
49
50impl Default for ParallelOptions {
51 fn default() -> Self {
52 ParallelOptions {
53 num_workers: None,
54 min_parallel_size: 10,
55 chunk_size: 1,
56 parallel_evaluations: true,
57 parallel_gradient: true,
58 }
59 }
60}
61
62#[allow(dead_code)]
67pub fn parallel_finite_diff_gradient<F>(
68 f: F,
69 x: ArrayView1<f64>,
70 options: &ParallelOptions,
71) -> Array1<f64>
72where
73 F: Fn(&ArrayView1<f64>) -> f64 + Sync,
74{
75 let n = x.len();
76 let eps = 1e-8;
77
78 if n < options.min_parallel_size || !options.parallel_gradient {
80 return sequential_finite_diff_gradient(f, x, eps);
81 }
82
83 compute_parallel_gradient(&f, x, eps)
85}
86
87#[allow(dead_code)]
89fn sequential_finite_diff_gradient<F>(f: F, x: ArrayView1<f64>, eps: f64) -> Array1<f64>
90where
91 F: Fn(&ArrayView1<f64>) -> f64,
92{
93 let n = x.len();
94 let mut grad = Array1::zeros(n);
95 let fx = f(&x);
96
97 for i in 0..n {
98 let mut x_plus = x.to_owned();
99 x_plus[i] += eps;
100 let fx_plus = f(&x_plus.view());
101 grad[i] = (fx_plus - fx) / eps;
102 }
103
104 grad
105}
106
107#[cfg(feature = "parallel")]
109#[allow(dead_code)]
110fn compute_parallel_gradient<F>(f: &F, x: ArrayView1<f64>, eps: f64) -> Array1<f64>
111where
112 F: Fn(&ArrayView1<f64>) -> f64 + Sync,
113{
114 let n = x.len();
115 let fx = f(&x);
116
117 let grad_vec: Vec<f64> = (0..n)
119 .into_par_iter()
120 .map(|i| {
121 let mut x_plus = x.to_owned();
122 x_plus[i] += eps;
123 let fx_plus = f(&x_plus.view());
124 (fx_plus - fx) / eps
125 })
126 .collect();
127
128 Array1::from_vec(grad_vec)
129}
130
131#[cfg(not(feature = "parallel"))]
133#[allow(dead_code)]
134fn compute_parallel_gradient<F>(f: &F, x: ArrayView1<f64>, eps: f64) -> Array1<f64>
135where
136 F: Fn(&ArrayView1<f64>) -> f64,
137{
138 sequential_finite_diff_gradient(|x| f(x), x, eps)
139}
140
141#[allow(dead_code)]
145pub fn parallel_evaluate_batch<F>(
146 f: F,
147 points: &[Array1<f64>],
148 options: &ParallelOptions,
149) -> Vec<f64>
150where
151 F: Fn(&ArrayView1<f64>) -> f64 + Sync,
152{
153 if points.len() < options.min_parallel_size || !options.parallel_evaluations {
154 points.iter().map(|x| f(&x.view())).collect()
156 } else {
157 #[cfg(feature = "parallel")]
159 {
160 points.par_iter().map(|x| f(&x.view())).collect()
161 }
162 #[cfg(not(feature = "parallel"))]
163 {
164 points.iter().map(|x| f(&x.view())).collect()
165 }
166 }
167}
168
169#[allow(dead_code)] pub struct ParallelMultiStart<F> {
174 objective: F,
175 bounds: Vec<(f64, f64)>,
176 options: ParallelOptions,
177}
178
179impl<F> ParallelMultiStart<F>
180where
181 F: Fn(&ArrayView1<f64>) -> f64 + Sync + Send,
182{
183 pub fn new(objective: F, bounds: Vec<(f64, f64)>, options: ParallelOptions) -> Self {
185 ParallelMultiStart {
186 objective,
187 bounds,
188 options,
189 }
190 }
191
192 pub fn run<O>(&self, starting_points: Vec<Array1<f64>>, optimizer: O) -> Vec<OptimizationResult>
194 where
195 O: Fn(&Array1<f64>, &F) -> OptimizationResult + Sync,
196 {
197 if starting_points.len() < self.options.min_parallel_size {
198 starting_points
200 .iter()
201 .map(|x0| optimizer(x0, &self.objective))
202 .collect()
203 } else {
204 #[cfg(feature = "parallel")]
206 {
207 starting_points
208 .par_iter()
209 .map(|x0| optimizer(x0, &self.objective))
210 .collect()
211 }
212 #[cfg(not(feature = "parallel"))]
213 {
214 starting_points
215 .iter()
216 .map(|x0| optimizer(x0, &self.objective))
217 .collect()
218 }
219 }
220 }
221
222 pub fn best_result(results: &[OptimizationResult]) -> Option<&OptimizationResult> {
224 results.iter().min_by(|a, b| {
225 a.function_value
226 .partial_cmp(&b.function_value)
227 .expect("Operation failed")
228 })
229 }
230}
231
232#[derive(Debug, Clone)]
234pub struct OptimizationResult {
235 pub x: Array1<f64>,
236 pub function_value: f64,
237 pub nit: usize,
238 pub success: bool,
239}
240
241#[allow(dead_code)]
243pub fn parallel_finite_diff_hessian<F>(
244 f: &F,
245 x: ArrayView1<f64>,
246 gradient: Option<&Array1<f64>>,
247 options: &ParallelOptions,
248) -> Array1<f64>
249where
251 F: Fn(&ArrayView1<f64>) -> f64 + Sync,
252{
253 let n = x.len();
254 let eps = 1e-8;
255
256 if n < options.min_parallel_size {
258 return sequential_finite_diff_hessian(f, x, gradient, eps);
259 }
260
261 let _grad = match gradient {
263 Some(g) => g.clone(),
264 None => parallel_finite_diff_gradient(f, x, options),
265 };
266
267 let num_elements = n * (n + 1) / 2;
269
270 #[cfg(feature = "parallel")]
272 let hessian_elements: Vec<f64> = (0..num_elements)
273 .into_par_iter()
274 .map(|idx| {
275 let (i, j) = index_to_upper_triangle(idx, n);
277
278 if i == j {
279 let mut x_plus = x.to_owned();
281 let mut x_minus = x.to_owned();
282 x_plus[i] += eps;
283 x_minus[i] -= eps;
284
285 let f_plus = f(&x_plus.view());
286 let f_minus = f(&x_minus.view());
287 let f_center = f(&x);
288
289 (f_plus - 2.0 * f_center + f_minus) / (eps * eps)
290 } else {
291 let mut x_pp = x.to_owned();
293 let mut x_pm = x.to_owned();
294 let mut x_mp = x.to_owned();
295 let mut x_mm = x.to_owned();
296
297 x_pp[i] += eps;
298 x_pp[j] += eps;
299 x_pm[i] += eps;
300 x_pm[j] -= eps;
301 x_mp[i] -= eps;
302 x_mp[j] += eps;
303 x_mm[i] -= eps;
304 x_mm[j] -= eps;
305
306 let f_pp = f(&x_pp.view());
307 let f_pm = f(&x_pm.view());
308 let f_mp = f(&x_mp.view());
309 let f_mm = f(&x_mm.view());
310
311 (f_pp - f_pm - f_mp + f_mm) / (4.0 * eps * eps)
312 }
313 })
314 .collect();
315
316 #[cfg(not(feature = "parallel"))]
317 let hessian_elements: Vec<f64> = (0..num_elements)
318 .map(|idx| {
319 let (i, j) = index_to_upper_triangle(idx, n);
321
322 if i == j {
323 let mut x_plus = x.to_owned();
325 let mut x_minus = x.to_owned();
326 x_plus[i] += eps;
327 x_minus[i] -= eps;
328
329 let f_plus = f(&x_plus.view());
330 let f_minus = f(&x_minus.view());
331 let f_center = f(&x);
332
333 (f_plus - 2.0 * f_center + f_minus) / (eps * eps)
334 } else {
335 let mut x_pp = x.to_owned();
337 let mut x_pm = x.to_owned();
338 let mut x_mp = x.to_owned();
339 let mut x_mm = x.to_owned();
340
341 x_pp[i] += eps;
342 x_pp[j] += eps;
343 x_pm[i] += eps;
344 x_pm[j] -= eps;
345 x_mp[i] -= eps;
346 x_mp[j] += eps;
347 x_mm[i] -= eps;
348 x_mm[j] -= eps;
349
350 let f_pp = f(&x_pp.view());
351 let f_pm = f(&x_pm.view());
352 let f_mp = f(&x_mp.view());
353 let f_mm = f(&x_mm.view());
354
355 (f_pp - f_pm - f_mp + f_mm) / (4.0 * eps * eps)
356 }
357 })
358 .collect();
359
360 Array1::from_vec(hessian_elements)
361}
362
363#[allow(dead_code)]
365fn sequential_finite_diff_hessian<F>(
366 f: &F,
367 x: ArrayView1<f64>,
368 _gradient: Option<&Array1<f64>>,
369 eps: f64,
370) -> Array1<f64>
371where
372 F: Fn(&ArrayView1<f64>) -> f64,
373{
374 let n = x.len();
375 let num_elements = n * (n + 1) / 2;
376 let mut hessian = Array1::zeros(num_elements);
377
378 for idx in 0..num_elements {
379 let (i, j) = index_to_upper_triangle(idx, n);
380
381 if i == j {
382 let mut x_plus = x.to_owned();
384 let mut x_minus = x.to_owned();
385 x_plus[i] += eps;
386 x_minus[i] -= eps;
387
388 let f_plus = f(&x_plus.view());
389 let f_minus = f(&x_minus.view());
390 let f_center = f(&x);
391
392 hessian[idx] = (f_plus - 2.0 * f_center + f_minus) / (eps * eps);
393 } else {
394 let mut x_pp = x.to_owned();
396 let mut x_pm = x.to_owned();
397 let mut x_mp = x.to_owned();
398 let mut x_mm = x.to_owned();
399
400 x_pp[i] += eps;
401 x_pp[j] += eps;
402 x_pm[i] += eps;
403 x_pm[j] -= eps;
404 x_mp[i] -= eps;
405 x_mp[j] += eps;
406 x_mm[i] -= eps;
407 x_mm[j] -= eps;
408
409 let f_pp = f(&x_pp.view());
410 let f_pm = f(&x_pm.view());
411 let f_mp = f(&x_mp.view());
412 let f_mm = f(&x_mm.view());
413
414 hessian[idx] = (f_pp - f_pm - f_mp + f_mm) / (4.0 * eps * eps);
415 }
416 }
417
418 hessian
419}
420
421#[allow(dead_code)]
423fn index_to_upper_triangle(_idx: usize, n: usize) -> (usize, usize) {
424 let mut i = 0;
426 let mut cumsum = 0;
427
428 while cumsum + i < _idx {
429 i += 1;
430 cumsum += i;
431 }
432
433 let j = _idx - cumsum;
434 (j, i)
435}
436
437#[allow(dead_code)]
441pub fn parallel_line_search<F>(
442 f: F,
443 x: &Array1<f64>,
444 direction: &Array1<f64>,
445 step_sizes: &[f64],
446 options: &ParallelOptions,
447) -> (f64, f64)
448where
450 F: Fn(&ArrayView1<f64>) -> f64 + Sync,
451{
452 let evaluations: Vec<(f64, f64)> = if step_sizes.len() < options.min_parallel_size {
453 step_sizes
455 .iter()
456 .map(|&alpha| {
457 let x_new = x + alpha * direction;
458 let value = f(&x_new.view());
459 (alpha, value)
460 })
461 .collect()
462 } else {
463 #[cfg(feature = "parallel")]
465 {
466 step_sizes
467 .par_iter()
468 .map(|&alpha| {
469 let x_new = x + alpha * direction;
470 let value = f(&x_new.view());
471 (alpha, value)
472 })
473 .collect()
474 }
475 #[cfg(not(feature = "parallel"))]
476 {
477 step_sizes
478 .iter()
479 .map(|&alpha| {
480 let x_new = x + alpha * direction;
481 let value = f(&x_new.view());
482 (alpha, value)
483 })
484 .collect()
485 }
486 };
487
488 evaluations
490 .into_iter()
491 .min_by(|(_, v1), (_, v2)| v1.partial_cmp(v2).expect("Operation failed"))
492 .unwrap_or((0.0, f64::INFINITY))
493}
494
495#[cfg(test)]
496mod tests {
497 use super::*;
498 use scirs2_core::ndarray::array;
499
500 fn quadratic(x: &ArrayView1<f64>) -> f64 {
501 x.iter().map(|&xi| xi * xi).sum()
502 }
503
504 #[test]
505 fn test_parallel_gradient() {
506 let x = array![1.0, 2.0, 3.0, 4.0, 5.0];
507 let options = ParallelOptions::default();
508
509 let grad_parallel = parallel_finite_diff_gradient(quadratic, x.view(), &options);
510 let grad_sequential = sequential_finite_diff_gradient(quadratic, x.view(), 1e-8);
511
512 for i in 0..x.len() {
514 assert!((grad_parallel[i] - grad_sequential[i]).abs() < 1e-10);
515 assert!((grad_parallel[i] - 2.0 * x[i]).abs() < 1e-6);
517 }
518 }
519
520 #[test]
521 fn test_parallel_batch_evaluation() {
522 let points = vec![
523 array![1.0, 2.0],
524 array![3.0, 4.0],
525 array![5.0, 6.0],
526 array![7.0, 8.0],
527 ];
528
529 let options = ParallelOptions {
530 min_parallel_size: 2,
531 ..Default::default()
532 };
533
534 let values = parallel_evaluate_batch(quadratic, &points, &options);
535
536 for (i, point) in points.iter().enumerate() {
538 let expected = quadratic(&point.view());
539 assert_eq!(values[i], expected);
540 }
541 }
542
543 #[test]
544 fn test_parallel_line_search() {
545 let x = array![1.0, 1.0];
546 let direction = array![-1.0, -1.0];
547 let step_sizes: Vec<f64> = (0..20).map(|i| i as f64 * 0.1).collect();
548
549 let options = ParallelOptions::default();
550 let (best_step, best_value) =
551 parallel_line_search(quadratic, &x, &direction, &step_sizes, &options);
552
553 assert!((best_step - 1.0).abs() < 0.1);
556 assert!(best_value < 0.1);
557 }
558}