1#[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
13pub 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 pub fn new(name: &str) -> Self {
29 Self { name: name.to_string(), transport: None, activity: None, code: None, is_constrained: true }
30 }
31
32 pub fn set_violation_code(mut self, code: ViolationCode) -> Self {
35 self.code = Some(code);
36 self
37 }
38
39 pub fn set_time_constrained(mut self, is_constrained: bool) -> Self {
42 self.is_constrained = is_constrained;
43 self
44 }
45
46 pub fn set_transport_cost(mut self, transport: Arc<dyn TransportCost>) -> Self {
48 self.transport = Some(transport);
49 self
50 }
51
52 pub fn set_activity_cost(mut self, activity: Arc<dyn ActivityCost>) -> Self {
55 self.activity = Some(activity);
56 self
57 }
58
59 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 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 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 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 (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 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 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}