vrp_core/construction/features/
reloads.rs

1//! This module provides functionality for reloading vehicle with new jobs at some place later in
2//! the tour. This is used to overcome a vehicle capacity limit. The feature has two flavors:
3//!  - simple: a basic reload place with unlimited number of jobs which can be loaded/unloaded from there
4//!  - shared: a resource constrained reload place
5
6#[cfg(test)]
7#[path = "../../../tests/unit/construction/features/reloads_test.rs"]
8mod reloads_test;
9
10use crate::construction::enablers::{FeatureCombinator, RouteIntervals, RouteIntervalsState};
11use crate::construction::features::capacity::*;
12use crate::construction::heuristics::*;
13use crate::models::common::{Demand, LoadOps, MultiDimLoad, SingleDimLoad};
14use crate::models::problem::{Job, Single};
15use crate::models::solution::{Activity, Route};
16use crate::models::*;
17use rosomaxa::utils::{GenericError, GenericResult};
18use std::cmp::Ordering;
19use std::collections::HashMap;
20use std::ops::{Add, Range, RangeInclusive, Sub};
21use std::sync::Arc;
22
23/// Represents a shared unique resource which is used to model reload with capacity constraint.
24pub trait SharedResource: LoadOps + Add + Sub + PartialOrd + Copy + Sized + Send + Sync + Default + 'static {}
25
26/// Represents a shared resource id.
27pub type SharedResourceId = usize;
28
29/// Provides a way to build reload feature with various parameters.
30#[allow(clippy::type_complexity)]
31pub struct ReloadFeatureFactory<T: LoadOps> {
32    name: String,
33    capacity_code: Option<ViolationCode>,
34    resource_code: Option<ViolationCode>,
35
36    is_reload_single_fn: Option<Arc<dyn Fn(&Single) -> bool + Send + Sync>>,
37    belongs_to_route_fn: Option<Arc<dyn Fn(&Route, &Job) -> bool + Send + Sync>>,
38    load_schedule_threshold_fn: Option<Box<dyn Fn(&T) -> T + Send + Sync>>,
39
40    // these fields are needed to be set for shared reload flavor
41    shared_resource_capacity_fn: Option<SharedResourceCapacityFn<T>>,
42    shared_resource_demand_fn: Option<SharedResourceDemandFn<T>>,
43    is_partial_solution_fn: Option<PartialSolutionFn>,
44}
45
46impl<T: SharedResource> ReloadFeatureFactory<T> {
47    /// Sets resource constraint violation code which is used to report back the reason of job's unassignment.
48    pub fn set_resource_code(mut self, code: ViolationCode) -> Self {
49        self.resource_code = Some(code);
50        self
51    }
52
53    /// Sets a shared resource capacity function.
54    pub fn set_shared_resource_capacity<F>(mut self, func: F) -> Self
55    where
56        F: Fn(&Activity) -> Option<(T, SharedResourceId)> + Send + Sync + 'static,
57    {
58        self.shared_resource_capacity_fn = Some(Arc::new(func));
59        self
60    }
61
62    /// Sets a shared resource demand function.
63    pub fn set_shared_demand_capacity<F>(mut self, func: F) -> Self
64    where
65        F: Fn(&Single) -> Option<T> + Send + Sync + 'static,
66    {
67        self.shared_resource_demand_fn = Some(Arc::new(func));
68        self
69    }
70
71    /// Sets a function which tells whether a given solution is partial.
72    pub fn set_is_partial_solution<F>(mut self, func: F) -> Self
73    where
74        F: Fn(&SolutionContext) -> bool + Send + Sync + 'static,
75    {
76        self.is_partial_solution_fn = Some(Arc::new(func));
77        self
78    }
79
80    /// Builds a shared reload flavor.
81    pub fn build_shared(mut self) -> GenericResult<Feature> {
82        let violation_code = self.resource_code.unwrap_or_default();
83
84        // read shared resource flavor properties
85        let Some(((resource_capacity_fn, resource_demand_fn), is_partial_solution_fn)) = self
86            .shared_resource_capacity_fn
87            .take()
88            .zip(self.shared_resource_demand_fn.take())
89            .zip(self.is_partial_solution_fn.take())
90        else {
91            return Err("shared_resource_capacity, shared_resource_demand and partial_solution must be set for shared reload feature".into());
92        };
93
94        let shared_resource_threshold_fn: SharedResourceThresholdFn<T> =
95            Box::new(move |route_ctx: &RouteContext, activity_idx, demand| {
96                route_ctx
97                    .state()
98                    .get_activity_state::<SharedResourceStateKey, T>(activity_idx)
99                    .map_or(true, |resource_available| resource_available.can_fit(demand))
100            });
101
102        let simple_reload = self.build(Some(shared_resource_threshold_fn))?;
103
104        let shared_resource = FeatureBuilder::default()
105            .with_name(self.name.as_str())
106            .with_constraint(SharedResourceConstraint {
107                violation_code,
108                resource_demand_fn: resource_demand_fn.clone(),
109                is_partial_solution_fn: is_partial_solution_fn.clone(),
110            })
111            .with_state(SharedResourceState { resource_capacity_fn, resource_demand_fn, is_partial_solution_fn })
112            .build()?;
113
114        FeatureCombinator::default().use_name(self.name).add_features(&[simple_reload, shared_resource]).combine()
115    }
116}
117
118impl<T: LoadOps> ReloadFeatureFactory<T> {
119    /// Creates a new instance of `ReloadFeatureFactory` with the given feature name
120    pub fn new(name: &str) -> Self {
121        Self {
122            name: name.to_string(),
123            capacity_code: None,
124            resource_code: None,
125            is_reload_single_fn: None,
126            belongs_to_route_fn: None,
127            load_schedule_threshold_fn: None,
128            shared_resource_capacity_fn: None,
129            shared_resource_demand_fn: None,
130            is_partial_solution_fn: None,
131        }
132    }
133
134    /// Sets capacity constraint violation code which is used to report back the reason of job's unassignment.
135    pub fn set_capacity_code(mut self, code: ViolationCode) -> Self {
136        self.capacity_code = Some(code);
137        self
138    }
139
140    /// Sets a function which specifies whether a given single job can be considered as a reload job.
141    pub fn set_is_reload_single<F>(mut self, func: F) -> Self
142    where
143        F: Fn(&Single) -> bool + Send + Sync + 'static,
144    {
145        self.is_reload_single_fn = Some(Arc::new(func));
146        self
147    }
148
149    /// Sets a function which specifies whether a given route can serve a given job. This function
150    /// should return false, if the job is not reload.
151    pub fn set_belongs_to_route<F>(mut self, func: F) -> Self
152    where
153        F: Fn(&Route, &Job) -> bool + Send + Sync + 'static,
154    {
155        self.belongs_to_route_fn = Some(Arc::new(func));
156        self
157    }
158
159    /// Sets a function which is used to decide whether reload should be considered for assignment
160    /// based on the left vehicle's capacity.
161    pub fn set_load_schedule_threshold<F>(mut self, func: F) -> Self
162    where
163        F: Fn(&T) -> T + Send + Sync + 'static,
164    {
165        self.load_schedule_threshold_fn = Some(Box::new(func));
166        self
167    }
168
169    /// Builds a simple reload flavor.
170    pub fn build_simple(mut self) -> GenericResult<Feature> {
171        self.build(None)
172    }
173
174    fn build(&mut self, shared_resource_threshold_fn: Option<SharedResourceThresholdFn<T>>) -> GenericResult<Feature> {
175        // TODO provide reasonable default to simplify code usage?
176
177        // read common properties
178        let is_marker_single_fn =
179            self.is_reload_single_fn.take().ok_or_else(|| GenericError::from("is_reload_single must be set"))?;
180        let is_assignable_fn =
181            self.belongs_to_route_fn.take().ok_or_else(|| GenericError::from("belongs_to_route must be set"))?;
182        let load_schedule_threshold_fn = self
183            .load_schedule_threshold_fn
184            .take()
185            .ok_or_else(|| GenericError::from("load_schedule_threshold must be set"))?;
186
187        // create route intervals used to control how tour is split into multiple sub-tours
188        let route_intervals = RouteIntervals::Multiple {
189            is_marker_single_fn,
190            is_new_interval_needed_fn: Arc::new(move |route_ctx| {
191                route_ctx
192                    .route()
193                    .tour
194                    .end_idx()
195                    .map(|end_idx| {
196                        let current: T =
197                            route_ctx.state().get_max_past_capacity_at(end_idx).cloned().unwrap_or_default();
198
199                        let max_capacity =
200                            route_ctx.route().actor.vehicle.dimens.get_vehicle_capacity().cloned().unwrap_or_default();
201                        let threshold_capacity = (load_schedule_threshold_fn)(&max_capacity);
202
203                        current.partial_cmp(&threshold_capacity) != Some(Ordering::Less)
204                    })
205                    .unwrap_or(false)
206            }),
207            is_obsolete_interval_fn: Arc::new(move |route_ctx, left, right| {
208                let capacity: T =
209                    route_ctx.route().actor.vehicle.dimens.get_vehicle_capacity().cloned().unwrap_or_default();
210
211                let fold_demand = |range: Range<usize>, demand_fn: fn(&Demand<T>) -> T| {
212                    route_ctx.route().tour.activities_slice(range.start, range.end).iter().fold(
213                        T::default(),
214                        |acc, activity| {
215                            activity
216                                .job
217                                .as_ref()
218                                .and_then(|job| job.dimens.get_job_demand())
219                                .map(|demand| acc + demand_fn(demand))
220                                .unwrap_or_else(|| acc)
221                        },
222                    )
223                };
224
225                let left_pickup = fold_demand(left.clone(), |demand| demand.pickup.0);
226                let right_delivery = fold_demand(right.clone(), |demand| demand.delivery.0);
227
228                // static delivery moved to left
229                let new_max_load_left =
230                    route_ctx.state().get_max_future_capacity_at::<T>(left.start).cloned().unwrap_or_default()
231                        + right_delivery;
232
233                // static pickup moved to right
234                let new_max_load_right =
235                    route_ctx.state().get_max_future_capacity_at::<T>(right.start).cloned().unwrap_or_default()
236                        + left_pickup;
237
238                let has_enough_vehicle_capacity =
239                    capacity.can_fit(&new_max_load_left) && capacity.can_fit(&new_max_load_right);
240
241                has_enough_vehicle_capacity
242                    && shared_resource_threshold_fn.as_ref().map_or(true, |shared_resource_threshold_fn| {
243                        // total static delivery at left
244                        let left_delivery = fold_demand(left.start..right.end, |demand| demand.delivery.0);
245
246                        (shared_resource_threshold_fn)(route_ctx, left.start, &left_delivery)
247                    })
248            }),
249            is_assignable_fn,
250            intervals_state: Arc::new(ReloadIntervalsState),
251        };
252
253        let violation_code = self.capacity_code.unwrap_or_default();
254
255        // NOTE: all reload feature flavors extend the capacity feature via route intervals
256        CapacityFeatureBuilder::<T>::new(self.name.as_str())
257            .set_violation_code(violation_code)
258            .set_route_intervals(route_intervals)
259            .build()
260    }
261}
262
263custom_route_intervals_state!(pub ReloadIntervals);
264
265// shared reload implementation
266
267// TODO: dedicated macro doesn't support Option<T> to be stored as a type
268struct SharedResourceStateKey;
269
270type SharedResourceCapacityFn<T> = Arc<dyn Fn(&Activity) -> Option<(T, SharedResourceId)> + Send + Sync>;
271type SharedResourceDemandFn<T> = Arc<dyn Fn(&Single) -> Option<T> + Send + Sync>;
272type SharedResourceThresholdFn<T> = Box<dyn Fn(&RouteContext, usize, &T) -> bool + Send + Sync>;
273type PartialSolutionFn = Arc<dyn Fn(&SolutionContext) -> bool + Send + Sync>;
274
275struct SharedResourceConstraint<T: SharedResource> {
276    violation_code: ViolationCode,
277    resource_demand_fn: SharedResourceDemandFn<T>,
278    is_partial_solution_fn: PartialSolutionFn,
279}
280
281impl<T: SharedResource> SharedResourceConstraint<T> {
282    fn evaluate_route(
283        &self,
284        solution_ctx: &SolutionContext,
285        route_ctx: &RouteContext,
286        job: &Job,
287    ) -> Option<ConstraintViolation> {
288        job.as_single()
289            .and_then(|job| {
290                (self.resource_demand_fn)(job)
291                    .zip(route_ctx.state().get_reload_intervals().and_then(|intervals| intervals.first()))
292            })
293            .and_then(|(_, _)| {
294                // NOTE cannot do resource assignment for partial solution
295                if (self.is_partial_solution_fn)(solution_ctx) {
296                    ConstraintViolation::fail(self.violation_code)
297                } else {
298                    ConstraintViolation::success()
299                }
300            })
301    }
302
303    fn evaluate_activity(
304        &self,
305        route_ctx: &RouteContext,
306        activity_ctx: &ActivityContext,
307    ) -> Option<ConstraintViolation> {
308        route_ctx
309            .state()
310            .get_reload_intervals()
311            .iter()
312            .flat_map(|intervals| intervals.iter())
313            .find(|(_, end_idx)| activity_ctx.index <= *end_idx)
314            .and_then(|&(start_idx, _)| {
315                route_ctx
316                    .state()
317                    .get_activity_state::<SharedResourceStateKey, Option<T>>(start_idx)
318                    .and_then(|resource_available| *resource_available)
319                    .and_then(|resource_available| {
320                        let resource_demand = activity_ctx
321                            .target
322                            .job
323                            .as_ref()
324                            .and_then(|job| (self.resource_demand_fn)(job.as_ref()))
325                            .unwrap_or_default();
326
327                        if resource_available
328                            .partial_cmp(&resource_demand)
329                            .map_or(false, |ordering| ordering == Ordering::Less)
330                        {
331                            ConstraintViolation::skip(self.violation_code)
332                        } else {
333                            ConstraintViolation::success()
334                        }
335                    })
336            })
337    }
338}
339
340impl<T: SharedResource> FeatureConstraint for SharedResourceConstraint<T> {
341    fn evaluate(&self, move_ctx: &MoveContext<'_>) -> Option<ConstraintViolation> {
342        match move_ctx {
343            MoveContext::Route { solution_ctx, route_ctx, job } => self.evaluate_route(solution_ctx, route_ctx, job),
344            MoveContext::Activity { route_ctx, activity_ctx } => self.evaluate_activity(route_ctx, activity_ctx),
345        }
346    }
347
348    fn merge(&self, source: Job, _: Job) -> Result<Job, ViolationCode> {
349        Ok(source)
350    }
351}
352
353struct SharedResourceState<T>
354where
355    T: SharedResource + Add<Output = T> + Sub<Output = T>,
356{
357    resource_capacity_fn: SharedResourceCapacityFn<T>,
358    resource_demand_fn: SharedResourceDemandFn<T>,
359    is_partial_solution_fn: PartialSolutionFn,
360}
361
362impl<T: SharedResource + Add<Output = T> + Sub<Output = T>> SharedResourceState<T> {
363    /// Calculates available resource based on consumption in the whole solution.
364    fn update_resource_consumption(&self, solution_ctx: &mut SolutionContext) {
365        // NOTE: we cannot estimate resource consumption in partial solutions
366        if (self.is_partial_solution_fn)(solution_ctx) {
367            return;
368        }
369
370        // first pass: get total demand for each shared resource
371        let total_demand = solution_ctx.routes.iter().fold(HashMap::<usize, T>::default(), |acc, route_ctx| {
372            route_ctx.state().get_reload_intervals().iter().flat_map(|intervals| intervals.iter()).fold(
373                acc,
374                |mut acc, &(start_idx, end_idx)| {
375                    // get total resource demand for given interval
376                    let activity = get_activity_by_idx(route_ctx.route(), start_idx);
377                    let resource_demand_with_id = (self.resource_capacity_fn)(activity)
378                        .map(|(_, resource_id)| (self.get_total_demand(route_ctx, start_idx..=end_idx), resource_id));
379
380                    if let Some((resource_demand, id)) = resource_demand_with_id {
381                        let entry = acc.entry(id).or_default();
382                        *entry = *entry + resource_demand;
383                    }
384
385                    acc
386                },
387            )
388        });
389
390        // second pass: store amount of available resources inside activity state
391        solution_ctx.routes.iter_mut().for_each(|route_ctx| {
392            let mut available_resources = vec![None; route_ctx.route().tour.total()];
393            let reload_intervals = route_ctx.state().get_reload_intervals().cloned().unwrap_or_default();
394
395            for (start_idx, _) in reload_intervals {
396                let activity_idx = get_activity_by_idx(route_ctx.route(), start_idx);
397                let resource_available =
398                    (self.resource_capacity_fn)(activity_idx).and_then(|(total_capacity, resource_id)| {
399                        total_demand.get(&resource_id).map(|total_demand| total_capacity - *total_demand)
400                    });
401
402                if let Some(resource_available) = resource_available {
403                    available_resources[start_idx] = Some(resource_available);
404                }
405            }
406
407            route_ctx.state_mut().set_activity_states::<SharedResourceStateKey, Option<T>>(available_resources)
408        });
409    }
410
411    /// Prevents resource consumption in given route by setting available to zero (default).
412    fn prevent_resource_consumption(&self, route_ctx: &mut RouteContext) {
413        let mut empty_resources = vec![None; route_ctx.route().tour.total()];
414
415        route_ctx.state().get_reload_intervals().cloned().unwrap_or_default().into_iter().for_each(
416            |(start_idx, end_idx)| {
417                let activity = get_activity_by_idx(route_ctx.route(), start_idx);
418                let has_resource_demand = (self.resource_capacity_fn)(activity).map_or(false, |(_, _)| {
419                    (start_idx..=end_idx)
420                        .filter_map(|idx| route_ctx.route().tour.get(idx))
421                        .filter_map(|activity| activity.job.as_ref())
422                        .any(|job| (self.resource_demand_fn)(job).is_some())
423                });
424
425                if has_resource_demand {
426                    empty_resources[start_idx] = Some(T::default());
427                }
428            },
429        );
430
431        route_ctx.state_mut().set_activity_states::<SharedResourceStateKey, _>(empty_resources)
432    }
433
434    fn get_total_demand(&self, route_ctx: &RouteContext, range: RangeInclusive<usize>) -> T {
435        range
436            .into_iter()
437            .filter_map(|idx| route_ctx.route().tour.get(idx))
438            .filter_map(|activity| activity.job.as_ref())
439            .fold(T::default(), |acc, job| acc + (self.resource_demand_fn)(job).unwrap_or_default())
440    }
441}
442
443impl<T: SharedResource + Add<Output = T> + Sub<Output = T>> FeatureState for SharedResourceState<T> {
444    fn accept_insertion(&self, solution_ctx: &mut SolutionContext, route_index: usize, _: &Job) {
445        self.accept_route_state(solution_ctx.routes.get_mut(route_index).unwrap());
446        self.update_resource_consumption(solution_ctx);
447    }
448
449    fn accept_route_state(&self, route_ctx: &mut RouteContext) {
450        // NOTE: we need to prevent any insertions with resource consumption in modified route.
451        //       This state will be overridden by update_resource_consumption after other accept
452        //       method calls.
453
454        self.prevent_resource_consumption(route_ctx);
455    }
456
457    fn accept_solution_state(&self, solution_ctx: &mut SolutionContext) {
458        self.update_resource_consumption(solution_ctx);
459    }
460}
461
462fn get_activity_by_idx(route: &Route, idx: usize) -> &Activity {
463    route.tour.get(idx).expect("cannot get activity by idx")
464}
465
466/// Implement `SharedResource` for multi dimensional load.
467impl SharedResource for MultiDimLoad {}
468
469/// Implement `SharedResource` for a single dimensional load.
470impl SharedResource for SingleDimLoad {}