Skip to content

Commit 91fabb2

Browse files
Prototype of fast service objective
Limited experimental implementation
1 parent fc5e0e5 commit 91fabb2

File tree

5 files changed

+122
-0
lines changed

5 files changed

+122
-0
lines changed
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
use super::*;
2+
use crate::construction::enablers::calculate_travel;
3+
use crate::models::solution::Activity;
4+
use std::marker::PhantomData;
5+
6+
/// Creates a feature to prefer a fast servicing of jobs.
7+
pub fn create_fast_service_feature<T: LoadOps>(
8+
name: &str,
9+
transport: Arc<dyn TransportCost + Send + Sync>,
10+
activity: Arc<dyn ActivityCost + Send + Sync>,
11+
) -> Result<Feature, GenericError> {
12+
FeatureBuilder::default()
13+
.with_name(name)
14+
.with_objective(FastServiceObjective::<T>::new(transport, activity))
15+
.build()
16+
}
17+
18+
struct FastServiceObjective<T> {
19+
transport: Arc<dyn TransportCost + Send + Sync>,
20+
activity: Arc<dyn ActivityCost + Send + Sync>,
21+
phantom: PhantomData<T>,
22+
}
23+
24+
impl<T: LoadOps> Objective for FastServiceObjective<T> {
25+
type Solution = InsertionContext;
26+
27+
fn fitness(&self, solution: &Self::Solution) -> f64 {
28+
solution
29+
.solution
30+
.routes
31+
.iter()
32+
.flat_map(|route_ctx| {
33+
let start_time = route_ctx.route().tour.start().unwrap().schedule.departure;
34+
route_ctx
35+
.route()
36+
.tour
37+
.all_activities()
38+
.filter(|a| self.is_static_delivery(a))
39+
.map(move |a| a.schedule.departure - start_time)
40+
})
41+
.sum::<Duration>() as Cost
42+
}
43+
}
44+
45+
impl<T: LoadOps> FeatureObjective for FastServiceObjective<T> {
46+
fn estimate(&self, move_ctx: &MoveContext<'_>) -> Cost {
47+
if let Some(cost) = self.estimate_job_service(move_ctx) {
48+
cost
49+
} else {
50+
Cost::default()
51+
}
52+
}
53+
}
54+
55+
impl<T: LoadOps> FastServiceObjective<T> {
56+
fn new(transport: Arc<dyn TransportCost + Send + Sync>, activity: Arc<dyn ActivityCost + Send + Sync>) -> Self {
57+
Self { transport, activity, phantom: Default::default() }
58+
}
59+
60+
fn estimate_job_service(&self, move_ctx: &MoveContext<'_>) -> Option<Cost> {
61+
let (route_ctx, activity_ctx) = match move_ctx {
62+
MoveContext::Route { .. } => return None,
63+
MoveContext::Activity { route_ctx, activity_ctx } => (route_ctx, activity_ctx),
64+
};
65+
66+
let (_, (prev_to_tar_dur, tar_to_next_dur)) =
67+
calculate_travel(route_ctx, activity_ctx, self.transport.as_ref());
68+
69+
// TODO add support for:
70+
// - reloads
71+
// - pickup jobs
72+
// - p&d jobs
73+
74+
// handle static delivery only
75+
if self.is_static_delivery(activity_ctx.target) {
76+
let start_time = route_ctx.route().tour.start().unwrap().schedule.departure;
77+
78+
let arrival = activity_ctx.prev.schedule.departure + prev_to_tar_dur;
79+
let departure = self.activity.estimate_departure(route_ctx.route(), activity_ctx.target, arrival);
80+
let target_cost = departure - start_time;
81+
82+
let next_delta = if let Some(next) = activity_ctx.next {
83+
let old_next_cost = next.schedule.arrival - start_time;
84+
85+
let arrival = departure + tar_to_next_dur;
86+
let departure = self.activity.estimate_departure(route_ctx.route(), next, arrival);
87+
let new_next_cost = departure - start_time;
88+
89+
new_next_cost - old_next_cost
90+
} else {
91+
Cost::default()
92+
};
93+
94+
Some(target_cost + next_delta)
95+
} else {
96+
None
97+
}
98+
}
99+
100+
fn is_static_delivery(&self, activity: &Activity) -> bool {
101+
activity
102+
.job
103+
.as_ref()
104+
.and_then::<&Demand<T>, _>(|job| job.dimens.get_demand())
105+
.map_or(false, |demand| demand.delivery.0.is_not_empty())
106+
}
107+
}

vrp-core/src/construction/features/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ use std::sync::Arc;
1111
mod capacity;
1212
pub use self::capacity::*;
1313

14+
mod fast_service;
15+
pub use self::fast_service::*;
16+
1417
mod fleet_usage;
1518
pub use self::fleet_usage::*;
1619

vrp-pragmatic/src/format/problem/goal_reader.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,13 @@ fn get_objective_features(
224224
}
225225
Objective::TourOrder => {
226226
create_tour_order_soft_feature("tour_order", TOUR_ORDER_KEY, get_tour_order_fn())
227+
},
228+
Objective::FastService => {
229+
if props.has_multi_dimen_capacity {
230+
create_fast_service_feature::<MultiDimLoad>("fast_service", transport.clone(), activity.clone())
231+
} else {
232+
create_fast_service_feature::<SingleDimLoad>("fast_service", transport.clone(), activity.clone())
233+
}
227234
}
228235
})
229236
.collect()

vrp-pragmatic/src/format/problem/model.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,10 @@ pub enum Objective {
645645
/// An objective to control order of job activities in the tour.
646646
#[serde(rename(deserialize = "tour-order", serialize = "tour-order"))]
647647
TourOrder,
648+
649+
/// An objective to prefer jobs to be served as soon as possible.
650+
#[serde(rename(deserialize = "fast-service", serialize = "fast-service"))]
651+
FastService,
648652
}
649653

650654
/// Specifies balance objective options. At the moment, it uses coefficient of variation as

vrp-pragmatic/src/validation/objectives.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ fn check_e1601_duplicate_objectives(objectives: &[&Objective]) -> Result<(), For
3939
BalanceDuration { .. } => acc.entry("balance-duration"),
4040
CompactTour { .. } => acc.entry("compact-tour"),
4141
TourOrder => acc.entry("tour-order"),
42+
FastService => acc.entry("fast-service"),
4243
}
4344
.and_modify(|count| *count += 1)
4445
.or_insert(1_usize);

0 commit comments

Comments
 (0)