solverforge-solver 0.15.0

Solver engine for SolverForge
Documentation
use std::time::Instant;

use solverforge_config::ConstructionObligation;
use solverforge_core::domain::PlanningSolution;
use solverforge_core::score::Score;
use solverforge_scoring::Director;

use super::decision::{
    is_first_fit_improvement, keep_current_allowed, select_best_fit, select_first_feasible,
    select_first_fit, ScoredChoiceTracker,
};
use super::evaluation::evaluate_trial_move;
use super::{ConstructionChoice, Placement, StrongestFitForager, WeakestFitForager};
use crate::heuristic::r#move::Move;
use crate::phase::control::should_interrupt_evaluation;
use crate::scope::{ProgressCallback, StepScope};

pub(super) fn select_first_fit_index<S, D, BestCb, M>(
    placement: &Placement<S, M>,
    construction_obligation: ConstructionObligation,
    step_scope: &mut StepScope<'_, '_, '_, S, D, BestCb>,
) -> Option<ConstructionChoice>
where
    S: PlanningSolution,
    D: Director<S>,
    BestCb: ProgressCallback<S>,
    M: Move<S>,
{
    let mut first_doable = None;
    let baseline_score =
        keep_current_allowed(placement.keep_current_legal(), construction_obligation)
            .then(|| step_scope.calculate_score());

    for (idx, m) in placement.moves.iter().enumerate() {
        let evaluation_started = Instant::now();
        if should_interrupt_evaluation(step_scope, idx) {
            return None;
        }
        if !m.is_doable(step_scope.score_director()) {
            step_scope
                .phase_scope_mut()
                .record_evaluated_move(evaluation_started.elapsed());
            continue;
        }

        if let Some(baseline_score) = baseline_score {
            let score = evaluate_trial_move(step_scope.score_director_mut(), m);
            step_scope.phase_scope_mut().record_score_calculation();
            if is_first_fit_improvement(baseline_score, score) {
                first_doable = Some(idx);
                step_scope
                    .phase_scope_mut()
                    .record_evaluated_move(evaluation_started.elapsed());
                break;
            }
        } else {
            first_doable = Some(idx);
            step_scope
                .phase_scope_mut()
                .record_evaluated_move(evaluation_started.elapsed());
            break;
        }
        step_scope
            .phase_scope_mut()
            .record_evaluated_move(evaluation_started.elapsed());
    }

    Some(select_first_fit(first_doable))
}

pub(super) fn select_best_fit_index<S, D, BestCb, M>(
    placement: &Placement<S, M>,
    construction_obligation: ConstructionObligation,
    step_scope: &mut StepScope<'_, '_, '_, S, D, BestCb>,
) -> Option<ConstructionChoice>
where
    S: PlanningSolution,
    D: Director<S>,
    BestCb: ProgressCallback<S>,
    M: Move<S>,
{
    let baseline_score =
        keep_current_allowed(placement.keep_current_legal(), construction_obligation)
            .then(|| step_scope.calculate_score());
    let mut tracker = ScoredChoiceTracker::default();

    for (idx, m) in placement.moves.iter().enumerate() {
        let evaluation_started = Instant::now();
        if should_interrupt_evaluation(step_scope, idx) {
            return None;
        }
        if !m.is_doable(step_scope.score_director()) {
            step_scope
                .phase_scope_mut()
                .record_evaluated_move(evaluation_started.elapsed());
            continue;
        }

        let score = evaluate_trial_move(step_scope.score_director_mut(), m);
        step_scope.phase_scope_mut().record_score_calculation();
        step_scope
            .phase_scope_mut()
            .record_evaluated_move(evaluation_started.elapsed());

        tracker.consider(idx, score);
    }

    Some(select_best_fit(tracker, baseline_score))
}

pub(super) fn select_first_feasible_index<S, D, BestCb, M>(
    placement: &Placement<S, M>,
    construction_obligation: ConstructionObligation,
    step_scope: &mut StepScope<'_, '_, '_, S, D, BestCb>,
) -> Option<ConstructionChoice>
where
    S: PlanningSolution,
    D: Director<S>,
    BestCb: ProgressCallback<S>,
    M: Move<S>,
{
    let baseline_score =
        keep_current_allowed(placement.keep_current_legal(), construction_obligation)
            .then(|| step_scope.calculate_score());

    let mut fallback_tracker = ScoredChoiceTracker::default();
    let mut first_feasible = None;

    for (idx, m) in placement.moves.iter().enumerate() {
        let evaluation_started = Instant::now();
        if should_interrupt_evaluation(step_scope, idx) {
            return None;
        }
        if !m.is_doable(step_scope.score_director()) {
            step_scope
                .phase_scope_mut()
                .record_evaluated_move(evaluation_started.elapsed());
            continue;
        }

        let score = evaluate_trial_move(step_scope.score_director_mut(), m);
        step_scope.phase_scope_mut().record_score_calculation();
        step_scope
            .phase_scope_mut()
            .record_evaluated_move(evaluation_started.elapsed());

        if score.is_feasible() {
            first_feasible = Some(idx);
            break;
        }

        fallback_tracker.consider(idx, score);
    }

    Some(select_first_feasible(
        first_feasible,
        fallback_tracker,
        baseline_score,
    ))
}

pub(super) fn select_weakest_fit_index<S, D, BestCb, M>(
    forager: &WeakestFitForager<S, M>,
    placement: &Placement<S, M>,
    construction_obligation: ConstructionObligation,
    step_scope: &mut StepScope<'_, '_, '_, S, D, BestCb>,
) -> Option<ConstructionChoice>
where
    S: PlanningSolution,
    D: Director<S>,
    BestCb: ProgressCallback<S>,
    M: Move<S>,
{
    let mut best_idx = None;
    let mut min_strength = None;

    for (evaluated, (idx, m)) in placement.moves.iter().enumerate().enumerate() {
        let evaluation_started = Instant::now();
        if should_interrupt_evaluation(step_scope, evaluated) {
            return None;
        }

        if !m.is_doable(step_scope.score_director()) {
            step_scope
                .phase_scope_mut()
                .record_evaluated_move(evaluation_started.elapsed());
            continue;
        }

        let strength = forager.strength(m, step_scope.score_director().working_solution());
        if min_strength.is_none_or(|best| strength < best) {
            best_idx = Some(idx);
            min_strength = Some(strength);
        }

        step_scope
            .phase_scope_mut()
            .record_evaluated_move(evaluation_started.elapsed());
    }

    let Some(best_idx) = best_idx else {
        return Some(ConstructionChoice::KeepCurrent);
    };

    if !keep_current_allowed(placement.keep_current_legal(), construction_obligation) {
        return Some(ConstructionChoice::Select(best_idx));
    }

    let baseline_score = step_scope.calculate_score();
    let score = evaluate_trial_move(step_scope.score_director_mut(), &placement.moves[best_idx]);
    step_scope.phase_scope_mut().record_score_calculation();

    Some(if score > baseline_score {
        ConstructionChoice::Select(best_idx)
    } else {
        ConstructionChoice::KeepCurrent
    })
}

pub(super) fn select_strongest_fit_index<S, D, BestCb, M>(
    forager: &StrongestFitForager<S, M>,
    placement: &Placement<S, M>,
    construction_obligation: ConstructionObligation,
    step_scope: &mut StepScope<'_, '_, '_, S, D, BestCb>,
) -> Option<ConstructionChoice>
where
    S: PlanningSolution,
    D: Director<S>,
    BestCb: ProgressCallback<S>,
    M: Move<S>,
{
    let mut best_idx = None;
    let mut max_strength = None;

    for (evaluated, (idx, m)) in placement.moves.iter().enumerate().enumerate() {
        let evaluation_started = Instant::now();
        if should_interrupt_evaluation(step_scope, evaluated) {
            return None;
        }

        if !m.is_doable(step_scope.score_director()) {
            step_scope
                .phase_scope_mut()
                .record_evaluated_move(evaluation_started.elapsed());
            continue;
        }

        let strength = forager.strength(m, step_scope.score_director().working_solution());
        if max_strength.is_none_or(|best| strength > best) {
            best_idx = Some(idx);
            max_strength = Some(strength);
        }

        step_scope
            .phase_scope_mut()
            .record_evaluated_move(evaluation_started.elapsed());
    }

    let Some(best_idx) = best_idx else {
        return Some(ConstructionChoice::KeepCurrent);
    };

    if !keep_current_allowed(placement.keep_current_legal(), construction_obligation) {
        return Some(ConstructionChoice::Select(best_idx));
    }

    let baseline_score = step_scope.calculate_score();
    let score = evaluate_trial_move(step_scope.score_director_mut(), &placement.moves[best_idx]);
    step_scope.phase_scope_mut().record_score_calculation();

    Some(if score > baseline_score {
        ConstructionChoice::Select(best_idx)
    } else {
        ConstructionChoice::KeepCurrent
    })
}