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::{PointState, State};
use crate::core::termination::Termination;
use crate::solver::cma_inject::MemeticInner;
#[derive(Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SolisWets<V, F: Scalar = f64> {
step_tolerance: Option<F>,
rho_init: F,
bias_gain: F,
bias_memory: F,
bias_decay: F,
expand_threshold: u32,
contract_threshold: u32,
expand_factor: F,
contract_factor: F,
seed: u64,
rng: ChaCha8Rng,
bias: Option<V>,
rho: F,
num_success: u32,
num_failure: u32,
accepted_iterate: bool,
}
impl<V, F: Scalar> SolisWets<V, F> {
pub fn step_size(&self) -> F {
self.rho
}
pub fn bias(&self) -> Option<&V> {
self.bias.as_ref()
}
pub fn success_count(&self) -> u32 {
self.num_success
}
pub fn failure_count(&self) -> u32 {
self.num_failure
}
pub fn with_absolute_step_size_tolerance(
mut self,
value: impl Into<Option<F>>,
) -> Self {
self.step_tolerance =
crate::core::convergence::optional_tolerance(value);
self
}
pub fn new(seed: u64) -> Self {
Self {
rho_init: F::one(),
step_tolerance: None,
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(),
seed,
rng: ChaCha8Rng::seed_from_u64(seed),
bias: None,
rho: F::one(),
num_success: 0,
num_failure: 0,
accepted_iterate: true,
}
}
pub fn with_initial_step_size(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, PointState<V, F>> for SolisWets<V, F>
where
F: Scalar,
P: CostFunction<Param = V, Output = F>,
V: Clone
+ VectorLen
+ SampleStandardNormal
+ ScaledAdd<F>
+ ScaleInPlace<F>,
{
type Error = P::Error;
fn init(
&mut self,
problem: &mut Problem<P>,
mut state: PointState<V, F>,
) -> Result<PointState<V, F>, Self::Error> {
state.reset();
self.reset_model(state.param(), self.rho_init, self.seed);
let cost = problem.cost(state.param())?;
state.replace(state.param().clone(), cost);
Ok(state)
}
fn next_iter(
&mut self,
problem: &mut Problem<P>,
mut state: PointState<V, F>,
) -> Result<crate::SolverStep<PointState<V, F>>, Self::Error> {
let f_x = state.cost();
self.accepted_iterate = false;
let bias = self.bias.as_mut().expect("SolisWets must be initialized");
let mut d = V::sample_standard_normal(state.param(), &mut self.rng);
d.scale_in_place(self.rho);
let mut step = bias.clone();
step.scaled_add(F::one(), &d);
let mut candidate = state.param().clone();
candidate.scaled_add(F::one(), &step);
let f_forward = problem.cost(&candidate)?;
if f_forward < f_x {
state.replace(candidate, f_forward);
self.accepted_iterate = true;
bias.scale_in_place(self.bias_memory);
bias.scaled_add(self.bias_gain, &step);
self.num_success += 1;
self.num_failure = 0;
} else {
let mut reversal = state.param().clone();
reversal.scaled_add(-F::one(), &step);
let f_reversal = problem.cost(&reversal)?;
if f_reversal < f_x {
state.replace(reversal, f_reversal);
self.accepted_iterate = true;
bias.scaled_add(-self.bias_gain, &step);
self.num_success += 1;
self.num_failure = 0;
} else {
bias.scale_in_place(self.bias_decay);
self.num_failure += 1;
self.num_success = 0;
}
}
if self.num_success >= self.expand_threshold {
self.num_success = 0;
self.rho = self.rho * self.expand_factor;
} else if self.num_failure >= self.contract_threshold {
self.num_failure = 0;
self.rho = self.rho * self.contract_factor;
}
Ok(crate::SolverStep::from((state, None)))
}
fn should_check_iterate_change(&self) -> bool {
self.accepted_iterate
}
fn terminate(&self, _state: &PointState<V, F>) -> Option<Termination<F>> {
let tolerance = self.step_tolerance?;
let metric = self.rho;
(metric.is_finite() && metric <= tolerance).then(|| {
Termination::upper_bound(
crate::ConvergenceTest::Radius,
metric,
tolerance,
tolerance,
None,
)
})
}
}
impl<V, F: Scalar> SolisWets<V, F>
where
V: Clone + VectorLen + ScaleInPlace<F>,
{
fn reset_model(&mut self, x: &V, scale: F, seed: u64) {
assert!(x.vec_len() >= 1, "SolisWets requires a non-empty x");
assert!(scale > F::zero(), "SolisWets requires a positive step size");
let mut bias = x.clone();
bias.scale_in_place(F::zero());
self.bias = Some(bias);
self.rho = scale;
self.num_success = 0;
self.num_failure = 0;
self.accepted_iterate = true;
self.rng = ChaCha8Rng::seed_from_u64(seed);
}
}
impl<V: Clone, F: Scalar> InitialState<V> for SolisWets<V, F> {
type State = PointState<V, F>;
fn seed(&self, x: &V) -> Self::State {
PointState::new(x.clone())
}
}
impl<V: Clone, F: Scalar> WarmStart<V> for SolisWets<V, F> {}
impl<V: Clone, F: Scalar> MemeticInner<V, F> for SolisWets<V, F> {
fn seed_scaled(&mut self, x: &V, sigma: F) -> Self::State {
assert!(sigma > F::zero(), "SolisWets requires a positive step size");
self.rho_init = sigma;
self.seed(x)
}
}
impl<V, F> crate::core::inner::ResumableInner<V, F> for SolisWets<V, F>
where
F: Scalar,
V: Clone + VectorLen + ScaleInPlace<F>,
{
type State = PointState<V, F>;
fn seed_chain(
&self,
x: &V,
fx: F,
scale: F,
seed: u64,
) -> (Self, Self::State) {
let mut sw = self.clone();
sw.seed = seed;
sw.rho_init = scale;
sw.reset_model(x, scale, seed);
let mut state = PointState::new(x.clone());
state.replace(x.clone(), fx);
(sw, state)
}
fn seeded_chain_is_initialized(&self) -> bool {
true
}
fn prepare_resume(&self, state: &mut Self::State) {
state.reset_progress();
}
fn configure_segment(
&mut self,
_state: &Self::State,
_control: &mut crate::RunControl<Self::State>,
) {
self.accepted_iterate = true;
}
}
#[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 crate::Constraints for AlwaysImproving {
type Set = crate::NoConstraints;
fn constraints(
&self,
) -> <Self::Set as crate::ConstraintDescription>::Borrowed<'_> {
crate::ConstraintSet::new()
}
}
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 crate::Constraints for Constant {
type Set = crate::NoConstraints;
fn constraints(
&self,
) -> <Self::Set as crate::ConstraintDescription>::Borrowed<'_> {
crate::ConstraintSet::new()
}
}
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 crate::Constraints for Sphere {
type Set = crate::NoConstraints;
fn constraints(
&self,
) -> <Self::Set as crate::ConstraintDescription>::Borrowed<'_> {
crate::ConstraintSet::new()
}
}
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).with_initial_step_size(0.5);
let mut problem = Problem::new(AlwaysImproving::new());
let state = PointState::new(vec![0.0, 0.0, 0.0]);
let state = solver.init(&mut problem, state).unwrap();
let x_old = state.param().clone();
let bias_old = solver.bias().unwrap().clone();
let (state, _, reason) =
solver.next_iter(&mut problem, state).unwrap().into_parts();
assert!(reason.is_none());
let expected: Vec<f64> = (0..3)
.map(|i| 0.2 * bias_old[i] + 0.4 * (state.param()[i] - x_old[i]))
.collect();
approx_eq(solver.bias().unwrap(), &expected, 1e-12);
assert_eq!(solver.success_count(), 1);
assert_eq!(solver.failure_count(), 0);
}
#[test]
fn reversal_success_updates_bias_and_moves_backward() {
for seed in 0..64 {
let mut solver =
SolisWets::<_, f64>::new(seed).with_initial_step_size(0.4);
let mut problem = Problem::new(Sphere);
let state = PointState::new(vec![0.3, -0.2]);
let state = solver.init(&mut problem, state).unwrap();
let x_old = state.param().clone();
let bias_old = solver.bias().unwrap().clone();
let f_old = state.cost();
let (state, _, _) =
solver.next_iter(&mut problem, state).unwrap().into_parts();
let moved = state.param() != &x_old;
let improved = state.cost() < f_old;
if moved && improved && solver.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.param()[i] - x_old[i])
})
.collect();
approx_eq(solver.bias().unwrap(), &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).with_initial_step_size(0.5);
let mut problem = Problem::new(Constant);
let state = PointState::new(vec![1.0, 2.0]);
let state = solver.init(&mut problem, state).unwrap();
solver.bias = Some(vec![0.8, -0.4]);
let (state, _, reason) =
solver.next_iter(&mut problem, state).unwrap().into_parts();
assert!(reason.is_none());
approx_eq(solver.bias().unwrap(), &[0.4, -0.2], 1e-12);
assert_eq!(solver.failure_count(), 1);
assert_eq!(solver.success_count(), 0);
assert_eq!(state.param(), &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 = PointState::new(vec![0.0; 4]);
let mut state = solver.init(&mut problem, state).unwrap();
for i in 1..=5 {
let (s, _, _) =
solver.next_iter(&mut problem, state).unwrap().into_parts();
state = s;
if i < 5 {
assert_eq!(solver.success_count(), i);
assert!(
(solver.step_size() - 1.0).abs() < 1e-15,
"rho moved early"
);
}
}
assert!((solver.step_size() - 2.0).abs() < 1e-15);
assert_eq!(solver.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 = PointState::new(vec![1.0; 3]);
let mut state = solver.init(&mut problem, state).unwrap();
for i in 1..=3 {
let (s, _, _) =
solver.next_iter(&mut problem, state).unwrap().into_parts();
state = s;
if i < 3 {
assert_eq!(solver.failure_count(), i);
assert!(
(solver.step_size() - 1.0).abs() < 1e-15,
"rho moved early"
);
}
}
assert!((solver.step_size() - 0.5).abs() < 1e-15);
assert_eq!(solver.failure_count(), 0);
}
#[test]
fn fresh_init_reevaluates_progress_and_resets_adaptation() {
let mut solver =
SolisWets::<_, f64>::new(11).with_initial_step_size(0.7);
let mut problem = Problem::new(Sphere);
let state = PointState::new(vec![1.5, -0.5]);
let mut state = solver.init(&mut problem, state).unwrap();
for _ in 0..10 {
state = solver.next_iter(&mut problem, state).unwrap().state;
}
state.replace(state.param().clone(), -1000.0);
let state = solver.init(&mut problem, state).unwrap();
assert_eq!(state.cost(), problem.inner().cost(state.param()).unwrap());
assert_eq!(solver.bias().unwrap(), &vec![0.0; 2]);
assert_eq!(solver.step_size(), 0.7);
assert_eq!(solver.success_count(), 0);
assert_eq!(solver.failure_count(), 0);
}
#[test]
fn seed_and_seed_scaled_set_rho() {
let mut solver =
SolisWets::<_, f64>::new(13).with_initial_step_size(0.25);
let mut p = Problem::new(Sphere);
let state = solver.seed(&vec![1.0, 2.0]);
let state = solver.init(&mut p, state).unwrap();
assert_eq!(solver.step_size(), 0.25);
assert_eq!(state.param(), &vec![1.0, 2.0]);
let state = solver.seed_scaled(&vec![1.0, 2.0], 0.05);
solver.init(&mut p, state).unwrap();
assert_eq!(solver.step_size(), 0.05);
}
}