vrp_core/construction/features/
fast_service.rs1#[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
9pub 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 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 pub fn set_violation_code(mut self, violation_code: ViolationCode) -> Self {
34 self.violation_code = Some(violation_code);
35 self
36 }
37
38 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 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 pub fn set_transport(mut self, transport: Arc<dyn TransportCost>) -> Self {
58 self.transport = Some(transport);
59 self
60 }
61
62 pub fn set_activity(mut self, activity: Arc<dyn ActivityCost>) -> Self {
64 self.activity = Some(activity);
65 self
66 }
67
68 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
86enum TimeIntervalType {
89 FromStart,
92 ToEnd,
94 FromFirstToLast,
96 FromStartToEnd,
99}
100
101type MultiJobRanges = HashMap<Job, (usize, usize)>;
103type DemandTypeFn = Arc<dyn Fn(&Single) -> Option<DemandType> + Send + Sync>;
105type 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 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 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 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 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 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}