solverforge-solver 0.13.0

Solver engine for SolverForge
Documentation
use std::any::TypeId;

use solverforge_config::{CustomPhaseConfig, PartitionedSearchConfig, PhaseConfig, SolverConfig};
use solverforge_core::domain::{PlanningSolution, SolutionDescriptor};
use solverforge_core::score::SoftScore;
use solverforge_scoring::ScoreDirector;

use crate::run::ChannelProgressCallback;
use crate::scope::{ProgressCallback, SolverScope};

use super::{CustomSearchPhase, Search, SearchContext};
use crate::builder::RuntimeModel;

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

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;
    }
}

#[derive(Debug)]
struct MarkerPhase(&'static str);

impl CustomSearchPhase<TestSolution> for MarkerPhase {
    fn solve<D, ProgressCb>(
        &mut self,
        _solver_scope: &mut SolverScope<'_, TestSolution, D, ProgressCb>,
    ) where
        D: solverforge_scoring::Director<TestSolution>,
        ProgressCb: ProgressCallback<TestSolution>,
    {
    }

    fn phase_type_name(&self) -> &'static str {
        self.0
    }
}

fn search_context() -> SearchContext<TestSolution> {
    SearchContext::new(
        SolutionDescriptor::new("TestSolution", TypeId::of::<TestSolution>()),
        RuntimeModel::new(Vec::new()),
        Some(7),
    )
}

fn custom_config(names: &[&str]) -> SolverConfig {
    SolverConfig {
        phases: names
            .iter()
            .map(|name| {
                PhaseConfig::Custom(CustomPhaseConfig {
                    name: (*name).to_string(),
                })
            })
            .collect(),
        ..SolverConfig::default()
    }
}

fn partitioned_config(name: &str) -> SolverConfig {
    SolverConfig {
        phases: vec![PhaseConfig::PartitionedSearch(PartitionedSearchConfig {
            partitioner: Some(name.to_string()),
            ..Default::default()
        })],
        ..SolverConfig::default()
    }
}

#[test]
fn custom_search_builds_registered_names_in_configured_order() {
    let search = search_context()
        .defaults()
        .phase("weekend_repair", |_| MarkerPhase("weekend_repair"))
        .phase("nurse_search", |_| MarkerPhase("nurse_search"));
    let phases = Search::<TestSolution>::build::<
        ScoreDirector<TestSolution, ()>,
        ChannelProgressCallback<TestSolution>,
    >(search, &custom_config(&["nurse_search", "weekend_repair"]));
    let debug = format!("{phases:?}");

    let nurse_pos = debug
        .find("nurse_search")
        .expect("configured nurse_search phase should be built");
    let weekend_pos = debug
        .find("weekend_repair")
        .expect("configured weekend_repair phase should be built");
    assert!(nurse_pos < weekend_pos);
}

#[test]
#[should_panic(expected = "custom phase `repair` was registered more than once")]
fn custom_search_rejects_duplicate_registered_names() {
    let _ = search_context()
        .defaults()
        .phase("repair", |_| MarkerPhase("first"))
        .phase("repair", |_| MarkerPhase("second"));
}

#[test]
#[should_panic(
    expected = "custom phase `missing` was not registered by the solution search function"
)]
fn custom_search_rejects_unregistered_configured_name() {
    let search = search_context()
        .defaults()
        .phase("registered", |_| MarkerPhase("registered"));
    let _ = Search::<TestSolution>::build::<
        ScoreDirector<TestSolution, ()>,
        ChannelProgressCallback<TestSolution>,
    >(search, &custom_config(&["missing"]));
}

#[test]
fn partitioned_search_builds_registered_partitioner_name() {
    let search = search_context()
        .defaults()
        .partitioned_phase("by_task", |_context, config| {
            assert_eq!(config.partitioner.as_deref(), Some("by_task"));
            MarkerPhase("by_task")
        });
    let phases = Search::<TestSolution>::build::<
        ScoreDirector<TestSolution, ()>,
        ChannelProgressCallback<TestSolution>,
    >(search, &partitioned_config("by_task"));
    let debug = format!("{phases:?}");

    assert!(debug.contains("by_task"));
}

#[test]
#[should_panic(
    expected = "partitioned_search partitioner `missing` was not registered by the solution search function"
)]
fn partitioned_search_rejects_unregistered_partitioner_name() {
    let search = search_context()
        .defaults()
        .partitioned_phase("registered", |_context, _config| MarkerPhase("registered"));
    let _ = Search::<TestSolution>::build::<
        ScoreDirector<TestSolution, ()>,
        ChannelProgressCallback<TestSolution>,
    >(search, &partitioned_config("missing"));
}

#[test]
#[should_panic(expected = "custom phase `missing` requires a typed solution search function")]
fn stock_runtime_rejects_custom_phase_without_search_registration() {
    let context = search_context();
    let _ = crate::runtime::build_phases(
        &custom_config(&["missing"]),
        context.descriptor(),
        context.model(),
    );
}

#[test]
#[should_panic(
    expected = "partitioned_search partitioner `missing` requires typed partitioner registration"
)]
fn stock_runtime_rejects_partitioned_search_without_registration() {
    let context = search_context();
    let _ = crate::runtime::build_phases(
        &partitioned_config("missing"),
        context.descriptor(),
        context.model(),
    );
}