vrp_core/construction/features/
capacity.rs

1//! Provides feature to add capacity limitation on a vehicle.
2
3#[cfg(test)]
4#[path = "../../../tests/unit/construction/features/capacity_test.rs"]
5mod capacity_test;
6
7use super::*;
8use crate::construction::enablers::*;
9use crate::models::solution::Activity;
10use std::marker::PhantomData;
11use std::sync::Arc;
12
13custom_activity_state!(CurrentCapacity typeof T: LoadOps);
14
15custom_activity_state!(MaxFutureCapacity typeof T: LoadOps);
16
17custom_activity_state!(MaxPastCapacity typeof T: LoadOps);
18
19custom_tour_state!(MaxVehicleLoad typeof Float);
20
21custom_dimension!(VehicleCapacity typeof T: LoadOps);
22
23/// A trait to get or set job demand.
24pub trait JobDemandDimension {
25    /// Sets job demand.
26    fn set_job_demand<T: LoadOps>(&mut self, demand: Demand<T>) -> &mut Self;
27
28    /// Gets job demand.
29    fn get_job_demand<T: LoadOps>(&self) -> Option<&Demand<T>>;
30}
31
32/// Provides a way to build capacity limit feature.
33pub struct CapacityFeatureBuilder<T: LoadOps> {
34    name: String,
35    route_intervals: Option<RouteIntervals>,
36    violation_code: Option<ViolationCode>,
37    phantom_data: PhantomData<T>,
38}
39
40impl<T: LoadOps> CapacityFeatureBuilder<T> {
41    /// Creates a new instance of `CapacityFeatureBuilder`
42    pub fn new(name: &str) -> Self {
43        Self { name: name.to_string(), route_intervals: None, violation_code: None, phantom_data: Default::default() }
44    }
45
46    /// Sets constraint violation code which is used to report back the reason of job's unassignment.
47    pub fn set_violation_code(mut self, violation_code: ViolationCode) -> Self {
48        self.violation_code = Some(violation_code);
49        self
50    }
51
52    /// Sets route intervals to trigger multi trip behavior (used with reload flavors).
53    pub fn set_route_intervals(mut self, route_intervals: RouteIntervals) -> Self {
54        self.route_intervals = Some(route_intervals);
55        self
56    }
57
58    /// Builds a feature.
59    pub fn build(self) -> GenericResult<Feature> {
60        let name = self.name.as_str();
61        let violation_code = self.violation_code.unwrap_or_default();
62
63        if let Some(route_intervals) = self.route_intervals {
64            create_multi_trip_feature(
65                name,
66                violation_code,
67                MarkerInsertionPolicy::Last,
68                Arc::new(CapacitatedMultiTrip::<T> { route_intervals, violation_code, phantom: Default::default() }),
69            )
70        } else {
71            create_multi_trip_feature(
72                name,
73                violation_code,
74                MarkerInsertionPolicy::Last,
75                Arc::new(CapacitatedMultiTrip::<T> {
76                    route_intervals: RouteIntervals::Single,
77                    violation_code,
78                    phantom: Default::default(),
79                }),
80            )
81        }
82    }
83}
84
85impl<T> FeatureConstraint for CapacitatedMultiTrip<T>
86where
87    T: LoadOps,
88{
89    fn evaluate(&self, move_ctx: &MoveContext<'_>) -> Option<ConstraintViolation> {
90        match move_ctx {
91            MoveContext::Route { route_ctx, job, .. } => self.evaluate_job(route_ctx, job),
92            MoveContext::Activity { route_ctx, activity_ctx } => self.evaluate_activity(route_ctx, activity_ctx),
93        }
94    }
95
96    fn merge(&self, source: Job, candidate: Job) -> Result<Job, ViolationCode> {
97        match (&source, &candidate) {
98            (Job::Single(s_source), Job::Single(s_candidate)) => {
99                let source_demand: Option<&Demand<T>> = s_source.dimens.get_job_demand();
100                let candidate_demand: Option<&Demand<T>> = s_candidate.dimens.get_job_demand();
101
102                match (source_demand, candidate_demand) {
103                    (None, None) | (Some(_), None) => Ok(source),
104                    _ => {
105                        let source_demand = source_demand.cloned().unwrap_or_default();
106                        let candidate_demand = candidate_demand.cloned().unwrap_or_default();
107                        let new_demand = source_demand + candidate_demand;
108
109                        let mut single = Single { places: s_source.places.clone(), dimens: s_source.dimens.clone() };
110                        single.dimens.set_job_demand(new_demand);
111
112                        Ok(Job::Single(Arc::new(single)))
113                    }
114                }
115            }
116            _ => Err(self.violation_code),
117        }
118    }
119}
120
121struct CapacitatedMultiTrip<T>
122where
123    T: LoadOps,
124{
125    route_intervals: RouteIntervals,
126    violation_code: ViolationCode,
127    phantom: PhantomData<T>,
128}
129
130impl<T> MultiTrip for CapacitatedMultiTrip<T>
131where
132    T: LoadOps,
133{
134    fn get_route_intervals(&self) -> &RouteIntervals {
135        &self.route_intervals
136    }
137
138    fn get_constraint(&self) -> &(dyn FeatureConstraint) {
139        self
140    }
141
142    fn recalculate_states(&self, route_ctx: &mut RouteContext) {
143        let marker_intervals = self
144            .get_route_intervals()
145            .get_marker_intervals(route_ctx)
146            .cloned()
147            .unwrap_or_else(|| vec![(0, route_ctx.route().tour.total() - 1)]);
148
149        let tour_len = route_ctx.route().tour.total();
150
151        let mut current_capacities = vec![T::default(); tour_len];
152        let mut max_past_capacities = vec![T::default(); tour_len];
153        let mut max_future_capacities = vec![T::default(); tour_len];
154
155        let (_, max_load) =
156            marker_intervals.into_iter().fold((T::default(), T::default()), |(acc, max), (start_idx, end_idx)| {
157                let route = route_ctx.route();
158
159                // determine static deliveries loaded at the begin and static pickups brought to the end
160                let (start_delivery, end_pickup) = route.tour.activities_slice(start_idx, end_idx).iter().fold(
161                    (acc, T::default()),
162                    |acc, activity| {
163                        self.get_demand(activity)
164                            .map(|demand| (acc.0 + demand.delivery.0, acc.1 + demand.pickup.0))
165                            .unwrap_or_else(|| acc)
166                    },
167                );
168
169                // determine actual load at each activity and max discovered in the past
170                let (current, _) = route.tour.activities_slice(start_idx, end_idx).iter().enumerate().fold(
171                    (start_delivery, T::default()),
172                    |(current, max), (idx, activity)| {
173                        let activity_idx = start_idx + idx;
174                        let change = self.get_demand(activity).map(|demand| demand.change()).unwrap_or_default();
175
176                        let current = current + change;
177                        let max = max.max_load(current);
178
179                        current_capacities[activity_idx] = current;
180                        max_past_capacities[activity_idx] = max;
181
182                        (current, max)
183                    },
184                );
185
186                let current_max = (start_idx..=end_idx).rev().fold(current, |max, activity_idx| {
187                    let max = max.max_load(current_capacities[activity_idx]);
188                    max_future_capacities[activity_idx] = max;
189
190                    max
191                });
192
193                (current - end_pickup, current_max.max_load(max))
194            });
195
196        route_ctx.state_mut().set_current_capacity_states(current_capacities);
197        route_ctx.state_mut().set_max_past_capacity_states(max_past_capacities);
198        route_ctx.state_mut().set_max_future_capacity_states(max_future_capacities);
199
200        if let Some(capacity) = route_ctx.route().actor.clone().vehicle.dimens.get_vehicle_capacity::<T>() {
201            route_ctx.state_mut().set_max_vehicle_load(max_load.ratio(capacity));
202        }
203    }
204
205    fn try_recover(&self, _: &mut SolutionContext, _: &[usize], _: &[Job]) -> bool {
206        // TODO try to recover if multi-trip is used
207        false
208    }
209}
210
211impl<T> CapacitatedMultiTrip<T>
212where
213    T: LoadOps,
214{
215    fn evaluate_job(&self, route_ctx: &RouteContext, job: &Job) -> Option<ConstraintViolation> {
216        let can_handle = match job {
217            Job::Single(job) => self.can_handle_demand_on_intervals(route_ctx, job.dimens.get_job_demand(), None),
218            Job::Multi(job) => job
219                .jobs
220                .iter()
221                .any(|job| self.can_handle_demand_on_intervals(route_ctx, job.dimens.get_job_demand(), None)),
222        };
223
224        if can_handle {
225            ConstraintViolation::success()
226        } else {
227            ConstraintViolation::fail(self.violation_code)
228        }
229    }
230
231    fn evaluate_activity(
232        &self,
233        route_ctx: &RouteContext,
234        activity_ctx: &ActivityContext,
235    ) -> Option<ConstraintViolation> {
236        let demand = self.get_demand(activity_ctx.target);
237
238        let violation = if activity_ctx.target.retrieve_job().map_or(false, |job| job.as_multi().is_some()) {
239            // NOTE multi job has dynamic demand which can go in another interval
240            if self.can_handle_demand_on_intervals(route_ctx, demand, Some(activity_ctx.index)) {
241                None
242            } else {
243                Some(false)
244            }
245        } else {
246            has_demand_violation(route_ctx, activity_ctx.index, demand, !self.has_markers(route_ctx))
247        };
248
249        violation.map(|stopped| ConstraintViolation { code: self.violation_code, stopped })
250    }
251
252    fn has_markers(&self, route_ctx: &RouteContext) -> bool {
253        self.route_intervals.get_marker_intervals(route_ctx).map_or(false, |intervals| intervals.len() > 1)
254    }
255
256    fn can_handle_demand_on_intervals(
257        &self,
258        route_ctx: &RouteContext,
259        demand: Option<&Demand<T>>,
260        insert_idx: Option<usize>,
261    ) -> bool {
262        let has_demand_violation = |activity_idx: usize| has_demand_violation(route_ctx, activity_idx, demand, true);
263
264        let has_demand_violation_on_borders = |start_idx: usize, end_idx: usize| {
265            has_demand_violation(start_idx).is_none() || has_demand_violation(end_idx).is_none()
266        };
267
268        self.route_intervals
269            .get_marker_intervals(route_ctx)
270            .map(|intervals| {
271                if let Some(insert_idx) = insert_idx {
272                    intervals
273                        .iter()
274                        .filter(|(_, end_idx)| insert_idx <= *end_idx)
275                        .all(|(start_idx, _)| has_demand_violation(insert_idx.max(*start_idx)).is_none())
276                } else {
277                    intervals.iter().any(|(start_idx, end_idx)| has_demand_violation_on_borders(*start_idx, *end_idx))
278                }
279            })
280            .unwrap_or_else(|| {
281                if let Some(insert_idx) = insert_idx {
282                    has_demand_violation(insert_idx).is_none()
283                } else {
284                    let last_idx = route_ctx.route().tour.end_idx().unwrap_or_default();
285                    has_demand_violation_on_borders(0, last_idx)
286                }
287            })
288    }
289
290    fn get_demand<'a>(&self, activity: &'a Activity) -> Option<&'a Demand<T>> {
291        activity.job.as_ref().and_then(|single| single.dimens.get_job_demand())
292    }
293}
294
295fn has_demand_violation<T: LoadOps>(
296    route_ctx: &RouteContext,
297    pivot_idx: usize,
298    demand: Option<&Demand<T>>,
299    stopped: bool,
300) -> Option<bool> {
301    let capacity: Option<&T> = route_ctx.route().actor.vehicle.dimens.get_vehicle_capacity();
302    let demand = demand?;
303
304    let capacity = if let Some(capacity) = capacity {
305        capacity
306    } else {
307        return Some(stopped);
308    };
309
310    let state = route_ctx.state();
311
312    // check how static delivery affects a past max load
313    if demand.delivery.0.is_not_empty() {
314        let past: T = state.get_max_past_capacity_at(pivot_idx).copied().unwrap_or_default();
315        if !capacity.can_fit(&(past + demand.delivery.0)) {
316            return Some(stopped);
317        }
318    }
319
320    // check how static pickup affect future max load
321    if demand.pickup.0.is_not_empty() {
322        let future: T = state.get_max_future_capacity_at(pivot_idx).copied().unwrap_or_default();
323        if !capacity.can_fit(&(future + demand.pickup.0)) {
324            return Some(false);
325        }
326    }
327
328    // check dynamic load change
329    let change = demand.change();
330    if change.is_not_empty() {
331        let future: T = state.get_max_future_capacity_at(pivot_idx).copied().unwrap_or_default();
332        if !capacity.can_fit(&(future + change)) {
333            return Some(false);
334        }
335
336        let current: T = state.get_current_capacity_at(pivot_idx).copied().unwrap_or_default();
337        if !capacity.can_fit(&(current + change)) {
338            return Some(false);
339        }
340    }
341
342    None
343}
344
345// TODO extend macro to support this.
346struct JobDemandDimenKey;
347impl JobDemandDimension for Dimensions {
348    fn set_job_demand<T: LoadOps>(&mut self, demand: Demand<T>) -> &mut Self {
349        self.set_value::<JobDemandDimenKey, _>(demand);
350        self
351    }
352
353    fn get_job_demand<T: LoadOps>(&self) -> Option<&Demand<T>> {
354        self.get_value::<JobDemandDimenKey, _>()
355    }
356}