vrp_core/construction/features/
breaks.rs1#[cfg(test)]
4#[path = "../../../tests/unit/construction/features/breaks_test.rs"]
5mod breaks_test;
6
7use super::*;
8use crate::construction::enablers::*;
9use crate::models::solution::Route;
10use std::collections::HashSet;
11use std::iter::once;
12
13#[derive(Clone)]
15pub enum BreakPolicy {
16 SkipIfNoIntersection,
18
19 SkipIfArrivalBeforeEnd,
21}
22
23pub struct BreakFeatureBuilder {
26 name: String,
27 violation_code: Option<ViolationCode>,
28 belongs_to_route_fn: Option<BelongsToRouteFn>,
29 is_break_single_fn: Option<BreakSingleFn>,
30 policy_fn: Option<BreakPolicyFn>,
31}
32
33impl BreakFeatureBuilder {
34 pub fn new(name: &str) -> Self {
36 Self {
37 name: name.to_string(),
38 violation_code: None,
39 belongs_to_route_fn: None,
40 is_break_single_fn: None,
41 policy_fn: None,
42 }
43 }
44
45 pub fn set_violation_code(mut self, violation_code: ViolationCode) -> Self {
48 self.violation_code = Some(violation_code);
49 self
50 }
51
52 pub fn set_is_break_single<F>(mut self, func: F) -> Self
54 where
55 F: Fn(&Single) -> bool + Send + Sync + 'static,
56 {
57 self.is_break_single_fn = Some(Arc::new(func));
58 self
59 }
60
61 pub fn set_policy<F>(mut self, func: F) -> Self
63 where
64 F: Fn(&Single) -> BreakPolicy + Send + Sync + 'static,
65 {
66 self.policy_fn = Some(Arc::new(func));
67 self
68 }
69
70 pub fn set_belongs_to_route<F>(mut self, func: F) -> Self
73 where
74 F: Fn(&Route, &Job) -> bool + Send + Sync + 'static,
75 {
76 self.belongs_to_route_fn = Some(Arc::new(func));
77 self
78 }
79
80 pub fn build(mut self) -> GenericResult<Feature> {
82 let is_break_single_fn =
83 self.is_break_single_fn.take().ok_or_else(|| GenericError::from("is_break_single must be set"))?;
84
85 let code = self.violation_code.take().unwrap_or_default();
86 let policy_fn = self.policy_fn.take().unwrap_or_else(|| Arc::new(|_| BreakPolicy::SkipIfNoIntersection));
87 let belongs_to_route_fn = self.belongs_to_route_fn.take().unwrap_or_else(|| {
88 Arc::new({
89 let is_break_single_fn = is_break_single_fn.clone();
90 move |_, job| job.as_single().map_or(false, |single| is_break_single_fn(single))
91 })
92 });
93
94 let break_fns = BreakFns { is_break_single_fn, belongs_to_route_fn, policy_fn };
95
96 let context_transition = ConcreteJobContextTransition {
97 remove_required: {
98 let break_fns = break_fns.clone();
99 move |solution_ctx, route_index, job| {
100 !is_required_job(&break_fns, solution_ctx.routes.as_slice(), route_index, job, true)
101 }
102 },
103 promote_required: {
104 let break_fns = break_fns.clone();
105 move |solution_ctx, route_index, job| {
106 is_required_job(&break_fns, solution_ctx.routes.as_slice(), route_index, job, false)
107 }
108 },
109 remove_locked: |_, _, _| false,
110 promote_locked: |_, _, _| false,
111 };
112
113 FeatureBuilder::default()
114 .with_name(self.name.as_str())
115 .with_constraint(OptionalBreakConstraint { break_fns: break_fns.clone(), code })
116 .with_objective(OptionalBreakObjective { break_fns: break_fns.clone() })
117 .with_state(OptionalBreakState { context_transition, break_fns })
118 .build()
119 }
120}
121
122type BreakSingleFn = Arc<dyn Fn(&Single) -> bool + Send + Sync>;
123type BelongsToRouteFn = Arc<dyn Fn(&Route, &Job) -> bool + Send + Sync>;
124type BreakPolicyFn = Arc<dyn Fn(&Single) -> BreakPolicy + Send + Sync>;
125
126#[derive(Clone)]
127struct BreakFns {
128 is_break_single_fn: BreakSingleFn,
129 belongs_to_route_fn: BelongsToRouteFn,
130 policy_fn: BreakPolicyFn,
131}
132
133struct OptionalBreakConstraint {
134 break_fns: BreakFns,
135 code: ViolationCode,
136}
137
138impl OptionalBreakConstraint {
139 fn evaluate_route(&self, route_ctx: &RouteContext, job: &Job) -> Option<ConstraintViolation> {
140 let single = job.as_single()?;
141
142 if (self.break_fns.is_break_single_fn)(single) && !(self.break_fns.belongs_to_route_fn)(route_ctx.route(), job)
144 {
145 ConstraintViolation::fail(self.code)
146 } else {
147 None
148 }
149 }
150
151 fn evaluate_activity(&self, activity_ctx: &ActivityContext) -> Option<ConstraintViolation> {
152 activity_ctx
153 .target
154 .job
155 .as_ref()
156 .filter(|single| (self.break_fns.is_break_single_fn)(single) && activity_ctx.prev.job.is_none())
158 .and_then(|_| ConstraintViolation::skip(self.code))
159 }
160}
161
162impl FeatureConstraint for OptionalBreakConstraint {
163 fn evaluate(&self, move_ctx: &MoveContext<'_>) -> Option<ConstraintViolation> {
164 match move_ctx {
165 MoveContext::Route { route_ctx, job, .. } => self.evaluate_route(route_ctx, job),
166 MoveContext::Activity { activity_ctx, .. } => self.evaluate_activity(activity_ctx),
167 }
168 }
169
170 fn merge(&self, source: Job, candidate: Job) -> Result<Job, ViolationCode> {
171 let any_is_break = once(&source)
172 .chain(once(&candidate))
173 .filter_map(|job| job.as_single())
174 .any(|single| (self.break_fns.is_break_single_fn)(single));
175
176 if any_is_break {
177 Err(self.code)
178 } else {
179 Ok(source)
180 }
181 }
182}
183
184struct OptionalBreakObjective {
185 break_fns: BreakFns,
186}
187
188impl FeatureObjective for OptionalBreakObjective {
189 fn fitness(&self, solution: &InsertionContext) -> Cost {
190 solution
191 .solution
192 .routes
193 .iter()
194 .flat_map(|route_ctx| route_ctx.route().tour.jobs())
195 .filter_map(|job| job.as_single())
196 .filter(|single| (self.break_fns.is_break_single_fn)(single))
197 .count() as Float
198 }
199
200 fn estimate(&self, move_ctx: &MoveContext<'_>) -> Cost {
201 match move_ctx {
202 MoveContext::Route { job, .. } => {
203 if job.as_single().map_or(false, |single| (self.break_fns.is_break_single_fn)(single)) {
204 1.
205 } else {
206 Cost::default()
207 }
208 }
209 MoveContext::Activity { .. } => Cost::default(),
210 }
211 }
212}
213
214struct OptionalBreakState<JT: JobContextTransition + Send + Sync> {
215 context_transition: JT,
216 break_fns: BreakFns,
217}
218
219impl<JT: JobContextTransition + Send + Sync> FeatureState for OptionalBreakState<JT> {
220 fn accept_insertion(&self, solution_ctx: &mut SolutionContext, route_index: usize, _: &Job) {
221 process_conditional_jobs(solution_ctx, Some(route_index), &self.context_transition);
222 }
223
224 fn accept_route_state(&self, _: &mut RouteContext) {}
225
226 fn accept_solution_state(&self, solution_ctx: &mut SolutionContext) {
227 process_conditional_jobs(solution_ctx, None, &self.context_transition);
228 self.remove_invalid_breaks(solution_ctx);
229 }
230}
231
232impl<JT: JobContextTransition + Send + Sync> OptionalBreakState<JT> {
233 fn remove_invalid_breaks(&self, solution_ctx: &mut SolutionContext) {
237 let breaks_to_remove = solution_ctx
238 .routes
239 .iter()
240 .flat_map(|route_ctx| {
241 route_ctx
242 .route()
243 .tour
244 .all_activities()
245 .fold((0, HashSet::new()), |(prev, mut breaks), activity| {
246 let current = activity.place.location;
247
248 let Some(break_single) = activity
249 .job
250 .as_ref()
251 .filter(|single| (self.break_fns.is_break_single_fn)(single))
252 .filter(|&single| !solution_ctx.locked.contains(&Job::Single(single.clone())))
253 else {
254 return (current, breaks);
255 };
256
257 let location_count = break_single.places.iter().filter(|p| p.location.is_some()).count();
259 assert!(
260 location_count == 0 || location_count == break_single.places.len(),
261 "break with multiple places is not supported"
262 );
263
264 let is_orphan =
265 prev != current && break_single.places.first().and_then(|p| p.location).is_none();
266 let is_not_on_time = !is_on_proper_time(route_ctx, break_single, &activity.schedule)
267 || !can_be_scheduled(route_ctx, break_single, &self.break_fns.policy_fn);
268 let is_ovrp_last =
269 route_ctx.route().tour.end().map_or(false, |end| std::ptr::eq(activity, end));
270
271 if is_orphan || is_not_on_time || is_ovrp_last {
272 breaks.insert(Job::Single(break_single.clone()));
273 }
274
275 (current, breaks)
276 })
277 .1
278 .into_iter()
279 })
280 .collect::<Vec<_>>();
281
282 breaks_to_remove.iter().for_each(|break_job| {
283 solution_ctx.routes.iter_mut().filter(|route_ctx| route_ctx.route().tour.contains(break_job)).for_each(
284 |route_ctx| {
285 assert!(route_ctx.route_mut().tour.remove(break_job), "cannot remove break from the tour");
286 },
287 )
288 });
289
290 solution_ctx.unassigned.extend(breaks_to_remove.into_iter().map(|b| (b, UnassignmentInfo::Unknown)));
291
292 solution_ctx.unassigned.retain(|job, _| {
294 let routes = solution_ctx.routes.as_slice();
295 if !is_required_job(&self.break_fns, routes, None, job, true) {
296 solution_ctx.ignored.push(job.clone());
297 false
298 } else {
299 true
300 }
301 });
302 }
303}
304
305fn is_required_job(
306 break_fns: &BreakFns,
307 routes: &[RouteContext],
308 route_index: Option<usize>,
309 job: &Job,
310 default: bool,
311) -> bool {
312 job.as_single().map_or(default, |single| is_required_single(break_fns, routes, route_index, single, default))
313}
314
315fn is_required_single(
317 break_fns: &BreakFns,
318 routes: &[RouteContext],
319 route_index: Option<usize>,
320 single: &Arc<Single>,
321 default: bool,
322) -> bool {
323 if !(break_fns.is_break_single_fn)(single) {
324 return default;
325 }
326
327 if let Some(route_index) = route_index {
328 can_be_scheduled(&routes[route_index], single, &break_fns.policy_fn)
329 } else {
330 routes.iter().any(|route_ctx| {
331 (break_fns.belongs_to_route_fn)(route_ctx.route(), &Job::Single(single.clone()))
332 && can_be_scheduled(route_ctx, single, &break_fns.policy_fn)
333 })
334 }
335}
336
337fn can_be_scheduled(route_ctx: &RouteContext, break_single: &Single, policy_fn: &BreakPolicyFn) -> bool {
339 let departure = route_ctx.route().tour.start().unwrap().schedule.departure;
340 let arrival = route_ctx.route().tour.end().map_or(0., |end| end.schedule.arrival);
341 let tour_tw = TimeWindow::new(departure, arrival);
342
343 let policy = policy_fn(break_single);
344
345 get_break_time_windows(break_single, departure).any(|break_tw| match policy {
346 BreakPolicy::SkipIfNoIntersection => break_tw.intersects(&tour_tw),
347 BreakPolicy::SkipIfArrivalBeforeEnd => tour_tw.end > break_tw.end,
348 })
349}
350
351fn get_break_time_windows(break_single: &'_ Single, departure: Timestamp) -> impl Iterator<Item = TimeWindow> + '_ {
352 break_single
353 .places
354 .first()
355 .expect("missing time window in a break job")
356 .times
357 .iter()
358 .map(move |span| span.to_time_window(departure))
359}
360
361fn is_on_proper_time(route_ctx: &RouteContext, break_job: &Single, actual_schedule: &Schedule) -> bool {
363 let departure = route_ctx.route().tour.start().unwrap().schedule.departure;
364 let actual_tw = TimeWindow::new(actual_schedule.arrival, actual_schedule.departure);
365
366 get_break_time_windows(break_job, departure).any(|tw| tw.intersects(&actual_tw))
367}