Skip to main content

vrp_core/construction/features/
recharge.rs

1//! An experimental feature which provides a way to insert recharge stations in the tour to recharge
2//! (refuel) vehicle.
3
4#[cfg(test)]
5#[path = "../../../tests/unit/construction/features/recharge_test.rs"]
6mod recharge_test;
7
8use super::*;
9use crate::construction::enablers::*;
10use crate::models::solution::Route;
11use std::collections::HashSet;
12use std::sync::Arc;
13
14/// Provides a way to build the recharge/refuel feature.
15#[allow(clippy::type_complexity)]
16pub struct RechargeFeatureBuilder {
17    name: String,
18    violation_code: Option<ViolationCode>,
19    transport: Option<Arc<dyn TransportCost>>,
20    belongs_to_route_fn: Option<Arc<dyn Fn(&Route, &Job) -> bool + Send + Sync>>,
21    is_recharge_single_fn: Option<RechargeSingleFn>,
22    distance_limit_fn: Option<RechargeDistanceLimitFn>,
23}
24
25impl RechargeFeatureBuilder {
26    /// Creates a new instance of `RechargeFeatureBuilder`.
27    pub fn new(name: &str) -> Self {
28        Self {
29            name: name.to_string(),
30            violation_code: None,
31            is_recharge_single_fn: None,
32            belongs_to_route_fn: None,
33            distance_limit_fn: None,
34            transport: None,
35        }
36    }
37
38    /// Sets constraint violation code which is used to report back the reason of job's unassignment.
39    pub fn set_violation_code(mut self, violation_code: ViolationCode) -> Self {
40        self.violation_code = Some(violation_code);
41        self
42    }
43
44    /// Sets transport costs to estimate distance.
45    pub fn set_transport(mut self, transport: Arc<dyn TransportCost>) -> Self {
46        self.transport = Some(transport);
47        self
48    }
49
50    /// Sets a function which specifies whether a given single job can be considered as a recharge job.
51    pub fn set_is_recharge_single<F>(mut self, func: F) -> Self
52    where
53        F: Fn(&Single) -> bool + Send + Sync + 'static,
54    {
55        self.is_recharge_single_fn = Some(Arc::new(func));
56        self
57    }
58
59    /// Sets a function which specifies whether a given route can serve a given job. This function
60    /// should return false, if the job is not recharge.
61    pub fn set_belongs_to_route<F>(mut self, func: F) -> Self
62    where
63        F: Fn(&Route, &Job) -> bool + Send + Sync + 'static,
64    {
65        self.belongs_to_route_fn = Some(Arc::new(func));
66        self
67    }
68
69    /// Specifies a distance limit function for recharge. It should return a fixed value for the same
70    /// actor all the time.
71    pub fn set_distance_limit<F>(mut self, func: F) -> Self
72    where
73        F: Fn(&Actor) -> Option<Distance> + Send + Sync + 'static,
74    {
75        self.distance_limit_fn = Some(Arc::new(func));
76        self
77    }
78
79    /// Builds the recharge feature if all dependencies are set.
80    pub fn build(&mut self) -> GenericResult<Feature> {
81        let is_marker_single_fn =
82            self.is_recharge_single_fn.take().ok_or_else(|| GenericError::from("is_reload_single must be set"))?;
83        let is_assignable_fn =
84            self.belongs_to_route_fn.take().ok_or_else(|| GenericError::from("belongs_to_route must be set"))?;
85
86        let transport = self.transport.take().ok_or_else(|| GenericError::from("transport must be set"))?;
87        let distance_limit_fn =
88            self.distance_limit_fn.take().ok_or_else(|| GenericError::from("distance_limit must be set"))?;
89
90        let code = self.violation_code.unwrap_or_default();
91
92        create_multi_trip_feature(
93            self.name.as_str(),
94            code,
95            MarkerInsertionPolicy::Any,
96            Arc::new(RechargeableMultiTrip {
97                route_intervals: RouteIntervals::Multiple {
98                    is_marker_single_fn: is_marker_single_fn.clone(),
99                    is_new_interval_needed_fn: Arc::new({
100                        let distance_limit_fn = distance_limit_fn.clone();
101                        move |route_ctx| {
102                            route_ctx
103                                .route()
104                                .tour
105                                .end_idx()
106                                .map(|end_idx| {
107                                    let current: Distance = route_ctx
108                                        .state()
109                                        .get_recharge_distance_at(end_idx)
110                                        .copied()
111                                        .unwrap_or_default();
112
113                                    (distance_limit_fn)(route_ctx.route().actor.as_ref())
114                                        .map_or(false, |threshold| current > threshold)
115                                })
116                                .unwrap_or(false)
117                        }
118                    }),
119                    is_obsolete_interval_fn: Arc::new({
120                        let distance_limit_fn = distance_limit_fn.clone();
121                        let transport = transport.clone();
122                        let get_counter = move |route_ctx: &RouteContext, activity_idx: usize| {
123                            route_ctx
124                                .state()
125                                .get_recharge_distance_at(activity_idx)
126                                .copied()
127                                .unwrap_or(Distance::default())
128                        };
129                        let get_distance = move |route: &Route, from_idx: usize, to_idx: usize| {
130                            route.tour.get(from_idx).zip(route.tour.get(to_idx)).map_or(
131                                Distance::default(),
132                                |(from, to)| {
133                                    transport.distance(
134                                        route,
135                                        from.place.location,
136                                        to.place.location,
137                                        TravelTime::Departure(from.schedule.departure),
138                                    )
139                                },
140                            )
141                        };
142                        move |route_ctx, left, right| {
143                            let end_idx = get_end_idx(route_ctx, right.end);
144
145                            let new_distance = get_counter(route_ctx, left.end) + get_counter(route_ctx, end_idx)
146                                - get_counter(route_ctx, right.start + 1)
147                                + get_distance(route_ctx.route(), left.end, right.start + 1);
148
149                            (distance_limit_fn)(route_ctx.route().actor.as_ref())
150                                .map_or(false, |threshold| new_distance <= threshold)
151                        }
152                    }),
153                    is_assignable_fn,
154                    intervals_state: Arc::new(RechargeIntervalsState),
155                },
156                transport,
157                code,
158                distance_limit_fn,
159                recharge_single_fn: is_marker_single_fn.clone(),
160            }),
161        )
162    }
163}
164
165type RechargeDistanceLimitFn = Arc<dyn Fn(&Actor) -> Option<Distance> + Send + Sync>;
166type RechargeSingleFn = Arc<dyn Fn(&Single) -> bool + Send + Sync>;
167
168custom_route_intervals_state!(RechargeIntervals);
169custom_activity_state!(RechargeDistance typeof Distance);
170
171struct RechargeableMultiTrip {
172    route_intervals: RouteIntervals,
173    transport: Arc<dyn TransportCost>,
174    code: ViolationCode,
175    distance_limit_fn: RechargeDistanceLimitFn,
176    recharge_single_fn: RechargeSingleFn,
177}
178
179impl MultiTrip for RechargeableMultiTrip {
180    fn get_route_intervals(&self) -> &RouteIntervals {
181        &self.route_intervals
182    }
183
184    fn get_constraint(&self) -> &(dyn FeatureConstraint) {
185        self
186    }
187
188    fn recalculate_states(&self, route_ctx: &mut RouteContext) {
189        if (self.distance_limit_fn)(route_ctx.route().actor.as_ref()).is_none() {
190            return;
191        }
192
193        let last_idx = route_ctx.route().tour.total() - 1;
194        let marker_intervals = self.route_intervals.resolve_marker_intervals(route_ctx).collect::<Vec<_>>();
195        let mut distance_counters = vec![Distance::default(); route_ctx.route().tour.total()];
196
197        marker_intervals.into_iter().for_each(|(start_idx, end_idx)| {
198            let route = route_ctx.route();
199
200            let end_idx = if end_idx != last_idx { end_idx + 1 } else { end_idx };
201
202            let _ = route
203                .tour
204                .activities_slice(start_idx, end_idx)
205                .windows(2)
206                .enumerate()
207                .filter_map(|(leg_idx, leg)| match leg {
208                    [prev, next] => Some((start_idx + leg_idx, prev, next)),
209                    _ => None,
210                })
211                .fold(Distance::default(), |acc, (activity_idx, prev, next)| {
212                    let distance = self.transport.distance(
213                        route,
214                        prev.place.location,
215                        next.place.location,
216                        TravelTime::Departure(prev.schedule.departure),
217                    );
218                    let counter = acc + distance;
219                    let next_idx = activity_idx + 1;
220
221                    distance_counters[next_idx] = counter;
222
223                    counter
224                });
225        });
226
227        route_ctx.state_mut().set_recharge_distance_states(distance_counters);
228    }
229
230    fn try_recover(&self, solution_ctx: &mut SolutionContext, route_indices: &[usize], _: &[Job]) -> bool {
231        let routes = &mut solution_ctx.routes;
232
233        let jobs: HashSet<_> = if route_indices.is_empty() {
234            solution_ctx
235                .ignored
236                .iter()
237                .filter(|job| job.as_single().map_or(false, |single| (self.recharge_single_fn)(single)))
238                .cloned()
239                .collect()
240        } else {
241            routes
242                .iter()
243                .enumerate()
244                .filter(|(idx, _)| route_indices.contains(idx))
245                .flat_map(|(_, route_ctx)| {
246                    solution_ctx
247                        .ignored
248                        .iter()
249                        .filter(|job| self.route_intervals.is_marker_assignable(route_ctx.route(), job))
250                })
251                .cloned()
252                .collect()
253        };
254
255        if jobs.is_empty() {
256            false
257        } else {
258            solution_ctx.ignored.retain(|job| !jobs.contains(job));
259            solution_ctx.locked.extend(jobs.iter().cloned());
260            solution_ctx.required.extend(jobs);
261
262            true
263        }
264    }
265}
266
267impl FeatureConstraint for RechargeableMultiTrip {
268    fn evaluate(&self, move_ctx: &MoveContext<'_>) -> Option<ConstraintViolation> {
269        match move_ctx {
270            MoveContext::Route { route_ctx, job, .. } => self.evaluate_job(route_ctx, job),
271            MoveContext::Activity { route_ctx, activity_ctx } => self.evaluate_activity(route_ctx, activity_ctx),
272        }
273    }
274
275    fn merge(&self, source: Job, _: Job) -> Result<Job, ViolationCode> {
276        Ok(source)
277    }
278}
279
280impl RechargeableMultiTrip {
281    fn evaluate_job(&self, _: &RouteContext, _: &Job) -> Option<ConstraintViolation> {
282        ConstraintViolation::success()
283    }
284
285    fn evaluate_activity(
286        &self,
287        route_ctx: &RouteContext,
288        activity_ctx: &ActivityContext,
289    ) -> Option<ConstraintViolation> {
290        let threshold = (self.distance_limit_fn)(route_ctx.route().actor.as_ref())?;
291
292        let interval_distance = self
293            .route_intervals
294            .resolve_marker_intervals(route_ctx)
295            .find(|(_, end_idx)| activity_ctx.index <= *end_idx)
296            .map(|(_, end_idx)| get_end_idx(route_ctx, end_idx))
297            .map(|end_idx| self.get_distance(route_ctx, end_idx))
298            .expect("invalid markers state");
299
300        let is_new_recharge =
301            activity_ctx.target.job.as_ref().map_or(false, |single| (self.recharge_single_fn)(single));
302
303        let is_violation = if is_new_recharge {
304            let ((prev_to_tar_distance, tar_to_next_distance), _) =
305                calculate_travel(route_ctx, activity_ctx, self.transport.as_ref());
306
307            // S ----- A ---- [X] ------ B ----- F
308
309            let current_distance = self.get_distance(route_ctx, activity_ctx.index);
310            // check S->X
311            let is_begin_violates = (current_distance + prev_to_tar_distance) > threshold;
312            // check X->F
313            let is_end_violates = if activity_ctx.next.is_some() {
314                let next_distance = self.get_distance(route_ctx, activity_ctx.index + 1);
315                let new_interval_distance = interval_distance - next_distance;
316
317                (new_interval_distance + tar_to_next_distance) > threshold
318            } else {
319                false
320            };
321
322            is_begin_violates || is_end_violates
323        } else {
324            let (distance_delta, _) = calculate_travel_delta(route_ctx, activity_ctx, self.transport.as_ref());
325
326            (interval_distance + distance_delta) > threshold
327        };
328
329        if is_violation {
330            ConstraintViolation::skip(self.code)
331        } else {
332            None
333        }
334    }
335}
336
337impl RechargeableMultiTrip {
338    fn get_distance(&self, route_ctx: &RouteContext, activity_idx: usize) -> Distance {
339        route_ctx.state().get_recharge_distance_at(activity_idx).copied().unwrap_or(Distance::default())
340    }
341}
342
343fn get_end_idx(route_ctx: &RouteContext, end_idx: usize) -> usize {
344    let last_idx = route_ctx.route().tour.total() - 1;
345    end_idx + if end_idx == last_idx { 0 } else { 1 }
346}