vrp_core/construction/features/
reloads.rs1#[cfg(test)]
7#[path = "../../../tests/unit/construction/features/reloads_test.rs"]
8mod reloads_test;
9
10use crate::construction::enablers::{FeatureCombinator, RouteIntervals, RouteIntervalsState};
11use crate::construction::features::capacity::*;
12use crate::construction::heuristics::*;
13use crate::models::common::{Demand, LoadOps, MultiDimLoad, SingleDimLoad};
14use crate::models::problem::{Job, Single};
15use crate::models::solution::{Activity, Route};
16use crate::models::*;
17use rosomaxa::utils::{GenericError, GenericResult};
18use std::cmp::Ordering;
19use std::collections::HashMap;
20use std::ops::{Add, Range, RangeInclusive, Sub};
21use std::sync::Arc;
22
23pub trait SharedResource: LoadOps + Add + Sub + PartialOrd + Copy + Sized + Send + Sync + Default + 'static {}
25
26pub type SharedResourceId = usize;
28
29#[allow(clippy::type_complexity)]
31pub struct ReloadFeatureFactory<T: LoadOps> {
32 name: String,
33 capacity_code: Option<ViolationCode>,
34 resource_code: Option<ViolationCode>,
35
36 is_reload_single_fn: Option<Arc<dyn Fn(&Single) -> bool + Send + Sync>>,
37 belongs_to_route_fn: Option<Arc<dyn Fn(&Route, &Job) -> bool + Send + Sync>>,
38 load_schedule_threshold_fn: Option<Box<dyn Fn(&T) -> T + Send + Sync>>,
39
40 shared_resource_capacity_fn: Option<SharedResourceCapacityFn<T>>,
42 shared_resource_demand_fn: Option<SharedResourceDemandFn<T>>,
43 is_partial_solution_fn: Option<PartialSolutionFn>,
44}
45
46impl<T: SharedResource> ReloadFeatureFactory<T> {
47 pub fn set_resource_code(mut self, code: ViolationCode) -> Self {
49 self.resource_code = Some(code);
50 self
51 }
52
53 pub fn set_shared_resource_capacity<F>(mut self, func: F) -> Self
55 where
56 F: Fn(&Activity) -> Option<(T, SharedResourceId)> + Send + Sync + 'static,
57 {
58 self.shared_resource_capacity_fn = Some(Arc::new(func));
59 self
60 }
61
62 pub fn set_shared_demand_capacity<F>(mut self, func: F) -> Self
64 where
65 F: Fn(&Single) -> Option<T> + Send + Sync + 'static,
66 {
67 self.shared_resource_demand_fn = Some(Arc::new(func));
68 self
69 }
70
71 pub fn set_is_partial_solution<F>(mut self, func: F) -> Self
73 where
74 F: Fn(&SolutionContext) -> bool + Send + Sync + 'static,
75 {
76 self.is_partial_solution_fn = Some(Arc::new(func));
77 self
78 }
79
80 pub fn build_shared(mut self) -> GenericResult<Feature> {
82 let violation_code = self.resource_code.unwrap_or_default();
83
84 let Some(((resource_capacity_fn, resource_demand_fn), is_partial_solution_fn)) = self
86 .shared_resource_capacity_fn
87 .take()
88 .zip(self.shared_resource_demand_fn.take())
89 .zip(self.is_partial_solution_fn.take())
90 else {
91 return Err("shared_resource_capacity, shared_resource_demand and partial_solution must be set for shared reload feature".into());
92 };
93
94 let shared_resource_threshold_fn: SharedResourceThresholdFn<T> =
95 Box::new(move |route_ctx: &RouteContext, activity_idx, demand| {
96 route_ctx
97 .state()
98 .get_activity_state::<SharedResourceStateKey, T>(activity_idx)
99 .map_or(true, |resource_available| resource_available.can_fit(demand))
100 });
101
102 let simple_reload = self.build(Some(shared_resource_threshold_fn))?;
103
104 let shared_resource = FeatureBuilder::default()
105 .with_name(self.name.as_str())
106 .with_constraint(SharedResourceConstraint {
107 violation_code,
108 resource_demand_fn: resource_demand_fn.clone(),
109 is_partial_solution_fn: is_partial_solution_fn.clone(),
110 })
111 .with_state(SharedResourceState { resource_capacity_fn, resource_demand_fn, is_partial_solution_fn })
112 .build()?;
113
114 FeatureCombinator::default().use_name(self.name).add_features(&[simple_reload, shared_resource]).combine()
115 }
116}
117
118impl<T: LoadOps> ReloadFeatureFactory<T> {
119 pub fn new(name: &str) -> Self {
121 Self {
122 name: name.to_string(),
123 capacity_code: None,
124 resource_code: None,
125 is_reload_single_fn: None,
126 belongs_to_route_fn: None,
127 load_schedule_threshold_fn: None,
128 shared_resource_capacity_fn: None,
129 shared_resource_demand_fn: None,
130 is_partial_solution_fn: None,
131 }
132 }
133
134 pub fn set_capacity_code(mut self, code: ViolationCode) -> Self {
136 self.capacity_code = Some(code);
137 self
138 }
139
140 pub fn set_is_reload_single<F>(mut self, func: F) -> Self
142 where
143 F: Fn(&Single) -> bool + Send + Sync + 'static,
144 {
145 self.is_reload_single_fn = Some(Arc::new(func));
146 self
147 }
148
149 pub fn set_belongs_to_route<F>(mut self, func: F) -> Self
152 where
153 F: Fn(&Route, &Job) -> bool + Send + Sync + 'static,
154 {
155 self.belongs_to_route_fn = Some(Arc::new(func));
156 self
157 }
158
159 pub fn set_load_schedule_threshold<F>(mut self, func: F) -> Self
162 where
163 F: Fn(&T) -> T + Send + Sync + 'static,
164 {
165 self.load_schedule_threshold_fn = Some(Box::new(func));
166 self
167 }
168
169 pub fn build_simple(mut self) -> GenericResult<Feature> {
171 self.build(None)
172 }
173
174 fn build(&mut self, shared_resource_threshold_fn: Option<SharedResourceThresholdFn<T>>) -> GenericResult<Feature> {
175 let is_marker_single_fn =
179 self.is_reload_single_fn.take().ok_or_else(|| GenericError::from("is_reload_single must be set"))?;
180 let is_assignable_fn =
181 self.belongs_to_route_fn.take().ok_or_else(|| GenericError::from("belongs_to_route must be set"))?;
182 let load_schedule_threshold_fn = self
183 .load_schedule_threshold_fn
184 .take()
185 .ok_or_else(|| GenericError::from("load_schedule_threshold must be set"))?;
186
187 let route_intervals = RouteIntervals::Multiple {
189 is_marker_single_fn,
190 is_new_interval_needed_fn: Arc::new(move |route_ctx| {
191 route_ctx
192 .route()
193 .tour
194 .end_idx()
195 .map(|end_idx| {
196 let current: T =
197 route_ctx.state().get_max_past_capacity_at(end_idx).cloned().unwrap_or_default();
198
199 let max_capacity =
200 route_ctx.route().actor.vehicle.dimens.get_vehicle_capacity().cloned().unwrap_or_default();
201 let threshold_capacity = (load_schedule_threshold_fn)(&max_capacity);
202
203 current.partial_cmp(&threshold_capacity) != Some(Ordering::Less)
204 })
205 .unwrap_or(false)
206 }),
207 is_obsolete_interval_fn: Arc::new(move |route_ctx, left, right| {
208 let capacity: T =
209 route_ctx.route().actor.vehicle.dimens.get_vehicle_capacity().cloned().unwrap_or_default();
210
211 let fold_demand = |range: Range<usize>, demand_fn: fn(&Demand<T>) -> T| {
212 route_ctx.route().tour.activities_slice(range.start, range.end).iter().fold(
213 T::default(),
214 |acc, activity| {
215 activity
216 .job
217 .as_ref()
218 .and_then(|job| job.dimens.get_job_demand())
219 .map(|demand| acc + demand_fn(demand))
220 .unwrap_or_else(|| acc)
221 },
222 )
223 };
224
225 let left_pickup = fold_demand(left.clone(), |demand| demand.pickup.0);
226 let right_delivery = fold_demand(right.clone(), |demand| demand.delivery.0);
227
228 let new_max_load_left =
230 route_ctx.state().get_max_future_capacity_at::<T>(left.start).cloned().unwrap_or_default()
231 + right_delivery;
232
233 let new_max_load_right =
235 route_ctx.state().get_max_future_capacity_at::<T>(right.start).cloned().unwrap_or_default()
236 + left_pickup;
237
238 let has_enough_vehicle_capacity =
239 capacity.can_fit(&new_max_load_left) && capacity.can_fit(&new_max_load_right);
240
241 has_enough_vehicle_capacity
242 && shared_resource_threshold_fn.as_ref().map_or(true, |shared_resource_threshold_fn| {
243 let left_delivery = fold_demand(left.start..right.end, |demand| demand.delivery.0);
245
246 (shared_resource_threshold_fn)(route_ctx, left.start, &left_delivery)
247 })
248 }),
249 is_assignable_fn,
250 intervals_state: Arc::new(ReloadIntervalsState),
251 };
252
253 let violation_code = self.capacity_code.unwrap_or_default();
254
255 CapacityFeatureBuilder::<T>::new(self.name.as_str())
257 .set_violation_code(violation_code)
258 .set_route_intervals(route_intervals)
259 .build()
260 }
261}
262
263custom_route_intervals_state!(pub ReloadIntervals);
264
265struct SharedResourceStateKey;
269
270type SharedResourceCapacityFn<T> = Arc<dyn Fn(&Activity) -> Option<(T, SharedResourceId)> + Send + Sync>;
271type SharedResourceDemandFn<T> = Arc<dyn Fn(&Single) -> Option<T> + Send + Sync>;
272type SharedResourceThresholdFn<T> = Box<dyn Fn(&RouteContext, usize, &T) -> bool + Send + Sync>;
273type PartialSolutionFn = Arc<dyn Fn(&SolutionContext) -> bool + Send + Sync>;
274
275struct SharedResourceConstraint<T: SharedResource> {
276 violation_code: ViolationCode,
277 resource_demand_fn: SharedResourceDemandFn<T>,
278 is_partial_solution_fn: PartialSolutionFn,
279}
280
281impl<T: SharedResource> SharedResourceConstraint<T> {
282 fn evaluate_route(
283 &self,
284 solution_ctx: &SolutionContext,
285 route_ctx: &RouteContext,
286 job: &Job,
287 ) -> Option<ConstraintViolation> {
288 job.as_single()
289 .and_then(|job| {
290 (self.resource_demand_fn)(job)
291 .zip(route_ctx.state().get_reload_intervals().and_then(|intervals| intervals.first()))
292 })
293 .and_then(|(_, _)| {
294 if (self.is_partial_solution_fn)(solution_ctx) {
296 ConstraintViolation::fail(self.violation_code)
297 } else {
298 ConstraintViolation::success()
299 }
300 })
301 }
302
303 fn evaluate_activity(
304 &self,
305 route_ctx: &RouteContext,
306 activity_ctx: &ActivityContext,
307 ) -> Option<ConstraintViolation> {
308 route_ctx
309 .state()
310 .get_reload_intervals()
311 .iter()
312 .flat_map(|intervals| intervals.iter())
313 .find(|(_, end_idx)| activity_ctx.index <= *end_idx)
314 .and_then(|&(start_idx, _)| {
315 route_ctx
316 .state()
317 .get_activity_state::<SharedResourceStateKey, Option<T>>(start_idx)
318 .and_then(|resource_available| *resource_available)
319 .and_then(|resource_available| {
320 let resource_demand = activity_ctx
321 .target
322 .job
323 .as_ref()
324 .and_then(|job| (self.resource_demand_fn)(job.as_ref()))
325 .unwrap_or_default();
326
327 if resource_available
328 .partial_cmp(&resource_demand)
329 .map_or(false, |ordering| ordering == Ordering::Less)
330 {
331 ConstraintViolation::skip(self.violation_code)
332 } else {
333 ConstraintViolation::success()
334 }
335 })
336 })
337 }
338}
339
340impl<T: SharedResource> FeatureConstraint for SharedResourceConstraint<T> {
341 fn evaluate(&self, move_ctx: &MoveContext<'_>) -> Option<ConstraintViolation> {
342 match move_ctx {
343 MoveContext::Route { solution_ctx, route_ctx, job } => self.evaluate_route(solution_ctx, route_ctx, job),
344 MoveContext::Activity { route_ctx, activity_ctx } => self.evaluate_activity(route_ctx, activity_ctx),
345 }
346 }
347
348 fn merge(&self, source: Job, _: Job) -> Result<Job, ViolationCode> {
349 Ok(source)
350 }
351}
352
353struct SharedResourceState<T>
354where
355 T: SharedResource + Add<Output = T> + Sub<Output = T>,
356{
357 resource_capacity_fn: SharedResourceCapacityFn<T>,
358 resource_demand_fn: SharedResourceDemandFn<T>,
359 is_partial_solution_fn: PartialSolutionFn,
360}
361
362impl<T: SharedResource + Add<Output = T> + Sub<Output = T>> SharedResourceState<T> {
363 fn update_resource_consumption(&self, solution_ctx: &mut SolutionContext) {
365 if (self.is_partial_solution_fn)(solution_ctx) {
367 return;
368 }
369
370 let total_demand = solution_ctx.routes.iter().fold(HashMap::<usize, T>::default(), |acc, route_ctx| {
372 route_ctx.state().get_reload_intervals().iter().flat_map(|intervals| intervals.iter()).fold(
373 acc,
374 |mut acc, &(start_idx, end_idx)| {
375 let activity = get_activity_by_idx(route_ctx.route(), start_idx);
377 let resource_demand_with_id = (self.resource_capacity_fn)(activity)
378 .map(|(_, resource_id)| (self.get_total_demand(route_ctx, start_idx..=end_idx), resource_id));
379
380 if let Some((resource_demand, id)) = resource_demand_with_id {
381 let entry = acc.entry(id).or_default();
382 *entry = *entry + resource_demand;
383 }
384
385 acc
386 },
387 )
388 });
389
390 solution_ctx.routes.iter_mut().for_each(|route_ctx| {
392 let mut available_resources = vec![None; route_ctx.route().tour.total()];
393 let reload_intervals = route_ctx.state().get_reload_intervals().cloned().unwrap_or_default();
394
395 for (start_idx, _) in reload_intervals {
396 let activity_idx = get_activity_by_idx(route_ctx.route(), start_idx);
397 let resource_available =
398 (self.resource_capacity_fn)(activity_idx).and_then(|(total_capacity, resource_id)| {
399 total_demand.get(&resource_id).map(|total_demand| total_capacity - *total_demand)
400 });
401
402 if let Some(resource_available) = resource_available {
403 available_resources[start_idx] = Some(resource_available);
404 }
405 }
406
407 route_ctx.state_mut().set_activity_states::<SharedResourceStateKey, Option<T>>(available_resources)
408 });
409 }
410
411 fn prevent_resource_consumption(&self, route_ctx: &mut RouteContext) {
413 let mut empty_resources = vec![None; route_ctx.route().tour.total()];
414
415 route_ctx.state().get_reload_intervals().cloned().unwrap_or_default().into_iter().for_each(
416 |(start_idx, end_idx)| {
417 let activity = get_activity_by_idx(route_ctx.route(), start_idx);
418 let has_resource_demand = (self.resource_capacity_fn)(activity).map_or(false, |(_, _)| {
419 (start_idx..=end_idx)
420 .filter_map(|idx| route_ctx.route().tour.get(idx))
421 .filter_map(|activity| activity.job.as_ref())
422 .any(|job| (self.resource_demand_fn)(job).is_some())
423 });
424
425 if has_resource_demand {
426 empty_resources[start_idx] = Some(T::default());
427 }
428 },
429 );
430
431 route_ctx.state_mut().set_activity_states::<SharedResourceStateKey, _>(empty_resources)
432 }
433
434 fn get_total_demand(&self, route_ctx: &RouteContext, range: RangeInclusive<usize>) -> T {
435 range
436 .into_iter()
437 .filter_map(|idx| route_ctx.route().tour.get(idx))
438 .filter_map(|activity| activity.job.as_ref())
439 .fold(T::default(), |acc, job| acc + (self.resource_demand_fn)(job).unwrap_or_default())
440 }
441}
442
443impl<T: SharedResource + Add<Output = T> + Sub<Output = T>> FeatureState for SharedResourceState<T> {
444 fn accept_insertion(&self, solution_ctx: &mut SolutionContext, route_index: usize, _: &Job) {
445 self.accept_route_state(solution_ctx.routes.get_mut(route_index).unwrap());
446 self.update_resource_consumption(solution_ctx);
447 }
448
449 fn accept_route_state(&self, route_ctx: &mut RouteContext) {
450 self.prevent_resource_consumption(route_ctx);
455 }
456
457 fn accept_solution_state(&self, solution_ctx: &mut SolutionContext) {
458 self.update_resource_consumption(solution_ctx);
459 }
460}
461
462fn get_activity_by_idx(route: &Route, idx: usize) -> &Activity {
463 route.tour.get(idx).expect("cannot get activity by idx")
464}
465
466impl SharedResource for MultiDimLoad {}
468
469impl SharedResource for SingleDimLoad {}