From 9702ececfe0156fe8ed3847ba65318cecb71628e Mon Sep 17 00:00:00 2001 From: reinterpretcat Date: Sat, 16 Sep 2023 21:15:05 +0200 Subject: [PATCH] Prototype of fast service objective Limited experimental implementation --- .../src/construction/features/fast_service.rs | 107 ++++++++++++++++++ vrp-core/src/construction/features/mod.rs | 3 + .../src/format/problem/goal_reader.rs | 7 ++ vrp-pragmatic/src/format/problem/model.rs | 4 + vrp-pragmatic/src/validation/objectives.rs | 1 + 5 files changed, 122 insertions(+) create mode 100644 vrp-core/src/construction/features/fast_service.rs diff --git a/vrp-core/src/construction/features/fast_service.rs b/vrp-core/src/construction/features/fast_service.rs new file mode 100644 index 000000000..0f01cfded --- /dev/null +++ b/vrp-core/src/construction/features/fast_service.rs @@ -0,0 +1,107 @@ +use super::*; +use crate::construction::enablers::calculate_travel; +use crate::models::solution::Activity; +use std::marker::PhantomData; + +/// Creates a feature to prefer a fast servicing of jobs. +pub fn create_fast_service_feature( + name: &str, + transport: Arc, + activity: Arc, +) -> Result { + FeatureBuilder::default() + .with_name(name) + .with_objective(FastServiceObjective::::new(transport, activity)) + .build() +} + +struct FastServiceObjective { + transport: Arc, + activity: Arc, + phantom: PhantomData, +} + +impl Objective for FastServiceObjective { + type Solution = InsertionContext; + + fn fitness(&self, solution: &Self::Solution) -> f64 { + solution + .solution + .routes + .iter() + .flat_map(|route_ctx| { + let start_time = route_ctx.route().tour.start().unwrap().schedule.departure; + route_ctx + .route() + .tour + .all_activities() + .filter(|a| self.is_static_delivery(a)) + .map(move |a| a.schedule.departure - start_time) + }) + .sum::() as Cost + } +} + +impl FeatureObjective for FastServiceObjective { + fn estimate(&self, move_ctx: &MoveContext<'_>) -> Cost { + if let Some(cost) = self.estimate_job_service(move_ctx) { + cost + } else { + Cost::default() + } + } +} + +impl FastServiceObjective { + fn new(transport: Arc, activity: Arc) -> Self { + Self { transport, activity, phantom: Default::default() } + } + + fn estimate_job_service(&self, move_ctx: &MoveContext<'_>) -> Option { + let (route_ctx, activity_ctx) = match move_ctx { + MoveContext::Route { .. } => return None, + MoveContext::Activity { route_ctx, activity_ctx } => (route_ctx, activity_ctx), + }; + + let (_, (prev_to_tar_dur, tar_to_next_dur)) = + calculate_travel(route_ctx, activity_ctx, self.transport.as_ref()); + + // TODO add support for: + // - reloads + // - pickup jobs + // - p&d jobs + + // handle static delivery only + if self.is_static_delivery(activity_ctx.target) { + let start_time = route_ctx.route().tour.start().unwrap().schedule.departure; + + let arrival = activity_ctx.prev.schedule.departure + prev_to_tar_dur; + let departure = self.activity.estimate_departure(route_ctx.route(), activity_ctx.target, arrival); + let target_cost = departure - start_time; + + let next_delta = if let Some(next) = activity_ctx.next { + let old_next_cost = next.schedule.arrival - start_time; + + let arrival = departure + tar_to_next_dur; + let departure = self.activity.estimate_departure(route_ctx.route(), next, arrival); + let new_next_cost = departure - start_time; + + new_next_cost - old_next_cost + } else { + Cost::default() + }; + + Some(target_cost + next_delta) + } else { + None + } + } + + fn is_static_delivery(&self, activity: &Activity) -> bool { + activity + .job + .as_ref() + .and_then::<&Demand, _>(|job| job.dimens.get_demand()) + .map_or(false, |demand| demand.delivery.0.is_not_empty()) + } +} diff --git a/vrp-core/src/construction/features/mod.rs b/vrp-core/src/construction/features/mod.rs index 638ea20ae..711229a5d 100644 --- a/vrp-core/src/construction/features/mod.rs +++ b/vrp-core/src/construction/features/mod.rs @@ -11,6 +11,9 @@ use std::sync::Arc; mod capacity; pub use self::capacity::*; +mod fast_service; +pub use self::fast_service::*; + mod fleet_usage; pub use self::fleet_usage::*; diff --git a/vrp-pragmatic/src/format/problem/goal_reader.rs b/vrp-pragmatic/src/format/problem/goal_reader.rs index 79ee6195c..1de147840 100644 --- a/vrp-pragmatic/src/format/problem/goal_reader.rs +++ b/vrp-pragmatic/src/format/problem/goal_reader.rs @@ -222,6 +222,13 @@ fn get_objective_features( } Objective::TourOrder => { create_tour_order_soft_feature("tour_order", TOUR_ORDER_KEY, get_tour_order_fn()) + }, + Objective::FastService => { + if props.has_multi_dimen_capacity { + create_fast_service_feature::("fast_service", transport.clone(), activity.clone()) + } else { + create_fast_service_feature::("fast_service", transport.clone(), activity.clone()) + } } }) .collect() diff --git a/vrp-pragmatic/src/format/problem/model.rs b/vrp-pragmatic/src/format/problem/model.rs index e45b5c401..02668f79c 100644 --- a/vrp-pragmatic/src/format/problem/model.rs +++ b/vrp-pragmatic/src/format/problem/model.rs @@ -645,6 +645,10 @@ pub enum Objective { /// An objective to control order of job activities in the tour. #[serde(rename(deserialize = "tour-order", serialize = "tour-order"))] TourOrder, + + /// An objective to prefer jobs to be served as soon as possible. + #[serde(rename(deserialize = "fast-service", serialize = "fast-service"))] + FastService, } /// Specifies balance objective options. At the moment, it uses coefficient of variation as diff --git a/vrp-pragmatic/src/validation/objectives.rs b/vrp-pragmatic/src/validation/objectives.rs index 1cabf0de2..55e7acc3f 100644 --- a/vrp-pragmatic/src/validation/objectives.rs +++ b/vrp-pragmatic/src/validation/objectives.rs @@ -39,6 +39,7 @@ fn check_e1601_duplicate_objectives(objectives: &[&Objective]) -> Result<(), For BalanceDuration { .. } => acc.entry("balance-duration"), CompactTour { .. } => acc.entry("compact-tour"), TourOrder => acc.entry("tour-order"), + FastService => acc.entry("fast-service"), } .and_modify(|count| *count += 1) .or_insert(1_usize);