#[path = "./common/routing.rs"]
mod common;
use crate::common::define_routing_data;
use std::collections::HashSet;
use std::iter::once;
use std::sync::Arc;
use vrp_core::prelude::*;
custom_dimension!(JobHardware typeof String);
custom_dimension!(VehicleHardware typeof HashSet<String>);
struct HardwareConstraint {
code: ViolationCode,
}
impl FeatureConstraint for HardwareConstraint {
fn evaluate(&self, move_ctx: &MoveContext<'_>) -> Option<ConstraintViolation> {
match move_ctx {
MoveContext::Route { route_ctx, job, .. } => {
let hardware_vehicle = route_ctx.route().actor.vehicle.dimens.get_vehicle_hardware();
let hardware_job = job.dimens().get_job_hardware();
match (hardware_job, hardware_vehicle) {
(None, _) => None,
(Some(hw_job), Some(hw_vehicle)) if hw_vehicle.contains(hw_job) => None,
_ => ConstraintViolation::fail(self.code),
}
}
MoveContext::Activity { .. } => None,
}
}
}
fn define_problem(goal: GoalContext, transport: Arc<dyn TransportCost + Send + Sync>) -> GenericResult<Problem> {
let single_jobs = (1..=4)
.map(|idx| {
SingleBuilder::default()
.id(format!("job{idx}").as_str())
.demand(Demand::delivery(1))
.dimension(|dimens| {
dimens.set_job_hardware("fridge".to_string());
})
.location(idx)?
.build_as_job()
})
.collect::<Result<Vec<_>, _>>()?;
let vehicles = (1..=2)
.map(|idx| {
VehicleBuilder::default()
.id(format!("v{idx}").as_str())
.add_detail(
VehicleDetailBuilder::default()
.set_start_location(0)
.set_end_location(0)
.build()?,
)
.dimension(|dimens| {
if idx % 2 == 0 {
dimens.set_vehicle_hardware(once("fridge".to_string()).collect());
}
})
.capacity(SingleDimLoad::new(2))
.build()
})
.collect::<Result<Vec<_>, _>>()?;
ProblemBuilder::default()
.add_jobs(single_jobs.into_iter())
.add_vehicles(vehicles.into_iter())
.with_goal(goal)
.with_transport_cost(transport)
.build()
}
fn define_goal(transport: Arc<dyn TransportCost + Send + Sync>) -> GenericResult<GoalContext> {
let minimize_unassigned = MinimizeUnassignedBuilder::new("min-unassigned").build()?;
let capacity_feature = CapacityFeatureBuilder::<SingleDimLoad>::new("capacity").build()?;
let transport_feature = TransportFeatureBuilder::new("min-distance")
.set_transport_cost(transport)
.set_time_constrained(false)
.build_minimize_distance()?;
let hardware_feature = FeatureBuilder::default()
.with_name("hardware")
.with_constraint(HardwareConstraint { code: ViolationCode::default() })
.build()?;
GoalContextBuilder::with_features(&[minimize_unassigned, transport_feature, capacity_feature, hardware_feature])?
.build()
}
fn main() -> GenericResult<()> {
let transport = Arc::new(define_routing_data()?);
let goal = define_goal(transport.clone())?;
let problem = Arc::new(define_problem(goal, transport)?);
let config = VrpConfigBuilder::new(problem.clone()).prebuild()?.with_max_generations(Some(10)).build()?;
let solution = Solver::new(problem, config).solve()?;
assert_eq!(
solution.unassigned.len(),
2,
"expected two assigned jobs due to hardware requirement and capacity constraints"
);
assert_eq!(solution.routes.len(), 1, "only one tour should be there: second vehicle cannot serve hardware jobs");
assert_eq!(solution.cost, 1050., "unexpected cost - closest to depot jobs should be assigned");
println!(
"\nIn solution, locations are visited in the following order:\n{:?}\n",
solution.get_locations().map(Iterator::collect::<Vec<_>>).collect::<Vec<_>>()
);
Ok(())
}