solverforge-solver 0.11.1

Solver engine for SolverForge
Documentation
use super::*;
use solverforge_config::{
    AcceptorConfig, HardRegressionPolicyConfig, LateAcceptanceConfig,
    SimulatedAnnealingCalibrationConfig, SimulatedAnnealingConfig, TabuSearchConfig,
};
use solverforge_core::score::{HardSoftScore, SoftScore};
use std::any::Any;

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

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

impl PlanningSolution for HardSoftTestSolution {
    type Score = HardSoftScore;

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

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

impl PlanningSolution for TestSolution {
    type Score = SoftScore;

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

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

#[test]
fn test_acceptor_builder_hill_climbing() {
    let config = AcceptorConfig::HillClimbing;
    let _acceptor: AnyAcceptor<TestSolution> = AcceptorBuilder::build(&config);
}

#[test]
fn test_acceptor_builder_tabu_search() {
    let config = AcceptorConfig::TabuSearch(TabuSearchConfig {
        entity_tabu_size: Some(10),
        ..Default::default()
    });
    let _acceptor: AnyAcceptor<TestSolution> = AcceptorBuilder::build(&config);
}

fn panic_message(payload: Box<dyn Any + Send>) -> String {
    match payload.downcast::<String>() {
        Ok(message) => *message,
        Err(payload) => match payload.downcast::<&'static str>() {
            Ok(message) => (*message).to_string(),
            Err(_) => "<non-string panic>".to_string(),
        },
    }
}

#[test]
fn test_acceptor_builder_tabu_search_normalizes_default_to_move_tabu() {
    let config = AcceptorConfig::TabuSearch(TabuSearchConfig::default());
    let acceptor: AnyAcceptor<TestSolution> = AcceptorBuilder::build(&config);
    let rendered = format!("{acceptor:?}");

    assert!(rendered.contains("move_tabu_size: Some(10)"));
    assert!(rendered.contains("entity_tabu_size: None"));
    assert!(rendered.contains("value_tabu_size: None"));
    assert!(rendered.contains("undo_move_tabu_size: None"));
    assert!(rendered.contains("aspiration_enabled: true"));
}

#[test]
fn test_acceptor_builder_tabu_search_rejects_zero_sizes() {
    for (field_name, config) in [
        (
            "entity_tabu_size",
            TabuSearchConfig {
                entity_tabu_size: Some(0),
                ..Default::default()
            },
        ),
        (
            "value_tabu_size",
            TabuSearchConfig {
                value_tabu_size: Some(0),
                ..Default::default()
            },
        ),
        (
            "move_tabu_size",
            TabuSearchConfig {
                move_tabu_size: Some(0),
                ..Default::default()
            },
        ),
        (
            "undo_move_tabu_size",
            TabuSearchConfig {
                undo_move_tabu_size: Some(0),
                ..Default::default()
            },
        ),
    ] {
        let result = std::panic::catch_unwind(|| {
            let config = AcceptorConfig::TabuSearch(config);
            let _: AnyAcceptor<TestSolution> = AcceptorBuilder::build(&config);
        });
        let message = panic_message(result.expect_err("zero tabu size must panic"));
        assert_eq!(
            message,
            format!("tabu_search field `{field_name}` must be greater than 0")
        );
    }
}

#[test]
fn test_acceptor_builder_tabu_search_helper_rejects_zero_size() {
    let result = std::panic::catch_unwind(|| {
        let _ = AcceptorBuilder::tabu_search::<TestSolution>(0);
    });
    let message = panic_message(result.expect_err("zero tabu size must panic"));
    assert_eq!(
        message,
        "tabu_search field `move_tabu_size` must be greater than 0"
    );
}

#[test]
fn test_acceptor_builder_simulated_annealing() {
    let config = AcceptorConfig::SimulatedAnnealing(SimulatedAnnealingConfig {
        level_temperatures: Some(vec![2.0]),
        decay_rate: None,
        hill_climbing_temperature: None,
        hard_regression_policy: None,
        calibration: None,
    });
    let _acceptor: AnyAcceptor<TestSolution> = AcceptorBuilder::build(&config);
}

#[test]
fn test_acceptor_builder_simulated_annealing_accepts_fractional_level_temperature() {
    let config = AcceptorConfig::SimulatedAnnealing(SimulatedAnnealingConfig {
        level_temperatures: Some(vec![2.5]),
        decay_rate: None,
        hill_climbing_temperature: None,
        hard_regression_policy: None,
        calibration: None,
    });
    let _acceptor: AnyAcceptor<TestSolution> = AcceptorBuilder::build(&config);
}

#[test]
fn test_acceptor_builder_simulated_annealing_accepts_hard_regression_policy() {
    let config = AcceptorConfig::SimulatedAnnealing(SimulatedAnnealingConfig {
        level_temperatures: Some(vec![2.5, 100.0]),
        hard_regression_policy: Some(HardRegressionPolicyConfig::NeverAcceptHardRegression),
        ..Default::default()
    });
    let acceptor: AnyAcceptor<HardSoftTestSolution> = AcceptorBuilder::build(&config);
    assert!(format!("{acceptor:?}").contains("NeverAcceptHardRegression"));
}

#[test]
fn test_acceptor_builder_simulated_annealing_rejects_wrong_temperature_level_count() {
    let result = std::panic::catch_unwind(|| {
        let config = AcceptorConfig::SimulatedAnnealing(SimulatedAnnealingConfig {
            level_temperatures: Some(vec![2.5]),
            ..Default::default()
        });
        let _: AnyAcceptor<HardSoftTestSolution> = AcceptorBuilder::build(&config);
    });
    let message = panic_message(result.expect_err("wrong level count must panic"));
    assert!(message.contains("level_temperatures length must match score level count"));
}

#[test]
fn test_acceptor_builder_simulated_annealing_rejects_invalid_decay_rate() {
    let result = std::panic::catch_unwind(|| {
        let config = AcceptorConfig::SimulatedAnnealing(SimulatedAnnealingConfig {
            decay_rate: Some(0.0),
            ..Default::default()
        });
        let _: AnyAcceptor<TestSolution> = AcceptorBuilder::build(&config);
    });
    let message = panic_message(result.expect_err("invalid decay rate must panic"));
    assert!(message.contains("decay_rate must be finite and in (0, 1]"));
}

#[test]
fn test_acceptor_builder_simulated_annealing_rejects_invalid_calibration_probability() {
    let result = std::panic::catch_unwind(|| {
        let config = AcceptorConfig::SimulatedAnnealing(SimulatedAnnealingConfig {
            calibration: Some(SimulatedAnnealingCalibrationConfig {
                target_acceptance_probability: Some(1.0),
                ..Default::default()
            }),
            ..Default::default()
        });
        let _: AnyAcceptor<TestSolution> = AcceptorBuilder::build(&config);
    });
    let message = panic_message(result.expect_err("invalid calibration probability must panic"));
    assert!(message.contains("target_acceptance_probability must be in (0, 1)"));
}

#[test]
fn test_acceptor_builder_late_acceptance() {
    let config = AcceptorConfig::LateAcceptance(LateAcceptanceConfig {
        late_acceptance_size: Some(500),
    });
    let _acceptor: AnyAcceptor<TestSolution> = AcceptorBuilder::build(&config);
}