solverforge-solver 0.11.1

Solver engine for SolverForge
Documentation
use super::*;
use crate::phase::localsearch::HillClimbingAcceptor;
use solverforge_core::domain::PlanningSolution;
use solverforge_core::score::{HardSoftScore, SoftScore};

#[derive(Clone, Debug)]
struct SimpleSol {
    score: Option<SoftScore>,
}

impl PlanningSolution for SimpleSol {
    type Score = SoftScore;

    fn score(&self) -> Option<Self::Score> {
        self.score
    }

    fn set_score(&mut self, score: Option<Self::Score>) {
        self.score = score;
    }
}

#[derive(Clone, Debug)]
struct HardSoftSol {
    score: Option<HardSoftScore>,
}

impl PlanningSolution for HardSoftSol {
    type Score = HardSoftScore;

    fn score(&self) -> Option<Self::Score> {
        self.score
    }

    fn set_score(&mut self, score: Option<Self::Score>) {
        self.score = score;
    }
}

#[test]
fn accepts_improving_and_equal_moves() {
    let mut acceptor = SimulatedAnnealingAcceptor::with_seed(1000.0, 0.99, 42);
    let last = SoftScore::of(-10);
    assert!(Acceptor::<SimpleSol>::is_accepted(
        &mut acceptor,
        &last,
        &SoftScore::of(-5),
        None,
    ));
    assert!(Acceptor::<SimpleSol>::is_accepted(
        &mut acceptor,
        &last,
        &last,
        None,
    ));
}

#[test]
fn single_level_high_temperature_accepts_most_worsening_moves() {
    let mut acceptor = SimulatedAnnealingAcceptor::with_seed(1_000_000.0, 0.99, 42);
    Acceptor::<SimpleSol>::phase_started(&mut acceptor, &SoftScore::of(0));
    let last = SoftScore::of(-10);
    let worse = SoftScore::of(-11);
    let mut accepted = 0;
    for _ in 0..100 {
        if Acceptor::<SimpleSol>::is_accepted(&mut acceptor, &last, &worse, None) {
            accepted += 1;
        }
    }
    assert!(accepted > 90);
}

#[test]
fn single_level_low_temperature_rejects_most_worsening_moves() {
    let mut acceptor = SimulatedAnnealingAcceptor::with_seed(0.001, 0.99, 42);
    Acceptor::<SimpleSol>::phase_started(&mut acceptor, &SoftScore::of(0));
    let last = SoftScore::of(-10);
    let worse = SoftScore::of(-20);
    let mut accepted = 0;
    for _ in 0..100 {
        if Acceptor::<SimpleSol>::is_accepted(&mut acceptor, &last, &worse, None) {
            accepted += 1;
        }
    }
    assert!(accepted < 5);
}

#[test]
fn temperature_decays_each_step_until_hill_climbing_threshold() {
    let mut acceptor = SimulatedAnnealingAcceptor::with_level_temperatures_and_seed(
        vec![100.0],
        0.5,
        20.0,
        HardRegressionPolicy::TemperatureControlled,
        42,
    );
    Acceptor::<SimpleSol>::phase_started(&mut acceptor, &SoftScore::of(0));
    assert!((acceptor.current_temperature_for_level(0) - 100.0).abs() < f64::EPSILON);
    Acceptor::<SimpleSol>::step_ended(&mut acceptor, &SoftScore::of(0), None);
    assert!((acceptor.current_temperature_for_level(0) - 50.0).abs() < f64::EPSILON);
    Acceptor::<SimpleSol>::step_ended(&mut acceptor, &SoftScore::of(0), None);
    assert!((acceptor.current_temperature_for_level(0) - 25.0).abs() < f64::EPSILON);
    Acceptor::<SimpleSol>::step_ended(&mut acceptor, &SoftScore::of(0), None);
    assert!((acceptor.current_temperature_for_level(0) - 20.0).abs() < f64::EPSILON);
}

#[test]
fn huge_soft_improvement_does_not_mask_hard_regression() {
    let mut acceptor = SimulatedAnnealingAcceptor::with_level_temperatures_and_seed(
        vec![1.0e-9, 1.0e12],
        1.0,
        1.0e-9,
        HardRegressionPolicy::TemperatureControlled,
        42,
    );
    Acceptor::<HardSoftSol>::phase_started(&mut acceptor, &HardSoftScore::of(-10, -1_000_000));

    let last = HardSoftScore::of(-10, -1_000_000);
    let worse_hard_better_soft = HardSoftScore::of(-11, 0);
    for _ in 0..100 {
        assert!(!Acceptor::<HardSoftSol>::is_accepted(
            &mut acceptor,
            &last,
            &worse_hard_better_soft,
            None,
        ));
    }
}

#[test]
fn hard_improvement_with_soft_regression_is_accepted_as_improving() {
    let mut acceptor = SimulatedAnnealingAcceptor::with_seed(0.0, 1.0, 42);
    let last = HardSoftScore::of(-2, 0);
    let better_hard_worse_soft = HardSoftScore::of(-1, -1_000_000);
    assert!(Acceptor::<HardSoftSol>::is_accepted(
        &mut acceptor,
        &last,
        &better_hard_worse_soft,
        None,
    ));
}

#[test]
fn unchanged_hard_soft_regression_uses_soft_temperature() {
    let mut acceptor = SimulatedAnnealingAcceptor::with_level_temperatures_and_seed(
        vec![0.0, 1_000_000.0],
        1.0,
        1.0e-9,
        HardRegressionPolicy::TemperatureControlled,
        42,
    );
    Acceptor::<HardSoftSol>::phase_started(&mut acceptor, &HardSoftScore::of(0, 0));
    let last = HardSoftScore::of(0, -10);
    let worse_soft = HardSoftScore::of(0, -11);
    let mut accepted = 0;
    for _ in 0..100 {
        if Acceptor::<HardSoftSol>::is_accepted(&mut acceptor, &last, &worse_soft, None) {
            accepted += 1;
        }
    }
    assert!(accepted > 90);
}

#[test]
fn never_accept_hard_regression_policy_rejects_hard_regressions_at_high_temperature() {
    let mut acceptor = SimulatedAnnealingAcceptor::with_level_temperatures_and_seed(
        vec![1.0e12, 1.0e12],
        1.0,
        1.0e-9,
        HardRegressionPolicy::NeverAcceptHardRegression,
        42,
    );
    Acceptor::<HardSoftSol>::phase_started(&mut acceptor, &HardSoftScore::of(0, 0));
    assert!(!Acceptor::<HardSoftSol>::is_accepted(
        &mut acceptor,
        &HardSoftScore::of(-10, 0),
        &HardSoftScore::of(-11, 10_000),
        None,
    ));
}

#[test]
fn cooled_simulated_annealing_matches_hill_climbing() {
    let mut annealing = SimulatedAnnealingAcceptor::with_level_temperatures_and_seed(
        vec![100.0, 100.0],
        0.1,
        1.1,
        HardRegressionPolicy::TemperatureControlled,
        42,
    );
    let mut hill = HillClimbingAcceptor::new();
    let initial = HardSoftScore::of(0, 0);
    Acceptor::<HardSoftSol>::phase_started(&mut annealing, &initial);
    Acceptor::<HardSoftSol>::phase_started(&mut hill, &initial);
    for _ in 0..3 {
        Acceptor::<HardSoftSol>::step_ended(&mut annealing, &initial, None);
    }

    for (last, candidate) in [
        (HardSoftScore::of(0, 0), HardSoftScore::of(0, -1)),
        (HardSoftScore::of(-1, 0), HardSoftScore::of(-2, 10_000)),
        (HardSoftScore::of(-1, 0), HardSoftScore::of(0, -10_000)),
    ] {
        assert_eq!(
            Acceptor::<HardSoftSol>::is_accepted(&mut annealing, &last, &candidate, None),
            Acceptor::<HardSoftSol>::is_accepted(&mut hill, &last, &candidate, None),
        );
    }
}

#[test]
fn sampled_calibration_derives_temperatures_per_level() {
    let calibration = SimulatedAnnealingCalibration {
        sample_size: 2,
        target_acceptance_probability: 0.5,
        fallback_temperature: 1.0,
    };
    let mut acceptor = SimulatedAnnealingAcceptor::with_calibration_and_seed(
        1.0,
        1.0e-9,
        HardRegressionPolicy::TemperatureControlled,
        calibration,
        42,
    );
    Acceptor::<HardSoftSol>::phase_started(&mut acceptor, &HardSoftScore::of(0, 0));

    assert!(!Acceptor::<HardSoftSol>::is_accepted(
        &mut acceptor,
        &HardSoftScore::of(0, 0),
        &HardSoftScore::of(-4, 0),
        None,
    ));
    let _ = Acceptor::<HardSoftSol>::is_accepted(
        &mut acceptor,
        &HardSoftScore::of(0, 0),
        &HardSoftScore::of(0, -10),
        None,
    );

    assert!(acceptor.current_temperature_for_level(0) > 5.0);
    assert!(acceptor.current_temperature_for_level(1) > 14.0);
}

#[test]
fn seeded_auto_calibration_starts_with_same_temperatures() {
    let initial = HardSoftScore::of(-576, -1000);
    let mut first = SimulatedAnnealingAcceptor::auto_calibrate_with_seed(0.999, 42);
    let mut second = SimulatedAnnealingAcceptor::auto_calibrate_with_seed(0.999, 42);

    Acceptor::<HardSoftSol>::phase_started(&mut first, &initial);
    Acceptor::<HardSoftSol>::phase_started(&mut second, &initial);

    assert_eq!(
        first.current_temperature_for_level(0),
        second.current_temperature_for_level(0)
    );
    assert_eq!(
        first.current_temperature_for_level(1),
        second.current_temperature_for_level(1)
    );
}