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 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 ndarray::{Array1, ArrayView1};
25use rayon::prelude::*;
26
27/// Options for parallel computation
28#[derive(Debug, Clone)]
29pub struct ParallelOptions {
30    /// Number of worker threads (None = use rayon default)
31    pub num_workers: Option<usize>,
32
33    /// Minimum problem size to enable parallelization
34    pub min_parallel_size: usize,
35
36    /// Chunk size for parallel operations
37    pub chunk_size: usize,
38
39    /// Enable parallel function evaluations
40    pub parallel_evaluations: bool,
41
42    /// Enable parallel gradient computation
43    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
58/// Parallel finite difference gradient approximation
59///
60/// Computes the gradient using parallel function evaluations when
61/// the dimension is large enough to benefit from parallelization.
62pub 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    // For small problems, use sequential computation
74    if n < options.min_parallel_size || !options.parallel_gradient {
75        return sequential_finite_diff_gradient(f, x, eps);
76    }
77
78    // Configure thread pool if specified
79    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
90/// Sequential finite difference gradient (fallback)
91fn 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
109/// Compute gradient in parallel
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/// Parallel evaluation of multiple points
132///
133/// Evaluates the objective function at multiple points in parallel.
134pub 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        // Sequential evaluation for small batches
144        points.iter().map(|x| f(&x.view())).collect()
145    } else {
146        // Parallel evaluation
147        points.par_iter().map(|x| f(&x.view())).collect()
148    }
149}
150
151/// Parallel multi-start optimization
152///
153/// Runs multiple optimization instances from different starting points in parallel.
154#[allow(dead_code)] // Bounds will be used in future implementation
155pub 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    /// Create a new parallel multi-start optimizer
166    pub fn new(objective: F, bounds: Vec<(f64, f64)>, options: ParallelOptions) -> Self {
167        ParallelMultiStart {
168            objective,
169            bounds,
170            options,
171        }
172    }
173
174    /// Run optimization from multiple starting points
175    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            // Sequential execution for small number of starts
181            starting_points
182                .iter()
183                .map(|x0| optimizer(x0, &self.objective))
184                .collect()
185        } else {
186            // Parallel execution
187            starting_points
188                .par_iter()
189                .map(|x0| optimizer(x0, &self.objective))
190                .collect()
191        }
192    }
193
194    /// Find the best result among all runs
195    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/// Result from an optimization run
203#[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
211/// Parallel computation of Hessian matrix using finite differences
212pub fn parallel_finite_diff_hessian<F>(
213    f: &F,
214    x: ArrayView1<f64>,
215    gradient: Option<&Array1<f64>>,
216    options: &ParallelOptions,
217) -> Array1<f64>
218// Returns upper triangle in row-major order
219where
220    F: Fn(&ArrayView1<f64>) -> f64 + Sync,
221{
222    let n = x.len();
223    let eps = 1e-8;
224
225    // For small problems, use sequential computation
226    if n < options.min_parallel_size {
227        return sequential_finite_diff_hessian(f, x, gradient, eps);
228    }
229
230    // Compute gradient if not provided
231    let _grad = match gradient {
232        Some(g) => g.clone(),
233        None => parallel_finite_diff_gradient(f, x, options),
234    };
235
236    // Number of elements in upper triangle (including diagonal)
237    let num_elements = n * (n + 1) / 2;
238
239    // Parallel computation of Hessian elements
240    let hessian_elements: Vec<f64> = (0..num_elements)
241        .into_par_iter()
242        .map(|idx| {
243            // Convert linear index to (i, j) coordinates
244            let (i, j) = index_to_upper_triangle(idx, n);
245
246            if i == j {
247                // Diagonal element: ∂²f/∂xᵢ²
248                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                // Off-diagonal element: ∂²f/∂xᵢ∂xⱼ
260                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
287/// Sequential Hessian computation (fallback)
288fn 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            // Diagonal element
306            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            // Off-diagonal element
318            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
344/// Convert linear index to (i, j) coordinates in upper triangle
345fn index_to_upper_triangle(idx: usize, _n: usize) -> (usize, usize) {
346    // Find row i such that i*(i+1)/2 <= idx < (i+1)*(i+2)/2
347    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
359/// Parallel line search
360///
361/// Evaluates multiple step sizes in parallel to find the best one.
362pub 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)
369// Returns (best_step, best_value)
370where
371    F: Fn(&ArrayView1<f64>) -> f64 + Sync,
372{
373    let evaluations: Vec<(f64, f64)> = if step_sizes.len() < options.min_parallel_size {
374        // Sequential evaluation
375        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        // Parallel evaluation
385        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    // Find the best step size
396    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        // Check that parallel and sequential give similar results
420        for i in 0..x.len() {
421            assert!((grad_parallel[i] - grad_sequential[i]).abs() < 1e-10);
422            // For quadratic function, gradient should be 2*x
423            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        // Verify results
444        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        // For quadratic function starting at (1,1) going in direction (-1,-1),
461        // the minimum should be at step size 1.0
462        assert!((best_step - 1.0).abs() < 0.1);
463        assert!(best_value < 0.1);
464    }
465}