routik-solver 0.1.0

Core VRP solver (CVRPTW): data model, cost-matrix trait, Clarke-Wright construction, local search, and simulated annealing.
Documentation
//! The objective function the metaheuristic minimises.
//!
//! Centralised here so every part of the solver scores a solution the same
//! way (per the solver rules: "Objectif centralisé dans `cost`"). By default
//! it minimises total distance; optional weights let callers also penalise the
//! number of vehicles used or the total time on the road.

use crate::model::Solution;

/// Weights of the linear objective `cost = w_d·distance + w_v·vehicles + w_t·time`.
///
/// The default is pure distance minimisation (`distance = 1`, the rest `0`),
/// which is what the classic VRPTW benchmarks score against.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Objective {
    /// Weight on total travelled distance.
    pub distance: f64,
    /// Weight on the number of vehicles (routes) used.
    pub vehicles: f64,
    /// Weight on total elapsed time (travel + wait + service).
    pub time: f64,
}

impl Default for Objective {
    fn default() -> Self {
        Self {
            distance: 1.0,
            vehicles: 0.0,
            time: 0.0,
        }
    }
}

impl Objective {
    /// Minimise total distance only.
    #[must_use]
    pub fn distance_only() -> Self {
        Self::default()
    }

    /// Score from already rolled-up totals. Kept separate from [`Self::of`] so
    /// the search loop can score incrementally without owning a [`Solution`].
    #[must_use]
    pub fn score(&self, total_distance: f64, route_count: usize, total_time: f64) -> f64 {
        self.distance * total_distance + self.vehicles * route_count as f64 + self.time * total_time
    }

    /// Score a complete [`Solution`].
    #[must_use]
    pub fn of(&self, solution: &Solution) -> f64 {
        self.score(
            solution.total_distance,
            solution.routes.len(),
            solution.total_time,
        )
    }
}