solverforge-solver 0.8.13

Solver engine for SolverForge
Documentation
// Tests for phase factories.

use super::*;
use crate::heuristic::r#move::ChangeMove;
use crate::heuristic::selector::{FromSolutionEntitySelector, StaticValueSelector};
use crate::phase::construction::{EntityPlacer, ForagerType, QueuedEntityPlacer};
use crate::scope::SolverScope;
use solverforge_core::domain::{EntityDescriptor, SolutionDescriptor, EntityCollectionExtractor};
use solverforge_core::score::SoftScore;
use solverforge_scoring::{Director, ScoreDirector};
use std::any::TypeId;

// ==================== Test Domain ====================

#[derive(Clone, Debug)]
struct Task {
    id: usize,
    priority: Option<i64>,
}

#[derive(Clone, Debug)]
struct TestSolution {
    tasks: Vec<Task>,
    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;
    }
}

fn get_tasks(s: &TestSolution) -> &Vec<Task> {
    &s.tasks
}

fn get_tasks_mut(s: &mut TestSolution) -> &mut Vec<Task> {
    &mut s.tasks
}

// Zero-erasure typed getter/setter for solution-level access
fn get_task_priority(s: &TestSolution, idx: usize) -> Option<i64> {
    s.tasks.get(idx).and_then(|t| t.priority)
}

fn set_task_priority(s: &mut TestSolution, idx: usize, v: Option<i64>) {
    if let Some(task) = s.tasks.get_mut(idx) {
        task.priority = v;
    }
}

// Score calculator: sum of priorities (higher is better), penalty for unassigned.
fn calculate_score(solution: &TestSolution) -> SoftScore {
    let mut score = 0i64;
    for task in &solution.tasks {
        match task.priority {
            Some(p) => score += p,
            None => score -= 100, // Penalty for unassigned
        }
    }
    SoftScore::of(score)
}

fn create_test_director(
    tasks: Vec<Task>,
) -> ScoreDirector<TestSolution, ()> {
    let solution = TestSolution { tasks, score: None };

    let extractor = Box::new(EntityCollectionExtractor::new(
        "Task",
        "tasks",
        get_tasks,
        get_tasks_mut,
    ));
    let entity_desc =
        EntityDescriptor::new("Task", TypeId::of::<Task>(), "tasks").with_extractor(extractor);

    let descriptor = SolutionDescriptor::new("TestSolution", TypeId::of::<TestSolution>())
        .with_entity(entity_desc);

    ScoreDirector::with_calculator(solution, descriptor, calculate_score)
}

fn create_unassigned_solver_scope(
    count: usize,
) -> SolverScope<TestSolution, impl Director<TestSolution>> {
    let tasks: Vec<Task> = (0..count).map(|id| Task { id, priority: None }).collect();
    let director = create_test_director(tasks);
    SolverScope::new(director)
}

type TestMove = ChangeMove<TestSolution, i64>;

// ==================== Helper Factories ====================

type TestPlacer = QueuedEntityPlacer<
    TestSolution,
    i64,
    FromSolutionEntitySelector,
    StaticValueSelector<TestSolution, i64>,
>;

fn create_placer_factory() -> impl Fn() -> TestPlacer + Send + Sync {
    || {
        QueuedEntityPlacer::new(
            FromSolutionEntitySelector::new(0),
            StaticValueSelector::new(vec![1i64, 2, 3, 4, 5]),
            get_task_priority,
            set_task_priority,
            0,
            "priority",
        )
    }
}

#[derive(Clone, Debug)]
struct ListVehicle {
    visits: Vec<usize>,
}

#[derive(Clone, Debug)]
struct ListSolution {
    vehicles: Vec<ListVehicle>,
    visit_pool: Vec<usize>,
    score: Option<SoftScore>,
}

impl PlanningSolution for ListSolution {
    type Score = SoftScore;

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

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

fn list_vehicles(solution: &ListSolution) -> &Vec<ListVehicle> {
    &solution.vehicles
}

fn list_vehicles_mut(solution: &mut ListSolution) -> &mut Vec<ListVehicle> {
    &mut solution.vehicles
}

fn list_element_count(solution: &ListSolution) -> usize {
    solution.visit_pool.len()
}

fn list_assigned_elements(solution: &ListSolution) -> Vec<usize> {
    solution
        .vehicles
        .iter()
        .flat_map(|vehicle| vehicle.visits.iter().copied())
        .collect()
}

fn list_entity_count(solution: &ListSolution) -> usize {
    solution.vehicles.len()
}

fn list_assign_element(solution: &mut ListSolution, entity_idx: usize, element: usize) {
    solution.vehicles[entity_idx].visits.push(element);
}

fn list_index_to_element(solution: &ListSolution, idx: usize) -> usize {
    solution.visit_pool[idx]
}

fn list_solution_descriptor() -> SolutionDescriptor {
    let extractor = Box::new(EntityCollectionExtractor::new(
        "Vehicle",
        "vehicles",
        list_vehicles,
        list_vehicles_mut,
    ));
    let entity_desc = EntityDescriptor::new("Vehicle", TypeId::of::<ListVehicle>(), "vehicles")
        .with_extractor(extractor);

    SolutionDescriptor::new("ListSolution", TypeId::of::<ListSolution>()).with_entity(entity_desc)
}

// ==================== Standard Variant Tests ====================

#[test]
fn test_local_search_type_variants() {
    let _hill = LocalSearchType::HillClimbing;
    let _tabu = LocalSearchType::TabuSearch { tabu_size: 10 };
    let _sa = LocalSearchType::SimulatedAnnealing {
        starting_temp: 1.0,
        decay_rate: 0.99,
    };
    let _late = LocalSearchType::LateAcceptance { size: 100 };
}

#[test]
fn test_forager_type_variants() {
    let _first = ForagerType::FirstFit;
    let _best = ForagerType::BestFit;
}

// ==================== ConstructionPhaseFactory Tests ====================

#[test]
fn test_construction_phase_factory_first_fit_creates_phase() {
    let factory =
        ConstructionPhaseFactory::<TestSolution, TestMove, _>::first_fit(create_placer_factory());

    let phase = factory.create_phase();
    assert_eq!(phase.phase_type_name(), "ConstructionHeuristic");
}

#[test]
fn test_construction_phase_factory_best_fit_creates_phase() {
    let factory =
        ConstructionPhaseFactory::<TestSolution, TestMove, _>::best_fit(create_placer_factory());

    let phase = factory.create_phase();
    assert_eq!(phase.phase_type_name(), "ConstructionHeuristic");
}

#[test]
fn test_construction_phase_factory_new_with_forager_type() {
    // Test FirstFit via new()
    let factory_first = ConstructionPhaseFactory::<TestSolution, TestMove, _>::new(
        ForagerType::FirstFit,
        create_placer_factory(),
    );
    let phase_first = factory_first.create_phase();
    assert_eq!(phase_first.phase_type_name(), "ConstructionHeuristic");

    // Test BestFit via new()
    let factory_best = ConstructionPhaseFactory::<TestSolution, TestMove, _>::new(
        ForagerType::BestFit,
        create_placer_factory(),
    );
    let phase_best = factory_best.create_phase();
    assert_eq!(phase_best.phase_type_name(), "ConstructionHeuristic");
}

#[test]
fn test_construction_phase_factory_first_fit_solves() {
    let factory =
        ConstructionPhaseFactory::<TestSolution, TestMove, _>::first_fit(create_placer_factory());

    let mut solver_scope = create_unassigned_solver_scope(3);

    // Verify tasks start unassigned
    let initial_solution = solver_scope.working_solution();
    for task in &initial_solution.tasks {
        assert!(task.priority.is_none());
    }

    // Create and run phase
    let mut phase = factory.create_phase();
    phase.solve(&mut solver_scope);

    // Verify all tasks are now assigned
    let final_solution = solver_scope.working_solution();
    for task in &final_solution.tasks {
        assert!(
            task.priority.is_some(),
            "Task {} should have priority assigned",
            task.id
        );
    }
}

#[test]
fn test_construction_phase_factory_best_fit_solves() {
    let factory =
        ConstructionPhaseFactory::<TestSolution, TestMove, _>::best_fit(create_placer_factory());

    let mut solver_scope = create_unassigned_solver_scope(3);

    // Verify tasks start unassigned
    let initial_solution = solver_scope.working_solution();
    for task in &initial_solution.tasks {
        assert!(task.priority.is_none());
    }

    // Create and run phase
    let mut phase = factory.create_phase();
    phase.solve(&mut solver_scope);

    // Verify all tasks are now assigned
    let final_solution = solver_scope.working_solution();
    for task in &final_solution.tasks {
        assert!(
            task.priority.is_some(),
            "Task {} should have priority assigned",
            task.id
        );
    }

    // Best fit should pick the highest priorities (5, 5, 5 or similar)
    // Since our score calculator rewards higher priorities
    let total_priority: i64 = final_solution.tasks.iter().filter_map(|t| t.priority).sum();
    // With BestFit and values [1,2,3,4,5], each task should get 5
    assert_eq!(
        total_priority, 15,
        "BestFit should assign highest priorities"
    );
}

#[test]
fn test_construction_phase_factory_creates_fresh_phases() {
    let factory =
        ConstructionPhaseFactory::<TestSolution, TestMove, _>::first_fit(create_placer_factory());

    // Create multiple phases - they should be independent
    let phase1 = factory.create_phase();
    let phase2 = factory.create_phase();

    // Both phases should work independently
    assert_eq!(phase1.phase_type_name(), "ConstructionHeuristic");
    assert_eq!(phase2.phase_type_name(), "ConstructionHeuristic");
}

#[test]
fn test_construction_phase_factory_implements_solver_phase_factory() {
    let factory =
        ConstructionPhaseFactory::<TestSolution, TestMove, _>::first_fit(create_placer_factory());

    // Verify we can use it as a trait object
    let factory_ref: &dyn SolverPhaseFactory<TestSolution> = &factory;
    let phase = factory_ref.create_phase();
    assert_eq!(phase.phase_type_name(), "ConstructionHeuristic");
}

#[test]
fn list_construction_phase_builder_still_appends_after_existing_elements() {
    let builder = ListConstructionPhaseBuilder::<ListSolution, usize>::new(
        list_element_count,
        list_assigned_elements,
        list_entity_count,
        list_assign_element,
        list_index_to_element,
        0,
    );
    let solution = ListSolution {
        vehicles: vec![ListVehicle { visits: vec![99] }],
        visit_pool: vec![10, 11],
        score: None,
    };
    let director = ScoreDirector::simple(solution, list_solution_descriptor(), |s, _| {
        s.vehicles.len()
    });
    let mut solver_scope = SolverScope::new(director);

    let mut phase = builder.create_phase();
    phase.solve(&mut solver_scope);

    assert_eq!(solver_scope.working_solution().vehicles[0].visits, vec![99, 10, 11]);
}