Skip to main content

vrp_core/construction/features/
transport.rs

1//! Provides the way to deal time/distance cost.
2
3#[cfg(test)]
4#[path = "../../../tests/unit/construction/features/transport_test.rs"]
5mod transport_test;
6
7use super::*;
8use crate::construction::enablers::*;
9use crate::models::common::Timestamp;
10use crate::models::problem::{ActivityCost, Single, TransportCost, TravelTime};
11use crate::models::solution::Activity;
12
13// TODO
14//  remove get_total_cost, get_route_costs, get_max_cost methods from contexts
15//  add validation rule which ensures usage of only one of these methods.
16
17/// Provides a way to build different flavors of time window feature.
18pub struct TransportFeatureBuilder {
19    name: String,
20    transport: Option<Arc<dyn TransportCost>>,
21    activity: Option<Arc<dyn ActivityCost>>,
22    code: Option<ViolationCode>,
23    is_constrained: bool,
24}
25
26impl TransportFeatureBuilder {
27    /// Creates a new instance of `TransportFeatureBuilder`.
28    pub fn new(name: &str) -> Self {
29        Self { name: name.to_string(), transport: None, activity: None, code: None, is_constrained: true }
30    }
31
32    /// Sets constraint violation code which is used to report back the reason of job's unassignment.
33    /// If not set, the default violation code is used.
34    pub fn set_violation_code(mut self, code: ViolationCode) -> Self {
35        self.code = Some(code);
36        self
37    }
38
39    /// Marks feature as non-constrained meaning that there no need to consider time as a hard constraint.
40    /// Default is true.
41    pub fn set_time_constrained(mut self, is_constrained: bool) -> Self {
42        self.is_constrained = is_constrained;
43        self
44    }
45
46    /// Sets transport costs to estimate distance.
47    pub fn set_transport_cost(mut self, transport: Arc<dyn TransportCost>) -> Self {
48        self.transport = Some(transport);
49        self
50    }
51
52    /// Sets activity costs to estimate job start/end time.
53    /// If omitted, then [SimpleActivityCost] is used by default.
54    pub fn set_activity_cost(mut self, activity: Arc<dyn ActivityCost>) -> Self {
55        self.activity = Some(activity);
56        self
57    }
58
59    /// Builds a flavor of transport feature which only updates activity schedules. No objective, no constraint.
60    pub fn build_schedule_updater(mut self) -> GenericResult<Feature> {
61        let (transport, activity) = self.get_costs()?;
62
63        FeatureBuilder::default()
64            .with_name(self.name.as_str())
65            .with_state(TransportState::new(transport, activity))
66            .build()
67    }
68
69    /// Creates the transport feature which considers duration for minimization as a global objective.
70    /// TODO: distance costs are still considered on local level.
71    pub fn build_minimize_duration(mut self) -> GenericResult<Feature> {
72        let (transport, activity) = self.get_costs()?;
73
74        create_feature(
75            self.name.as_str(),
76            transport,
77            activity,
78            self.code.unwrap_or_default(),
79            self.is_constrained,
80            Box::new(move |insertion_ctx| {
81                insertion_ctx.solution.routes.iter().fold(Cost::default(), move |acc, route_ctx| {
82                    acc + route_ctx.state().get_total_duration().cloned().unwrap_or(0.)
83                })
84            }),
85        )
86    }
87
88    /// Creates the transport feature which considers distance for minimization as a global objective.
89    /// TODO: duration costs are still considered on local level.
90    pub fn build_minimize_distance(mut self) -> GenericResult<Feature> {
91        let (transport, activity) = self.get_costs()?;
92        create_feature(
93            self.name.as_str(),
94            transport,
95            activity,
96            self.code.unwrap_or_default(),
97            self.is_constrained,
98            Box::new(move |insertion_ctx| {
99                insertion_ctx.solution.routes.iter().fold(Cost::default(), move |acc, route_ctx| {
100                    acc + route_ctx.state().get_total_distance().copied().unwrap_or(0.)
101                })
102            }),
103        )
104    }
105
106    /// Creates the transport feature which considers distance and duration for minimization.
107    pub fn build_minimize_cost(mut self) -> GenericResult<Feature> {
108        let (transport, activity) = self.get_costs()?;
109
110        create_feature(
111            self.name.as_str(),
112            transport,
113            activity,
114            self.code.unwrap_or_default(),
115            self.is_constrained,
116            Box::new(|insertion_ctx| insertion_ctx.get_total_cost().unwrap_or_default()),
117        )
118    }
119
120    fn get_costs(&mut self) -> GenericResult<(Arc<dyn TransportCost>, Arc<dyn ActivityCost>)> {
121        let transport = self.transport.take().ok_or_else(|| GenericError::from("transport must be set"))?;
122        let activity = self.activity.take().unwrap_or_else(|| Arc::new(SimpleActivityCost::default()));
123
124        Ok((transport, activity))
125    }
126}
127
128fn create_feature(
129    name: &str,
130    transport: Arc<dyn TransportCost>,
131    activity: Arc<dyn ActivityCost>,
132    time_window_code: ViolationCode,
133    is_constrained: bool,
134    fitness_fn: Box<dyn Fn(&InsertionContext) -> Float + Send + Sync>,
135) -> Result<Feature, GenericError> {
136    let builder = FeatureBuilder::default()
137        .with_name(name)
138        .with_state(TransportState::new(transport.clone(), activity.clone()))
139        .with_objective(TransportObjective { transport: transport.clone(), activity: activity.clone(), fitness_fn });
140
141    if is_constrained {
142        builder
143            .with_constraint(TransportConstraint {
144                transport: transport.clone(),
145                activity: activity.clone(),
146                time_window_code,
147            })
148            .build()
149    } else {
150        builder.build()
151    }
152}
153
154struct TransportConstraint {
155    transport: Arc<dyn TransportCost>,
156    activity: Arc<dyn ActivityCost>,
157    time_window_code: ViolationCode,
158}
159
160impl TransportConstraint {
161    fn evaluate_job(&self, route_ctx: &RouteContext, job: &Job) -> Option<ConstraintViolation> {
162        let date = route_ctx.route().tour.start().unwrap().schedule.departure;
163        let check_single = |single: &Arc<Single>| {
164            single
165                .places
166                .iter()
167                .flat_map(|place| place.times.iter())
168                .any(|time| time.intersects(date, &route_ctx.route().actor.detail.time))
169        };
170
171        let has_time_intersection = match job {
172            Job::Single(single) => check_single(single),
173            Job::Multi(multi) => multi.jobs.iter().all(check_single),
174        };
175
176        if has_time_intersection {
177            None
178        } else {
179            ConstraintViolation::fail(self.time_window_code)
180        }
181    }
182
183    fn evaluate_activity(
184        &self,
185        route_ctx: &RouteContext,
186        activity_ctx: &ActivityContext,
187    ) -> Option<ConstraintViolation> {
188        let actor = route_ctx.route().actor.as_ref();
189        let route = route_ctx.route();
190
191        let prev = activity_ctx.prev;
192        let target = activity_ctx.target;
193        let next = activity_ctx.next;
194
195        let departure = prev.schedule.departure;
196
197        if actor.detail.time.end < prev.place.time.start
198            || actor.detail.time.end < target.place.time.start
199            || next.map_or(false, |next| actor.detail.time.end < next.place.time.start)
200        {
201            return ConstraintViolation::fail(self.time_window_code);
202        }
203
204        let (next_act_location, latest_arr_time_at_next) = if let Some(next) = next {
205            let latest_arrival = route_ctx.state().get_latest_arrival_at(activity_ctx.index + 1).copied();
206            (next.place.location, latest_arrival.unwrap_or(next.place.time.end))
207        } else {
208            // open vrp
209            (target.place.location, target.place.time.end.min(actor.detail.time.end))
210        };
211
212        let arr_time_at_next = departure
213            + self.transport.duration(route, prev.place.location, next_act_location, TravelTime::Departure(departure));
214
215        if arr_time_at_next > latest_arr_time_at_next {
216            return ConstraintViolation::fail(self.time_window_code);
217        }
218        if target.place.time.start > latest_arr_time_at_next {
219            return ConstraintViolation::skip(self.time_window_code);
220        }
221
222        let arr_time_at_target = departure
223            + self.transport.duration(
224                route,
225                prev.place.location,
226                target.place.location,
227                TravelTime::Departure(departure),
228            );
229
230        let latest_departure_at_target = latest_arr_time_at_next
231            - self.transport.duration(
232                route,
233                target.place.location,
234                next_act_location,
235                TravelTime::Arrival(latest_arr_time_at_next),
236            );
237
238        let latest_arr_time_at_target =
239            target.place.time.end.min(self.activity.estimate_arrival(route, target, latest_departure_at_target));
240
241        if arr_time_at_target > latest_arr_time_at_target {
242            return ConstraintViolation::skip(self.time_window_code);
243        }
244
245        if next.is_none() {
246            return ConstraintViolation::success();
247        }
248
249        let end_time_at_target = self.activity.estimate_departure(route, target, arr_time_at_target);
250
251        let arr_time_at_next = end_time_at_target
252            + self.transport.duration(
253                route,
254                target.place.location,
255                next_act_location,
256                TravelTime::Departure(end_time_at_target),
257            );
258
259        if arr_time_at_next > latest_arr_time_at_next {
260            ConstraintViolation::skip(self.time_window_code)
261        } else {
262            ConstraintViolation::success()
263        }
264    }
265}
266
267impl FeatureConstraint for TransportConstraint {
268    fn evaluate(&self, move_ctx: &MoveContext<'_>) -> Option<ConstraintViolation> {
269        match move_ctx {
270            MoveContext::Route { route_ctx, job, .. } => self.evaluate_job(route_ctx, job),
271            MoveContext::Activity { route_ctx, activity_ctx } => self.evaluate_activity(route_ctx, activity_ctx),
272        }
273    }
274
275    fn merge(&self, source: Job, _: Job) -> Result<Job, ViolationCode> {
276        // NOTE we don't change temporal parameters here, it is responsibility of the caller
277        Ok(source)
278    }
279}
280
281struct TransportObjective {
282    activity: Arc<dyn ActivityCost>,
283    transport: Arc<dyn TransportCost>,
284    fitness_fn: Box<dyn Fn(&InsertionContext) -> Float + Send + Sync>,
285}
286
287impl TransportObjective {
288    fn estimate_route(&self, route_ctx: &RouteContext) -> Float {
289        if route_ctx.route().tour.has_jobs() {
290            0.
291        } else {
292            route_ctx.route().actor.driver.costs.fixed + route_ctx.route().actor.vehicle.costs.fixed
293        }
294    }
295
296    fn estimate_activity(&self, route_ctx: &RouteContext, activity_ctx: &ActivityContext) -> Float {
297        let prev = activity_ctx.prev;
298        let target = activity_ctx.target;
299        let next = activity_ctx.next;
300
301        let (tp_cost_left, act_cost_left, dep_time_left) =
302            self.analyze_route_leg(route_ctx, prev, target, prev.schedule.departure);
303
304        let (tp_cost_right, act_cost_right, dep_time_right) = if let Some(next) = next {
305            self.analyze_route_leg(route_ctx, target, next, dep_time_left)
306        } else {
307            (0., 0., 0.)
308        };
309
310        let new_costs = tp_cost_left + tp_cost_right + act_cost_left + act_cost_right;
311
312        // no jobs yet or open vrp.
313        if !route_ctx.route().tour.has_jobs() || next.is_none() {
314            return new_costs;
315        }
316
317        let next = next.unwrap();
318        let waiting_time = route_ctx.state().get_waiting_time_at(activity_ctx.index + 1).copied().unwrap_or_default();
319
320        let (tp_cost_old, act_cost_old, dep_time_old) =
321            self.analyze_route_leg(route_ctx, prev, next, prev.schedule.departure);
322
323        let waiting_cost = waiting_time.min(Float::default().max(dep_time_right - dep_time_old))
324            * route_ctx.route().actor.vehicle.costs.per_waiting_time;
325
326        let old_costs = tp_cost_old + act_cost_old + waiting_cost;
327
328        new_costs - old_costs
329    }
330
331    fn analyze_route_leg(
332        &self,
333        route_ctx: &RouteContext,
334        start: &Activity,
335        end: &Activity,
336        time: Timestamp,
337    ) -> (Cost, Cost, Timestamp) {
338        let route = route_ctx.route();
339
340        let arrival = time
341            + self.transport.duration(route, start.place.location, end.place.location, TravelTime::Departure(time));
342        let departure = self.activity.estimate_departure(route, end, arrival);
343
344        let transport_cost =
345            self.transport.cost(route, start.place.location, end.place.location, TravelTime::Departure(time));
346        let activity_cost = self.activity.cost(route, end, arrival);
347
348        (transport_cost, activity_cost, departure)
349    }
350}
351
352impl FeatureObjective for TransportObjective {
353    fn fitness(&self, solution: &InsertionContext) -> Cost {
354        (self.fitness_fn)(solution)
355    }
356
357    fn estimate(&self, move_ctx: &MoveContext<'_>) -> Cost {
358        match move_ctx {
359            MoveContext::Route { route_ctx, .. } => self.estimate_route(route_ctx),
360            MoveContext::Activity { route_ctx, activity_ctx } => self.estimate_activity(route_ctx, activity_ctx),
361        }
362    }
363}
364
365struct TransportState {
366    transport: Arc<dyn TransportCost>,
367    activity: Arc<dyn ActivityCost>,
368}
369
370impl TransportState {
371    fn new(transport: Arc<dyn TransportCost>, activity: Arc<dyn ActivityCost>) -> Self {
372        Self { transport, activity }
373    }
374}
375
376impl FeatureState for TransportState {
377    fn accept_insertion(&self, solution_ctx: &mut SolutionContext, route_index: usize, _: &Job) {
378        let route_ctx = solution_ctx.routes.get_mut(route_index).unwrap();
379        self.accept_route_state(route_ctx);
380    }
381
382    fn accept_route_state(&self, route_ctx: &mut RouteContext) {
383        update_route_schedule(route_ctx, self.activity.as_ref(), self.transport.as_ref());
384    }
385
386    fn accept_solution_state(&self, solution_ctx: &mut SolutionContext) {
387        solution_ctx.routes.iter_mut().filter(|route_ctx| route_ctx.is_stale()).for_each(|route_ctx| {
388            update_route_schedule(route_ctx, self.activity.as_ref(), self.transport.as_ref());
389        })
390    }
391}