From 86cf918f8c6ef566053e5bcba66ab2ec833263bc Mon Sep 17 00:00:00 2001 From: Vittorio Distefano Date: Thu, 5 Nov 2026 01:45:54 +0100 Subject: [PATCH] test(cvrp): score route distance so the time-window k-opt test can fail The `list_cvrp_k_opt_time_window` domain had an empty constraint set, so every route scored the same and the solve returned the initial route whatever list k-opt construction did; the test asserting that a time-window-breaking reversal is rejected passed with the feasibility check removed. The domain now penalizes each route's travelled distance with the same directed distance its problem data uses, so an applied reversal changes the returned solution and bypassing the check fails the test. Closes #188. --- .../domain/plan.rs | 44 +++++++++++++------ 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/crates/solverforge/tests/list_cvrp_k_opt_time_window/domain/plan.rs b/crates/solverforge/tests/list_cvrp_k_opt_time_window/domain/plan.rs index 4ce99c91..d794f45a 100644 --- a/crates/solverforge/tests/list_cvrp_k_opt_time_window/domain/plan.rs +++ b/crates/solverforge/tests/list_cvrp_k_opt_time_window/domain/plan.rs @@ -2,6 +2,7 @@ use std::sync::Arc; use solverforge::cvrp::ProblemData; use solverforge::prelude::*; +use solverforge::stream::ConstraintFactory; use super::Route; @@ -40,23 +41,40 @@ impl solverforge::cvrp::VrpSolution for Plan { } } -fn constraints() -> impl ConstraintSet {} +/// Directed distance between two nodes, the depot being node 0: 1 along +/// `0 → 1 → 2 → 3 → 4 → 0` and for `3 → 2`, 50 for `1 → 3` and `2 → 4`, 100 +/// otherwise. +fn distance(from: usize, to: usize) -> i64 { + match (from, to) { + _ if from == to => 0, + (0, 1) | (1, 2) | (2, 3) | (3, 4) | (4, 0) | (3, 2) => 1, + (1, 3) | (2, 4) => 50, + _ => 100, + } +} -pub fn build_plan() -> Plan { - let mut distance_matrix = vec![vec![100_i64; 5]; 5]; - for (idx, row) in distance_matrix.iter_mut().enumerate() { - row[idx] = 0; +/// Each route's travelled distance, depot to depot. +fn route_distance(route: &Route) -> i64 { + let mut previous = 0; + let mut total = 0; + for &visit in route.visits.iter().chain(std::iter::once(&0)) { + total += distance(previous, visit); + previous = visit; } + total +} - distance_matrix[0][1] = 1; - distance_matrix[1][3] = 50; - distance_matrix[3][2] = 1; - distance_matrix[2][4] = 50; - distance_matrix[4][0] = 1; +fn constraints() -> impl ConstraintSet { + (ConstraintFactory::::new() + .for_each(Plan::routes()) + .penalize(|route: &Route| HardSoftScore::of(0, route_distance(route))) + .named("route distance"),) +} - distance_matrix[1][2] = 1; - distance_matrix[2][3] = 1; - distance_matrix[3][4] = 1; +pub fn build_plan() -> Plan { + let distance_matrix = (0..5) + .map(|from| (0..5).map(|to| distance(from, to)).collect()) + .collect(); let mut travel_times = vec![vec![0_i64; 5]; 5]; travel_times[1][2] = 10;