custom_objective/
custom_objective.rs

1//! This example demonstrates how to add a job priority feature which prioritized assignment of
2//! some jobs over others. It is based on closed (no return to a depot) CVRP variant.
3//!
4//! Here, we assign a top priority to a few jobs, so that we expect them to be preferred for assignment.
5//!
6//! Key points here:
7//! - how to define custom objective function with [FeatureObjective] and how to use it within the solver's framework
8//! - how to use state management capabilities for custom fitness calculation optimization with [FeatureState]
9//!
10
11#[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
20// a property for the job priority: true if it is a high prio job
21custom_dimension!(JobPriority typeof bool);
22// a state property to keep track of solution fitness for the priority feature
23custom_solution_state!(PriorityFitness typeof Cost);
24
25/// Provides a way to guide the search to achieve a goal of optimization considering priority jobs
26/// assignment as the most important aspect.
27struct PriorityObjective;
28
29impl FeatureObjective for PriorityObjective {
30    fn fitness(&self, solution: &InsertionContext) -> Cost {
31        // estimate global objective fitness: get the number of jobs with top priority in the solution
32
33        let solution_ctx = &solution.solution;
34        // read it from state if present: this is a performance optimization as fitness function
35        // is called frequently. You don't need the state if fitness can be calculated very fast
36        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        // estimate local objective fitness: map job priority to cost penalty
41        match move_ctx {
42            MoveContext::Route { job, .. } => estimate_job_cost(job),
43            MoveContext::Activity { .. } => 0.,
44        }
45    }
46}
47
48/// Provides a way to cache some calculations to speed up overall search procedure.
49struct PriorityState;
50
51impl FeatureState for PriorityState {
52    fn accept_insertion(&self, solution_ctx: &mut SolutionContext, route_index: usize, _job: &Job) {
53        // normally, delegate state updating to accept_route_state
54        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        // do nothing for this example
59    }
60
61    fn accept_solution_state(&self, solution_ctx: &mut SolutionContext) {
62        // once the solution state is fixed, calculate the feature's fitness and store it in state
63        let fitness = calculate_solution_fitness(solution_ctx);
64        solution_ctx.state.set_priority_fitness(fitness);
65    }
66}
67
68/// Estimates an impact of a job in the solution:
69/// As the solver tries to minimize each objective:
70///  - return 1 if the job is not prioritized
71///  - return 0 if the job is a top-prio
72fn 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
76/// Estimates solution fitness: iterate over every inserted job in the solution,
77/// estimate the cost of its insertion and sum it.
78fn 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
82/// Specifies four delivery jobs with demand=1 (two of them are with top priority) and a single vehicle
83/// with capacity=2 which doesn't need to return to the depot.
84fn define_problem(goal: GoalContext, transport: Arc<dyn TransportCost>) -> GenericResult<Problem> {
85    // create 4 jobs where two are having top prio
86    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                    // mark two jobs as top priority (2 and 4 locations)
93                    dimens.set_job_priority(idx % 2 == 0);
94                })
95                .location(idx)?
96                .build_as_job()
97        })
98        .collect::<Result<Vec<_>, _>>()?;
99
100    // define a single vehicle with limited capacity which doesn't need to return back to the depot
101    let vehicle = VehicleBuilder::default()
102        .id("v1".to_string().as_str())
103        .add_detail(VehicleDetailBuilder::default().set_start_location(0).build()?)
104        // only two jobs can be served by the vehicle
105        .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
116/// Defines optimization goal as CVRP variant with a priority objective function on top.
117fn 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    // create our custom feature that consists of an objective and a state
126    let priority_feature = FeatureBuilder::default()
127        .with_name("maximize-priority")
128        .with_objective(PriorityObjective)
129        .with_state(PriorityState)
130        .build()?;
131
132    // configure goal of optimization
133    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    // run the VRP solver and get the best known solution
146    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}