vrp_core/construction/features/
recharge.rs1#[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#[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 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 pub fn set_violation_code(mut self, violation_code: ViolationCode) -> Self {
40 self.violation_code = Some(violation_code);
41 self
42 }
43
44 pub fn set_transport(mut self, transport: Arc<dyn TransportCost>) -> Self {
46 self.transport = Some(transport);
47 self
48 }
49
50 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 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 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 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 let current_distance = self.get_distance(route_ctx, activity_ctx.index);
310 let is_begin_violates = (current_distance + prev_to_tar_distance) > threshold;
312 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}