scirs2_optimize/parallel/
mod.rs

1//! Parallel computation utilities for optimization algorithms
2//!
3//! This module provides parallel implementations of computationally intensive
4//! operations used in optimization algorithms, including:
5//! - Parallel function evaluations
6//! - Parallel gradient approximations
7//! - Multi-start optimization with parallel workers
8//!
9//! # Example
10//!
11//! ```
12//! use scirs2_optimize::parallel::{parallel_finite_diff_gradient, ParallelOptions};
13//! use scirs2_core::ndarray::{array, ArrayView1};
14//!
15//! fn objective(x: &ArrayView1<f64>) -> f64 {
16//!     x.iter().map(|&xi| xi.powi(2)).sum()
17//! }
18//!
19//! let x = array![1.0, 2.0, 3.0];
20//! let options = ParallelOptions::default();
21//! let gradient = parallel_finite_diff_gradient(objective, x.view(), &options);
22//! ```
23
24use scirs2_core::ndarray::{Array1, ArrayView1};
25use scirs2_core::parallel_ops::*;
26
27// Conditional imports for parallel operations
28#[cfg(feature = "parallel")]
29use scirs2_core::parallel_ops::{IntoParallelIterator, ParallelIterator};
30
31/// Options for parallel computation
32#[derive(Debug, Clone)]
33pub struct ParallelOptions {
34    /// Number of worker threads (None = use core parallel default)
35    pub num_workers: Option<usize>,
36
37    /// Minimum problem size to enable parallelization
38    pub min_parallel_size: usize,
39
40    /// Chunk size for parallel operations
41    pub chunk_size: usize,
42
43    /// Enable parallel function evaluations
44    pub parallel_evaluations: bool,
45
46    /// Enable parallel gradient computation
47    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/// Parallel finite difference gradient approximation
63///
64/// Computes the gradient using parallel function evaluations when
65/// the dimension is large enough to benefit from parallelization.
66#[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    // For small problems, use sequential computation
79    if n < options.min_parallel_size || !options.parallel_gradient {
80        return sequential_finite_diff_gradient(f, x, eps);
81    }
82
83    // Use core parallel abstractions
84    compute_parallel_gradient(&f, x, eps)
85}
86
87/// Sequential finite difference gradient (fallback)
88#[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/// Compute gradient in parallel
108#[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    // Parallel computation of gradient components
118    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/// Sequential fallback for gradient computation
132#[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/// Parallel evaluation of multiple points
142///
143/// Evaluates the objective function at multiple points in parallel.
144#[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        // Sequential evaluation for small batches
155        points.iter().map(|x| f(&x.view())).collect()
156    } else {
157        // Parallel evaluation
158        #[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/// Parallel multi-start optimization
170///
171/// Runs multiple optimization instances from different starting points in parallel.
172#[allow(dead_code)] // Bounds will be used in future implementation
173pub 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    /// Create a new parallel multi-start optimizer
184    pub fn new(objective: F, bounds: Vec<(f64, f64)>, options: ParallelOptions) -> Self {
185        ParallelMultiStart {
186            objective,
187            bounds,
188            options,
189        }
190    }
191
192    /// Run optimization from multiple starting points
193    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            // Sequential execution for small number of starts
199            starting_points
200                .iter()
201                .map(|x0| optimizer(x0, &self.objective))
202                .collect()
203        } else {
204            // Parallel execution
205            #[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    /// Find the best result among all runs
223    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/// Result from an optimization run
233#[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/// Parallel computation of Hessian matrix using finite differences
242#[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>
249// Returns upper triangle in row-major order
250where
251    F: Fn(&ArrayView1<f64>) -> f64 + Sync,
252{
253    let n = x.len();
254    let eps = 1e-8;
255
256    // For small problems, use sequential computation
257    if n < options.min_parallel_size {
258        return sequential_finite_diff_hessian(f, x, gradient, eps);
259    }
260
261    // Compute gradient if not provided
262    let _grad = match gradient {
263        Some(g) => g.clone(),
264        None => parallel_finite_diff_gradient(f, x, options),
265    };
266
267    // Number of elements in upper triangle (including diagonal)
268    let num_elements = n * (n + 1) / 2;
269
270    // Parallel computation of Hessian elements
271    #[cfg(feature = "parallel")]
272    let hessian_elements: Vec<f64> = (0..num_elements)
273        .into_par_iter()
274        .map(|idx| {
275            // Convert linear index to (i, j) coordinates
276            let (i, j) = index_to_upper_triangle(idx, n);
277
278            if i == j {
279                // Diagonal element: ∂²f/∂xᵢ²
280                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                // Off-diagonal element: ∂²f/∂xᵢ∂xⱼ
292                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            // Convert linear index to (i, j) coordinates
320            let (i, j) = index_to_upper_triangle(idx, n);
321
322            if i == j {
323                // Diagonal element: ∂²f/∂xᵢ²
324                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                // Off-diagonal element: ∂²f/∂xᵢ∂xⱼ
336                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/// Sequential Hessian computation (fallback)
364#[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            // Diagonal element
383            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            // Off-diagonal element
395            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/// Convert linear index to (i, j) coordinates in upper triangle
422#[allow(dead_code)]
423fn index_to_upper_triangle(_idx: usize, n: usize) -> (usize, usize) {
424    // Find row i such that i*(i+1)/2 <= _idx < (i+1)*(i+2)/2
425    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/// Parallel line search
438///
439/// Evaluates multiple step sizes in parallel to find the best one.
440#[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)
448// Returns (best_step, best_value)
449where
450    F: Fn(&ArrayView1<f64>) -> f64 + Sync,
451{
452    let evaluations: Vec<(f64, f64)> = if step_sizes.len() < options.min_parallel_size {
453        // Sequential evaluation
454        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        // Parallel evaluation
464        #[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    // Find the best step size
489    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        // Check that parallel and sequential give similar results
513        for i in 0..x.len() {
514            assert!((grad_parallel[i] - grad_sequential[i]).abs() < 1e-10);
515            // For quadratic function, gradient should be 2*x
516            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        // Verify results
537        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        // For quadratic function starting at (1,1) going in direction (-1,-1),
554        // the minimum should be at step size 1.0
555        assert!((best_step - 1.0).abs() < 0.1);
556        assert!(best_value < 0.1);
557    }
558}