solverforge-solver 0.11.1

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

use solverforge_core::domain::PlanningSolution;
use solverforge_scoring::Director;
use tracing::debug;

use crate::heuristic::r#move::Move;
use crate::heuristic::selector::move_selector::MoveCandidateRef;
use crate::scope::{ProgressCallback, StepScope};
use crate::stats::whole_units_per_second;

const VND_PROGRESS_EVALUATION_INTERVAL: u64 = 0x1FFF;

pub(crate) struct VndProgress {
    moves_generated: u64,
    moves_evaluated: u64,
    last_progress_time: Instant,
    last_progress_evaluated: u64,
}

impl VndProgress {
    pub(crate) fn new() -> Self {
        Self {
            moves_generated: 0,
            moves_evaluated: 0,
            last_progress_time: Instant::now(),
            last_progress_evaluated: 0,
        }
    }

    pub(crate) fn moves_evaluated(&self) -> u64 {
        self.moves_evaluated
    }

    pub(crate) fn record_generated(&mut self) {
        self.moves_generated += 1;
    }

    pub(crate) fn record_evaluated(&mut self) {
        self.moves_evaluated += 1;
    }

    pub(crate) fn maybe_report<S, D, ProgressCb>(
        &mut self,
        step_scope: &StepScope<'_, '_, '_, S, D, ProgressCb>,
        current_score: &S::Score,
    ) where
        S: PlanningSolution,
        D: Director<S>,
        ProgressCb: ProgressCallback<S>,
    {
        if self.moves_evaluated & VND_PROGRESS_EVALUATION_INTERVAL != 0 {
            return;
        }
        let now = Instant::now();
        let elapsed = now.duration_since(self.last_progress_time);
        if elapsed.as_secs() < 1 {
            return;
        }
        let current_speed =
            whole_units_per_second(self.moves_evaluated - self.last_progress_evaluated, elapsed);
        let stats = step_scope.phase_scope().solver_scope().stats();
        debug!(
            event = "progress",
            phase = "Variable Neighborhood Descent",
            steps = step_scope.step_index(),
            moves_generated = self.moves_generated,
            moves_evaluated = self.moves_evaluated,
            moves_accepted = stats.moves_accepted,
            score_calculations = stats.score_calculations,
            speed = current_speed,
            acceptance_rate = format!("{:.1}%", stats.acceptance_rate() * 100.0),
            current_score = %current_score,
        );
        step_scope.phase_scope().solver_scope().report_progress();
        self.last_progress_time = now;
        self.last_progress_evaluated = self.moves_evaluated;
    }
}

pub(crate) fn candidate_selector_label<S, M>(mov: &MoveCandidateRef<'_, S, M>) -> String
where
    S: PlanningSolution,
    M: Move<S>,
{
    if mov.variable_name() == "compound_scalar" || mov.variable_name() == "conflict_repair" {
        return mov.variable_name().to_string();
    }
    let mut label = None;
    mov.for_each_affected_entity(&mut |affected| {
        if label.is_none() {
            label = Some(affected.variable_name.to_string());
        }
    });
    label.unwrap_or_else(|| "move".to_string())
}