1#[cfg(test)]
5#[path = "../../tests/unit/checker/checker_test.rs"]
6mod checker_test;
7
8use crate::format::problem::*;
9use crate::format::solution::*;
10use crate::format::{CoordIndex, Location};
11use crate::parse_time;
12use std::collections::{HashMap, HashSet};
13use std::sync::Arc;
14use vrp_core::construction::clustering::vicinity::ClusterConfig;
15use vrp_core::construction::clustering::vicinity::VisitPolicy;
16use vrp_core::models::common::{Duration, Profile, TimeWindow};
17use vrp_core::models::solution::{Commute as DomainCommute, CommuteInfo as DomainCommuteInfo};
18use vrp_core::models::Problem as CoreProblem;
19use vrp_core::prelude::{GenericError, GenericResult};
20use vrp_core::solver::processing::ClusterConfigExtraProperty;
21use vrp_core::utils::Float;
22
23pub struct CheckerContext {
25 pub problem: Problem,
27 pub matrices: Option<Vec<Matrix>>,
29 pub solution: Solution,
31
32 job_map: HashMap<String, Job>,
33 coord_index: CoordIndex,
34 profile_index: HashMap<String, usize>,
35 core_problem: Arc<CoreProblem>,
36 clustering: Option<ClusterConfig>,
37}
38
39#[allow(dead_code)] enum ActivityType {
42 Terminal,
43 Job(Job),
44 Break(VehicleBreak),
45 Reload(VehicleReload),
46 Recharge(VehicleRechargeStation),
47}
48
49impl CheckerContext {
50 pub fn new(
52 core_problem: Arc<CoreProblem>,
53 problem: Problem,
54 matrices: Option<Vec<Matrix>>,
55 solution: Solution,
56 ) -> Result<Self, Vec<GenericError>> {
57 let job_map = problem.plan.jobs.iter().map(|job| (job.id.clone(), job.clone())).collect();
58 let clustering = core_problem.extras.get_cluster_config().map(|config| config.as_ref().clone());
59 let coord_index = CoordIndex::new(&problem);
60 let profile_index = if matrices.is_none() {
61 HashMap::new()
62 } else {
63 get_matrices(&matrices)
64 .and_then(|matrices| get_profile_index(&problem, matrices.as_slice()))
65 .map_err(|err| vec![err])?
66 };
67
68 Ok(Self { problem, matrices, solution, job_map, coord_index, profile_index, core_problem, clustering })
69 }
70
71 pub fn check(&self) -> Result<(), Vec<GenericError>> {
73 let (_, errors) = check_vehicle_load(self)
75 .err()
76 .into_iter()
77 .chain(check_relations(self).err())
78 .chain(check_breaks(self).err())
79 .chain(check_assignment(self).err())
80 .chain(check_routing(self).err())
81 .chain(check_limits(self).err())
82 .flatten()
83 .fold((HashSet::new(), Vec::default()), |(mut used, mut errors), error| {
84 if !used.contains(&error) {
85 errors.push(error.clone());
86 used.insert(error);
87 }
88
89 (used, errors)
90 });
91
92 if errors.is_empty() {
93 Ok(())
94 } else {
95 Err(errors)
96 }
97 }
98
99 fn get_vehicle(&self, vehicle_id: &str) -> GenericResult<&VehicleType> {
101 self.problem
102 .fleet
103 .vehicles
104 .iter()
105 .find(|v| v.vehicle_ids.contains(&vehicle_id.to_string()))
106 .ok_or_else(|| format!("cannot find vehicle with id '{vehicle_id}'").into())
107 }
108
109 fn get_vehicle_profile(&self, vehicle_id: &str) -> GenericResult<Profile> {
110 let profile = &self.get_vehicle(vehicle_id)?.profile;
111 let index = self
112 .profile_index
113 .get(profile.matrix.as_str())
114 .cloned()
115 .ok_or(format!("cannot get matrix for '{}' profile", profile.matrix))?;
116
117 Ok(Profile { index, scale: profile.scale.unwrap_or(1.) })
118 }
119
120 fn get_activity_time(&self, stop: &Stop, activity: &Activity) -> TimeWindow {
122 let schedule = stop.schedule();
123
124 let time = activity
125 .time
126 .clone()
127 .unwrap_or_else(|| Interval { start: schedule.arrival.clone(), end: schedule.departure.clone() });
128
129 TimeWindow::new(parse_time(&time.start), parse_time(&time.end))
130 }
131
132 fn get_activity_location(&self, stop: &Stop, activity: &Activity) -> Option<Location> {
134 activity.location.clone().or_else(|| match stop {
135 Stop::Point(stop) => Some(stop.location.clone()),
136 Stop::Transit(_) => None,
137 })
138 }
139
140 fn get_vehicle_shift(&self, tour: &Tour) -> GenericResult<VehicleShift> {
142 let tour_time = TimeWindow::new(
143 parse_time(
144 &tour.stops.first().as_ref().ok_or_else(|| "cannot get first activity".to_string())?.schedule().arrival,
145 ),
146 parse_time(
147 &tour.stops.last().as_ref().ok_or_else(|| "cannot get last activity".to_string())?.schedule().arrival,
148 ),
149 );
150
151 self.get_vehicle(&tour.vehicle_id)?
152 .shifts
153 .iter()
154 .find(|shift| {
155 let shift_time = TimeWindow::new(
156 parse_time(&shift.start.earliest),
157 shift.end.as_ref().map_or_else(|| Float::MAX, |place| parse_time(&place.latest)),
158 );
159 shift_time.intersects(&tour_time)
160 })
161 .cloned()
162 .ok_or_else(|| format!("cannot find shift for tour with vehicle if: '{}'", tour.vehicle_id).into())
163 }
164
165 fn get_stop_activity_types(&self, stop: &Stop) -> Vec<String> {
167 stop.activities().iter().map(|a| a.activity_type.clone()).collect()
168 }
169
170 fn get_activity_type(&self, tour: &Tour, stop: &Stop, activity: &Activity) -> GenericResult<ActivityType> {
172 let shift = self.get_vehicle_shift(tour)?;
173 let time = self.get_activity_time(stop, activity);
174 let location = self.get_activity_location(stop, activity);
175
176 match activity.activity_type.as_str() {
177 "departure" | "arrival" => Ok(ActivityType::Terminal),
178
179 "pickup" | "delivery" | "service" | "replacement" => {
180 self.job_map.get(activity.job_id.as_str()).map_or_else(
181 || Err(format!("cannot find job with id '{}'", activity.job_id).into()),
182 |job| Ok(ActivityType::Job(job.clone())),
183 )
184 }
185
186 "break" => shift
187 .breaks
188 .as_ref()
189 .and_then(|breaks| {
190 breaks
191 .iter()
192 .find(|b| get_break_time_window(tour, b).map(|tw| tw.intersects(&time)).unwrap_or(false))
194 })
195 .map(|b| ActivityType::Break(b.clone()))
196 .ok_or_else(|| format!("cannot find break for tour '{}'", tour.vehicle_id).into()),
197
198 "reload" => shift
199 .reloads
200 .as_ref()
201 .and_then(|reload| {
203 reload.iter().find(|r| {
204 location.as_ref().map_or(false, |location| r.location == *location) && r.tag == activity.job_tag
205 })
206 })
207 .map(|r| ActivityType::Reload(r.clone()))
208 .ok_or_else(|| format!("cannot find reload for tour '{}'", tour.vehicle_id).into()),
209
210 "recharge" => shift
211 .recharges
212 .as_ref()
213 .and_then(|recharges| {
214 recharges.stations.iter().find(|r| {
215 location.as_ref().map_or(false, |location| r.location == *location) && r.tag == activity.job_tag
216 })
217 })
218 .map(|r| ActivityType::Recharge(r.clone()))
219 .ok_or_else(|| format!("cannot find recharge for tour '{}'", tour.vehicle_id).into()),
220
221 _ => Err(format!("unknown activity type: '{}'", activity.activity_type).into()),
222 }
223 }
224
225 fn get_job_by_id(&self, job_id: &str) -> Option<&Job> {
226 self.problem.plan.jobs.iter().find(|job| job.id == job_id)
227 }
228
229 fn get_commute_info(
230 &self,
231 profile: Option<Profile>,
232 parking: Duration,
233 stop: &PointStop,
234 activity_idx: usize,
235 ) -> GenericResult<Option<DomainCommute>> {
236 let get_activity_location_by_idx = |idx: usize| {
237 stop.activities
238 .get(idx)
239 .and_then(|activity| activity.location.as_ref())
240 .and_then(|location| self.get_location_index(location).ok())
241 };
242
243 let get_activity_commute_by_idx = |idx: usize| -> Option<DomainCommute> {
244 stop.activities
245 .get(idx)
246 .and_then(|activity| activity.commute.as_ref())
247 .map(|commute| commute.to_domain(&self.coord_index))
248 };
249
250 match (&self.clustering, &profile, get_activity_commute_by_idx(activity_idx)) {
251 (Some(config), Some(profile), Some(commute)) => {
252 let stop_location = self.get_location_index(&stop.location).ok();
253 match (commute.is_zero_distance(), activity_idx) {
255 (true, _) => Ok(Some(commute)),
256 (false, 0) => Err("cannot have commute at first activity in the stop".into()),
258 (false, idx) => {
259 let prev_location = if matches!(config.visiting, VisitPolicy::Return) {
260 stop_location
261 } else {
262 get_activity_location_by_idx(idx - 1)
263 };
264 let curr_location = get_activity_location_by_idx(idx);
265
266 match (curr_location, prev_location) {
267 (Some(curr_location), Some(prev_location)) => {
268 let (f_distance, f_duration) =
269 self.get_matrix_data(profile, prev_location, curr_location)?;
270
271 let has_next_commute = get_activity_location_by_idx(idx + 1)
272 .zip(get_activity_commute_by_idx(idx + 1))
273 .is_some();
274 let (b_location, b_distance, b_duration) = match (&config.visiting, has_next_commute) {
275 (VisitPolicy::Return, _) | (VisitPolicy::ClosedContinuation, false) => {
276 let stop_location = stop_location.ok_or("no location for clustered stop")?;
277 let (b_distance, b_duration) =
278 self.get_matrix_data(profile, curr_location, stop_location)?;
279
280 (stop_location, b_distance, b_duration)
281 }
282 (VisitPolicy::OpenContinuation, _) | (VisitPolicy::ClosedContinuation, true) => {
283 (curr_location, 0_i64, 0_i64)
284 }
285 };
286
287 let f_duration = if f_duration == 0 { parking } else { f_duration as Float };
289
290 Ok(Some(DomainCommute {
291 forward: DomainCommuteInfo {
292 location: prev_location,
293 distance: f_distance as Float,
294 duration: f_duration,
295 },
296 backward: DomainCommuteInfo {
297 location: b_location,
298 distance: b_distance as Float,
299 duration: b_duration as Float,
300 },
301 }))
302 }
303 _ => Err("cannot find next commute info".into()),
304 }
305 }
306 }
307 }
308 _ => Ok(None),
309 }
310 }
311
312 fn visit_job<F1, F2, R>(
313 &self,
314 activity: &Activity,
315 activity_type: &ActivityType,
316 job_visitor: F1,
317 other_visitor: F2,
318 ) -> GenericResult<R>
319 where
320 F1: Fn(&Job, &JobTask) -> R,
321 F2: Fn() -> R,
322 {
323 match activity_type {
324 ActivityType::Job(job) => {
325 let pickups = job_task_size(&job.pickups);
326 let deliveries = job_task_size(&job.deliveries);
327 let tasks = pickups + deliveries + job_task_size(&job.services) + job_task_size(&job.replacements);
328
329 if tasks < 2 || (tasks == 2 && pickups == 1 && deliveries == 1) {
330 match_job_task(activity.activity_type.as_str(), job, |tasks| tasks.first())
331 } else {
332 activity.job_tag.as_ref().ok_or_else(|| {
333 GenericError::from(format!(
334 "checker requires that multi job activity must have tag: '{}'",
335 activity.job_id
336 ))
337 })?;
338
339 match_job_task(activity.activity_type.as_str(), job, |tasks| {
340 tasks.iter().find(|task| task.places.iter().any(|place| place.tag == activity.job_tag))
341 })
342 }
343 .map(|task| job_visitor(job, task))
344 }
345 .ok_or_else(|| "cannot match activity to job place".into()),
346 _ => Ok(other_visitor()),
347 }
348 }
349
350 fn get_location_index(&self, location: &Location) -> GenericResult<usize> {
351 self.coord_index
352 .get_by_loc(location)
353 .ok_or_else(|| format!("cannot find coordinate in coord index: {location:?}").into())
354 }
355
356 fn get_matrix_data(&self, profile: &Profile, from_idx: usize, to_idx: usize) -> GenericResult<(i64, i64)> {
357 let matrices = get_matrices(&self.matrices)?;
358 let matrix =
359 matrices.get(profile.index).ok_or_else(|| format!("cannot find matrix with index {}", profile.index))?;
360
361 let is_special_from_idx = self.coord_index.is_special_index(from_idx);
362 let is_special_to_idx = self.coord_index.is_special_index(to_idx);
363
364 if is_special_from_idx || is_special_to_idx {
365 return Ok((0, 0));
367 }
368
369 let matrix_size = get_matrix_size(matrices.as_slice());
370 let matrix_idx = from_idx * matrix_size + to_idx;
371
372 let distance = get_matrix_value(matrix_idx, &matrix.distances)?;
373 let duration = get_matrix_value(matrix_idx, &matrix.travel_times)?;
374 let duration = (duration as Float * profile.scale) as i64;
375
376 Ok((distance, duration))
377 }
378}
379
380fn job_task_size(tasks: &Option<Vec<JobTask>>) -> usize {
381 tasks.as_ref().map_or(0, |p| p.len())
382}
383
384fn match_job_task<'a>(
385 activity_type: &str,
386 job: &'a Job,
387 tasks_fn: impl Fn(&'a Vec<JobTask>) -> Option<&'a JobTask>,
388) -> Option<&'a JobTask> {
389 let tasks = match activity_type {
390 "pickup" => job.pickups.as_ref(),
391 "delivery" => job.deliveries.as_ref(),
392 "service" => job.services.as_ref(),
393 "replacement" => job.replacements.as_ref(),
394 _ => None,
395 };
396
397 tasks.and_then(tasks_fn)
398}
399
400fn parse_time_window(tw: &[String]) -> TimeWindow {
401 TimeWindow::new(parse_time(tw.first().unwrap()), parse_time(tw.last().unwrap()))
402}
403
404fn get_time_window(stop: &Stop, activity: &Activity) -> TimeWindow {
405 let schedule = stop.schedule();
406 let (start, end) = activity
407 .time
408 .as_ref()
409 .map_or_else(|| (&schedule.arrival, &schedule.departure), |interval| (&interval.start, &interval.end));
410
411 TimeWindow::new(parse_time(start), parse_time(end))
412}
413
414fn get_matrix_size(matrices: &[Matrix]) -> usize {
415 (matrices.first().unwrap().travel_times.len() as Float).sqrt().round() as usize
416}
417
418fn get_matrix_value(idx: usize, matrix_values: &[i64]) -> GenericResult<i64> {
419 matrix_values
420 .get(idx)
421 .cloned()
422 .ok_or_else(|| format!("attempt to get value out of bounds: {} vs {}", idx, matrix_values.len()).into())
423}
424
425fn get_matrices(matrices: &Option<Vec<Matrix>>) -> GenericResult<&Vec<Matrix>> {
426 let matrices = matrices.as_ref().unwrap();
427
428 if matrices.iter().any(|matrix| matrix.timestamp.is_some()) {
429 return Err("not implemented: time aware routing check".into());
430 }
431
432 Ok(matrices)
433}
434
435fn get_profile_index(problem: &Problem, matrices: &[Matrix]) -> GenericResult<HashMap<String, usize>> {
436 let profiles = problem.fleet.profiles.len();
437 if profiles != matrices.len() {
438 return Err(format!(
439 "precondition failed: amount of matrices supplied ({}) does not match profile specified ({})",
440 matrices.len(),
441 profiles,
442 )
443 .into());
444 }
445
446 Ok(problem
447 .fleet
448 .profiles
449 .iter()
450 .enumerate()
451 .map(|(idx, profile)| (profile.name.to_string(), idx))
452 .collect::<HashMap<_, _>>())
453}
454
455mod assignment;
456use crate::checker::assignment::check_assignment;
457
458mod capacity;
459use crate::checker::capacity::check_vehicle_load;
460
461mod limits;
462use crate::checker::limits::check_limits;
463
464mod breaks;
465use crate::checker::breaks::{check_breaks, get_break_time_window};
466
467mod relations;
468use crate::checker::relations::check_relations;
469
470mod routing;
471use crate::checker::routing::check_routing;