1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
//! This example demonstrates how to add a custom hard constraint to a CVRP variant.
//!
//! Here, we introduce a hardware requirement of a job so that only vehicles with matching
//! hardware capabilities can be used to serve it.
//!
//! Key points of implementation:
//! - adding custom dimension (property) for a job and a vehicle with dedicated [custom_dimension] macro
//! - assigning hardware requirements to the jobs and vehicles by setting custom properties accordingly
//! - adding custom [FeatureConstraint] which reads custom properties from the job and the vehicle and checks whether they match
//!

#[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::*;

// Specify two custom properties: one for a job and one for a vehicle
custom_dimension!(JobHardware typeof String);
custom_dimension!(VehicleHardware typeof HashSet<String>);

/// Provides a way to put custom hard constraints on job/activity - vehicle assignment.
struct HardwareConstraint {
    code: ViolationCode,
}

impl FeatureConstraint for HardwareConstraint {
    fn evaluate(&self, move_ctx: &MoveContext<'_>) -> Option<ConstraintViolation> {
        match move_ctx {
            // matching job to route is evaluated before activity matching, so use it to improve search procedure
            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),
                }
            }
            // matching activity to route is called for every possible insertion point in the tour
            // we don't need it here as hard constraint can be validated on route-job level
            MoveContext::Activity { .. } => None,
        }
    }
}

/// Specifies a CVRP problem variant: 4 delivery jobs with demand=1 and 2 vehicles with capacity=2 in each.
fn define_problem(goal: GoalContext, transport: Arc<dyn TransportCost + Send + Sync>) -> GenericResult<Problem> {
    // create 4 jobs when second and forth have fridge requirement
    let single_jobs = (1..=4)
        .map(|idx| {
            SingleBuilder::default()
                .id(format!("job{idx}").as_str())
                .demand(Demand::delivery(1))
                .dimension(|dimens| {
                    // all jobs have fridge requirements, but only one vehicle will be allowed to serve them
                    dimens.set_job_hardware("fridge".to_string());
                })
                .location(idx)?
                .build_as_job()
        })
        .collect::<Result<Vec<_>, _>>()?;

    // create 2 vehicles
    let vehicles = (1..=2)
        .map(|idx| {
            VehicleBuilder::default()
                .id(format!("v{idx}").as_str())
                .add_detail(
                    VehicleDetailBuilder::default()
                        // vehicle starts at location with index 0 in routing matrix
                        .set_start_location(0)
                        // vehicle should return to location with index 0
                        .set_end_location(0)
                        .build()?,
                )
                .dimension(|dimens| {
                    if idx % 2 == 0 {
                        // only one vehicle has a hardware requirement set to 'fridge'
                        dimens.set_vehicle_hardware(once("fridge".to_string()).collect());
                    }
                })
                // each vehicle has capacity=2, so it can serve at most 2 jobs
                .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()
}

/// Defines CVRP variant with a custom constraint as a goal of optimization.
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()?;
    // create our custom feature
    let hardware_feature = FeatureBuilder::default()
        .with_name("hardware")
        .with_constraint(HardwareConstraint { code: ViolationCode::default() })
        .build()?;

    // configure goal of optimization
    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()?;

    // run the VRP solver and get the best known solution
    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");

    // simple way to explore the solution, more advanced are available too
    println!(
        "\nIn solution, locations are visited in the following order:\n{:?}\n",
        solution.get_locations().map(Iterator::collect::<Vec<_>>).collect::<Vec<_>>()
    );

    Ok(())
}