use crate::core::inner::{InitialState, WarmStart};
use crate::core::math::{SampleStandardNormal, Scalar, ScaleInPlace, ScaledAdd, VectorLen};
use crate::core::problem::{CostFunction, Problem};
use crate::core::rng::{ChaCha8Rng, SeedableRng};
use crate::core::solver::Solver;
use crate::core::state::SolisWetsState;
use crate::core::termination::TerminationReason;
use crate::solver::cma_inject::MemeticInner;
#[derive(Clone)]
pub struct SolisWets<F = f64> {
rho_init: F,
bias_gain: F,
bias_memory: F,
bias_decay: F,
expand_threshold: u32,
contract_threshold: u32,
expand_factor: F,
contract_factor: F,
rng: ChaCha8Rng,
}
impl<F: Scalar> SolisWets<F> {
pub fn new(seed: u64) -> Self {
Self {
rho_init: F::one(),
bias_gain: F::from_f64(0.4).unwrap(),
bias_memory: F::from_f64(0.2).unwrap(),
bias_decay: F::from_f64(0.5).unwrap(),
expand_threshold: 5,
contract_threshold: 3,
expand_factor: F::from_f64(2.0).unwrap(),
contract_factor: F::from_f64(0.5).unwrap(),
rng: ChaCha8Rng::seed_from_u64(seed),
}
}
pub fn with_rho_init(mut self, rho_init: F) -> Self {
assert!(
rho_init > F::zero(),
"rho_init must be > 0, got {:?}",
rho_init
);
self.rho_init = rho_init;
self
}
pub fn with_bias_gain(mut self, bias_gain: F) -> Self {
assert!(
bias_gain >= F::zero(),
"bias_gain must be >= 0, got {:?}",
bias_gain
);
self.bias_gain = bias_gain;
self
}
pub fn with_bias_memory(mut self, bias_memory: F) -> Self {
assert!(
bias_memory >= F::zero(),
"bias_memory must be >= 0, got {:?}",
bias_memory
);
self.bias_memory = bias_memory;
self
}
pub fn with_bias_decay(mut self, bias_decay: F) -> Self {
assert!(
bias_decay >= F::zero(),
"bias_decay must be >= 0, got {:?}",
bias_decay
);
self.bias_decay = bias_decay;
self
}
pub fn with_expand_threshold(mut self, expand_threshold: u32) -> Self {
assert!(
expand_threshold >= 1,
"expand_threshold must be >= 1, got {}",
expand_threshold
);
self.expand_threshold = expand_threshold;
self
}
pub fn with_contract_threshold(mut self, contract_threshold: u32) -> Self {
assert!(
contract_threshold >= 1,
"contract_threshold must be >= 1, got {}",
contract_threshold
);
self.contract_threshold = contract_threshold;
self
}
pub fn with_expand_factor(mut self, expand_factor: F) -> Self {
assert!(
expand_factor > F::zero(),
"expand_factor must be > 0, got {:?}",
expand_factor
);
self.expand_factor = expand_factor;
self
}
pub fn with_contract_factor(mut self, contract_factor: F) -> Self {
assert!(
contract_factor > F::zero(),
"contract_factor must be > 0, got {:?}",
contract_factor
);
self.contract_factor = contract_factor;
self
}
}
impl<P, V, F> Solver<P, SolisWetsState<V, F>> for SolisWets<F>
where
F: Scalar,
P: CostFunction<Param = V, Output = F>,
V: Clone + SampleStandardNormal + ScaledAdd<F> + ScaleInPlace<F>,
{
type Error = P::Error;
fn init(
&mut self,
problem: &mut Problem<P>,
mut state: SolisWetsState<V, F>,
) -> Result<SolisWetsState<V, F>, Self::Error> {
if state.cost.is_none() {
state.cost = Some(problem.cost(&state.x)?);
}
Ok(state)
}
fn next_iter(
&mut self,
problem: &mut Problem<P>,
mut state: SolisWetsState<V, F>,
) -> Result<(SolisWetsState<V, F>, Option<TerminationReason>), Self::Error> {
let f_x = state
.cost
.expect("SolisWets::next_iter called before init evaluated the start point");
let mut d = V::sample_standard_normal(&state.x, &mut self.rng);
d.scale_in_place(state.rho);
let mut step = state.bias.clone();
step.scaled_add(F::one(), &d);
let mut candidate = state.x.clone();
candidate.scaled_add(F::one(), &step);
let f_forward = problem.cost(&candidate)?;
if f_forward < f_x {
state.x = candidate;
state.cost = Some(f_forward);
state.bias.scale_in_place(self.bias_memory);
state.bias.scaled_add(self.bias_gain, &step);
state.num_success += 1;
state.num_failure = 0;
} else {
let mut reversal = state.x.clone();
reversal.scaled_add(-F::one(), &step);
let f_reversal = problem.cost(&reversal)?;
if f_reversal < f_x {
state.x = reversal;
state.cost = Some(f_reversal);
state.bias.scaled_add(-self.bias_gain, &step);
state.num_success += 1;
state.num_failure = 0;
} else {
state.bias.scale_in_place(self.bias_decay);
state.num_failure += 1;
state.num_success = 0;
}
}
if state.num_success >= self.expand_threshold {
state.num_success = 0;
state.rho = state.rho * self.expand_factor;
} else if state.num_failure >= self.contract_threshold {
state.num_failure = 0;
state.rho = state.rho * self.contract_factor;
}
Ok((state, None))
}
}
impl<V, F> InitialState<V> for SolisWets<F>
where
F: Scalar,
V: Clone + VectorLen + ScaleInPlace<F>,
{
type State = SolisWetsState<V, F>;
fn seed(&self, x: &V) -> SolisWetsState<V, F> {
SolisWetsState::new(x.clone(), self.rho_init)
}
}
impl<V, F> WarmStart<V> for SolisWets<F>
where
F: Scalar,
V: Clone + VectorLen + ScaleInPlace<F>,
{
}
impl<V, F> MemeticInner<V, F> for SolisWets<F>
where
F: Scalar,
V: Clone + VectorLen + ScaleInPlace<F>,
{
fn seed_scaled(&self, x: &V, sigma: F) -> SolisWetsState<V, F> {
SolisWetsState::new(x.clone(), sigma)
}
}
impl<V, F> crate::core::inner::ResumableInner<V, F> for SolisWets<F>
where
F: Scalar,
V: Clone + VectorLen + ScaleInPlace<F>,
{
type State = SolisWetsState<V, F>;
fn seed_chain(&self, x: &V, fx: F, scale: F, seed: u64) -> (Self, Self::State) {
let sw = Self {
rng: ChaCha8Rng::seed_from_u64(seed),
..self.clone()
};
let mut state = SolisWetsState::new(x.clone(), scale);
state.cost = Some(fx);
(sw, state)
}
fn prepare_resume(&self, state: &mut Self::State) {
state.iter = 0;
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::Cell;
struct AlwaysImproving {
next: Cell<f64>,
}
impl AlwaysImproving {
fn new() -> Self {
Self {
next: Cell::new(1000.0),
}
}
}
impl CostFunction for AlwaysImproving {
type Param = Vec<f64>;
type Output = f64;
type Error = std::convert::Infallible;
fn cost(&self, _x: &Vec<f64>) -> Result<f64, Self::Error> {
let c = self.next.get();
self.next.set(c - 1.0);
Ok(c)
}
}
struct Constant;
impl CostFunction for Constant {
type Param = Vec<f64>;
type Output = f64;
type Error = std::convert::Infallible;
fn cost(&self, _x: &Vec<f64>) -> Result<f64, Self::Error> {
Ok(1.0)
}
}
struct Sphere;
impl CostFunction for Sphere {
type Param = Vec<f64>;
type Output = f64;
type Error = std::convert::Infallible;
fn cost(&self, x: &Vec<f64>) -> Result<f64, Self::Error> {
Ok(x.iter().map(|xi| xi * xi).sum())
}
}
fn approx_eq(a: &[f64], b: &[f64], tol: f64) {
assert_eq!(a.len(), b.len());
for (ai, bi) in a.iter().zip(b) {
assert!((ai - bi).abs() < tol, "{a:?} != {b:?}");
}
}
#[test]
fn forward_success_updates_bias_and_counters() {
let mut solver = SolisWets::<f64>::new(1);
let mut problem = Problem::new(AlwaysImproving::new());
let state = SolisWetsState::new(vec![0.0, 0.0, 0.0], 0.5);
let state = solver.init(&mut problem, state).unwrap();
let x_old = state.x.clone();
let bias_old = state.bias.clone();
let (state, reason) = solver.next_iter(&mut problem, state).unwrap();
assert!(reason.is_none());
let expected: Vec<f64> = (0..3)
.map(|i| 0.2 * bias_old[i] + 0.4 * (state.x[i] - x_old[i]))
.collect();
approx_eq(&state.bias, &expected, 1e-12);
assert_eq!(state.success_count(), 1);
assert_eq!(state.failure_count(), 0);
}
#[test]
fn reversal_success_updates_bias_and_moves_backward() {
for seed in 0..64 {
let mut solver = SolisWets::<f64>::new(seed);
let mut problem = Problem::new(Sphere);
let state = SolisWetsState::new(vec![0.3, -0.2], 0.4);
let state = solver.init(&mut problem, state).unwrap();
let x_old = state.x.clone();
let bias_old = state.bias.clone();
let f_old = state.cost.unwrap();
let (state, _) = solver.next_iter(&mut problem, state).unwrap();
let moved = state.x != x_old;
let improved = state.cost.unwrap() < f_old;
if moved && improved && state.success_count() == 1 {
let evals_this_iter = problem.counts().cost_evals;
if evals_this_iter == 3 {
let expected: Vec<f64> = (0..2)
.map(|i| bias_old[i] + 0.4 * (state.x[i] - x_old[i]))
.collect();
approx_eq(&state.bias, &expected, 1e-12);
return;
}
}
}
panic!("no seed in 0..64 produced a first-iteration reversal success");
}
#[test]
fn failure_decays_bias_and_counts() {
let mut solver = SolisWets::<f64>::new(3);
let mut problem = Problem::new(Constant);
let mut state = SolisWetsState::new(vec![1.0, 2.0], 0.5);
state.bias = vec![0.8, -0.4];
let state = solver.init(&mut problem, state).unwrap();
let (state, reason) = solver.next_iter(&mut problem, state).unwrap();
assert!(reason.is_none());
approx_eq(&state.bias, &[0.4, -0.2], 1e-12);
assert_eq!(state.failure_count(), 1);
assert_eq!(state.success_count(), 0);
assert_eq!(state.x, vec![1.0, 2.0]); assert_eq!(problem.counts().cost_evals, 3);
}
#[test]
fn expansion_fires_at_threshold_and_resets_counter() {
let mut solver = SolisWets::<f64>::new(5);
let mut problem = Problem::new(AlwaysImproving::new());
let state = SolisWetsState::new(vec![0.0; 4], 1.0);
let mut state = solver.init(&mut problem, state).unwrap();
for i in 1..=5 {
let (s, _) = solver.next_iter(&mut problem, state).unwrap();
state = s;
if i < 5 {
assert_eq!(state.success_count(), i);
assert!((state.rho() - 1.0).abs() < 1e-15, "rho moved early");
}
}
assert!((state.rho() - 2.0).abs() < 1e-15);
assert_eq!(state.success_count(), 0);
}
#[test]
fn contraction_fires_at_threshold_and_resets_counter() {
let mut solver = SolisWets::<f64>::new(7);
let mut problem = Problem::new(Constant);
let state = SolisWetsState::new(vec![1.0; 3], 1.0);
let mut state = solver.init(&mut problem, state).unwrap();
for i in 1..=3 {
let (s, _) = solver.next_iter(&mut problem, state).unwrap();
state = s;
if i < 3 {
assert_eq!(state.failure_count(), i);
assert!((state.rho() - 1.0).abs() < 1e-15, "rho moved early");
}
}
assert!((state.rho() - 0.5).abs() < 1e-15);
assert_eq!(state.failure_count(), 0);
}
#[test]
fn init_is_resume_idempotent() {
let mut solver = SolisWets::<f64>::new(11);
let mut problem = Problem::new(Sphere);
let state = SolisWetsState::new(vec![1.5, -0.5], 0.7);
let mut state = solver.init(&mut problem, state).unwrap();
for _ in 0..10 {
let (s, _) = solver.next_iter(&mut problem, state).unwrap();
state = s;
}
let x = state.x.clone();
let cost = state.cost;
let bias = state.bias.clone();
let rho = state.rho;
let (ns, nf) = (state.num_success, state.num_failure);
let evals_before = problem.counts().cost_evals;
let state = solver.init(&mut problem, state).unwrap();
assert_eq!(state.x, x);
assert_eq!(state.cost, cost);
assert_eq!(state.bias, bias);
assert_eq!(state.rho, rho);
assert_eq!((state.num_success, state.num_failure), (ns, nf));
assert_eq!(problem.counts().cost_evals, evals_before);
}
#[test]
fn seed_and_seed_scaled_set_rho() {
let solver = SolisWets::<f64>::new(13).with_rho_init(0.25);
let s: SolisWetsState<Vec<f64>> = solver.seed(&vec![1.0, 2.0]);
assert!((s.rho() - 0.25).abs() < 1e-15);
assert_eq!(s.x, vec![1.0, 2.0]);
let s: SolisWetsState<Vec<f64>> = solver.seed_scaled(&vec![1.0, 2.0], 0.05);
assert!((s.rho() - 0.05).abs() < 1e-15);
}
}