solverforge-fsr / src /constraints /route_metrics_tests.rs
github-actions[bot]
chore: sync uc-fsr Space
2574e86
use super::route_metrics::{leg_for, route_stats};
use crate::domain::{
FieldServicePlan, Location, ServiceVisit, ServiceVisitInit, TechnicianRoute,
TechnicianRouteInit, TravelLeg, TravelLegInit,
};
use solverforge::ConstraintSet;
#[test]
fn route_stats_accounts_for_travel_service_and_lateness() {
let plan = sample_plan(vec![0, 1]);
let stats = route_stats(&plan, &plan.technician_routes[0]);
assert_eq!(stats.travel_seconds, 1_800);
assert_eq!(stats.service_minutes, 75);
assert_eq!(stats.late_minutes, 0);
assert_eq!(stats.route_minutes, 125);
assert_eq!(stats.overtime_minutes, 55);
assert_eq!(stats.valid_visits, 2);
assert_eq!(stats.scored_travel_legs, 3);
assert_eq!(stats.missing_skill_visits, 0);
assert_eq!(stats.missing_part_visits, 1);
}
#[test]
fn travel_leg_lookup_prefers_row_major_contract() {
let plan = sample_plan(vec![0]);
let leg = leg_for(&plan, 0, 1).expect("leg should exist");
assert_eq!(leg.id, "leg-0-1");
assert!(leg.reachable);
}
#[test]
fn full_constraint_set_reports_expected_hard_penalties() {
let constraints = crate::constraints::create_constraints();
let score = constraints.evaluate_all(&sample_plan(vec![0, 1]));
assert_eq!(score.hard(), -56);
assert!(score.soft() < 0);
}
#[test]
fn route_constraint_match_counts_describe_underlying_route_matches() {
let constraints = crate::constraints::create_constraints();
let results = constraints.evaluate_each(&sample_plan(vec![0, 1]));
let match_count = |name: &str| {
results
.iter()
.find(|result| result.name == name)
.map(|result| result.match_count)
.unwrap_or_else(|| panic!("missing constraint result for {name}"))
};
assert_eq!(match_count("Balance Workload"), 1);
assert_eq!(match_count("Minimize Travel"), 3);
assert_eq!(match_count("Priority Slack"), 2);
assert_eq!(match_count("Required Parts"), 1);
assert_eq!(match_count("Shift Capacity"), 1);
assert_eq!(match_count("Territory Affinity"), 2);
}
fn sample_plan(visits: Vec<usize>) -> FieldServicePlan {
let locations = vec![
Location::new(
"loc-0",
"Hub",
"Hub".to_string(),
45_700_000,
9_670_000,
"depot".to_string(),
),
Location::new(
"loc-1",
"Customer 1",
"Customer 1".to_string(),
45_710_000,
9_680_000,
"customer".to_string(),
),
Location::new(
"loc-2",
"Customer 2",
"Customer 2".to_string(),
45_720_000,
9_690_000,
"customer".to_string(),
),
];
let service_visits = vec![
ServiceVisit::new(ServiceVisitInit {
id: "visit-0".to_string(),
name: "Boiler".to_string(),
customer: "Customer 1".to_string(),
location_idx: 1,
duration_minutes: 30,
earliest_minute: 510,
latest_minute: 540,
required_skill_mask: 0b001,
required_parts_mask: 0b010,
priority: 3,
territory: "center".to_string(),
}),
ServiceVisit::new(ServiceVisitInit {
id: "visit-1".to_string(),
name: "Lift".to_string(),
customer: "Customer 2".to_string(),
location_idx: 2,
duration_minutes: 45,
earliest_minute: 540,
latest_minute: 570,
required_skill_mask: 0b001,
required_parts_mask: 0b100,
priority: 2,
territory: "center".to_string(),
}),
];
let travel_legs = row_major_legs(3);
let mut route = TechnicianRoute::new(TechnicianRouteInit {
id: "route-0".to_string(),
technician_id: "tech-0".to_string(),
technician_name: "Ada".to_string(),
color: "#2563eb".to_string(),
start_location_idx: 0,
end_location_idx: 0,
shift_start_minute: 480,
shift_end_minute: 585,
max_route_minutes: 90,
skill_mask: 0b001,
inventory_mask: 0b010,
territory: "center".to_string(),
});
route.visits = visits;
FieldServicePlan::new(locations, service_visits, travel_legs, vec![route])
}
fn row_major_legs(width: usize) -> Vec<TravelLeg> {
(0..width)
.flat_map(|from| {
(0..width).map(move |to| {
let same = from == to;
TravelLeg::new(TravelLegInit {
id: format!("leg-{from}-{to}"),
name: format!("leg-{from}-{to}"),
from_location_idx: from,
to_location_idx: to,
duration_seconds: if same { 0 } else { 600 },
distance_meters: if same { 0 } else { 2_000 },
reachable: true,
})
})
})
.collect()
}