use crate::error::OptimizeError;
use crate::sparse_numdiff::{sparse_jacobian, SparseFiniteDiffOptions};
use crate::unconstrained::line_search::backtracking_line_search;
use crate::unconstrained::result::OptimizeResult;
use crate::unconstrained::utils::check_convergence;
use crate::unconstrained::Options;
use scirs2_core::ndarray::{Array1, Array2, ArrayView1, Axis};
use scirs2_core::random::{Rng, RngExt};
use scirs2_sparse::{csr_array::CsrArray, sparray::SparseArray};
#[derive(Debug, Clone)]
pub struct SparseOptimizationOptions {
pub base_options: Options,
pub sparse_options: SparseFiniteDiffOptions,
pub sparsity_pattern: Option<CsrArray<f64>>,
pub use_sparse_operations: bool,
pub sparse_threshold: usize,
}
impl Default for SparseOptimizationOptions {
fn default() -> Self {
Self {
base_options: Options::default(),
sparse_options: SparseFiniteDiffOptions::default(),
sparsity_pattern: None,
use_sparse_operations: true,
sparse_threshold: 100, }
}
}
#[allow(dead_code)]
pub fn minimize_sparse_bfgs<F, S>(
fun: F,
x0: Array1<f64>,
options: &SparseOptimizationOptions,
) -> Result<OptimizeResult<S>, OptimizeError>
where
F: Fn(&ArrayView1<f64>) -> S + Clone + Sync,
S: Into<f64> + Clone,
{
let n = x0.len();
let base_opts = &options.base_options;
let sparse_opts = &options.sparse_options;
let use_sparse = options.use_sparse_operations && n >= options.sparse_threshold;
let mut x = x0.to_owned();
let bounds = base_opts.bounds.as_ref();
if let Some(bounds) = bounds {
bounds.project(x.as_slice_mut().expect("Operation failed"));
}
let mut f = fun(&x.view()).into();
let mut g = if use_sparse && options.sparsity_pattern.is_some() {
compute_sparse_gradient(&fun, &x.view(), sparse_opts)?
} else {
let mut fun_wrapper = |x_val: &ArrayView1<f64>| fun(x_val);
crate::unconstrained::utils::finite_difference_gradient(
&mut fun_wrapper,
&x.view(),
base_opts.eps,
)?
};
let mut h_inv = if use_sparse {
None
} else {
Some(Array2::eye(n))
};
let mut s_history: Vec<Array1<f64>> = Vec::new();
let mut y_history: Vec<Array1<f64>> = Vec::new();
let max_history = 10;
let mut iter = 0;
let mut nfev = 1;
while iter < base_opts.max_iter {
if g.mapv(|gi| gi.abs()).sum() < base_opts.gtol {
break;
}
let mut p = if use_sparse && !s_history.is_empty() {
compute_lbfgs_direction(&g, &s_history, &y_history)
} else if let Some(ref h) = h_inv {
-h.dot(&g)
} else {
-&g
};
if let Some(bounds) = bounds {
for i in 0..n {
let mut can_decrease = true;
let mut can_increase = true;
if let Some(lb) = bounds.lower[i] {
if x[i] <= lb + base_opts.eps {
can_decrease = false;
}
}
if let Some(ub) = bounds.upper[i] {
if x[i] >= ub - base_opts.eps {
can_increase = false;
}
}
if (g[i] > 0.0 && !can_decrease) || (g[i] < 0.0 && !can_increase) {
p[i] = 0.0;
}
}
if p.mapv(|pi| pi.abs()).sum() < 1e-10 {
break;
}
}
let alpha_init = 1.0;
let mut fun_wrapper = |x_val: &ArrayView1<f64>| fun(x_val);
let (alpha, f_new) = backtracking_line_search(
&mut fun_wrapper,
&x.view(),
f,
&p.view(),
&g.view(),
alpha_init,
0.0001,
0.5,
bounds,
);
nfev += 1;
let s = alpha * &p;
let x_new = &x + &s;
if s.mapv(|si| si.abs()).sum() < base_opts.xtol {
x = x_new;
break;
}
let g_new = if use_sparse && options.sparsity_pattern.is_some() {
compute_sparse_gradient(&fun, &x_new.view(), sparse_opts)?
} else {
let mut fun_wrapper = |x_val: &ArrayView1<f64>| fun(x_val);
crate::unconstrained::utils::finite_difference_gradient(
&mut fun_wrapper,
&x_new.view(),
base_opts.eps,
)?
};
nfev += n;
let y = &g_new - &g;
if check_convergence(
f - f_new,
0.0,
g_new.mapv(|gi| gi.abs()).sum(),
base_opts.ftol,
0.0,
base_opts.gtol,
) {
x = x_new;
g = g_new;
break;
}
let s_dot_y = s.dot(&y);
if s_dot_y > 1e-10 {
if use_sparse {
s_history.push(s.clone());
y_history.push(y.clone());
if s_history.len() > max_history {
s_history.remove(0);
y_history.remove(0);
}
} else if let Some(ref mut h) = h_inv {
let rho = 1.0 / s_dot_y;
let i_mat = Array2::eye(n);
let y_col = y.clone().insert_axis(Axis(1));
let s_row = s.clone().insert_axis(Axis(0));
let y_s_t = y_col.dot(&s_row);
let term1 = &i_mat - &(&y_s_t * rho);
let s_col = s.clone().insert_axis(Axis(1));
let y_row = y.clone().insert_axis(Axis(0));
let s_y_t = s_col.dot(&y_row);
let term2 = &i_mat - &(&s_y_t * rho);
let term3 = term1.dot(h);
*h = term3.dot(&term2) + rho * s_col.dot(&s_row);
}
}
x = x_new;
f = f_new;
g = g_new;
iter += 1;
}
if let Some(bounds) = bounds {
bounds.project(x.as_slice_mut().expect("Operation failed"));
}
let final_fun = fun(&x.view());
Ok(OptimizeResult {
x,
fun: final_fun,
nit: iter,
func_evals: nfev,
nfev,
success: iter < base_opts.max_iter,
message: if iter < base_opts.max_iter {
"Sparse optimization terminated successfully.".to_string()
} else {
"Maximum iterations reached.".to_string()
},
jacobian: Some(g),
hessian: None,
})
}
#[allow(dead_code)]
pub fn compute_sparse_gradient<F, S>(
fun: &F,
x: &ArrayView1<f64>,
options: &SparseFiniteDiffOptions,
) -> Result<Array1<f64>, OptimizeError>
where
F: Fn(&ArrayView1<f64>) -> S + Clone + Sync,
S: Into<f64> + Clone,
{
let wrapper_fn = |x_val: &ArrayView1<f64>| -> Array1<f64> {
let result: f64 = fun(x_val).into();
Array1::from(vec![result])
};
let mut rows = vec![0];
let mut cols = vec![];
let mut data = vec![];
for i in 0..x.len() {
rows.push(0);
cols.push(i);
data.push(1.0);
}
rows.remove(0);
let sparsity =
CsrArray::from_triplets(&rows, &cols, &data, (1, x.len()), false).map_err(|e| {
OptimizeError::ValueError(format!("Failed to create sparsity pattern: {}", e))
})?;
let jacobian = sparse_jacobian(wrapper_fn, x, None, Some(&sparsity), Some(options.clone()))?;
let grad_array = jacobian.to_array();
Ok(grad_array.row(0).to_owned())
}
#[allow(dead_code)]
fn finite_difference_gradient_sparse<F, S>(
fun: &mut F,
x: &ArrayView1<f64>,
options: &SparseFiniteDiffOptions,
) -> Result<Array1<f64>, OptimizeError>
where
F: FnMut(&ArrayView1<f64>) -> S,
S: Into<f64>,
{
let n = x.len();
let mut grad = Array1::<f64>::zeros(n);
let step = options.rel_step.unwrap_or(1e-8);
let f0 = fun(x).into();
let mut x_perturbed = x.to_owned();
for i in 0..n {
let h = step * (1.0 + x[i].abs());
x_perturbed[i] = x[i] + h;
let f_plus = fun(&x_perturbed.view()).into();
if !f_plus.is_finite() {
return Err(OptimizeError::ComputationError(
"Function returned non-finite value during gradient computation".to_string(),
));
}
grad[i] = (f_plus - f0) / h;
x_perturbed[i] = x[i]; }
Ok(grad)
}
#[allow(dead_code)]
fn compute_lbfgs_direction(
g: &Array1<f64>,
s_history: &[Array1<f64>],
y_history: &[Array1<f64>],
) -> Array1<f64> {
let m = s_history.len();
if m == 0 {
return -g; }
let mut q = g.clone();
let mut alpha = vec![0.0; m];
for i in (0..m).rev() {
let rho_i = 1.0 / y_history[i].dot(&s_history[i]);
alpha[i] = rho_i * s_history[i].dot(&q);
q = &q - alpha[i] * &y_history[i];
}
let gamma = if m > 0 {
s_history[m - 1].dot(&y_history[m - 1]) / y_history[m - 1].dot(&y_history[m - 1])
} else {
1.0
};
let mut r = gamma * q;
for i in 0..m {
let rho_i = 1.0 / y_history[i].dot(&s_history[i]);
let beta = rho_i * y_history[i].dot(&r);
r = &r + (alpha[i] - beta) * &s_history[i];
}
-r }
#[allow(dead_code)]
pub fn auto_detect_sparsity<F, S>(
fun: F,
x: &ArrayView1<f64>,
num_samples: Option<usize>,
threshold: Option<f64>,
) -> Result<CsrArray<f64>, OptimizeError>
where
F: Fn(&ArrayView1<f64>) -> S + Clone,
S: Into<f64> + Clone,
{
let n = x.len();
let num_samples = num_samples.unwrap_or(std::cmp::min(n, 10));
let threshold = threshold.unwrap_or(1e-10);
let mut sparsity_pattern = Array2::<f64>::zeros((1, n));
for _sample in 0..num_samples {
let mut x_pert = x.to_owned();
for i in 0..n {
x_pert[i] += 1e-6 * scirs2_core::random::rng().random_range(-0.5..0.5);
}
let options = SparseFiniteDiffOptions::default();
if let Ok(grad) =
finite_difference_gradient_sparse(&mut fun.clone(), &x_pert.view(), &options)
{
for i in 0..n {
if grad[i].abs() > threshold {
sparsity_pattern[[0, i]] = 1.0;
}
}
}
}
let mut rows = Vec::new();
let mut cols = Vec::new();
let mut data = Vec::new();
for j in 0..n {
if sparsity_pattern[[0, j]] > 0.0 {
rows.push(0);
cols.push(j);
data.push(1.0);
}
}
if rows.is_empty() {
for j in 0..n {
rows.push(0);
cols.push(j);
data.push(1.0);
}
}
CsrArray::from_triplets(&rows, &cols, &data, (1, n), false)
.map_err(|e| OptimizeError::ValueError(format!("Failed to create sparsity pattern: {}", e)))
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
#[test]
fn test_sparse_bfgs_quadratic() {
let quadratic = |x: &ArrayView1<f64>| -> f64 {
x.mapv(|xi| xi.powi(2)).sum()
};
let n = 50; let x0 = Array1::ones(n);
let mut options = SparseOptimizationOptions::default();
options.sparse_threshold = 10;
let result = minimize_sparse_bfgs(quadratic, x0, &options).expect("Operation failed");
assert!(result.success);
for i in 0..n {
assert_abs_diff_eq!(result.x[i], 0.0, epsilon = 1e-5);
}
}
#[test]
fn test_auto_detect_sparsity() {
let sparse_fn = |x: &ArrayView1<f64>| -> f64 {
x[0].powi(2) + x[1].powi(2)
};
let x = Array1::zeros(10);
let sparsity = auto_detect_sparsity(sparse_fn, &x.view(), Some(5), Some(1e-8))
.expect("Operation failed");
let dense = sparsity.to_array();
assert!(dense[[0, 0]] > 0.0);
assert!(dense[[0, 1]] > 0.0);
for i in 2..10 {
assert!(dense[[0, i]].abs() < 1e-8);
}
}
}