vehicle-routing-rust-pre / src /constraints.rs
blackopsrepl's picture
Upload 24 files
44dceed verified
//! Score calculator for Vehicle Routing Problem.
//!
//! # Constraints
//!
//! - **Vehicle capacity** (hard): Total demand must not exceed vehicle capacity
//! - **Time windows** (hard): Service must complete before max end time
//! - **Minimize travel time** (soft): Reduce total driving time
//!
//! # Design
//!
//! Uses a simple score calculator function with full solution access.
//! No global state or RwLock overhead - direct array indexing into the plan's
//! travel time matrix and visits.
use solverforge::prelude::*;
use crate::domain::{Vehicle, VehicleRoutePlan};
/// Calculates the score for a vehicle routing solution.
///
/// # Hard constraints
/// - Vehicle capacity: penalize excess demand
/// - Time windows: penalize late arrivals
///
/// # Soft constraints
/// - Minimize total travel time (in minutes)
///
/// # Examples
///
/// ```
/// use vehicle_routing::constraints::calculate_score;
/// use vehicle_routing::domain::{Location, Visit, Vehicle, VehicleRoutePlan};
/// use solverforge::prelude::Score; // For is_feasible()
///
/// let depot = Location::new(0, 0.0, 0.0);
/// let locations = vec![depot.clone()];
/// let visits = vec![Visit::new(0, "A", depot.clone()).with_demand(5)];
/// let mut vehicle = Vehicle::new(0, "V1", 10, depot);
/// vehicle.visits = vec![0];
///
/// let mut plan = VehicleRoutePlan::new("test", locations, visits, vec![vehicle]);
/// plan.finalize();
///
/// let score = calculate_score(&plan);
/// assert!(score.is_feasible()); // Demand 5 <= capacity 10
/// ```
pub fn calculate_score(plan: &VehicleRoutePlan) -> HardSoftScore {
let mut hard = 0i64;
let mut soft = 0i64;
for vehicle in &plan.vehicles {
// =====================================================================
// HARD: Vehicle Capacity
// =====================================================================
let total_demand: i32 = vehicle
.visits
.iter()
.filter_map(|&idx| plan.visits.get(idx))
.map(|v| v.demand)
.sum();
if total_demand > vehicle.capacity {
hard -= (total_demand - vehicle.capacity) as i64;
}
// =====================================================================
// HARD: Time Windows
// =====================================================================
let late_minutes = calculate_late_minutes_for_vehicle(plan, vehicle);
if late_minutes > 0 {
hard -= late_minutes;
}
// =====================================================================
// SOFT: Minimize Travel Time
// =====================================================================
let driving_seconds = plan.total_driving_time(vehicle);
soft -= driving_seconds / 60; // Convert to minutes
}
HardSoftScore::of(hard, soft)
}
/// Calculates total late minutes for a vehicle's route.
///
/// A visit is late if service finishes after `max_end_time`.
fn calculate_late_minutes_for_vehicle(plan: &VehicleRoutePlan, vehicle: &Vehicle) -> i64 {
if vehicle.visits.is_empty() {
return 0;
}
let mut total_late = 0i64;
let mut current_time = vehicle.departure_time;
let mut current_loc_idx = vehicle.home_location.index;
for &visit_idx in &vehicle.visits {
let Some(visit) = plan.visits.get(visit_idx) else {
continue;
};
// Travel to this visit
let travel = plan.travel_time(current_loc_idx, visit.location.index);
let arrival = current_time + travel;
// Service starts at max(arrival, min_start_time)
let service_start = arrival.max(visit.min_start_time);
let service_end = service_start + visit.service_duration;
// Check if late (service finishes after max_end_time)
if service_end > visit.max_end_time {
let late_seconds = service_end - visit.max_end_time;
// Round up to minutes
total_late += (late_seconds + 59) / 60;
}
current_time = service_end;
current_loc_idx = visit.location.index;
}
total_late
}
// ============================================================================
// Helper functions (for analyze endpoint)
// ============================================================================
/// Calculates total late minutes for a vehicle's route (public API).
///
/// # Examples
///
/// ```
/// use vehicle_routing::constraints::calculate_late_minutes;
/// use vehicle_routing::domain::{Location, Visit, Vehicle, VehicleRoutePlan};
///
/// let depot = Location::new(0, 0.0, 0.0);
/// let customer = Location::new(1, 0.0, 1.0); // ~111 km away, ~2.2 hours at 50 km/h
///
/// let locations = vec![depot.clone(), customer.clone()];
/// let visits = vec![
/// Visit::new(0, "A", customer)
/// .with_time_window(0, 8 * 3600 + 30 * 60) // Must finish by 8:30am
/// .with_service_duration(300), // 5 min service
/// ];
/// let mut vehicle = Vehicle::new(0, "V1", 100, depot);
/// vehicle.departure_time = 8 * 3600; // Depart at 8am
/// vehicle.visits = vec![0];
///
/// let mut plan = VehicleRoutePlan::new("test", locations, visits, vec![vehicle.clone()]);
/// plan.finalize();
///
/// // Vehicle departs 8am, travels ~2.2 hours, arrives ~10:13am
/// // Service ends ~10:18am, but max_end is 8:30am
/// // Late by ~108 minutes
/// let late = calculate_late_minutes(&plan, &vehicle);
/// assert!(late > 100);
/// ```
#[inline]
pub fn calculate_late_minutes(plan: &VehicleRoutePlan, vehicle: &Vehicle) -> i64 {
calculate_late_minutes_for_vehicle(plan, vehicle)
}
/// Calculates excess demand for a vehicle (0 if under capacity).
///
/// # Examples
///
/// ```
/// use vehicle_routing::constraints::calculate_excess_capacity;
/// use vehicle_routing::domain::{Location, Visit, Vehicle, VehicleRoutePlan};
///
/// let depot = Location::new(0, 0.0, 0.0);
/// let locations = vec![depot.clone()];
/// let visits = vec![
/// Visit::new(0, "A", depot.clone()).with_demand(60),
/// Visit::new(1, "B", depot.clone()).with_demand(50),
/// ];
/// let mut vehicle = Vehicle::new(0, "V1", 100, depot);
/// vehicle.visits = vec![0, 1]; // Total demand = 110
///
/// let mut plan = VehicleRoutePlan::new("test", locations, visits, vec![vehicle.clone()]);
/// plan.finalize();
///
/// // Excess = 110 - 100 = 10
/// assert_eq!(calculate_excess_capacity(&plan, &vehicle), 10);
/// ```
#[inline]
pub fn calculate_excess_capacity(plan: &VehicleRoutePlan, vehicle: &Vehicle) -> i32 {
let total_demand: i32 = vehicle
.visits
.iter()
.filter_map(|&idx| plan.visits.get(idx))
.map(|v| v.demand)
.sum();
(total_demand - vehicle.capacity).max(0)
}