solverforge-solver 0.11.1

Solver engine for SolverForge
Documentation
use std::any::TypeId;
use std::hint::black_box;
use std::time::Instant;

use crate::heuristic::selector::entity::FromSolutionEntitySelector;
use crate::heuristic::selector::move_selector::MoveSelector;
use crate::heuristic::selector::nearby_list_change::{
    CrossEntityDistanceMeter, NearbyListChangeMoveSelector,
};
use crate::heuristic::selector::nearby_list_swap::NearbyListSwapMoveSelector;
use solverforge_core::domain::{
    EntityCollectionExtractor, EntityDescriptor, PlanningSolution, SolutionDescriptor,
};
use solverforge_core::score::SoftScore;
use solverforge_scoring::ScoreDirector;

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

#[derive(Clone, Debug)]
struct Plan {
    vehicles: Vec<Vehicle>,
    score: Option<SoftScore>,
}

impl PlanningSolution for Plan {
    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, Copy, Debug, Default)]
struct EqualDistanceMeter;

impl CrossEntityDistanceMeter<Plan> for EqualDistanceMeter {
    fn distance(
        &self,
        _solution: &Plan,
        _src_entity: usize,
        _src_pos: usize,
        _dst_entity: usize,
        _dst_pos: usize,
    ) -> f64 {
        1.0
    }
}

#[derive(Clone, Copy, Debug, Default)]
struct PositionDistanceMeter;

impl CrossEntityDistanceMeter<Plan> for PositionDistanceMeter {
    fn distance(
        &self,
        _solution: &Plan,
        src_entity: usize,
        src_pos: usize,
        dst_entity: usize,
        dst_pos: usize,
    ) -> f64 {
        let entity_gap = src_entity.abs_diff(dst_entity) as f64;
        let position_gap = src_pos.abs_diff(dst_pos) as f64;
        entity_gap * 100.0 + position_gap
    }
}

fn get_vehicles(plan: &Plan) -> &Vec<Vehicle> {
    &plan.vehicles
}

fn get_vehicles_mut(plan: &mut Plan) -> &mut Vec<Vehicle> {
    &mut plan.vehicles
}

fn descriptor() -> SolutionDescriptor {
    let extractor = Box::new(EntityCollectionExtractor::new(
        "Vehicle",
        "vehicles",
        get_vehicles,
        get_vehicles_mut,
    ));
    let entity_desc = EntityDescriptor::new("Vehicle", TypeId::of::<Vehicle>(), "vehicles")
        .with_extractor(extractor);
    SolutionDescriptor::new("Plan", TypeId::of::<Plan>()).with_entity(entity_desc)
}

fn create_director(vehicles: Vec<Vehicle>) -> ScoreDirector<Plan, ()> {
    let solution = Plan {
        vehicles,
        score: None,
    };
    ScoreDirector::simple(solution, descriptor(), |plan, _| plan.vehicles.len())
}

fn list_len(plan: &Plan, entity_idx: usize) -> usize {
    plan.vehicles
        .get(entity_idx)
        .map_or(0, |vehicle| vehicle.visits.len())
}

fn list_remove(plan: &mut Plan, entity_idx: usize, pos: usize) -> Option<usize> {
    plan.vehicles
        .get_mut(entity_idx)
        .and_then(|vehicle| (pos < vehicle.visits.len()).then(|| vehicle.visits.remove(pos)))
}

fn list_insert(plan: &mut Plan, entity_idx: usize, pos: usize, value: usize) {
    if let Some(vehicle) = plan.vehicles.get_mut(entity_idx) {
        vehicle.visits.insert(pos, value);
    }
}

fn list_get(plan: &Plan, entity_idx: usize, pos: usize) -> Option<usize> {
    plan.vehicles
        .get(entity_idx)
        .and_then(|vehicle| vehicle.visits.get(pos))
        .copied()
}

fn list_set(plan: &mut Plan, entity_idx: usize, pos: usize, value: usize) {
    if let Some(vehicle) = plan.vehicles.get_mut(entity_idx) {
        vehicle.visits[pos] = value;
    }
}

fn benchmark_cursor(name: &str, runs: usize, mut run: impl FnMut() -> usize) {
    let mut samples = Vec::with_capacity(runs);
    let warmup_count = black_box(run());
    assert!(warmup_count > 0, "{name} benchmark must enumerate moves");

    for _ in 0..runs {
        let start = Instant::now();
        let move_count = black_box(run());
        samples.push((move_count, start.elapsed()));
    }

    samples.sort_by_key(|(_, elapsed)| *elapsed);
    let (median_count, median_elapsed) = samples[runs / 2];
    let throughput = median_count as f64 / median_elapsed.as_secs_f64();
    eprintln!(
        "{name}: {median_count} moves, median {:?}, {:.0} moves/sec",
        median_elapsed, throughput
    );
}

fn benchmark_plan(vehicle_count: usize, visits_per_vehicle: usize) -> Vec<Vehicle> {
    (0..vehicle_count)
        .map(|vehicle_idx| Vehicle {
            visits: (0..visits_per_vehicle)
                .map(|visit_idx| vehicle_idx * 1_000 + visit_idx)
                .collect(),
        })
        .collect()
}

#[test]
fn nearby_list_change_keeps_stable_tie_order() {
    let director = create_director(vec![
        Vehicle {
            visits: vec![10, 20, 30],
        },
        Vehicle {
            visits: vec![40, 50],
        },
    ]);

    let selector = NearbyListChangeMoveSelector::<Plan, usize, _, _>::new(
        FromSolutionEntitySelector::new(0),
        EqualDistanceMeter,
        3,
        list_len,
        list_get,
        list_remove,
        list_insert,
        "visits",
        0,
    );

    let moves: Vec<_> = selector
        .iter_moves(&director)
        .map(|m| {
            (
                m.source_entity_index(),
                m.source_position(),
                m.dest_entity_index(),
                m.dest_position(),
            )
        })
        .collect();

    assert_eq!(
        &moves[..3],
        &[(0, 0, 0, 2), (0, 0, 1, 0), (0, 0, 1, 1)],
        "equal-distance nearby list change candidates must preserve source enumeration order"
    );
}

#[test]
fn nearby_list_swap_emits_unique_pairs_in_stable_order() {
    let director = create_director(vec![
        Vehicle {
            visits: vec![10, 20],
        },
        Vehicle {
            visits: vec![30, 40],
        },
    ]);

    let selector = NearbyListSwapMoveSelector::<Plan, usize, _, _>::new(
        FromSolutionEntitySelector::new(0),
        EqualDistanceMeter,
        4,
        list_len,
        list_get,
        list_set,
        "visits",
        0,
    );

    let moves: Vec<_> = selector
        .iter_moves(&director)
        .map(|m| {
            (
                m.first_entity_index(),
                m.first_position(),
                m.second_entity_index(),
                m.second_position(),
            )
        })
        .collect();

    assert_eq!(
        moves,
        vec![
            (0, 0, 0, 1),
            (0, 0, 1, 0),
            (0, 0, 1, 1),
            (0, 1, 1, 0),
            (0, 1, 1, 1),
            (1, 0, 1, 1),
        ],
        "nearby list swap must preserve canonical pair order without emitting duplicates"
    );
}

#[test]
fn bench_nearby_list_change_cursor_enumeration() {
    let director = create_director(benchmark_plan(12, 24));
    let selector = NearbyListChangeMoveSelector::<Plan, usize, _, _>::new(
        FromSolutionEntitySelector::new(0),
        PositionDistanceMeter,
        16,
        list_len,
        list_get,
        list_remove,
        list_insert,
        "visits",
        0,
    );

    benchmark_cursor("nearby_list_change_cursor", 15, || {
        selector.iter_moves(&director).count()
    });
}

#[test]
fn bench_nearby_list_swap_cursor_enumeration() {
    let director = create_director(benchmark_plan(12, 24));
    let selector = NearbyListSwapMoveSelector::<Plan, usize, _, _>::new(
        FromSolutionEntitySelector::new(0),
        PositionDistanceMeter,
        16,
        list_len,
        list_get,
        list_set,
        "visits",
        0,
    );

    benchmark_cursor("nearby_list_swap_cursor", 15, || {
        selector.iter_moves(&director).count()
    });
}