vrp_pragmatic/validation/
routing.rs1#[cfg(test)]
2#[path = "../../tests/unit/validation/routing_test.rs"]
3mod routing_test;
4
5use super::*;
6use crate::utils::combine_error_results;
7use std::collections::HashSet;
8use vrp_core::prelude::Float;
9
10fn check_e1500_duplicated_profiles(ctx: &ValidationContext) -> Result<(), FormatError> {
12 get_duplicates(ctx.problem.fleet.profiles.iter().map(|p| &p.name)).map_or(Ok(()), |names| {
13 Err(FormatError::new(
14 "E1500".to_string(),
15 "duplicated profile names".to_string(),
16 format!("remove duplicates of profiles with the names: '{}'", names.join(", ")),
17 ))
18 })
19}
20
21fn check_e1501_empty_profiles(ctx: &ValidationContext) -> Result<(), FormatError> {
23 if ctx.problem.fleet.profiles.is_empty() {
24 Err(FormatError::new(
25 "E1501".to_string(),
26 "empty profile collection".to_string(),
27 "specify at least one profile".to_string(),
28 ))
29 } else {
30 Ok(())
31 }
32}
33
34fn check_e1502_no_location_type_mix(_ctx: &ValidationContext, location_types: (bool, bool)) -> Result<(), FormatError> {
36 let (has_coordinates, has_indices) = location_types;
37
38 if has_coordinates && has_indices {
39 Err(FormatError::new(
40 "E1502".to_string(),
41 "mixing different location types".to_string(),
42 "use either coordinates or indices for all locations".to_string(),
43 ))
44 } else {
45 Ok(())
46 }
47}
48
49fn check_e1503_no_matrix_when_indices_used(
51 ctx: &ValidationContext,
52 location_types: (bool, bool),
53) -> Result<(), FormatError> {
54 let (_, has_indices) = location_types;
55
56 if has_indices && ctx.matrices.map_or(true, |matrices| matrices.is_empty()) {
57 Err(FormatError::new(
58 "E1503".to_string(),
59 "location indices requires routing matrix to be specified".to_string(),
60 "either use coordinates everywhere or specify routing matrix".to_string(),
61 ))
62 } else {
63 Ok(())
64 }
65}
66
67fn check_e1504_index_size_mismatch(ctx: &ValidationContext) -> Result<(), FormatError> {
69 let max_index = ctx.coord_index.max_matrix_index();
70
71 let (matrix_size, is_correct_index) = ctx
72 .matrices
73 .and_then(|matrices| matrices.first())
74 .map(|matrix| (matrix.distances.len() as Float).sqrt().round() as usize)
75 .map_or((0_usize, true), |matrix_size| (matrix_size, max_index + 1 == matrix_size));
76
77 if !is_correct_index {
78 Err(FormatError::new(
79 "E1504".to_string(),
80 "amount of locations does not match matrix dimension".to_string(),
81 format!(
82 "check matrix size: max location index '{max_index}' + 1 should be equal to matrix size ('{matrix_size}')"
83 ),
84 ))
85 } else {
86 Ok(())
87 }
88}
89
90fn check_e1505_profiles_exist(ctx: &ValidationContext) -> Result<(), FormatError> {
92 let known_matrix_profiles = ctx.problem.fleet.profiles.iter().map(|p| p.name.clone()).collect::<HashSet<_>>();
93
94 let unknown_vehicle_profiles = ctx
95 .problem
96 .fleet
97 .vehicles
98 .iter()
99 .map(|vehicle| vehicle.profile.matrix.clone())
100 .chain(ctx.problem.plan.clustering.iter().map(|clustering| match clustering {
101 Clustering::Vicinity { profile, .. } => profile.matrix.clone(),
102 }))
103 .filter(|matrix| !known_matrix_profiles.contains(matrix))
104 .collect::<HashSet<_>>();
105
106 if unknown_vehicle_profiles.is_empty() {
107 Ok(())
108 } else {
109 let unknown_profiles = unknown_vehicle_profiles.into_iter().collect::<Vec<_>>();
110 Err(FormatError::new(
111 "E1505".to_string(),
112 "unknown matrix profile name in vehicle or vicinity clustering profile".to_string(),
113 format!("ensure that matrix profiles '{}' are defined in profiles", unknown_profiles.join(", ")),
114 ))
115 }
116}
117
118pub fn validate_routing(ctx: &ValidationContext) -> Result<(), MultiFormatError> {
120 let location_types = (ctx.coord_index.has_coordinates(), ctx.coord_index.has_indices());
121
122 combine_error_results(&[
123 check_e1500_duplicated_profiles(ctx),
124 check_e1501_empty_profiles(ctx),
125 check_e1502_no_location_type_mix(ctx, location_types),
126 check_e1503_no_matrix_when_indices_used(ctx, location_types),
127 check_e1504_index_size_mismatch(ctx),
128 check_e1505_profiles_exist(ctx),
129 ])
130 .map_err(From::from)
131}