Skip to main content

vrp_core/construction/features/
compatibility.rs

1//! A compatibility feature provides the way to avoid assigning some jobs in the same tour.
2
3#[cfg(test)]
4#[path = "../../../tests/unit/construction/features/compatibility_test.rs"]
5mod compatibility_test;
6
7use super::*;
8
9custom_dimension!(JobCompatibility typeof String);
10custom_tour_state!(CurrentCompatibility typeof String);
11
12/// Creates a compatibility feature as a hard constraint.
13pub fn create_compatibility_feature(name: &str, code: ViolationCode) -> Result<Feature, GenericError> {
14    FeatureBuilder::default()
15        .with_name(name)
16        .with_constraint(CompatibilityConstraint { code })
17        .with_state(CompatibilityState {})
18        .build()
19}
20
21struct CompatibilityConstraint {
22    code: ViolationCode,
23}
24
25impl FeatureConstraint for CompatibilityConstraint {
26    fn evaluate(&self, move_ctx: &MoveContext<'_>) -> Option<ConstraintViolation> {
27        match move_ctx {
28            MoveContext::Route { route_ctx, job, .. } => job.dimens().get_job_compatibility().and_then(|job_compat| {
29                match route_ctx.state().get_current_compatibility() {
30                    None => None,
31                    Some(route_compat) if job_compat == route_compat => None,
32                    _ => ConstraintViolation::fail(self.code),
33                }
34            }),
35            MoveContext::Activity { .. } => None,
36        }
37    }
38
39    fn merge(&self, source: Job, candidate: Job) -> Result<Job, ViolationCode> {
40        match (source.dimens().get_job_compatibility(), candidate.dimens().get_job_compatibility()) {
41            (None, None) => Ok(source),
42            (Some(s_compat), Some(c_compat)) if s_compat == c_compat => Ok(source),
43            _ => Err(self.code),
44        }
45    }
46}
47
48struct CompatibilityState {}
49
50impl FeatureState for CompatibilityState {
51    fn accept_insertion(&self, solution_ctx: &mut SolutionContext, route_index: usize, job: &Job) {
52        if job.dimens().get_job_compatibility().is_some() {
53            self.accept_route_state(solution_ctx.routes.get_mut(route_index).unwrap())
54        }
55    }
56
57    fn accept_route_state(&self, route_ctx: &mut RouteContext) {
58        let new_comp = get_route_compatibility(route_ctx);
59        let current_compat = route_ctx.state().get_current_compatibility();
60
61        match (new_comp, current_compat) {
62            (None, None) => {}
63            (None, Some(_)) => {
64                route_ctx.state_mut().remove_current_compatibility();
65            }
66            (Some(value), None) | (Some(value), Some(_)) => route_ctx.state_mut().set_current_compatibility(value),
67        }
68    }
69
70    fn accept_solution_state(&self, _: &mut SolutionContext) {}
71}
72
73fn get_route_compatibility(route_ctx: &RouteContext) -> Option<String> {
74    route_ctx.route().tour.jobs().filter_map(|job| job.dimens().get_job_compatibility()).next().cloned()
75}