use scirs2_core::ndarray::{Array1, ArrayView1};
use scirs2_core::parallel_ops::*;
#[cfg(feature = "parallel")]
use scirs2_core::parallel_ops::{IntoParallelIterator, ParallelIterator};
#[derive(Debug, Clone)]
pub struct ParallelOptions {
pub num_workers: Option<usize>,
pub min_parallel_size: usize,
pub chunk_size: usize,
pub parallel_evaluations: bool,
pub parallel_gradient: bool,
}
impl Default for ParallelOptions {
fn default() -> Self {
ParallelOptions {
num_workers: None,
min_parallel_size: 10,
chunk_size: 1,
parallel_evaluations: true,
parallel_gradient: true,
}
}
}
#[allow(dead_code)]
pub fn parallel_finite_diff_gradient<F>(
f: F,
x: ArrayView1<f64>,
options: &ParallelOptions,
) -> Array1<f64>
where
F: Fn(&ArrayView1<f64>) -> f64 + Sync,
{
let n = x.len();
let eps = 1e-8;
if n < options.min_parallel_size || !options.parallel_gradient {
return sequential_finite_diff_gradient(f, x, eps);
}
compute_parallel_gradient(&f, x, eps)
}
#[allow(dead_code)]
fn sequential_finite_diff_gradient<F>(f: F, x: ArrayView1<f64>, eps: f64) -> Array1<f64>
where
F: Fn(&ArrayView1<f64>) -> f64,
{
let n = x.len();
let mut grad = Array1::zeros(n);
let fx = f(&x);
for i in 0..n {
let mut x_plus = x.to_owned();
x_plus[i] += eps;
let fx_plus = f(&x_plus.view());
grad[i] = (fx_plus - fx) / eps;
}
grad
}
#[cfg(feature = "parallel")]
#[allow(dead_code)]
fn compute_parallel_gradient<F>(f: &F, x: ArrayView1<f64>, eps: f64) -> Array1<f64>
where
F: Fn(&ArrayView1<f64>) -> f64 + Sync,
{
let n = x.len();
let fx = f(&x);
let grad_vec: Vec<f64> = (0..n)
.into_par_iter()
.map(|i| {
let mut x_plus = x.to_owned();
x_plus[i] += eps;
let fx_plus = f(&x_plus.view());
(fx_plus - fx) / eps
})
.collect();
Array1::from_vec(grad_vec)
}
#[cfg(not(feature = "parallel"))]
#[allow(dead_code)]
fn compute_parallel_gradient<F>(f: &F, x: ArrayView1<f64>, eps: f64) -> Array1<f64>
where
F: Fn(&ArrayView1<f64>) -> f64,
{
sequential_finite_diff_gradient(|x| f(x), x, eps)
}
#[allow(dead_code)]
pub fn parallel_evaluate_batch<F>(
f: F,
points: &[Array1<f64>],
options: &ParallelOptions,
) -> Vec<f64>
where
F: Fn(&ArrayView1<f64>) -> f64 + Sync,
{
if points.len() < options.min_parallel_size || !options.parallel_evaluations {
points.iter().map(|x| f(&x.view())).collect()
} else {
#[cfg(feature = "parallel")]
{
points.par_iter().map(|x| f(&x.view())).collect()
}
#[cfg(not(feature = "parallel"))]
{
points.iter().map(|x| f(&x.view())).collect()
}
}
}
#[allow(dead_code)] pub struct ParallelMultiStart<F> {
objective: F,
bounds: Vec<(f64, f64)>,
options: ParallelOptions,
}
impl<F> ParallelMultiStart<F>
where
F: Fn(&ArrayView1<f64>) -> f64 + Sync + Send,
{
pub fn new(objective: F, bounds: Vec<(f64, f64)>, options: ParallelOptions) -> Self {
ParallelMultiStart {
objective,
bounds,
options,
}
}
pub fn run<O>(&self, starting_points: Vec<Array1<f64>>, optimizer: O) -> Vec<OptimizationResult>
where
O: Fn(&Array1<f64>, &F) -> OptimizationResult + Sync,
{
if starting_points.len() < self.options.min_parallel_size {
starting_points
.iter()
.map(|x0| optimizer(x0, &self.objective))
.collect()
} else {
#[cfg(feature = "parallel")]
{
starting_points
.par_iter()
.map(|x0| optimizer(x0, &self.objective))
.collect()
}
#[cfg(not(feature = "parallel"))]
{
starting_points
.iter()
.map(|x0| optimizer(x0, &self.objective))
.collect()
}
}
}
pub fn best_result(results: &[OptimizationResult]) -> Option<&OptimizationResult> {
results.iter().min_by(|a, b| {
a.function_value
.partial_cmp(&b.function_value)
.expect("Operation failed")
})
}
}
#[derive(Debug, Clone)]
pub struct OptimizationResult {
pub x: Array1<f64>,
pub function_value: f64,
pub nit: usize,
pub success: bool,
}
#[allow(dead_code)]
pub fn parallel_finite_diff_hessian<F>(
f: &F,
x: ArrayView1<f64>,
gradient: Option<&Array1<f64>>,
options: &ParallelOptions,
) -> Array1<f64>
where
F: Fn(&ArrayView1<f64>) -> f64 + Sync,
{
let n = x.len();
let eps = 1e-8;
if n < options.min_parallel_size {
return sequential_finite_diff_hessian(f, x, gradient, eps);
}
let _grad = match gradient {
Some(g) => g.clone(),
None => parallel_finite_diff_gradient(f, x, options),
};
let num_elements = n * (n + 1) / 2;
#[cfg(feature = "parallel")]
let hessian_elements: Vec<f64> = (0..num_elements)
.into_par_iter()
.map(|idx| {
let (i, j) = index_to_upper_triangle(idx, n);
if i == j {
let mut x_plus = x.to_owned();
let mut x_minus = x.to_owned();
x_plus[i] += eps;
x_minus[i] -= eps;
let f_plus = f(&x_plus.view());
let f_minus = f(&x_minus.view());
let f_center = f(&x);
(f_plus - 2.0 * f_center + f_minus) / (eps * eps)
} else {
let mut x_pp = x.to_owned();
let mut x_pm = x.to_owned();
let mut x_mp = x.to_owned();
let mut x_mm = x.to_owned();
x_pp[i] += eps;
x_pp[j] += eps;
x_pm[i] += eps;
x_pm[j] -= eps;
x_mp[i] -= eps;
x_mp[j] += eps;
x_mm[i] -= eps;
x_mm[j] -= eps;
let f_pp = f(&x_pp.view());
let f_pm = f(&x_pm.view());
let f_mp = f(&x_mp.view());
let f_mm = f(&x_mm.view());
(f_pp - f_pm - f_mp + f_mm) / (4.0 * eps * eps)
}
})
.collect();
#[cfg(not(feature = "parallel"))]
let hessian_elements: Vec<f64> = (0..num_elements)
.map(|idx| {
let (i, j) = index_to_upper_triangle(idx, n);
if i == j {
let mut x_plus = x.to_owned();
let mut x_minus = x.to_owned();
x_plus[i] += eps;
x_minus[i] -= eps;
let f_plus = f(&x_plus.view());
let f_minus = f(&x_minus.view());
let f_center = f(&x);
(f_plus - 2.0 * f_center + f_minus) / (eps * eps)
} else {
let mut x_pp = x.to_owned();
let mut x_pm = x.to_owned();
let mut x_mp = x.to_owned();
let mut x_mm = x.to_owned();
x_pp[i] += eps;
x_pp[j] += eps;
x_pm[i] += eps;
x_pm[j] -= eps;
x_mp[i] -= eps;
x_mp[j] += eps;
x_mm[i] -= eps;
x_mm[j] -= eps;
let f_pp = f(&x_pp.view());
let f_pm = f(&x_pm.view());
let f_mp = f(&x_mp.view());
let f_mm = f(&x_mm.view());
(f_pp - f_pm - f_mp + f_mm) / (4.0 * eps * eps)
}
})
.collect();
Array1::from_vec(hessian_elements)
}
#[allow(dead_code)]
fn sequential_finite_diff_hessian<F>(
f: &F,
x: ArrayView1<f64>,
_gradient: Option<&Array1<f64>>,
eps: f64,
) -> Array1<f64>
where
F: Fn(&ArrayView1<f64>) -> f64,
{
let n = x.len();
let num_elements = n * (n + 1) / 2;
let mut hessian = Array1::zeros(num_elements);
for idx in 0..num_elements {
let (i, j) = index_to_upper_triangle(idx, n);
if i == j {
let mut x_plus = x.to_owned();
let mut x_minus = x.to_owned();
x_plus[i] += eps;
x_minus[i] -= eps;
let f_plus = f(&x_plus.view());
let f_minus = f(&x_minus.view());
let f_center = f(&x);
hessian[idx] = (f_plus - 2.0 * f_center + f_minus) / (eps * eps);
} else {
let mut x_pp = x.to_owned();
let mut x_pm = x.to_owned();
let mut x_mp = x.to_owned();
let mut x_mm = x.to_owned();
x_pp[i] += eps;
x_pp[j] += eps;
x_pm[i] += eps;
x_pm[j] -= eps;
x_mp[i] -= eps;
x_mp[j] += eps;
x_mm[i] -= eps;
x_mm[j] -= eps;
let f_pp = f(&x_pp.view());
let f_pm = f(&x_pm.view());
let f_mp = f(&x_mp.view());
let f_mm = f(&x_mm.view());
hessian[idx] = (f_pp - f_pm - f_mp + f_mm) / (4.0 * eps * eps);
}
}
hessian
}
#[allow(dead_code)]
fn index_to_upper_triangle(_idx: usize, n: usize) -> (usize, usize) {
let mut i = 0;
let mut cumsum = 0;
while cumsum + i < _idx {
i += 1;
cumsum += i;
}
let j = _idx - cumsum;
(j, i)
}
#[allow(dead_code)]
pub fn parallel_line_search<F>(
f: F,
x: &Array1<f64>,
direction: &Array1<f64>,
step_sizes: &[f64],
options: &ParallelOptions,
) -> (f64, f64)
where
F: Fn(&ArrayView1<f64>) -> f64 + Sync,
{
let evaluations: Vec<(f64, f64)> = if step_sizes.len() < options.min_parallel_size {
step_sizes
.iter()
.map(|&alpha| {
let x_new = x + alpha * direction;
let value = f(&x_new.view());
(alpha, value)
})
.collect()
} else {
#[cfg(feature = "parallel")]
{
step_sizes
.par_iter()
.map(|&alpha| {
let x_new = x + alpha * direction;
let value = f(&x_new.view());
(alpha, value)
})
.collect()
}
#[cfg(not(feature = "parallel"))]
{
step_sizes
.iter()
.map(|&alpha| {
let x_new = x + alpha * direction;
let value = f(&x_new.view());
(alpha, value)
})
.collect()
}
};
evaluations
.into_iter()
.min_by(|(_, v1), (_, v2)| v1.partial_cmp(v2).expect("Operation failed"))
.unwrap_or((0.0, f64::INFINITY))
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::array;
fn quadratic(x: &ArrayView1<f64>) -> f64 {
x.iter().map(|&xi| xi * xi).sum()
}
#[test]
fn test_parallel_gradient() {
let x = array![1.0, 2.0, 3.0, 4.0, 5.0];
let options = ParallelOptions::default();
let grad_parallel = parallel_finite_diff_gradient(quadratic, x.view(), &options);
let grad_sequential = sequential_finite_diff_gradient(quadratic, x.view(), 1e-8);
for i in 0..x.len() {
assert!((grad_parallel[i] - grad_sequential[i]).abs() < 1e-10);
assert!((grad_parallel[i] - 2.0 * x[i]).abs() < 1e-6);
}
}
#[test]
fn test_parallel_batch_evaluation() {
let points = vec![
array![1.0, 2.0],
array![3.0, 4.0],
array![5.0, 6.0],
array![7.0, 8.0],
];
let options = ParallelOptions {
min_parallel_size: 2,
..Default::default()
};
let values = parallel_evaluate_batch(quadratic, &points, &options);
for (i, point) in points.iter().enumerate() {
let expected = quadratic(&point.view());
assert_eq!(values[i], expected);
}
}
#[test]
fn test_parallel_line_search() {
let x = array![1.0, 1.0];
let direction = array![-1.0, -1.0];
let step_sizes: Vec<f64> = (0..20).map(|i| i as f64 * 0.1).collect();
let options = ParallelOptions::default();
let (best_step, best_value) =
parallel_line_search(quadratic, &x, &direction, &step_sizes, &options);
assert!((best_step - 1.0).abs() < 0.1);
assert!(best_value < 0.1);
}
}