#[cfg(test)]
#[path = "../../../tests/unit/construction/features/breaks_test.rs"]
mod breaks_test;
use super::*;
use crate::construction::enablers::*;
use crate::models::solution::Route;
use std::collections::HashSet;
use std::iter::once;
#[derive(Clone)]
pub enum BreakPolicy {
SkipIfNoIntersection,
SkipIfArrivalBeforeEnd,
}
pub struct BreakFeatureBuilder {
name: String,
violation_code: Option<ViolationCode>,
belongs_to_route_fn: Option<BelongsToRouteFn>,
is_break_single_fn: Option<BreakSingleFn>,
policy_fn: Option<BreakPolicyFn>,
}
impl BreakFeatureBuilder {
pub fn new(name: &str) -> Self {
Self {
name: name.to_string(),
violation_code: None,
belongs_to_route_fn: None,
is_break_single_fn: None,
policy_fn: None,
}
}
pub fn set_violation_code(mut self, violation_code: ViolationCode) -> Self {
self.violation_code = Some(violation_code);
self
}
pub fn set_is_break_single<F>(mut self, func: F) -> Self
where
F: Fn(&Single) -> bool + Send + Sync + 'static,
{
self.is_break_single_fn = Some(Arc::new(func));
self
}
pub fn set_policy<F>(mut self, func: F) -> Self
where
F: Fn(&Single) -> BreakPolicy + Send + Sync + 'static,
{
self.policy_fn = Some(Arc::new(func));
self
}
pub fn set_belongs_to_route<F>(mut self, func: F) -> Self
where
F: Fn(&Route, &Job) -> bool + Send + Sync + 'static,
{
self.belongs_to_route_fn = Some(Arc::new(func));
self
}
pub fn build(mut self) -> GenericResult<Feature> {
let is_break_single_fn =
self.is_break_single_fn.take().ok_or_else(|| GenericError::from("is_break_single must be set"))?;
let code = self.violation_code.take().unwrap_or_default();
let policy_fn = self.policy_fn.take().unwrap_or_else(|| Arc::new(|_| BreakPolicy::SkipIfNoIntersection));
let belongs_to_route_fn = self.belongs_to_route_fn.take().unwrap_or_else(|| {
Arc::new({
let is_break_single_fn = is_break_single_fn.clone();
move |_, job| job.as_single().map_or(false, |single| is_break_single_fn(single))
})
});
let break_fns = BreakFns { is_break_single_fn, belongs_to_route_fn, policy_fn };
let context_transition = ConcreteJobContextTransition {
remove_required: {
let break_fns = break_fns.clone();
move |solution_ctx, route_index, job| {
!is_required_job(&break_fns, solution_ctx.routes.as_slice(), route_index, job, true)
}
},
promote_required: {
let break_fns = break_fns.clone();
move |solution_ctx, route_index, job| {
is_required_job(&break_fns, solution_ctx.routes.as_slice(), route_index, job, false)
}
},
remove_locked: |_, _, _| false,
promote_locked: |_, _, _| false,
};
FeatureBuilder::default()
.with_name(self.name.as_str())
.with_constraint(OptionalBreakConstraint { break_fns: break_fns.clone(), code })
.with_objective(OptionalBreakObjective { break_fns: break_fns.clone() })
.with_state(OptionalBreakState { context_transition, break_fns })
.build()
}
}
type BreakSingleFn = Arc<dyn Fn(&Single) -> bool + Send + Sync>;
type BelongsToRouteFn = Arc<dyn Fn(&Route, &Job) -> bool + Send + Sync>;
type BreakPolicyFn = Arc<dyn Fn(&Single) -> BreakPolicy + Send + Sync>;
#[derive(Clone)]
struct BreakFns {
is_break_single_fn: BreakSingleFn,
belongs_to_route_fn: BelongsToRouteFn,
policy_fn: BreakPolicyFn,
}
struct OptionalBreakConstraint {
break_fns: BreakFns,
code: ViolationCode,
}
impl OptionalBreakConstraint {
fn evaluate_route(&self, route_ctx: &RouteContext, job: &Job) -> Option<ConstraintViolation> {
let single = job.as_single()?;
if (self.break_fns.is_break_single_fn)(single) && !(self.break_fns.belongs_to_route_fn)(route_ctx.route(), job)
{
ConstraintViolation::fail(self.code)
} else {
None
}
}
fn evaluate_activity(&self, activity_ctx: &ActivityContext) -> Option<ConstraintViolation> {
activity_ctx
.target
.job
.as_ref()
.filter(|single| (self.break_fns.is_break_single_fn)(single) && activity_ctx.prev.job.is_none())
.and_then(|_| ConstraintViolation::skip(self.code))
}
}
impl FeatureConstraint for OptionalBreakConstraint {
fn evaluate(&self, move_ctx: &MoveContext<'_>) -> Option<ConstraintViolation> {
match move_ctx {
MoveContext::Route { route_ctx, job, .. } => self.evaluate_route(route_ctx, job),
MoveContext::Activity { activity_ctx, .. } => self.evaluate_activity(activity_ctx),
}
}
fn merge(&self, source: Job, candidate: Job) -> Result<Job, ViolationCode> {
let any_is_break = once(&source)
.chain(once(&candidate))
.filter_map(|job| job.as_single())
.any(|single| (self.break_fns.is_break_single_fn)(single));
if any_is_break {
Err(self.code)
} else {
Ok(source)
}
}
}
struct OptionalBreakObjective {
break_fns: BreakFns,
}
impl FeatureObjective for OptionalBreakObjective {
fn fitness(&self, solution: &InsertionContext) -> Cost {
solution
.solution
.routes
.iter()
.flat_map(|route_ctx| route_ctx.route().tour.jobs())
.filter_map(|job| job.as_single())
.filter(|single| (self.break_fns.is_break_single_fn)(single))
.count() as f64
}
fn estimate(&self, move_ctx: &MoveContext<'_>) -> Cost {
match move_ctx {
MoveContext::Route { job, .. } => {
if job.as_single().map_or(false, |single| (self.break_fns.is_break_single_fn)(single)) {
1.
} else {
Cost::default()
}
}
MoveContext::Activity { .. } => Cost::default(),
}
}
}
struct OptionalBreakState<JT: JobContextTransition + Send + Sync> {
context_transition: JT,
break_fns: BreakFns,
}
impl<JT: JobContextTransition + Send + Sync> FeatureState for OptionalBreakState<JT> {
fn accept_insertion(&self, solution_ctx: &mut SolutionContext, route_index: usize, _: &Job) {
process_conditional_jobs(solution_ctx, Some(route_index), &self.context_transition);
}
fn accept_route_state(&self, _: &mut RouteContext) {}
fn accept_solution_state(&self, solution_ctx: &mut SolutionContext) {
process_conditional_jobs(solution_ctx, None, &self.context_transition);
self.remove_invalid_breaks(solution_ctx);
}
}
impl<JT: JobContextTransition + Send + Sync> OptionalBreakState<JT> {
fn remove_invalid_breaks(&self, solution_ctx: &mut SolutionContext) {
let breaks_to_remove = solution_ctx
.routes
.iter()
.flat_map(|route_ctx| {
route_ctx
.route()
.tour
.all_activities()
.fold((0, HashSet::new()), |(prev, mut breaks), activity| {
let current = activity.place.location;
let Some(break_single) = activity
.job
.as_ref()
.filter(|single| (self.break_fns.is_break_single_fn)(single))
.filter(|&single| !solution_ctx.locked.contains(&Job::Single(single.clone())))
else {
return (current, breaks);
};
let location_count = break_single.places.iter().filter(|p| p.location.is_some()).count();
assert!(
location_count == 0 || location_count == break_single.places.len(),
"break with multiple places is not supported"
);
let is_orphan =
prev != current && break_single.places.first().and_then(|p| p.location).is_none();
let is_not_on_time = !is_on_proper_time(route_ctx, break_single, &activity.schedule)
|| !can_be_scheduled(route_ctx, break_single, &self.break_fns.policy_fn);
let is_ovrp_last =
route_ctx.route().tour.end().map_or(false, |end| std::ptr::eq(activity, end));
if is_orphan || is_not_on_time || is_ovrp_last {
breaks.insert(Job::Single(break_single.clone()));
}
(current, breaks)
})
.1
.into_iter()
})
.collect::<Vec<_>>();
breaks_to_remove.iter().for_each(|break_job| {
solution_ctx.routes.iter_mut().filter(|route_ctx| route_ctx.route().tour.contains(break_job)).for_each(
|route_ctx| {
assert!(route_ctx.route_mut().tour.remove(break_job), "cannot remove break from the tour");
},
)
});
solution_ctx.unassigned.extend(breaks_to_remove.into_iter().map(|b| (b, UnassignmentInfo::Unknown)));
solution_ctx.unassigned.retain(|job, _| {
let routes = solution_ctx.routes.as_slice();
if !is_required_job(&self.break_fns, routes, None, job, true) {
solution_ctx.ignored.push(job.clone());
false
} else {
true
}
});
}
}
fn is_required_job(
break_fns: &BreakFns,
routes: &[RouteContext],
route_index: Option<usize>,
job: &Job,
default: bool,
) -> bool {
job.as_single().map_or(default, |single| is_required_single(break_fns, routes, route_index, single, default))
}
fn is_required_single(
break_fns: &BreakFns,
routes: &[RouteContext],
route_index: Option<usize>,
single: &Arc<Single>,
default: bool,
) -> bool {
if !(break_fns.is_break_single_fn)(single) {
return default;
}
if let Some(route_index) = route_index {
can_be_scheduled(&routes[route_index], single, &break_fns.policy_fn)
} else {
routes.iter().any(|route_ctx| {
(break_fns.belongs_to_route_fn)(route_ctx.route(), &Job::Single(single.clone()))
&& can_be_scheduled(route_ctx, single, &break_fns.policy_fn)
})
}
}
fn can_be_scheduled(route_ctx: &RouteContext, break_single: &Single, policy_fn: &BreakPolicyFn) -> bool {
let departure = route_ctx.route().tour.start().unwrap().schedule.departure;
let arrival = route_ctx.route().tour.end().map_or(0., |end| end.schedule.arrival);
let tour_tw = TimeWindow::new(departure, arrival);
let policy = policy_fn(break_single);
get_break_time_windows(break_single, departure).any(|break_tw| match policy {
BreakPolicy::SkipIfNoIntersection => break_tw.intersects(&tour_tw),
BreakPolicy::SkipIfArrivalBeforeEnd => tour_tw.end > break_tw.end,
})
}
fn get_break_time_windows(break_single: &'_ Single, departure: f64) -> impl Iterator<Item = TimeWindow> + '_ {
break_single
.places
.first()
.expect("missing time window in a break job")
.times
.iter()
.map(move |span| span.to_time_window(departure))
}
fn is_on_proper_time(route_ctx: &RouteContext, break_job: &Single, actual_schedule: &Schedule) -> bool {
let departure = route_ctx.route().tour.start().unwrap().schedule.departure;
let actual_tw = TimeWindow::new(actual_schedule.arrival, actual_schedule.departure);
get_break_time_windows(break_job, departure).any(|tw| tw.intersects(&actual_tw))
}