custom_constraint/
custom_constraint.rs1#[path = "./common/routing.rs"]
13mod common;
14use crate::common::define_routing_data;
15
16use std::collections::HashSet;
17use std::iter::once;
18use std::sync::Arc;
19use vrp_core::prelude::*;
20
21custom_dimension!(JobHardware typeof String);
23custom_dimension!(VehicleHardware typeof HashSet<String>);
24
25struct HardwareConstraint {
27 code: ViolationCode,
28}
29
30impl FeatureConstraint for HardwareConstraint {
31 fn evaluate(&self, move_ctx: &MoveContext<'_>) -> Option<ConstraintViolation> {
32 match move_ctx {
33 MoveContext::Route { route_ctx, job, .. } => {
35 let hardware_vehicle = route_ctx.route().actor.vehicle.dimens.get_vehicle_hardware();
36 let hardware_job = job.dimens().get_job_hardware();
37
38 match (hardware_job, hardware_vehicle) {
39 (None, _) => None,
40 (Some(hw_job), Some(hw_vehicle)) if hw_vehicle.contains(hw_job) => None,
41 _ => ConstraintViolation::fail(self.code),
42 }
43 }
44 MoveContext::Activity { .. } => None,
47 }
48 }
49}
50
51fn define_problem(goal: GoalContext, transport: Arc<dyn TransportCost>) -> GenericResult<Problem> {
53 let single_jobs = (1..=4)
55 .map(|idx| {
56 SingleBuilder::default()
57 .id(format!("job{idx}").as_str())
58 .demand(Demand::delivery(1))
59 .dimension(|dimens| {
60 dimens.set_job_hardware("fridge".to_string());
62 })
63 .location(idx)?
64 .build_as_job()
65 })
66 .collect::<Result<Vec<_>, _>>()?;
67
68 let vehicles = (1..=2)
70 .map(|idx| {
71 VehicleBuilder::default()
72 .id(format!("v{idx}").as_str())
73 .add_detail(
74 VehicleDetailBuilder::default()
75 .set_start_location(0)
77 .set_end_location(0)
79 .build()?,
80 )
81 .dimension(|dimens| {
82 if idx % 2 == 0 {
83 dimens.set_vehicle_hardware(once("fridge".to_string()).collect());
85 }
86 })
87 .capacity(SingleDimLoad::new(2))
89 .build()
90 })
91 .collect::<Result<Vec<_>, _>>()?;
92
93 ProblemBuilder::default()
94 .add_jobs(single_jobs.into_iter())
95 .add_vehicles(vehicles.into_iter())
96 .with_goal(goal)
97 .with_transport_cost(transport)
98 .build()
99}
100
101fn define_goal(transport: Arc<dyn TransportCost>) -> GenericResult<GoalContext> {
103 let minimize_unassigned = MinimizeUnassignedBuilder::new("min-unassigned").build()?;
104 let capacity_feature = CapacityFeatureBuilder::<SingleDimLoad>::new("capacity").build()?;
105 let transport_feature = TransportFeatureBuilder::new("min-distance")
106 .set_transport_cost(transport)
107 .set_time_constrained(false)
108 .build_minimize_distance()?;
109 let hardware_feature = FeatureBuilder::default()
111 .with_name("hardware")
112 .with_constraint(HardwareConstraint { code: ViolationCode::default() })
113 .build()?;
114
115 GoalContextBuilder::with_features(&[minimize_unassigned, transport_feature, capacity_feature, hardware_feature])?
117 .build()
118}
119
120fn main() -> GenericResult<()> {
121 let transport = Arc::new(define_routing_data()?);
122
123 let goal = define_goal(transport.clone())?;
124 let problem = Arc::new(define_problem(goal, transport)?);
125
126 let config = VrpConfigBuilder::new(problem.clone()).prebuild()?.with_max_generations(Some(10)).build()?;
127
128 let solution = Solver::new(problem, config).solve()?;
130
131 assert_eq!(
132 solution.unassigned.len(),
133 2,
134 "expected two assigned jobs due to hardware requirement and capacity constraints"
135 );
136 assert_eq!(solution.routes.len(), 1, "only one tour should be there: second vehicle cannot serve hardware jobs");
137 assert_eq!(solution.cost, 1050., "unexpected cost - closest to depot jobs should be assigned");
138
139 println!(
141 "\nIn solution, locations are visited in the following order:\n{:?}\n",
142 solution.get_locations().map(Iterator::collect::<Vec<_>>).collect::<Vec<_>>()
143 );
144
145 Ok(())
146}