vrp_core/construction/features/
fast_service.rs

1#[cfg(test)]
2#[path = "../../../tests/unit/construction/features/fast_service_test.rs"]
3mod fast_service_test;
4
5use super::*;
6use crate::construction::enablers::{calculate_travel, calculate_travel_delta};
7use std::collections::HashMap;
8
9/// Provides the way to build fast service feature.
10pub struct FastServiceFeatureBuilder {
11    name: String,
12    violation_code: Option<ViolationCode>,
13    demand_type_fn: Option<DemandTypeFn>,
14    is_filtered_job_fn: Option<IsFilteredJobFn>,
15    transport: Option<Arc<dyn TransportCost>>,
16    activity: Option<Arc<dyn ActivityCost>>,
17}
18
19impl FastServiceFeatureBuilder {
20    /// Creates a new instance of `RechargeFeatureBuilder`.
21    pub fn new(name: &str) -> Self {
22        Self {
23            name: name.to_string(),
24            violation_code: None,
25            demand_type_fn: None,
26            is_filtered_job_fn: None,
27            transport: None,
28            activity: None,
29        }
30    }
31
32    /// Sets constraint violation code which is used to report back the reason of job's unassignment.
33    pub fn set_violation_code(mut self, violation_code: ViolationCode) -> Self {
34        self.violation_code = Some(violation_code);
35        self
36    }
37
38    /// Sets a function to get job's demand type.
39    pub fn set_demand_type_fn<F>(mut self, func: F) -> Self
40    where
41        F: Fn(&Single) -> Option<DemandType> + Send + Sync + 'static,
42    {
43        self.demand_type_fn = Some(Arc::new(func));
44        self
45    }
46
47    /// Sets a function which tells whether the job should NOT be considered for estimation.
48    pub fn set_is_filtered_job<F>(mut self, func: F) -> Self
49    where
50        F: Fn(&Job) -> bool + Send + Sync + 'static,
51    {
52        self.is_filtered_job_fn = Some(Arc::new(func));
53        self
54    }
55
56    /// Sets transport costs to estimate distance.
57    pub fn set_transport(mut self, transport: Arc<dyn TransportCost>) -> Self {
58        self.transport = Some(transport);
59        self
60    }
61
62    /// Sets activity costs to estimate job start/end time.
63    pub fn set_activity(mut self, activity: Arc<dyn ActivityCost>) -> Self {
64        self.activity = Some(activity);
65        self
66    }
67
68    /// Builds fast service feature.
69    pub fn build(mut self) -> GenericResult<Feature> {
70        let transport = self.transport.take().ok_or_else(|| GenericError::from("transport must be set"))?;
71        let activity = self.activity.take().ok_or_else(|| GenericError::from("activity must be set"))?;
72
73        let demand_type_fn =
74            self.demand_type_fn.take().ok_or_else(|| GenericError::from("demand_type_fn must be set"))?;
75
76        let is_filtered_job_fn = self.is_filtered_job_fn.take().unwrap_or_else(|| Arc::new(|_| false));
77
78        FeatureBuilder::default()
79            .with_name(self.name.as_str())
80            .with_state(FastServiceState::default())
81            .with_objective(FastServiceObjective::new(demand_type_fn, is_filtered_job_fn, transport, activity))
82            .build()
83    }
84}
85
86/// Defines how time interval should be calculated for different types of the jobs specified by their
87/// demand type.
88enum TimeIntervalType {
89    /// Time is counted from the start of the start (or latest reload). Corresponds to static
90    /// delivery demand jobs.
91    FromStart,
92    /// Time is counted till the end of the tour (or next reload). Corresponds to static pickup jobs.
93    ToEnd,
94    /// Time is counted from first activity to the last of the job. Corresponds to dynamic demand jobs.
95    FromFirstToLast,
96    /// Time is counted from start to the end of the tour (or previous and next reload).
97    /// Corresponds to static pickup/delivery jobs.
98    FromStartToEnd,
99}
100
101/// Keeps track of first and last activity in the tour for specific multi job.
102type MultiJobRanges = HashMap<Job, (usize, usize)>;
103/// A function to get a demand type from the job.
104type DemandTypeFn = Arc<dyn Fn(&Single) -> Option<DemandType> + Send + Sync>;
105/// Returns true if job should not be considered for estimation.
106type IsFilteredJobFn = Arc<dyn Fn(&Job) -> bool + Send + Sync>;
107
108custom_tour_state!(MultiJobRanges typeof MultiJobRanges);
109
110struct FastServiceObjective {
111    demand_type_fn: DemandTypeFn,
112    is_filtered_job_fn: IsFilteredJobFn,
113    transport: Arc<dyn TransportCost>,
114    activity: Arc<dyn ActivityCost>,
115}
116
117impl FeatureObjective for FastServiceObjective {
118    fn fitness(&self, solution: &InsertionContext) -> Cost {
119        solution
120            .solution
121            .routes
122            .iter()
123            .flat_map(|route_ctx| {
124                route_ctx.route().tour.jobs().filter(|job| !(self.is_filtered_job_fn)(job)).map(|job| match job {
125                    Job::Single(_) => self.estimate_single_job(route_ctx, job),
126                    Job::Multi(_) => self.estimate_multi_job(route_ctx, job),
127                })
128            })
129            .sum::<Cost>()
130    }
131
132    fn estimate(&self, move_ctx: &MoveContext<'_>) -> Cost {
133        let (route_ctx, activity_ctx) = match move_ctx {
134            MoveContext::Route { .. } => return Cost::default(),
135            MoveContext::Activity { route_ctx, activity_ctx } => (route_ctx, activity_ctx),
136        };
137
138        let activity_idx = activity_ctx.index;
139
140        let (single, job) =
141            if let Some((single, job)) = activity_ctx.target.job.as_ref().zip(activity_ctx.target.retrieve_job()) {
142                (single, job)
143            } else {
144                return self.get_departure(route_ctx, activity_ctx) - self.get_start_time(route_ctx, activity_idx);
145            };
146
147        // NOTE: for simplicity, we ignore impact on already inserted jobs on local objective level
148        match self.get_time_interval_type(&job, single.as_ref()) {
149            TimeIntervalType::FromStart => {
150                self.get_departure(route_ctx, activity_ctx) - self.get_start_time(route_ctx, activity_idx)
151            }
152            TimeIntervalType::ToEnd => {
153                let departure = self.get_departure(route_ctx, activity_ctx);
154                let (_, duration_delta) = calculate_travel_delta(route_ctx, activity_ctx, self.transport.as_ref());
155
156                self.get_end_time(route_ctx, activity_idx) + duration_delta - departure
157            }
158            TimeIntervalType::FromFirstToLast => self.get_cost_for_multi_job(route_ctx, activity_ctx),
159            TimeIntervalType::FromStartToEnd => {
160                self.get_end_time(route_ctx, activity_idx) - self.get_start_time(route_ctx, activity_idx)
161            }
162        }
163    }
164}
165
166impl FastServiceObjective {
167    fn new(
168        demand_type_fn: DemandTypeFn,
169        is_filtered_job_fn: IsFilteredJobFn,
170        transport: Arc<dyn TransportCost>,
171        activity: Arc<dyn ActivityCost>,
172    ) -> Self {
173        Self { demand_type_fn, is_filtered_job_fn, transport, activity }
174    }
175
176    fn get_start_time(&self, route_ctx: &RouteContext, activity_idx: usize) -> Timestamp {
177        let (start_idx, _) = self.get_route_interval(route_ctx, activity_idx);
178        route_ctx.route().tour[start_idx].schedule.departure
179    }
180
181    fn get_end_time(&self, route_ctx: &RouteContext, activity_idx: usize) -> Timestamp {
182        let (_, end_idx) = self.get_route_interval(route_ctx, activity_idx);
183        route_ctx.route().tour[end_idx].schedule.arrival
184    }
185
186    fn get_departure(&self, route_ctx: &RouteContext, activity_ctx: &ActivityContext) -> Timestamp {
187        // TODO optimize: clients are interested also in travel delta, so we can do needed calculations once
188        //      and avoid `calculate_travel_delta` call later
189        let (_, (prev_to_tar_dur, _)) = calculate_travel(route_ctx, activity_ctx, self.transport.as_ref());
190        let arrival = activity_ctx.prev.schedule.departure + prev_to_tar_dur;
191
192        self.activity.estimate_departure(route_ctx.route(), activity_ctx.target, arrival)
193    }
194
195    fn get_cost_for_multi_job(&self, route_ctx: &RouteContext, activity_ctx: &ActivityContext) -> Cost {
196        let departure = self.get_departure(route_ctx, activity_ctx);
197        let range = route_ctx
198            .state()
199            .get_multi_job_ranges()
200            .zip(activity_ctx.target.retrieve_job())
201            .and_then(|(jobs, job)| jobs.get(&job))
202            .copied();
203
204        let (start_idx, end_idx) = if let Some(range) = range {
205            (range.0, range.1)
206        } else {
207            return departure - self.get_start_time(route_ctx, activity_ctx.index);
208        };
209
210        let (_, duration_delta) = calculate_travel_delta(route_ctx, activity_ctx, self.transport.as_ref());
211
212        // NOTE ignore impact of insertion
213        match (start_idx, activity_ctx.index, end_idx) {
214            (start_idx, activity_idx, end_idx) if activity_idx <= start_idx => {
215                route_ctx.route().tour[end_idx].schedule.departure - departure + duration_delta
216            }
217            (start_idx, activity_idx, end_idx) if activity_idx >= end_idx => {
218                departure - route_ctx.route().tour[start_idx].schedule.departure + duration_delta
219            }
220            _ => Cost::default(),
221        }
222    }
223
224    fn get_route_interval(&self, route_ctx: &RouteContext, activity_idx: usize) -> (usize, usize) {
225        let last_idx = (route_ctx.route().tour.total() as i32 - 1).max(0) as usize;
226
227        route_ctx
228            .state()
229            .get_reload_intervals()
230            .and_then(|intervals| intervals.iter().find(|(start, end)| *start <= activity_idx && *end > activity_idx))
231            .copied()
232            .unwrap_or((0, last_idx))
233    }
234
235    fn estimate_single_job(&self, route_ctx: &RouteContext, job: &Job) -> Cost {
236        let single = job.to_single();
237        let tour = &route_ctx.route().tour;
238        let activity_idx = tour.index(job).expect("cannot find index for job");
239        let activity = &tour[activity_idx];
240
241        (match self.get_time_interval_type(job, single) {
242            TimeIntervalType::FromStart => activity.schedule.departure - self.get_start_time(route_ctx, activity_idx),
243            TimeIntervalType::ToEnd => self.get_end_time(route_ctx, activity_idx) - activity.schedule.departure,
244            TimeIntervalType::FromStartToEnd => {
245                self.get_end_time(route_ctx, activity_idx) - self.get_start_time(route_ctx, activity_idx)
246            }
247            TimeIntervalType::FromFirstToLast => unreachable!("this type is only for multi job"),
248        }) as Cost
249    }
250
251    fn estimate_multi_job(&self, route_ctx: &RouteContext, job: &Job) -> Cost {
252        route_ctx
253            .state()
254            .get_multi_job_ranges()
255            .and_then(|job_ranges| job_ranges.get(job))
256            .map(|&(start_idx, end_idx)| {
257                self.get_end_time(route_ctx, end_idx) - self.get_start_time(route_ctx, start_idx)
258            })
259            .unwrap_or_default()
260    }
261
262    fn get_time_interval_type(&self, job: &Job, single: &Single) -> TimeIntervalType {
263        if job.as_multi().is_some() {
264            return TimeIntervalType::FromFirstToLast;
265        }
266
267        match (self.demand_type_fn)(single) {
268            Some(DemandType::Delivery) => TimeIntervalType::FromStart,
269            Some(DemandType::Pickup) => TimeIntervalType::ToEnd,
270            Some(_) => TimeIntervalType::FromStartToEnd,
271            None => TimeIntervalType::FromStart,
272        }
273    }
274}
275
276#[derive(Default)]
277struct FastServiceState {}
278
279impl FeatureState for FastServiceState {
280    fn accept_insertion(&self, solution_ctx: &mut SolutionContext, route_index: usize, _: &Job) {
281        self.accept_route_state(&mut solution_ctx.routes[route_index]);
282    }
283
284    fn accept_route_state(&self, route_ctx: &mut RouteContext) {
285        // keep track of [start, end] positions of all multi jobs in the given tour
286        let multi_job_ranges: MultiJobRanges = route_ctx
287            .route()
288            .tour
289            .jobs()
290            .filter(|job| job.as_multi().is_some())
291            .map(|job| {
292                let tour = &route_ctx.route().tour;
293                let start_idx = tour.index(job).expect("job start index cannot be found");
294                let end_idx = tour.index_last(job).expect("job end index cannot be found");
295
296                (job.clone(), (start_idx, end_idx))
297            })
298            .collect();
299
300        // NOTE: always override existing state to avoid stale information about multi-jobs
301        route_ctx.state_mut().set_multi_job_ranges(multi_job_ranges);
302    }
303
304    fn accept_solution_state(&self, solution_ctx: &mut SolutionContext) {
305        solution_ctx
306            .routes
307            .iter_mut()
308            .filter(|route_ctx| route_ctx.is_stale())
309            .for_each(|route_ctx| self.accept_route_state(route_ctx))
310    }
311}