vrp_pragmatic/validation/
routing.rs

1#[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
10/// Checks that no duplicated profile names specified.
11fn 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
21/// Checks that profiles collection is not empty.
22fn 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
34/// Checks that only one type of location is used.
35fn 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
49/// Checks that routing matrix is supplied when location indices are used.
50fn 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
67/// Checks that coord index has a proper maximum index for
68fn 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
90/// Checks that no duplicated profile names specified.
91fn 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
118/// Validates routing rules.
119pub 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}