use crate::{Optimizer, OptimizerError, OptimizerResult};
use parking_lot::RwLock as PLRwLock;
use scirs2_core::RngExt;
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use torsh_core::{device::CpuDevice, DType};
use torsh_tensor::Tensor;
pub trait ObjectiveFunction: Send + Sync {
fn evaluate(&self, parameters: &[f32]) -> OptimizerResult<f32>;
fn dimension(&self) -> usize;
fn bounds(&self) -> Option<(Vec<f32>, Vec<f32>)> {
None
}
fn name(&self) -> &str {
"Unknown"
}
}
#[derive(Debug, Clone)]
pub struct GradientFreeConfig {
pub max_evaluations: usize,
pub tolerance: f32,
pub max_stagnation: usize,
pub device: Arc<CpuDevice>,
pub seed: Option<u64>,
pub verbose: bool,
}
impl Default for GradientFreeConfig {
fn default() -> Self {
Self {
max_evaluations: 10000,
tolerance: 1e-6,
max_stagnation: 100,
device: Arc::new(CpuDevice::new()),
seed: None,
verbose: false,
}
}
}
#[derive(Debug, Clone)]
pub struct OptimizationResult {
pub best_parameters: Vec<f32>,
pub best_value: f32,
pub evaluations: usize,
pub iterations: usize,
pub converged: bool,
pub convergence_reason: String,
pub history: Vec<(Vec<f32>, f32)>,
}
#[derive(Debug)]
pub struct NelderMead {
config: GradientFreeConfig,
alpha: f32,
gamma: f32,
rho: f32,
sigma: f32,
}
impl NelderMead {
pub fn new(config: GradientFreeConfig) -> Self {
Self {
config,
alpha: 1.0, gamma: 2.0, rho: 0.5, sigma: 0.5, }
}
pub fn with_coefficients(mut self, alpha: f32, gamma: f32, rho: f32, sigma: f32) -> Self {
self.alpha = alpha;
self.gamma = gamma;
self.rho = rho;
self.sigma = sigma;
self
}
pub fn optimize<F: ObjectiveFunction>(
&self,
objective: &F,
initial_point: &[f32],
) -> OptimizerResult<OptimizationResult> {
let n = objective.dimension();
if initial_point.len() != n {
return Err(OptimizerError::InvalidInput(format!(
"Initial point dimension {} doesn't match objective dimension {}",
initial_point.len(),
n
)));
}
let mut simplex = self.initialize_simplex(initial_point)?;
let mut values = Vec::with_capacity(n + 1);
let mut evaluations = 0;
let mut iterations = 0;
let mut history = Vec::new();
let mut best_value = f32::INFINITY;
let mut best_params = initial_point.to_vec();
let mut stagnation_count = 0;
for vertex in &simplex {
let value = objective.evaluate(vertex)?;
values.push(value);
evaluations += 1;
if value < best_value {
best_value = value;
best_params = vertex.clone();
stagnation_count = 0;
}
history.push((vertex.clone(), value));
}
while evaluations < self.config.max_evaluations
&& stagnation_count < self.config.max_stagnation
{
let mut indices: Vec<usize> = (0..simplex.len()).collect();
indices.sort_by(|&a, &b| {
values[a]
.partial_cmp(&values[b])
.unwrap_or(std::cmp::Ordering::Equal)
});
let best_idx = indices[0];
let worst_idx = indices[n];
let second_worst_idx = indices[n - 1];
let range = values[worst_idx] - values[best_idx];
if range < self.config.tolerance {
return Ok(OptimizationResult {
best_parameters: best_params,
best_value,
evaluations,
iterations,
converged: true,
convergence_reason: "Tolerance reached".to_string(),
history,
});
}
let centroid = self.compute_centroid(&simplex, &indices[..n])?;
let reflected = self.reflect(&simplex[worst_idx], ¢roid)?;
let reflected_value = objective.evaluate(&reflected)?;
evaluations += 1;
history.push((reflected.clone(), reflected_value));
if reflected_value < best_value {
best_value = reflected_value;
best_params = reflected.clone();
stagnation_count = 0;
} else {
stagnation_count += 1;
}
if values[best_idx] <= reflected_value && reflected_value < values[second_worst_idx] {
simplex[worst_idx] = reflected;
values[worst_idx] = reflected_value;
} else if reflected_value < values[best_idx] {
let expanded = self.expand(&reflected, ¢roid)?;
let expanded_value = objective.evaluate(&expanded)?;
evaluations += 1;
history.push((expanded.clone(), expanded_value));
if expanded_value < reflected_value {
simplex[worst_idx] = expanded.clone();
values[worst_idx] = expanded_value;
if expanded_value < best_value {
best_value = expanded_value;
best_params = expanded;
stagnation_count = 0;
}
} else {
simplex[worst_idx] = reflected;
values[worst_idx] = reflected_value;
}
} else {
let contracted = if reflected_value < values[worst_idx] {
self.contract_outside(&reflected, ¢roid)?
} else {
self.contract_inside(&simplex[worst_idx], ¢roid)?
};
let contracted_value = objective.evaluate(&contracted)?;
evaluations += 1;
history.push((contracted.clone(), contracted_value));
if contracted_value < values[worst_idx].min(reflected_value) {
simplex[worst_idx] = contracted.clone();
values[worst_idx] = contracted_value;
if contracted_value < best_value {
best_value = contracted_value;
best_params = contracted;
stagnation_count = 0;
}
} else {
for i in 1..=n {
let vertex_idx = indices[i];
simplex[vertex_idx] =
self.shrink(&simplex[vertex_idx], &simplex[best_idx])?;
values[vertex_idx] = objective.evaluate(&simplex[vertex_idx])?;
evaluations += 1;
history.push((simplex[vertex_idx].clone(), values[vertex_idx]));
if values[vertex_idx] < best_value {
best_value = values[vertex_idx];
best_params = simplex[vertex_idx].clone();
stagnation_count = 0;
}
}
}
}
iterations += 1;
if self.config.verbose && iterations % 100 == 0 {
println!("Iteration {}: Best value = {:.6e}", iterations, best_value);
}
}
let converged = stagnation_count < self.config.max_stagnation;
let reason = if converged {
"Maximum evaluations reached".to_string()
} else {
"Stagnation limit reached".to_string()
};
Ok(OptimizationResult {
best_parameters: best_params,
best_value,
evaluations,
iterations,
converged,
convergence_reason: reason,
history,
})
}
fn initialize_simplex(&self, initial_point: &[f32]) -> OptimizerResult<Vec<Vec<f32>>> {
let n = initial_point.len();
let mut simplex = Vec::with_capacity(n + 1);
simplex.push(initial_point.to_vec());
for i in 0..n {
let mut vertex = initial_point.to_vec();
let step = if initial_point[i].abs() > 1e-6 {
initial_point[i] * 0.05 } else {
0.00025 };
vertex[i] += step;
simplex.push(vertex);
}
Ok(simplex)
}
fn compute_centroid(
&self,
simplex: &[Vec<f32>],
indices: &[usize],
) -> OptimizerResult<Vec<f32>> {
let n = simplex[0].len();
let mut centroid = vec![0.0; n];
for &idx in indices {
for i in 0..n {
centroid[i] += simplex[idx][i];
}
}
for i in 0..n {
centroid[i] /= indices.len() as f32;
}
Ok(centroid)
}
fn reflect(&self, worst: &[f32], centroid: &[f32]) -> OptimizerResult<Vec<f32>> {
let mut reflected = Vec::with_capacity(worst.len());
for i in 0..worst.len() {
reflected.push(centroid[i] + self.alpha * (centroid[i] - worst[i]));
}
Ok(reflected)
}
fn expand(&self, reflected: &[f32], centroid: &[f32]) -> OptimizerResult<Vec<f32>> {
let mut expanded = Vec::with_capacity(reflected.len());
for i in 0..reflected.len() {
expanded.push(centroid[i] + self.gamma * (reflected[i] - centroid[i]));
}
Ok(expanded)
}
fn contract_outside(&self, reflected: &[f32], centroid: &[f32]) -> OptimizerResult<Vec<f32>> {
let mut contracted = Vec::with_capacity(reflected.len());
for i in 0..reflected.len() {
contracted.push(centroid[i] + self.rho * (reflected[i] - centroid[i]));
}
Ok(contracted)
}
fn contract_inside(&self, worst: &[f32], centroid: &[f32]) -> OptimizerResult<Vec<f32>> {
let mut contracted = Vec::with_capacity(worst.len());
for i in 0..worst.len() {
contracted.push(centroid[i] + self.rho * (worst[i] - centroid[i]));
}
Ok(contracted)
}
fn shrink(&self, vertex: &[f32], best: &[f32]) -> OptimizerResult<Vec<f32>> {
let mut shrunk = Vec::with_capacity(vertex.len());
for i in 0..vertex.len() {
shrunk.push(best[i] + self.sigma * (vertex[i] - best[i]));
}
Ok(shrunk)
}
}
#[derive(Debug)]
pub struct ParticleSwarmOptimizer {
config: GradientFreeConfig,
num_particles: usize,
inertia: f32,
c1: f32,
c2: f32,
max_velocity: f32,
}
impl ParticleSwarmOptimizer {
pub fn new(config: GradientFreeConfig, num_particles: usize) -> Self {
Self {
config,
num_particles,
inertia: 0.9,
c1: 2.0,
c2: 2.0,
max_velocity: 1.0,
}
}
pub fn with_parameters(mut self, inertia: f32, c1: f32, c2: f32, max_velocity: f32) -> Self {
self.inertia = inertia;
self.c1 = c1;
self.c2 = c2;
self.max_velocity = max_velocity;
self
}
pub fn optimize<F: ObjectiveFunction>(
&self,
objective: &F,
initial_bounds: &[(f32, f32)],
) -> OptimizerResult<OptimizationResult> {
let dimension = objective.dimension();
if initial_bounds.len() != dimension {
return Err(OptimizerError::InvalidInput(
"Bounds dimension doesn't match objective dimension".to_string(),
));
}
let mut positions = Vec::with_capacity(self.num_particles);
let mut velocities = Vec::with_capacity(self.num_particles);
let mut personal_best_positions = Vec::with_capacity(self.num_particles);
let mut personal_best_values = Vec::with_capacity(self.num_particles);
let mut current_values = Vec::with_capacity(self.num_particles);
use scirs2_core::random::{Random, Rng, SeedableRng};
let mut rng = if let Some(seed) = self.config.seed {
Random::seed(seed)
} else {
Random::seed(0)
};
for _ in 0..self.num_particles {
let mut position = Vec::with_capacity(dimension);
let mut velocity = Vec::with_capacity(dimension);
for i in 0..dimension {
let (min_bound, max_bound) = initial_bounds[i];
position.push(rng.random::<f32>() * (max_bound - min_bound) + min_bound);
velocity.push(rng.random::<f32>() * self.max_velocity * 2.0 - self.max_velocity);
}
positions.push(position);
velocities.push(velocity);
}
let mut evaluations = 0;
let mut global_best_position = vec![0.0; dimension];
let mut global_best_value = f32::INFINITY;
let mut history = Vec::new();
for i in 0..self.num_particles {
let value = objective.evaluate(&positions[i])?;
current_values.push(value);
personal_best_positions.push(positions[i].clone());
personal_best_values.push(value);
evaluations += 1;
history.push((positions[i].clone(), value));
if value < global_best_value {
global_best_value = value;
global_best_position = positions[i].clone();
}
}
let mut iterations = 0;
let mut stagnation_count = 0;
while evaluations < self.config.max_evaluations
&& stagnation_count < self.config.max_stagnation
{
let old_global_best = global_best_value;
for i in 0..self.num_particles {
for j in 0..dimension {
let r1 = rng.random::<f32>();
let r2 = rng.random::<f32>();
velocities[i][j] = self.inertia * velocities[i][j]
+ self.c1 * r1 * (personal_best_positions[i][j] - positions[i][j])
+ self.c2 * r2 * (global_best_position[j] - positions[i][j]);
velocities[i][j] = velocities[i][j]
.max(-self.max_velocity)
.min(self.max_velocity);
}
for j in 0..dimension {
positions[i][j] += velocities[i][j];
let (min_bound, max_bound) = initial_bounds[j];
positions[i][j] = positions[i][j].max(min_bound).min(max_bound);
}
let value = objective.evaluate(&positions[i])?;
current_values[i] = value;
evaluations += 1;
history.push((positions[i].clone(), value));
if value < personal_best_values[i] {
personal_best_values[i] = value;
personal_best_positions[i] = positions[i].clone();
if value < global_best_value {
global_best_value = value;
global_best_position = positions[i].clone();
}
}
}
if (global_best_value - old_global_best).abs() < self.config.tolerance {
stagnation_count += 1;
} else {
stagnation_count = 0;
}
iterations += 1;
if self.config.verbose && iterations % 10 == 0 {
println!(
"Iteration {}: Best value = {:.6e}",
iterations, global_best_value
);
}
}
let converged = stagnation_count < self.config.max_stagnation;
let reason = if converged {
"Maximum evaluations reached".to_string()
} else {
"Stagnation limit reached".to_string()
};
Ok(OptimizationResult {
best_parameters: global_best_position,
best_value: global_best_value,
evaluations,
iterations,
converged,
convergence_reason: reason,
history,
})
}
}
#[derive(Debug)]
pub struct RandomSearch {
config: GradientFreeConfig,
}
impl RandomSearch {
pub fn new(config: GradientFreeConfig) -> Self {
Self { config }
}
pub fn optimize<F: ObjectiveFunction>(
&self,
objective: &F,
bounds: &[(f32, f32)],
) -> OptimizerResult<OptimizationResult> {
let dimension = objective.dimension();
if bounds.len() != dimension {
return Err(OptimizerError::InvalidInput(
"Bounds dimension doesn't match objective dimension".to_string(),
));
}
use scirs2_core::random::{Random, Rng, SeedableRng};
let mut rng = if let Some(seed) = self.config.seed {
Random::seed(seed)
} else {
Random::seed(0)
};
let mut best_parameters = Vec::with_capacity(dimension);
let mut best_value = f32::INFINITY;
let mut evaluations = 0;
let mut history = Vec::new();
while evaluations < self.config.max_evaluations {
let mut point = Vec::with_capacity(dimension);
for i in 0..dimension {
let (min_bound, max_bound) = bounds[i];
point.push(rng.random::<f32>() * (max_bound - min_bound) + min_bound);
}
let value = objective.evaluate(&point)?;
evaluations += 1;
history.push((point.clone(), value));
if value < best_value {
best_value = value;
best_parameters = point;
}
if self.config.verbose && evaluations % 1000 == 0 {
println!(
"Evaluation {}: Best value = {:.6e}",
evaluations, best_value
);
}
}
Ok(OptimizationResult {
best_parameters,
best_value,
evaluations,
iterations: evaluations,
converged: false,
convergence_reason: "Maximum evaluations reached".to_string(),
history,
})
}
}
pub mod test_functions {
use super::*;
pub struct Sphere {
pub dimension: usize,
}
impl ObjectiveFunction for Sphere {
fn evaluate(&self, parameters: &[f32]) -> OptimizerResult<f32> {
Ok(parameters.iter().map(|&x| x * x).sum())
}
fn dimension(&self) -> usize {
self.dimension
}
fn bounds(&self) -> Option<(Vec<f32>, Vec<f32>)> {
Some((vec![-5.0; self.dimension], vec![5.0; self.dimension]))
}
fn name(&self) -> &str {
"Sphere"
}
}
pub struct Rosenbrock {
pub dimension: usize,
}
impl ObjectiveFunction for Rosenbrock {
fn evaluate(&self, parameters: &[f32]) -> OptimizerResult<f32> {
let mut sum = 0.0;
for i in 0..parameters.len() - 1 {
let term1 = parameters[i + 1] - parameters[i] * parameters[i];
let term2 = 1.0 - parameters[i];
sum += 100.0 * term1 * term1 + term2 * term2;
}
Ok(sum)
}
fn dimension(&self) -> usize {
self.dimension
}
fn bounds(&self) -> Option<(Vec<f32>, Vec<f32>)> {
Some((vec![-2.0; self.dimension], vec![2.0; self.dimension]))
}
fn name(&self) -> &str {
"Rosenbrock"
}
}
pub struct Rastrigin {
pub dimension: usize,
pub a: f32,
}
impl Rastrigin {
pub fn new(dimension: usize) -> Self {
Self { dimension, a: 10.0 }
}
}
impl ObjectiveFunction for Rastrigin {
fn evaluate(&self, parameters: &[f32]) -> OptimizerResult<f32> {
let n = parameters.len() as f32;
let mut sum = self.a * n;
for &x in parameters {
sum += x * x - self.a * (2.0 * std::f32::consts::PI * x).cos();
}
Ok(sum)
}
fn dimension(&self) -> usize {
self.dimension
}
fn bounds(&self) -> Option<(Vec<f32>, Vec<f32>)> {
Some((vec![-5.12; self.dimension], vec![5.12; self.dimension]))
}
fn name(&self) -> &str {
"Rastrigin"
}
}
}
#[cfg(test)]
mod tests {
use super::test_functions::*;
use super::*;
#[test]
fn test_nelder_mead_sphere() {
let config = GradientFreeConfig {
max_evaluations: 1000,
tolerance: 1e-6,
..Default::default()
};
let optimizer = NelderMead::new(config);
let objective = Sphere { dimension: 2 };
let initial_point = vec![1.0, 1.0];
let result = optimizer.optimize(&objective, &initial_point).unwrap();
assert!(result.best_value < 1e-5);
assert!(result.best_parameters.iter().all(|&x| x.abs() < 0.1));
}
#[test]
fn test_pso_sphere() {
let config = GradientFreeConfig {
max_evaluations: 2000,
tolerance: 1e-6,
seed: Some(42),
..Default::default()
};
let optimizer = ParticleSwarmOptimizer::new(config, 20);
let objective = Sphere { dimension: 2 };
let bounds = vec![(-5.0, 5.0), (-5.0, 5.0)];
let result = optimizer.optimize(&objective, &bounds).unwrap();
assert!(result.best_value < 1e-3);
}
#[test]
fn test_random_search() {
let config = GradientFreeConfig {
max_evaluations: 5000,
seed: Some(42),
..Default::default()
};
let optimizer = RandomSearch::new(config);
let objective = Sphere { dimension: 2 };
let bounds = vec![(-5.0, 5.0), (-5.0, 5.0)];
let result = optimizer.optimize(&objective, &bounds).unwrap();
assert!(result.best_value < 1.0);
assert_eq!(result.evaluations, 5000);
}
}