vrp_core/construction/features/
breaks.rs

1//! A vehicle break features.
2
3#[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/// Specifies break policy.
14#[derive(Clone)]
15pub enum BreakPolicy {
16    /// Allows to skip break if actual tour schedule doesn't intersect with vehicle time window.
17    SkipIfNoIntersection,
18
19    /// Allows to skip break if vehicle arrives before break's time window end.
20    SkipIfArrivalBeforeEnd,
21}
22
23/// Provides a way to build a feature to schedule an optional break. Here, optional means that break
24/// sometimes can be skipped due to constraint violations or suboptimal search path in solution space.
25pub 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    /// Creates a new instance of `BreakFeatureBuilder`.
35    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    /// Sets constraint violation code which is used to report back the reason of job's unassignment.
46    /// If not set, default violation code is used.
47    pub fn set_violation_code(mut self, violation_code: ViolationCode) -> Self {
48        self.violation_code = Some(violation_code);
49        self
50    }
51
52    /// Sets a function which specifies whether a given single job can be considered as a break job.
53    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    /// Sets a break policy. If not set, then [BreakPolicy::SkipIfNoIntersection] is used.
62    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    /// Sets a function which specifies whether a given route can serve a given break. This function
71    /// should return false, if the job is not break. If not set, any break job can be assigned to any route.
72    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    /// Builds a optional break feature.
81    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        // reject break for another vehicle
143        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            // reject inserting break at the very beginning
157            .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    /// Removes breaks which conditions are violated after ruin:
234    /// * break without location served separately when original job is removed, but break is kept.
235    /// * break is defined by interval, but its time is violated. This might happen due to departure time rescheduling.
236    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                        // NOTE break should have location defined for all places or for none of them
258                        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        // NOTE remove stale breaks from the violation list
293        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
315/// Mark single job as ignored only if it has a break type and vehicle id is not present in routes
316fn 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
337/// Checks whether break can be scheduled in route.
338fn 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
361/// Checks whether break is scheduled on time as its time can be invalid due to departure time optimizations.
362fn 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}