use crate::error::OptimizeError;
use crate::unconstrained::{line_search::backtracking_line_search, OptimizeResult};
use scirs2_core::ndarray::{Array1, ArrayView1};
use scirs2_core::random::rngs::StdRng;
use scirs2_core::random::{Rng, RngExt, SeedableRng};
use std::collections::VecDeque;
#[derive(Debug, Clone)]
pub struct SubspaceOptions {
pub max_iter: usize,
pub tol: f64,
pub subspace_dim: usize,
pub block_size: usize,
pub coord_max_iter: usize,
pub memory_limit: usize,
pub seed: Option<u64>,
pub adaptive_subspace: bool,
pub subspace_update_freq: usize,
pub min_improvement: f64,
}
impl Default for SubspaceOptions {
fn default() -> Self {
Self {
max_iter: 1000,
tol: 1e-6,
subspace_dim: 100,
block_size: 50,
coord_max_iter: 10,
memory_limit: 20,
seed: None,
adaptive_subspace: true,
subspace_update_freq: 10,
min_improvement: 1e-12,
}
}
}
#[derive(Debug, Clone, Copy)]
pub enum SubspaceMethod {
RandomCoordinateDescent,
BlockCoordinateDescent,
RandomSubspace,
AdaptiveSubspace,
CyclicalCoordinateDescent,
}
struct SubspaceState {
gradient_history: VecDeque<Array1<f64>>,
function_history: VecDeque<f64>,
current_subspace: Option<Vec<Array1<f64>>>,
rng: StdRng,
#[allow(dead_code)]
update_counter: usize,
}
impl SubspaceState {
fn new(seed: Option<u64>) -> Self {
let rng = match seed {
Some(s) => StdRng::seed_from_u64(s),
None => StdRng::seed_from_u64(42), };
Self {
gradient_history: VecDeque::new(),
function_history: VecDeque::new(),
current_subspace: None,
rng,
update_counter: 0,
}
}
fn add_gradient(&mut self, grad: Array1<f64>, memory_limit: usize) {
self.gradient_history.push_back(grad);
if self.gradient_history.len() > memory_limit {
self.gradient_history.pop_front();
}
}
fn add_function_value(&mut self, fval: f64) {
self.function_history.push_back(fval);
if self.function_history.len() > 50 {
self.function_history.pop_front();
}
}
fn generate_random_subspace(&mut self, n: usize, subspace_dim: usize) -> Vec<Array1<f64>> {
let mut basis = Vec::new();
for _ in 0..subspace_dim.min(n) {
let mut vec = Array1::zeros(n);
let num_nonzeros = (n / 10).clamp(1, 20); for _ in 0..num_nonzeros {
let idx = self.rng.random_range(0..n);
vec[idx] = self.rng.random_range(-1.0..1.0); }
let norm = vec.mapv(|x: f64| x.powi(2)).sum().sqrt();
if norm > 1e-12 {
vec /= norm;
}
basis.push(vec);
}
basis
}
fn generate_adaptive_subspace(&self, subspace_dim: usize) -> Vec<Array1<f64>> {
if self.gradient_history.len() < 2 {
return Vec::new();
}
let _n = self.gradient_history[0].len();
let mut basis = Vec::new();
let recent_grads: Vec<_> = self
.gradient_history
.iter()
.rev()
.take(subspace_dim)
.collect();
for grad in recent_grads {
let norm = grad.mapv(|x: f64| x.powi(2)).sum().sqrt();
if norm > 1e-12 {
basis.push(grad / norm);
}
if basis.len() >= subspace_dim {
break;
}
}
if basis.len() < subspace_dim && self.gradient_history.len() > 1 {
for i in 1..self.gradient_history.len() {
if basis.len() >= subspace_dim {
break;
}
let diff = &self.gradient_history[i] - &self.gradient_history[i - 1];
let norm = diff.mapv(|x: f64| x.powi(2)).sum().sqrt();
if norm > 1e-12 {
basis.push(diff / norm);
}
}
}
orthogonalize_basis(&mut basis);
basis
}
}
#[allow(dead_code)]
fn orthogonalize_basis(basis: &mut Vec<Array1<f64>>) {
for i in 0..basis.len() {
let norm = basis[i].mapv(|x: f64| x.powi(2)).sum().sqrt();
if norm > 1e-12 {
basis[i] = &basis[i] / norm;
} else {
continue;
}
for j in i + 1..basis.len() {
let dot_product = basis[i].dot(&basis[j]);
basis[j] = &basis[j] - dot_product * &basis[i];
}
}
basis.retain(|v| v.mapv(|x: f64| x.powi(2)).sum().sqrt() > 1e-12);
}
#[allow(dead_code)]
pub fn minimize_random_coordinate_descent<F>(
mut fun: F,
x0: Array1<f64>,
options: Option<SubspaceOptions>,
) -> Result<OptimizeResult<f64>, OptimizeError>
where
F: FnMut(&ArrayView1<f64>) -> f64,
{
let options = options.unwrap_or_default();
let mut x = x0.clone();
let mut state = SubspaceState::new(options.seed);
let mut nfev = 0;
let n = x.len();
let mut best_f = fun(&x.view());
nfev += 1;
for iter in 0..options.max_iter {
let mut improved = false;
for _ in 0..options.coord_max_iter {
let coord = state.rng.random_range(0..n);
let _f_current = fun(&x.view());
nfev += 1;
let eps = 1e-4;
let mut x_plus = x.clone();
let mut x_minus = x.clone();
x_plus[coord] += eps;
x_minus[coord] -= eps;
let f_plus = fun(&x_plus.view());
let f_minus = fun(&x_minus.view());
nfev += 2;
let grad_coord = (f_plus - f_minus) / (2.0 * eps);
if grad_coord.abs() > options.tol {
let direction = -grad_coord.signum();
let step_size = find_step_size(&mut fun, &x, coord, direction, &mut nfev);
if step_size > 0.0 {
x[coord] += direction * step_size;
let new_f = fun(&x.view());
nfev += 1;
if new_f < best_f - options.min_improvement {
best_f = new_f;
improved = true;
}
}
}
}
if !improved {
return Ok(OptimizeResult {
x,
fun: best_f,
nit: iter,
func_evals: nfev,
nfev,
jacobian: None,
hessian: None,
success: true,
message: "Optimization terminated successfully.".to_string(),
});
}
}
Ok(OptimizeResult {
x,
fun: best_f,
nit: options.max_iter,
func_evals: nfev,
nfev,
jacobian: None,
hessian: None,
success: false,
message: "Maximum iterations reached.".to_string(),
})
}
#[allow(dead_code)]
pub fn minimize_block_coordinate_descent<F>(
mut fun: F,
x0: Array1<f64>,
options: Option<SubspaceOptions>,
) -> Result<OptimizeResult<f64>, OptimizeError>
where
F: FnMut(&ArrayView1<f64>) -> f64,
{
let options = options.unwrap_or_default();
let mut x = x0.clone();
let _state = SubspaceState::new(options.seed);
let mut nfev = 0;
let n = x.len();
let mut best_f = fun(&x.view());
nfev += 1;
for iter in 0..options.max_iter {
let mut improved = false;
let num_blocks = n.div_ceil(options.block_size);
for block_idx in 0..num_blocks {
let start_idx = block_idx * options.block_size;
let end_idx = ((block_idx + 1) * options.block_size).min(n);
let block_improved =
optimize_block(&mut fun, &mut x, start_idx, end_idx, &options, &mut nfev)?;
if block_improved {
improved = true;
let new_f = fun(&x.view());
nfev += 1;
if new_f < best_f {
best_f = new_f;
}
}
}
if !improved {
return Ok(OptimizeResult {
x,
fun: best_f,
nit: iter,
func_evals: nfev,
nfev,
jacobian: None,
hessian: None,
success: true,
message: "Optimization terminated successfully.".to_string(),
});
}
}
Ok(OptimizeResult {
x,
fun: best_f,
nit: options.max_iter,
func_evals: nfev,
nfev,
jacobian: None,
hessian: None,
success: false,
message: "Maximum iterations reached.".to_string(),
})
}
#[allow(dead_code)]
pub fn minimize_random_subspace<F>(
mut fun: F,
x0: Array1<f64>,
options: Option<SubspaceOptions>,
) -> Result<OptimizeResult<f64>, OptimizeError>
where
F: FnMut(&ArrayView1<f64>) -> f64,
{
let options = options.unwrap_or_default();
let mut x = x0.clone();
let mut state = SubspaceState::new(options.seed);
let mut nfev = 0;
let n = x.len();
let mut best_f = fun(&x.view());
nfev += 1;
for iter in 0..options.max_iter {
if iter % options.subspace_update_freq == 0 || state.current_subspace.is_none() {
state.current_subspace = Some(state.generate_random_subspace(n, options.subspace_dim));
}
let subspace = state
.current_subspace
.as_ref()
.expect("Operation failed")
.clone();
if subspace.is_empty() {
break;
}
let grad = compute_finite_diff_gradient(&mut fun, &x, &mut nfev);
state.add_gradient(grad.clone(), options.memory_limit);
state.add_function_value(best_f);
let mut subspace_grad = Array1::zeros(subspace.len());
for (i, basis_vec) in subspace.iter().enumerate() {
subspace_grad[i] = grad.dot(basis_vec);
}
let grad_norm = subspace_grad.mapv(|x: f64| x.powi(2)).sum().sqrt();
if grad_norm < options.tol {
return Ok(OptimizeResult {
x,
fun: best_f,
nit: iter,
func_evals: nfev,
nfev,
jacobian: Some(grad),
hessian: None,
success: true,
message: "Optimization terminated successfully.".to_string(),
});
}
let mut search_direction = Array1::zeros(n);
for (i, &coeff) in subspace_grad.iter().enumerate() {
search_direction = search_direction + coeff * &subspace[i];
}
let direction_norm = search_direction.mapv(|x: f64| x.powi(2)).sum().sqrt();
if direction_norm > 1e-12 {
search_direction /= direction_norm;
} else {
continue;
}
let (step_size, _) = backtracking_line_search(
&mut |x_view| fun(x_view),
&x.view(),
best_f,
&search_direction.view(),
&(-&grad).view(),
1.0,
1e-4,
0.5,
None,
);
nfev += 1;
let x_new = &x - step_size * &search_direction;
let f_new = fun(&x_new.view());
nfev += 1;
if f_new < best_f - options.min_improvement {
x = x_new;
best_f = f_new;
}
}
let final_grad = compute_finite_diff_gradient(&mut fun, &x, &mut nfev);
Ok(OptimizeResult {
x,
fun: best_f,
nit: options.max_iter,
func_evals: nfev,
nfev,
jacobian: Some(final_grad),
hessian: None,
success: false,
message: "Maximum iterations reached.".to_string(),
})
}
#[allow(dead_code)]
pub fn minimize_adaptive_subspace<F>(
mut fun: F,
x0: Array1<f64>,
options: Option<SubspaceOptions>,
) -> Result<OptimizeResult<f64>, OptimizeError>
where
F: FnMut(&ArrayView1<f64>) -> f64,
{
let options = options.unwrap_or_default();
let mut x = x0.clone();
let mut state = SubspaceState::new(options.seed);
let mut nfev = 0;
let mut best_f = fun(&x.view());
nfev += 1;
for iter in 0..options.max_iter {
let grad = compute_finite_diff_gradient(&mut fun, &x, &mut nfev);
state.add_gradient(grad.clone(), options.memory_limit);
state.add_function_value(best_f);
if iter % options.subspace_update_freq == 0 && state.gradient_history.len() > 1 {
let new_subspace = state.generate_adaptive_subspace(options.subspace_dim);
if !new_subspace.is_empty() {
state.current_subspace = Some(new_subspace);
}
}
let search_direction = if let Some(ref subspace) = state.current_subspace {
if !subspace.is_empty() {
let mut projected_grad = Array1::zeros(x.len());
for basis_vec in subspace {
let projection = grad.dot(basis_vec);
projected_grad = projected_grad + projection * basis_vec;
}
projected_grad
} else {
grad.clone()
}
} else {
grad.clone()
};
let grad_norm = search_direction.mapv(|x: f64| x.powi(2)).sum().sqrt();
if grad_norm < options.tol {
return Ok(OptimizeResult {
x,
fun: best_f,
nit: iter,
func_evals: nfev,
nfev,
jacobian: Some(grad),
hessian: None,
success: true,
message: "Optimization terminated successfully.".to_string(),
});
}
let (step_size, _) = backtracking_line_search(
&mut |x_view| fun(x_view),
&x.view(),
best_f,
&(-&search_direction).view(),
&(-&grad).view(),
1.0,
1e-4,
0.5,
None,
);
let x_new = &x - step_size * &search_direction;
let f_new = fun(&x_new.view());
nfev += 1;
if f_new < best_f - options.min_improvement {
x = x_new;
best_f = f_new;
}
}
let final_grad = compute_finite_diff_gradient(&mut fun, &x, &mut nfev);
Ok(OptimizeResult {
x,
fun: best_f,
nit: options.max_iter,
func_evals: nfev,
nfev,
jacobian: Some(final_grad),
hessian: None,
success: false,
message: "Maximum iterations reached.".to_string(),
})
}
#[allow(dead_code)]
pub fn minimize_subspace<F>(
fun: F,
x0: Array1<f64>,
method: SubspaceMethod,
options: Option<SubspaceOptions>,
) -> Result<OptimizeResult<f64>, OptimizeError>
where
F: FnMut(&ArrayView1<f64>) -> f64,
{
match method {
SubspaceMethod::RandomCoordinateDescent => {
minimize_random_coordinate_descent(fun, x0, options)
}
SubspaceMethod::BlockCoordinateDescent => {
minimize_block_coordinate_descent(fun, x0, options)
}
SubspaceMethod::RandomSubspace => minimize_random_subspace(fun, x0, options),
SubspaceMethod::AdaptiveSubspace => minimize_adaptive_subspace(fun, x0, options),
SubspaceMethod::CyclicalCoordinateDescent => {
minimize_cyclical_coordinate_descent(fun, x0, options)
}
}
}
#[allow(dead_code)]
pub fn minimize_cyclical_coordinate_descent<F>(
mut fun: F,
x0: Array1<f64>,
options: Option<SubspaceOptions>,
) -> Result<OptimizeResult<f64>, OptimizeError>
where
F: FnMut(&ArrayView1<f64>) -> f64,
{
let options = options.unwrap_or_default();
let mut x = x0.clone();
let mut nfev = 0;
let n = x.len();
let mut best_f = fun(&x.view());
nfev += 1;
for iter in 0..options.max_iter {
let mut improved = false;
for coord in 0..n {
let _f_current = fun(&x.view());
nfev += 1;
let eps = 1e-6;
let mut x_plus = x.clone();
let mut x_minus = x.clone();
x_plus[coord] += eps;
x_minus[coord] -= eps;
let f_plus = fun(&x_plus.view());
let f_minus = fun(&x_minus.view());
nfev += 2;
let grad_coord = (f_plus - f_minus) / (2.0 * eps);
if grad_coord.abs() > options.tol {
let direction = -grad_coord.signum();
let step_size = find_step_size(&mut fun, &x, coord, direction, &mut nfev);
if step_size > 0.0 {
x[coord] += direction * step_size;
let new_f = fun(&x.view());
nfev += 1;
if new_f < best_f - options.min_improvement {
best_f = new_f;
improved = true;
}
}
}
}
if !improved {
return Ok(OptimizeResult {
x,
fun: best_f,
nit: iter,
func_evals: nfev,
nfev,
jacobian: None,
hessian: None,
success: true,
message: "Optimization terminated successfully.".to_string(),
});
}
}
Ok(OptimizeResult {
x,
fun: best_f,
nit: options.max_iter,
func_evals: nfev,
nfev,
jacobian: None,
hessian: None,
success: false,
message: "Maximum iterations reached.".to_string(),
})
}
#[allow(dead_code)]
fn find_step_size<F>(
fun: &mut F,
x: &Array1<f64>,
coord: usize,
direction: f64,
nfev: &mut usize,
) -> f64
where
F: FnMut(&ArrayView1<f64>) -> f64,
{
let f0 = fun(&x.view());
*nfev += 1;
let mut step = 1.0;
let mut best_step = 0.0;
let mut best_f = f0;
for _ in 0..10 {
let mut x_new = x.clone();
x_new[coord] += direction * step;
let f_new = fun(&x_new.view());
*nfev += 1;
if f_new < best_f {
best_f = f_new;
best_step = step;
} else {
break; }
step *= 2.0; }
if best_step > 0.0 {
step = best_step * 0.1;
for _ in 0..5 {
let mut x_new = x.clone();
x_new[coord] += direction * step;
let f_new = fun(&x_new.view());
*nfev += 1;
if f_new < best_f {
best_f = f_new;
best_step = step;
}
step += best_step * 0.1;
if step > best_step * 2.0 {
break;
}
}
}
best_step
}
#[allow(dead_code)]
fn optimize_block<F>(
fun: &mut F,
x: &mut Array1<f64>,
start_idx: usize,
end_idx: usize,
options: &SubspaceOptions,
nfev: &mut usize,
) -> Result<bool, OptimizeError>
where
F: FnMut(&ArrayView1<f64>) -> f64,
{
let mut improved = false;
let block_size = end_idx - start_idx;
let mut block_x = Array1::zeros(block_size);
for i in 0..block_size {
block_x[i] = x[start_idx + i];
}
let f_orig = fun(&x.view());
*nfev += 1;
for _iter in 0..options.coord_max_iter {
let mut block_improved = false;
for i in 0..block_size {
let coord_idx = start_idx + i;
let eps = 1e-6;
let original_val = x[coord_idx];
x[coord_idx] = original_val + eps;
let f_plus = fun(&x.view());
x[coord_idx] = original_val - eps;
let f_minus = fun(&x.view());
x[coord_idx] = original_val; *nfev += 2;
let grad_coord = (f_plus - f_minus) / (2.0 * eps);
if grad_coord.abs() > options.tol {
let step = -0.01 * grad_coord.signum();
x[coord_idx] += step;
let f_new = fun(&x.view());
*nfev += 1;
if f_new < f_orig {
block_improved = true;
improved = true;
} else {
x[coord_idx] = original_val; }
}
}
if !block_improved {
break;
}
}
Ok(improved)
}
#[allow(dead_code)]
fn compute_finite_diff_gradient<F>(fun: &mut F, x: &Array1<f64>, nfev: &mut usize) -> Array1<f64>
where
F: FnMut(&ArrayView1<f64>) -> f64,
{
let n = x.len();
let mut grad = Array1::zeros(n);
let eps = 1e-8;
let f0 = fun(&x.view());
*nfev += 1;
for i in 0..n {
let mut x_plus = x.clone();
x_plus[i] += eps;
let f_plus = fun(&x_plus.view());
*nfev += 1;
grad[i] = (f_plus - f0) / eps;
}
grad
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
use scirs2_core::ndarray::array;
#[test]
fn test_random_coordinate_descent() {
let fun = |x: &ArrayView1<f64>| x.iter().map(|&xi| xi.powi(2)).sum::<f64>();
let x0 = Array1::from_vec(vec![1.0; 10]);
let options = SubspaceOptions {
max_iter: 100,
tol: 1e-6,
coord_max_iter: 5,
seed: Some(42),
..Default::default()
};
let result =
minimize_random_coordinate_descent(fun, x0, Some(options)).expect("Operation failed");
assert!(result.success);
for &xi in result.x.iter() {
assert_abs_diff_eq!(xi, 0.0, epsilon = 1e-2);
}
assert!(result.fun < 1e-2);
}
#[test]
fn test_block_coordinate_descent() {
let fun = |x: &ArrayView1<f64>| {
x.iter()
.enumerate()
.map(|(i, &xi)| (i + 1) as f64 * xi.powi(2))
.sum::<f64>()
};
let x0 = Array1::from_vec(vec![1.0; 20]);
let options = SubspaceOptions {
max_iter: 50,
block_size: 5,
tol: 1e-6,
seed: Some(42),
..Default::default()
};
let result =
minimize_block_coordinate_descent(fun, x0, Some(options)).expect("Operation failed");
assert!(result.success);
for &xi in result.x.iter() {
assert_abs_diff_eq!(xi, 0.0, epsilon = 1e-2);
}
}
#[test]
fn test_cyclical_coordinate_descent() {
let fun = |x: &ArrayView1<f64>| x[0].powi(2) + 2.0 * x[1].powi(2);
let x0 = array![2.0, 2.0];
let options = SubspaceOptions {
max_iter: 50,
tol: 1e-6,
seed: Some(42),
..Default::default()
};
let result =
minimize_cyclical_coordinate_descent(fun, x0, Some(options)).expect("Operation failed");
assert!(result.success);
assert_abs_diff_eq!(result.x[0], 0.0, epsilon = 1e-2);
assert_abs_diff_eq!(result.x[1], 0.0, epsilon = 1e-2);
assert!(result.fun < 1e-2);
}
#[test]
fn test_random_subspace() {
let fun = |x: &ArrayView1<f64>| x.iter().map(|&xi| xi.powi(2)).sum::<f64>();
let x0 = Array1::from_vec(vec![1.0; 50]); let options = SubspaceOptions {
max_iter: 100,
subspace_dim: 10,
tol: 1e-3,
seed: Some(42),
..Default::default()
};
let result = minimize_random_subspace(fun, x0, Some(options)).expect("Operation failed");
assert!(result.fun <= 50.0); }
#[test]
fn test_subspace_method_enum() {
let fun = |x: &ArrayView1<f64>| x[0].powi(2) + x[1].powi(2);
let x0 = array![1.0, 1.0];
let options = SubspaceOptions {
max_iter: 20,
tol: 1e-6,
seed: Some(42),
..Default::default()
};
let methods = [
SubspaceMethod::RandomCoordinateDescent,
SubspaceMethod::BlockCoordinateDescent,
SubspaceMethod::CyclicalCoordinateDescent,
SubspaceMethod::RandomSubspace,
SubspaceMethod::AdaptiveSubspace,
];
for method in &methods {
let result = minimize_subspace(fun, x0.clone(), *method, Some(options.clone()));
assert!(result.is_ok(), "Method {:?} failed", method);
}
}
}