custom_objective/
custom_objective.rs1#[path = "./common/routing.rs"]
12mod common;
13
14use crate::common::define_routing_data;
15use std::iter::once;
16use std::sync::Arc;
17
18use vrp_core::prelude::*;
19
20custom_dimension!(JobPriority typeof bool);
22custom_solution_state!(PriorityFitness typeof Cost);
24
25struct PriorityObjective;
28
29impl FeatureObjective for PriorityObjective {
30 fn fitness(&self, solution: &InsertionContext) -> Cost {
31 let solution_ctx = &solution.solution;
34 solution_ctx.state.get_priority_fitness().copied().unwrap_or_else(|| calculate_solution_fitness(solution_ctx))
37 }
38
39 fn estimate(&self, move_ctx: &MoveContext<'_>) -> Cost {
40 match move_ctx {
42 MoveContext::Route { job, .. } => estimate_job_cost(job),
43 MoveContext::Activity { .. } => 0.,
44 }
45 }
46}
47
48struct PriorityState;
50
51impl FeatureState for PriorityState {
52 fn accept_insertion(&self, solution_ctx: &mut SolutionContext, route_index: usize, _job: &Job) {
53 self.accept_route_state(solution_ctx.routes.get_mut(route_index).unwrap());
55 }
56
57 fn accept_route_state(&self, _route_ctx: &mut RouteContext) {
58 }
60
61 fn accept_solution_state(&self, solution_ctx: &mut SolutionContext) {
62 let fitness = calculate_solution_fitness(solution_ctx);
64 solution_ctx.state.set_priority_fitness(fitness);
65 }
66}
67
68fn estimate_job_cost(job: &Job) -> Cost {
73 job.dimens().get_job_priority().filter(|&is_high_prio| *is_high_prio).map_or(1., |_| 0.)
74}
75
76fn calculate_solution_fitness(solution_ctx: &SolutionContext) -> Cost {
79 solution_ctx.routes.iter().flat_map(|route_ctx| route_ctx.route().tour.jobs()).map(estimate_job_cost).sum::<Cost>()
80}
81
82fn define_problem(goal: GoalContext, transport: Arc<dyn TransportCost>) -> GenericResult<Problem> {
85 let single_jobs = (1..=4)
87 .map(|idx| {
88 SingleBuilder::default()
89 .id(format!("job{idx}").as_str())
90 .demand(Demand::delivery(1))
91 .dimension(|dimens| {
92 dimens.set_job_priority(idx % 2 == 0);
94 })
95 .location(idx)?
96 .build_as_job()
97 })
98 .collect::<Result<Vec<_>, _>>()?;
99
100 let vehicle = VehicleBuilder::default()
102 .id("v1".to_string().as_str())
103 .add_detail(VehicleDetailBuilder::default().set_start_location(0).build()?)
104 .capacity(SingleDimLoad::new(2))
106 .build()?;
107
108 ProblemBuilder::default()
109 .add_jobs(single_jobs.into_iter())
110 .add_vehicles(once(vehicle))
111 .with_goal(goal)
112 .with_transport_cost(transport)
113 .build()
114}
115
116fn define_goal(transport: Arc<dyn TransportCost>) -> GenericResult<GoalContext> {
118 let minimize_unassigned = MinimizeUnassignedBuilder::new("min-unassigned").build()?;
119 let capacity_feature = CapacityFeatureBuilder::<SingleDimLoad>::new("capacity").build()?;
120 let transport_feature = TransportFeatureBuilder::new("min-distance")
121 .set_transport_cost(transport)
122 .set_time_constrained(false)
123 .build_minimize_distance()?;
124
125 let priority_feature = FeatureBuilder::default()
127 .with_name("maximize-priority")
128 .with_objective(PriorityObjective)
129 .with_state(PriorityState)
130 .build()?;
131
132 GoalContextBuilder::with_features(&[priority_feature, minimize_unassigned, transport_feature, capacity_feature])?
134 .build()
135}
136
137fn main() -> GenericResult<()> {
138 let transport = Arc::new(define_routing_data()?);
139
140 let goal = define_goal(transport.clone())?;
141 let problem = Arc::new(define_problem(goal, transport)?);
142
143 let config = VrpConfigBuilder::new(problem.clone()).prebuild()?.with_max_generations(Some(10)).build()?;
144
145 let solution = Solver::new(problem, config).solve()?;
147
148 assert_eq!(solution.unassigned.len(), 2, "expected two assigned jobs due to capacity constraint");
149 assert_eq!(solution.routes.len(), 1, "only one tour should be there");
150 assert_eq!(
151 solution.get_locations().map(Iterator::collect::<Vec<_>>).collect::<Vec<_>>(),
152 vec![vec![0, 2, 4]],
153 "tour doesn't serve only top-prio jobs"
154 );
155 assert_eq!(solution.cost, 545., "unexpected cost - closest to depot jobs should be assigned");
156
157 Ok(())
158}