solverforge-deliveries / src /domain /plan_tests.rs
github-actions[bot]
chore: sync uc-deliveries Space
f6213fc
use super::*;
use crate::data::{generate, DemoData};
use crate::domain::{prepare_plan, DeliveryKind, UNASSIGNED_DELIVERY_HARD_PENALTY};
use solverforge::{Director, ScoreDirector, SolverConfig, SolverEvent, SolverManager};
fn tiny_plan() -> Plan {
Plan::new(
"tiny",
vec![
Delivery::new(
0,
"A",
DeliveryKind::Residential,
(39.9526, -75.1652),
1,
(8 * 3600, 18 * 3600),
10 * 60,
),
Delivery::new(
1,
"B",
DeliveryKind::Business,
(39.9626, -75.1752),
1,
(8 * 3600, 18 * 3600),
10 * 60,
),
],
vec![Vehicle::new(0, "Van 1", 4, 39.9526, -75.1652, 8 * 3600)],
)
}
fn prepared_tiny_plan_with_route() -> Plan {
let mut plan = tiny_plan();
plan.routing_mode = RoutingMode::StraightLine;
plan.vehicles[0].delivery_order = vec![0, 1];
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("tokio runtime")
.block_on(async {
prepare_plan(&mut plan)
.await
.expect("plan preparation should work");
});
plan
}
#[test]
fn score_director_populates_vehicle_route_shadows() {
let plan = prepared_tiny_plan_with_route();
assert_eq!(
plan.vehicles[0].route_total_demand, 0,
"prepared transport data should not eagerly populate solver shadows"
);
let mut director = ScoreDirector::with_descriptor(
plan,
crate::constraints::create_constraints(),
Plan::descriptor(),
Plan::entity_count,
);
let score = director.calculate_score();
let vehicle = &director.working_solution().vehicles[0];
assert_eq!(vehicle.total_assigned_demand(), 2);
assert_eq!(vehicle.capacity_overage(), 0);
assert!(
vehicle.total_travel_seconds() > 0,
"route travel should be maintained as a shadow value"
);
assert_eq!(score.hard(), 0);
}
#[test]
fn vehicle_route_shadows_refresh_after_list_variable_changes() {
let plan = prepared_tiny_plan_with_route();
let mut director = ScoreDirector::with_descriptor(
plan,
crate::constraints::create_constraints(),
Plan::descriptor(),
Plan::entity_count,
);
director.calculate_score();
assert_eq!(
director.working_solution().vehicles[0].total_assigned_demand(),
2
);
director.before_variable_changed(0, 0);
director.working_solution_mut().vehicles[0]
.delivery_order
.clear();
director.after_variable_changed(0, 0);
let score = director.calculate_score();
let vehicle = &director.working_solution().vehicles[0];
assert_eq!(vehicle.total_assigned_demand(), 0);
assert_eq!(vehicle.total_travel_seconds(), 0);
assert_eq!(vehicle.time_window_violation_seconds(), 0);
assert_eq!(score.hard(), -(2 * UNASSIGNED_DELIVERY_HARD_PENALTY));
}
#[test]
fn generated_list_runtime_is_non_trivial_and_builds_routes() {
static MANAGER: SolverManager<Plan> = SolverManager::new();
let mut plan = tiny_plan();
plan.routing_mode = RoutingMode::StraightLine;
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("tokio runtime")
.block_on(async {
prepare_plan(&mut plan)
.await
.expect("plan preparation should work");
});
assert!(
Plan::test_has_list_variable(),
"delivery plan should expose a list variable"
);
assert_eq!(Plan::test_total_list_entities(&plan), 1);
assert_eq!(Plan::test_total_list_elements(&plan), 2);
assert!(
!Plan::test_is_trivial(&plan),
"prepared plan should not be trivial"
);
let config =
SolverConfig::from_toml_str(include_str!("../../solver.toml")).expect("valid config");
assert_eq!(
Plan::test_phase_count(&config),
3,
"expected Clarke-Wright construction + list k-opt + local search"
);
let (job_id, mut receiver) = MANAGER.solve(plan).expect("solve should start");
let mut saw_non_empty_best = false;
loop {
match receiver
.blocking_recv()
.expect("event stream should reach a terminal event")
{
SolverEvent::BestSolution { solution, .. } => {
if solution
.vehicles
.iter()
.any(|vehicle| !vehicle.delivery_order.is_empty())
{
saw_non_empty_best = true;
MANAGER.cancel(job_id).expect("job cancel should succeed");
}
}
SolverEvent::Completed { .. } | SolverEvent::Cancelled { .. } => break,
SolverEvent::Failed { error, .. } => {
panic!("solve unexpectedly failed: {error}");
}
SolverEvent::Progress { .. }
| SolverEvent::PauseRequested { .. }
| SolverEvent::Paused { .. }
| SolverEvent::Resumed { .. } => {}
}
}
MANAGER
.delete(job_id)
.expect("completed test job should delete");
assert!(
saw_non_empty_best,
"expected a non-empty best solution before cancellation"
);
}
#[test]
fn seeded_philadelphia_plan_emits_a_non_empty_best_solution() {
static MANAGER: SolverManager<Plan> = SolverManager::new();
let mut plan = generate(DemoData::Philadelphia);
plan.routing_mode = RoutingMode::StraightLine;
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("tokio runtime")
.block_on(async {
prepare_plan(&mut plan)
.await
.expect("plan preparation should work");
});
let (job_id, mut receiver) = MANAGER.solve(plan).expect("solve should start");
let mut saw_non_empty_best = false;
let mut first_non_empty_best: Option<Plan> = None;
loop {
match receiver
.blocking_recv()
.expect("event stream should reach a terminal event")
{
SolverEvent::BestSolution { solution, .. } => {
if solution
.vehicles
.iter()
.any(|vehicle| !vehicle.delivery_order.is_empty())
{
saw_non_empty_best = true;
first_non_empty_best.get_or_insert(solution.clone());
MANAGER.cancel(job_id).expect("job cancel should succeed");
}
}
SolverEvent::Completed { .. } | SolverEvent::Cancelled { .. } => break,
SolverEvent::Failed { error, .. } => {
panic!("solve unexpectedly failed: {error}");
}
SolverEvent::Progress { .. }
| SolverEvent::PauseRequested { .. }
| SolverEvent::Paused { .. }
| SolverEvent::Resumed { .. } => {}
}
}
MANAGER
.delete(job_id)
.expect("completed test job should delete");
assert!(
saw_non_empty_best,
"expected a non-empty best solution for the seeded Philadelphia plan"
);
let best = first_non_empty_best.expect("should retain the first non-empty best solution");
assert!(
best.vehicles
.iter()
.any(|vehicle| vehicle.delivery_order.len() > 1),
"expected at least one multi-stop route after construction"
);
let director = ScoreDirector::with_descriptor(
best.clone(),
crate::constraints::create_constraints(),
Plan::descriptor(),
Plan::entity_count,
);
assert_eq!(director.entity_count(0), Some(best.vehicles.len()));
}