solverforge-solver 0.12.0

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

use solverforge_config::{ConstructionHeuristicConfig, ConstructionObligation};
use solverforge_core::domain::PlanningSolution;
use solverforge_core::score::Score;
use solverforge_scoring::Director;
use tracing::info;

use crate::builder::CoverageGroupBinding;
use crate::heuristic::r#move::{CompoundScalarMove, Move};
use crate::phase::construction::evaluation::evaluate_trial_move;
use crate::phase::hard_delta::{hard_score_delta, HardScoreDelta};
use crate::scope::{PhaseScope, ProgressCallback, SolverScope, StepScope};
use crate::stats::{format_duration, whole_units_per_second};

use super::candidate::{
    optional_coverage_moves, remaining_required_count, required_coverage_moves, CoverageMoveOptions,
};

pub(crate) fn solve_coverage_construction<S, D, ProgressCb>(
    config: Option<&ConstructionHeuristicConfig>,
    group: &CoverageGroupBinding<S>,
    solver_scope: &mut SolverScope<'_, S, D, ProgressCb>,
) -> bool
where
    S: PlanningSolution + 'static,
    S::Score: Score + Copy,
    D: Director<S>,
    ProgressCb: ProgressCallback<S>,
{
    let options = CoverageMoveOptions::for_construction(
        group,
        config.and_then(|cfg| cfg.value_candidate_limit),
        config.and_then(|cfg| cfg.group_candidate_limit),
    );
    let construction_obligation = config
        .map(|cfg| cfg.construction_obligation)
        .unwrap_or_default();
    let phase_name = "Coverage Construction";
    let mut phase_scope = PhaseScope::with_phase_type(solver_scope, 0, phase_name);
    let phase_index = phase_scope.phase_index();
    let start_score = phase_scope
        .solver_scope()
        .current_score()
        .map(|score| score.to_string())
        .unwrap_or_else(|| "none".to_string());
    let mut ran_step = false;
    let mut last_progress_time = Instant::now();

    info!(
        event = "phase_start",
        phase = phase_name,
        group = group.group_name,
        phase_index = phase_index,
        score = start_score,
    );
    phase_scope.solver_scope().report_progress();

    loop {
        if phase_scope
            .solver_scope_mut()
            .should_terminate_construction()
        {
            break;
        }
        let remaining =
            remaining_required_count(group, phase_scope.score_director().working_solution());
        if remaining == 0 {
            break;
        }
        let moves = required_coverage_moves(
            group,
            phase_scope.score_director().working_solution(),
            options,
        );
        let Some((mov, score)) =
            select_required_move(&mut phase_scope, moves, construction_obligation)
        else {
            phase_scope.record_construction_slot_no_doable();
            phase_scope.record_coverage_required_remaining(group.group_name, remaining as u64);
            break;
        };
        commit_coverage_move(&mut phase_scope, &mov, score, &mut ran_step);
        if last_progress_time.elapsed().as_secs() >= 1 {
            phase_scope.solver_scope().report_progress();
            last_progress_time = Instant::now();
        }
    }

    if remaining_required_count(group, phase_scope.score_director().working_solution()) == 0 {
        loop {
            if phase_scope
                .solver_scope_mut()
                .should_terminate_construction()
            {
                break;
            }
            let moves = optional_coverage_moves(
                group,
                phase_scope.score_director().working_solution(),
                options,
            );
            let Some((mov, score)) = select_optional_move(&mut phase_scope, moves) else {
                break;
            };
            commit_coverage_move(&mut phase_scope, &mov, score, &mut ran_step);
            if last_progress_time.elapsed().as_secs() >= 1 {
                phase_scope.solver_scope().report_progress();
                last_progress_time = Instant::now();
            }
        }
    }

    let remaining =
        remaining_required_count(group, phase_scope.score_director().working_solution());
    phase_scope.record_coverage_required_remaining(group.group_name, remaining as u64);
    if ran_step {
        phase_scope.update_best_solution();
    }

    phase_scope.solver_scope().report_progress();
    let best_score = phase_scope
        .solver_scope()
        .best_score()
        .map(|score| score.to_string())
        .unwrap_or_else(|| "none".to_string());
    let duration = phase_scope.elapsed();
    let steps = phase_scope.step_count();
    let speed = whole_units_per_second(steps, duration);
    let stats = phase_scope.stats();

    info!(
        event = "phase_end",
        phase = phase_name,
        group = group.group_name,
        phase_index = phase_index,
        required_remaining = remaining,
        duration = %format_duration(duration),
        steps = steps,
        moves_generated = stats.moves_generated,
        moves_evaluated = stats.moves_evaluated,
        moves_accepted = stats.moves_accepted,
        score_calculations = stats.score_calculations,
        generation_time = %format_duration(stats.generation_time()),
        evaluation_time = %format_duration(stats.evaluation_time()),
        speed = speed,
        score = best_score,
    );

    ran_step
}

fn select_required_move<S, D, ProgressCb>(
    phase_scope: &mut PhaseScope<'_, '_, S, D, ProgressCb>,
    moves: Vec<CompoundScalarMove<S>>,
    construction_obligation: ConstructionObligation,
) -> Option<(CompoundScalarMove<S>, S::Score)>
where
    S: PlanningSolution,
    S::Score: Score + Copy,
    D: Director<S>,
    ProgressCb: ProgressCallback<S>,
{
    let current = phase_scope.calculate_score();
    let mode = match construction_obligation {
        ConstructionObligation::PreserveUnassigned => CoverageSelectionMode::RequiredPreserve,
        ConstructionObligation::AssignWhenCandidateExists => {
            CoverageSelectionMode::RequiredAssignWhenCandidateExists
        }
    };
    select_move(phase_scope, moves, current, mode)
}

fn select_optional_move<S, D, ProgressCb>(
    phase_scope: &mut PhaseScope<'_, '_, S, D, ProgressCb>,
    moves: Vec<CompoundScalarMove<S>>,
) -> Option<(CompoundScalarMove<S>, S::Score)>
where
    S: PlanningSolution,
    S::Score: Score + Copy,
    D: Director<S>,
    ProgressCb: ProgressCallback<S>,
{
    let current = phase_scope.calculate_score();
    select_move(phase_scope, moves, current, CoverageSelectionMode::Optional)
}

#[derive(Clone, Copy)]
enum CoverageSelectionMode {
    RequiredPreserve,
    RequiredAssignWhenCandidateExists,
    Optional,
}

fn select_move<S, D, ProgressCb>(
    phase_scope: &mut PhaseScope<'_, '_, S, D, ProgressCb>,
    moves: Vec<CompoundScalarMove<S>>,
    current: S::Score,
    mode: CoverageSelectionMode,
) -> Option<(CompoundScalarMove<S>, S::Score)>
where
    S: PlanningSolution,
    S::Score: Score + Copy,
    D: Director<S>,
    ProgressCb: ProgressCallback<S>,
{
    for mov in moves {
        let generation_started = Instant::now();
        phase_scope.record_generated_move(generation_started.elapsed());
        let evaluation_started = Instant::now();
        if !mov.is_doable(phase_scope.score_director()) {
            phase_scope.record_evaluated_move(evaluation_started.elapsed());
            phase_scope.record_move_not_doable();
            continue;
        }
        let score = evaluate_trial_move(phase_scope.score_director_mut(), &mov);
        phase_scope.record_score_calculation();
        phase_scope.record_evaluated_move(evaluation_started.elapsed());
        if matches!(
            mode,
            CoverageSelectionMode::RequiredAssignWhenCandidateExists
        ) {
            return Some((mov, score));
        }
        let hard_delta = hard_score_delta(current, score);
        if hard_delta == Some(HardScoreDelta::Worse) {
            continue;
        }
        match mode {
            CoverageSelectionMode::RequiredPreserve => {
                if hard_delta == Some(HardScoreDelta::Improving) || score >= current {
                    return Some((mov, score));
                }
            }
            CoverageSelectionMode::RequiredAssignWhenCandidateExists => unreachable!(),
            CoverageSelectionMode::Optional => {
                if score > current {
                    return Some((mov, score));
                }
            }
        }
    }
    None
}

fn commit_coverage_move<S, D, ProgressCb>(
    phase_scope: &mut PhaseScope<'_, '_, S, D, ProgressCb>,
    mov: &CompoundScalarMove<S>,
    score: S::Score,
    ran_step: &mut bool,
) where
    S: PlanningSolution,
    S::Score: Score + Copy,
    D: Director<S>,
    ProgressCb: ProgressCallback<S>,
{
    *ran_step = true;
    let mut step_scope = StepScope::new(phase_scope);
    step_scope.phase_scope_mut().record_move_accepted();
    step_scope.apply_committed_move(mov);
    step_scope.phase_scope_mut().record_move_applied();
    step_scope
        .phase_scope_mut()
        .record_construction_slot_assigned();
    step_scope.set_step_score(score);
    step_scope.complete();
}