solverforge-solver 0.12.0

Solver engine for SolverForge
Documentation
use crate::builder::selector::CoverageRepairSelector;
use crate::heuristic::r#move::Move;
use crate::heuristic::selector::move_selector::{MoveCursor, MoveSelector};

fn coverage_repair_selector(
    value_candidate_limit: Option<usize>,
    max_moves_per_step: Option<usize>,
    require_hard_improvement: bool,
) -> CoverageRepairSelector<CoveragePlan> {
    coverage_repair_selector_with_model(
        coverage_model(),
        value_candidate_limit,
        max_moves_per_step,
        require_hard_improvement,
    )
}

fn coverage_repair_selector_with_model(
    model: RuntimeModel<CoveragePlan, usize, DefaultMeter, DefaultMeter>,
    value_candidate_limit: Option<usize>,
    max_moves_per_step: Option<usize>,
    require_hard_improvement: bool,
) -> CoverageRepairSelector<CoveragePlan> {
    CoverageRepairSelector::new(
        model.coverage_groups()[0],
        value_candidate_limit,
        max_moves_per_step,
        require_hard_improvement,
    )
}

fn repair_move_results(
    plan: &CoveragePlan,
    selector: &CoverageRepairSelector<CoveragePlan>,
) -> Vec<CoveragePlan> {
    let director = CoverageDirector {
        working_solution: plan.clone(),
        descriptor: coverage_plan_descriptor(),
    };
    let mut cursor = selector.open_cursor(&director);
    let mut results = Vec::new();
    while let Some(id) = cursor.next_candidate() {
        let mov = cursor.take_candidate(id);
        let mut trial = CoverageDirector {
            working_solution: plan.clone(),
            descriptor: coverage_plan_descriptor(),
        };
        assert!(mov.is_doable(&trial));
        mov.do_move(&mut trial);
        trial.calculate_score();
        results.push(trial.working_solution);
    }
    results
}

#[test]
fn coverage_repair_selector_emits_only_hard_improving_required_moves() {
    let plan = coverage_plan(
        2,
        vec![
            coverage_slot(true, 0, None, &[0]),
            coverage_slot(true, 0, Some(0), &[0, 1]),
        ],
    );
    let selector = coverage_repair_selector(None, Some(8), true);
    let director = CoverageDirector {
        working_solution: plan.clone(),
        descriptor: coverage_plan_descriptor(),
    };
    let mut cursor = selector.open_cursor(&director);
    let mut emitted = 0;

    while let Some(id) = cursor.next_candidate() {
        let mov = cursor.take_candidate(id);
        let mut trial = CoverageDirector {
            working_solution: plan.clone(),
            descriptor: coverage_plan_descriptor(),
        };
        let current = trial.calculate_score();
        assert!(mov.requires_hard_improvement());
        assert!(mov.is_doable(&trial));
        mov.do_move(&mut trial);
        let next = trial.calculate_score();
        assert!(next.hard() > current.hard());
        emitted += 1;
    }

    assert!(emitted > 0);
}

#[test]
fn coverage_repair_falls_back_to_moving_preferred_keeper() {
    let plan = coverage_plan(
        2,
        vec![
            coverage_slot(true, 0, Some(0), &[0, 1]),
            coverage_slot(true, 0, Some(0), &[0]),
        ],
    );
    let selector = coverage_repair_selector(None, Some(8), true);
    let results = repair_move_results(&plan, &selector);

    assert_eq!(results.len(), 1);
    assert_eq!(results[0].slots[0].assigned, Some(1));
    assert_eq!(results[0].slots[1].assigned, Some(0));
    assert_eq!(results[0].score, Some(HardSoftScore::of(0, 0)));
}

#[test]
fn coverage_repair_orders_capacity_conflict_groups_by_coverage_order() {
    let plan = coverage_plan(
        2,
        vec![
            coverage_slot(true, 0, Some(1), &[1]),
            coverage_slot(false, 0, Some(1), &[1]),
            coverage_slot(true, 0, Some(0), &[0]),
            coverage_slot(false, 0, Some(0), &[0]),
        ],
    );
    let selector = coverage_repair_selector(None, Some(1), false);
    let results = repair_move_results(&plan, &selector);

    assert_eq!(results.len(), 1);
    assert_eq!(results[0].slots[1].assigned, None);
    assert_eq!(results[0].slots[3].assigned, Some(0));
}

#[test]
fn coverage_repair_honors_value_candidate_limit_when_relocating_blocker() {
    let plan = coverage_plan(
        2,
        vec![
            coverage_slot(true, 0, None, &[0]),
            coverage_slot(true, 0, Some(0), &[0, 1]),
        ],
    );
    let limited_selector = coverage_repair_selector(Some(1), Some(8), false);
    let unlimited_selector = coverage_repair_selector(None, Some(8), false);

    assert!(repair_move_results(&plan, &limited_selector).is_empty());

    let unlimited_results = repair_move_results(&plan, &unlimited_selector);
    assert_eq!(unlimited_results.len(), 1);
    assert_eq!(unlimited_results[0].slots[0].assigned, Some(0));
    assert_eq!(unlimited_results[0].slots[1].assigned, Some(1));
}

#[test]
fn coverage_repair_uses_group_cap_when_selector_cap_is_omitted() {
    let model = coverage_model_with_limits(CoverageGroupLimits {
        max_moves_per_step: Some(1),
        max_augmenting_depth: Some(3),
        ..CoverageGroupLimits::new()
    });
    let plan = coverage_plan(
        3,
        vec![
            coverage_slot(true, 0, None, &[0, 1, 2]),
            coverage_slot(true, 0, None, &[0, 1, 2]),
            coverage_slot(true, 0, None, &[0, 1, 2]),
        ],
    );
    let selector = coverage_repair_selector_with_model(model, None, None, false);

    assert_eq!(repair_move_results(&plan, &selector).len(), 1);
}

#[test]
fn coverage_repair_selector_cap_overrides_group_cap() {
    let model = coverage_model_with_limits(CoverageGroupLimits {
        max_moves_per_step: Some(1),
        max_augmenting_depth: Some(3),
        ..CoverageGroupLimits::new()
    });
    let plan = coverage_plan(
        3,
        vec![
            coverage_slot(true, 0, None, &[0, 1, 2]),
            coverage_slot(true, 0, None, &[0, 1, 2]),
            coverage_slot(true, 0, None, &[0, 1, 2]),
        ],
    );
    let selector = coverage_repair_selector_with_model(model, None, Some(2), false);

    assert_eq!(repair_move_results(&plan, &selector).len(), 2);
}