scirs2_optimize/parallel/
mod.rs1use ndarray::{Array1, ArrayView1};
25use rayon::prelude::*;
26
27#[derive(Debug, Clone)]
29pub struct ParallelOptions {
30 pub num_workers: Option<usize>,
32
33 pub min_parallel_size: usize,
35
36 pub chunk_size: usize,
38
39 pub parallel_evaluations: bool,
41
42 pub parallel_gradient: bool,
44}
45
46impl Default for ParallelOptions {
47 fn default() -> Self {
48 ParallelOptions {
49 num_workers: None,
50 min_parallel_size: 10,
51 chunk_size: 1,
52 parallel_evaluations: true,
53 parallel_gradient: true,
54 }
55 }
56}
57
58pub fn parallel_finite_diff_gradient<F>(
63 f: F,
64 x: ArrayView1<f64>,
65 options: &ParallelOptions,
66) -> Array1<f64>
67where
68 F: Fn(&ArrayView1<f64>) -> f64 + Sync,
69{
70 let n = x.len();
71 let eps = 1e-8;
72
73 if n < options.min_parallel_size || !options.parallel_gradient {
75 return sequential_finite_diff_gradient(f, x, eps);
76 }
77
78 if let Some(num_workers) = options.num_workers {
80 rayon::ThreadPoolBuilder::new()
81 .num_threads(num_workers)
82 .build()
83 .unwrap()
84 .install(|| compute_parallel_gradient(&f, x, eps))
85 } else {
86 compute_parallel_gradient(&f, x, eps)
87 }
88}
89
90fn sequential_finite_diff_gradient<F>(f: F, x: ArrayView1<f64>, eps: f64) -> Array1<f64>
92where
93 F: Fn(&ArrayView1<f64>) -> f64,
94{
95 let n = x.len();
96 let mut grad = Array1::zeros(n);
97 let fx = f(&x);
98
99 for i in 0..n {
100 let mut x_plus = x.to_owned();
101 x_plus[i] += eps;
102 let fx_plus = f(&x_plus.view());
103 grad[i] = (fx_plus - fx) / eps;
104 }
105
106 grad
107}
108
109fn 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
131pub fn parallel_evaluate_batch<F>(
135 f: F,
136 points: &[Array1<f64>],
137 options: &ParallelOptions,
138) -> Vec<f64>
139where
140 F: Fn(&ArrayView1<f64>) -> f64 + Sync,
141{
142 if points.len() < options.min_parallel_size || !options.parallel_evaluations {
143 points.iter().map(|x| f(&x.view())).collect()
145 } else {
146 points.par_iter().map(|x| f(&x.view())).collect()
148 }
149}
150
151#[allow(dead_code)] pub struct ParallelMultiStart<F> {
156 objective: F,
157 bounds: Vec<(f64, f64)>,
158 options: ParallelOptions,
159}
160
161impl<F> ParallelMultiStart<F>
162where
163 F: Fn(&ArrayView1<f64>) -> f64 + Sync + Send,
164{
165 pub fn new(objective: F, bounds: Vec<(f64, f64)>, options: ParallelOptions) -> Self {
167 ParallelMultiStart {
168 objective,
169 bounds,
170 options,
171 }
172 }
173
174 pub fn run<O>(&self, starting_points: Vec<Array1<f64>>, optimizer: O) -> Vec<OptimizationResult>
176 where
177 O: Fn(&Array1<f64>, &F) -> OptimizationResult + Sync,
178 {
179 if starting_points.len() < self.options.min_parallel_size {
180 starting_points
182 .iter()
183 .map(|x0| optimizer(x0, &self.objective))
184 .collect()
185 } else {
186 starting_points
188 .par_iter()
189 .map(|x0| optimizer(x0, &self.objective))
190 .collect()
191 }
192 }
193
194 pub fn best_result(results: &[OptimizationResult]) -> Option<&OptimizationResult> {
196 results
197 .iter()
198 .min_by(|a, b| a.function_value.partial_cmp(&b.function_value).unwrap())
199 }
200}
201
202#[derive(Debug, Clone)]
204pub struct OptimizationResult {
205 pub x: Array1<f64>,
206 pub function_value: f64,
207 pub iterations: usize,
208 pub success: bool,
209}
210
211pub fn parallel_finite_diff_hessian<F>(
213 f: &F,
214 x: ArrayView1<f64>,
215 gradient: Option<&Array1<f64>>,
216 options: &ParallelOptions,
217) -> Array1<f64>
218where
220 F: Fn(&ArrayView1<f64>) -> f64 + Sync,
221{
222 let n = x.len();
223 let eps = 1e-8;
224
225 if n < options.min_parallel_size {
227 return sequential_finite_diff_hessian(f, x, gradient, eps);
228 }
229
230 let _grad = match gradient {
232 Some(g) => g.clone(),
233 None => parallel_finite_diff_gradient(f, x, options),
234 };
235
236 let num_elements = n * (n + 1) / 2;
238
239 let hessian_elements: Vec<f64> = (0..num_elements)
241 .into_par_iter()
242 .map(|idx| {
243 let (i, j) = index_to_upper_triangle(idx, n);
245
246 if i == j {
247 let mut x_plus = x.to_owned();
249 let mut x_minus = x.to_owned();
250 x_plus[i] += eps;
251 x_minus[i] -= eps;
252
253 let f_plus = f(&x_plus.view());
254 let f_minus = f(&x_minus.view());
255 let f_center = f(&x);
256
257 (f_plus - 2.0 * f_center + f_minus) / (eps * eps)
258 } else {
259 let mut x_pp = x.to_owned();
261 let mut x_pm = x.to_owned();
262 let mut x_mp = x.to_owned();
263 let mut x_mm = x.to_owned();
264
265 x_pp[i] += eps;
266 x_pp[j] += eps;
267 x_pm[i] += eps;
268 x_pm[j] -= eps;
269 x_mp[i] -= eps;
270 x_mp[j] += eps;
271 x_mm[i] -= eps;
272 x_mm[j] -= eps;
273
274 let f_pp = f(&x_pp.view());
275 let f_pm = f(&x_pm.view());
276 let f_mp = f(&x_mp.view());
277 let f_mm = f(&x_mm.view());
278
279 (f_pp - f_pm - f_mp + f_mm) / (4.0 * eps * eps)
280 }
281 })
282 .collect();
283
284 Array1::from_vec(hessian_elements)
285}
286
287fn sequential_finite_diff_hessian<F>(
289 f: &F,
290 x: ArrayView1<f64>,
291 _gradient: Option<&Array1<f64>>,
292 eps: f64,
293) -> Array1<f64>
294where
295 F: Fn(&ArrayView1<f64>) -> f64,
296{
297 let n = x.len();
298 let num_elements = n * (n + 1) / 2;
299 let mut hessian = Array1::zeros(num_elements);
300
301 for idx in 0..num_elements {
302 let (i, j) = index_to_upper_triangle(idx, n);
303
304 if i == j {
305 let mut x_plus = x.to_owned();
307 let mut x_minus = x.to_owned();
308 x_plus[i] += eps;
309 x_minus[i] -= eps;
310
311 let f_plus = f(&x_plus.view());
312 let f_minus = f(&x_minus.view());
313 let f_center = f(&x);
314
315 hessian[idx] = (f_plus - 2.0 * f_center + f_minus) / (eps * eps);
316 } else {
317 let mut x_pp = x.to_owned();
319 let mut x_pm = x.to_owned();
320 let mut x_mp = x.to_owned();
321 let mut x_mm = x.to_owned();
322
323 x_pp[i] += eps;
324 x_pp[j] += eps;
325 x_pm[i] += eps;
326 x_pm[j] -= eps;
327 x_mp[i] -= eps;
328 x_mp[j] += eps;
329 x_mm[i] -= eps;
330 x_mm[j] -= eps;
331
332 let f_pp = f(&x_pp.view());
333 let f_pm = f(&x_pm.view());
334 let f_mp = f(&x_mp.view());
335 let f_mm = f(&x_mm.view());
336
337 hessian[idx] = (f_pp - f_pm - f_mp + f_mm) / (4.0 * eps * eps);
338 }
339 }
340
341 hessian
342}
343
344fn index_to_upper_triangle(idx: usize, _n: usize) -> (usize, usize) {
346 let mut i = 0;
348 let mut cumsum = 0;
349
350 while cumsum + i < idx {
351 i += 1;
352 cumsum += i;
353 }
354
355 let j = idx - cumsum;
356 (j, i)
357}
358
359pub fn parallel_line_search<F>(
363 f: F,
364 x: &Array1<f64>,
365 direction: &Array1<f64>,
366 step_sizes: &[f64],
367 options: &ParallelOptions,
368) -> (f64, f64)
369where
371 F: Fn(&ArrayView1<f64>) -> f64 + Sync,
372{
373 let evaluations: Vec<(f64, f64)> = if step_sizes.len() < options.min_parallel_size {
374 step_sizes
376 .iter()
377 .map(|&alpha| {
378 let x_new = x + alpha * direction;
379 let value = f(&x_new.view());
380 (alpha, value)
381 })
382 .collect()
383 } else {
384 step_sizes
386 .par_iter()
387 .map(|&alpha| {
388 let x_new = x + alpha * direction;
389 let value = f(&x_new.view());
390 (alpha, value)
391 })
392 .collect()
393 };
394
395 evaluations
397 .into_iter()
398 .min_by(|(_, v1), (_, v2)| v1.partial_cmp(v2).unwrap())
399 .unwrap_or((0.0, f64::INFINITY))
400}
401
402#[cfg(test)]
403mod tests {
404 use super::*;
405 use ndarray::array;
406
407 fn quadratic(x: &ArrayView1<f64>) -> f64 {
408 x.iter().map(|&xi| xi * xi).sum()
409 }
410
411 #[test]
412 fn test_parallel_gradient() {
413 let x = array![1.0, 2.0, 3.0, 4.0, 5.0];
414 let options = ParallelOptions::default();
415
416 let grad_parallel = parallel_finite_diff_gradient(quadratic, x.view(), &options);
417 let grad_sequential = sequential_finite_diff_gradient(quadratic, x.view(), 1e-8);
418
419 for i in 0..x.len() {
421 assert!((grad_parallel[i] - grad_sequential[i]).abs() < 1e-10);
422 assert!((grad_parallel[i] - 2.0 * x[i]).abs() < 1e-6);
424 }
425 }
426
427 #[test]
428 fn test_parallel_batch_evaluation() {
429 let points = vec![
430 array![1.0, 2.0],
431 array![3.0, 4.0],
432 array![5.0, 6.0],
433 array![7.0, 8.0],
434 ];
435
436 let options = ParallelOptions {
437 min_parallel_size: 2,
438 ..Default::default()
439 };
440
441 let values = parallel_evaluate_batch(quadratic, &points, &options);
442
443 for (i, point) in points.iter().enumerate() {
445 let expected = quadratic(&point.view());
446 assert_eq!(values[i], expected);
447 }
448 }
449
450 #[test]
451 fn test_parallel_line_search() {
452 let x = array![1.0, 1.0];
453 let direction = array![-1.0, -1.0];
454 let step_sizes: Vec<f64> = (0..20).map(|i| i as f64 * 0.1).collect();
455
456 let options = ParallelOptions::default();
457 let (best_step, best_value) =
458 parallel_line_search(quadratic, &x, &direction, &step_sizes, &options);
459
460 assert!((best_step - 1.0).abs() < 0.1);
463 assert!(best_value < 0.1);
464 }
465}