Skip to main content

vrp_pragmatic/validation/
vehicles.rs

1#[cfg(test)]
2#[path = "../../tests/unit/validation/vehicles_test.rs"]
3mod vehicles_test;
4
5use super::*;
6use crate::utils::combine_error_results;
7use crate::validation::common::get_time_windows;
8use crate::{parse_time, parse_time_safe};
9use std::collections::HashSet;
10use vrp_core::models::common::TimeWindow;
11
12/// Checks that fleet has no vehicle with duplicate type ids.
13fn check_e1300_no_vehicle_types_with_duplicate_type_ids(ctx: &ValidationContext) -> Result<(), FormatError> {
14    get_duplicates(ctx.vehicles().map(|vehicle| &vehicle.type_id)).map_or(Ok(()), |ids| {
15        Err(FormatError::new(
16            "E1300".to_string(),
17            "duplicated vehicle type ids".to_string(),
18            format!("remove duplicated vehicle type ids: {}", ids.join(", ")),
19        ))
20    })
21}
22
23/// Checks that fleet has no vehicle with duplicate ids.
24fn check_e1301_no_vehicle_types_with_duplicate_ids(ctx: &ValidationContext) -> Result<(), FormatError> {
25    get_duplicates(ctx.vehicles().flat_map(|vehicle| vehicle.vehicle_ids.iter())).map_or(Ok(()), |ids| {
26        Err(FormatError::new(
27            "E1301".to_string(),
28            "duplicated vehicle ids".to_string(),
29            format!("remove duplicated vehicle ids: {}", ids.join(", ")),
30        ))
31    })
32}
33
34/// Checks that vehicle shift time is correct.
35fn check_e1302_vehicle_shift_time(ctx: &ValidationContext) -> Result<(), FormatError> {
36    let type_ids = ctx
37        .vehicles()
38        .filter_map(|vehicle| {
39            let tws = vehicle
40                .shifts
41                .iter()
42                .map(|shift| {
43                    vec![
44                        shift.start.earliest.clone(),
45                        shift.end.as_ref().map_or_else(|| shift.start.earliest.clone(), |end| end.latest.clone()),
46                    ]
47                })
48                .collect::<Vec<_>>();
49            if check_raw_time_windows(&tws, false) {
50                None
51            } else {
52                Some(vehicle.type_id.to_string())
53            }
54        })
55        .collect::<Vec<_>>();
56
57    if type_ids.is_empty() {
58        Ok(())
59    } else {
60        Err(FormatError::new(
61            "E1302".to_string(),
62            "invalid start or end times in vehicle shift".to_string(),
63            format!(
64                "ensure that start and end time conform shift time rules, vehicle type ids: {}",
65                type_ids.join(", ")
66            ),
67        ))
68    }
69}
70
71/// Checks that break time window is correct.
72fn check_e1303_vehicle_breaks_time_is_correct(ctx: &ValidationContext) -> Result<(), FormatError> {
73    let type_ids = get_invalid_type_ids(
74        ctx,
75        Box::new(|_, shift, shift_time| {
76            shift
77                .breaks
78                .as_ref()
79                .map(|breaks| {
80                    let tws = breaks
81                        .iter()
82                        .filter_map(|b| match b {
83                            VehicleBreak::Optional { time: VehicleOptionalBreakTime::TimeWindow(tw), .. } => {
84                                Some(get_time_window_from_vec(tw))
85                            }
86                            VehicleBreak::Required {
87                                time: VehicleRequiredBreakTime::OffsetTime { earliest, latest },
88                                duration,
89                            } => {
90                                let departure = parse_time(&shift.start.earliest);
91                                Some(Some(TimeWindow::new(departure + *earliest, departure + *latest + *duration)))
92                            }
93                            VehicleBreak::Required {
94                                time: VehicleRequiredBreakTime::ExactTime { earliest, latest },
95                                duration,
96                            } => Some(
97                                parse_time_safe(earliest)
98                                    .ok()
99                                    .zip(parse_time_safe(latest).ok())
100                                    .map(|(start, end)| TimeWindow::new(start, end + *duration)),
101                            ),
102                            _ => None,
103                        })
104                        .collect::<Vec<_>>();
105
106                    check_shift_time_windows(shift_time, tws, false)
107                })
108                .unwrap_or(true)
109        }),
110    );
111
112    if type_ids.is_empty() {
113        Ok(())
114    } else {
115        Err(FormatError::new(
116            "E1303".to_string(),
117            "invalid break time windows in vehicle shift".to_string(),
118            format!("ensure that break conform rules, vehicle type ids: '{}'", type_ids.join(", ")),
119        ))
120    }
121}
122
123/// Checks that reload time windows are correct.
124fn check_e1304_vehicle_reload_time_is_correct(ctx: &ValidationContext) -> Result<(), FormatError> {
125    let type_ids = get_invalid_type_ids(
126        ctx,
127        Box::new(|_, shift, shift_time| {
128            shift
129                .reloads
130                .as_ref()
131                .map(|reloads| {
132                    let tws = reloads
133                        .iter()
134                        .filter_map(|reload| reload.times.as_ref())
135                        .flat_map(|tws| get_time_windows(tws))
136                        .collect::<Vec<_>>();
137
138                    check_shift_time_windows(shift_time, tws, true)
139                })
140                .unwrap_or(true)
141        }),
142    );
143
144    if type_ids.is_empty() {
145        Ok(())
146    } else {
147        Err(FormatError::new(
148            "E1304".to_string(),
149            "invalid reload time windows in vehicle shift".to_string(),
150            format!("ensure that reload conform rules, vehicle type ids: '{}'", type_ids.join(", ")),
151        ))
152    }
153}
154
155/// Checks that vehicle area restrictions are valid.
156fn check_e1306_vehicle_has_no_zero_costs(ctx: &ValidationContext) -> Result<(), FormatError> {
157    let type_ids = ctx
158        .vehicles()
159        .filter(|vehicle| vehicle.costs.time == 0. && vehicle.costs.distance == 0.)
160        .map(|vehicle| vehicle.type_id.to_string())
161        .collect::<Vec<_>>();
162
163    if type_ids.is_empty() {
164        Ok(())
165    } else {
166        Err(FormatError::new(
167            "E1306".to_string(),
168            "time and duration costs are zeros".to_string(),
169            format!(
170                "ensure that either time or distance cost is non-zero, \
171                 vehicle type ids: '{}'",
172                type_ids.join(", ")
173            ),
174        ))
175    }
176}
177
178fn check_e1307_vehicle_offset_break_rescheduling(ctx: &ValidationContext) -> Result<(), FormatError> {
179    let type_ids = get_invalid_type_ids(
180        ctx,
181        Box::new(|_, shift, _| {
182            shift
183                .breaks
184                .as_ref()
185                .map(|breaks| {
186                    let has_time_offset = breaks.iter().any(|br| {
187                        matches!(
188                            br,
189                            VehicleBreak::Required { time: VehicleRequiredBreakTime::OffsetTime { .. }, .. }
190                                | VehicleBreak::Optional { time: VehicleOptionalBreakTime::TimeOffset { .. }, .. }
191                        )
192                    });
193                    let has_rescheduling =
194                        shift.start.latest.as_ref().map_or(true, |latest| *latest != shift.start.earliest);
195
196                    !(has_time_offset && has_rescheduling)
197                })
198                .unwrap_or(true)
199        }),
200    );
201
202    if type_ids.is_empty() {
203        Ok(())
204    } else {
205        Err(FormatError::new(
206            "E1307".to_string(),
207            "time offset interval for break is used with departure rescheduling".to_string(),
208            format!("when time offset is used, start.latest should be set equal to start.earliest in the shift, check vehicle type ids: '{}'", type_ids.join(", ")),
209        ))
210    }
211}
212
213fn check_e1308_vehicle_reload_resources(ctx: &ValidationContext) -> Result<(), FormatError> {
214    let reload_resource_ids = ctx
215        .problem
216        .fleet
217        .resources
218        .iter()
219        .flat_map(|resources| resources.iter())
220        .map(|resource| match resource {
221            VehicleResource::Reload { id, .. } => id.to_string(),
222        })
223        .collect::<Vec<_>>();
224
225    let unique_resource_ids = reload_resource_ids.iter().cloned().collect::<HashSet<_>>();
226
227    if reload_resource_ids.len() != unique_resource_ids.len() {
228        return Err(FormatError::new(
229            "E1308".to_string(),
230            "invalid vehicle reload resource".to_string(),
231            "make sure that fleet reload resource ids are unique".to_string(),
232        ));
233    }
234
235    let type_ids = get_invalid_type_ids(
236        ctx,
237        Box::new(move |_, shift, _| {
238            shift
239                .reloads
240                .as_ref()
241                .iter()
242                .flat_map(|reloads| reloads.iter())
243                .filter_map(|reload| reload.resource_id.as_ref())
244                .all(|resource_id| unique_resource_ids.contains(resource_id))
245        }),
246    );
247
248    if type_ids.is_empty() {
249        Ok(())
250    } else {
251        Err(FormatError::new(
252            "E1308".to_string(),
253            "invalid vehicle reload resource".to_string(),
254            format!(
255                "make sure that fleet has all reload resources defined, check vehicle type ids: '{}'",
256                type_ids.join(", ")
257            ),
258        ))
259    }
260}
261
262type CheckShiftFn = Box<dyn Fn(&VehicleType, &VehicleShift, Option<TimeWindow>) -> bool>;
263
264fn get_invalid_type_ids(ctx: &ValidationContext, check_shift_fn: CheckShiftFn) -> Vec<String> {
265    ctx.vehicles()
266        .filter_map(|vehicle| {
267            let all_correct =
268                vehicle.shifts.iter().all(|shift| (check_shift_fn)(vehicle, shift, get_shift_time_window(shift)));
269
270            if all_correct {
271                None
272            } else {
273                Some(vehicle.type_id.clone())
274            }
275        })
276        .collect::<Vec<_>>()
277}
278
279fn check_shift_time_windows(
280    shift_time: Option<TimeWindow>,
281    tws: Vec<Option<TimeWindow>>,
282    skip_intersection_check: bool,
283) -> bool {
284    tws.is_empty()
285        || (check_time_windows(&tws, skip_intersection_check)
286            && shift_time
287                .as_ref()
288                .map_or(true, |shift_time| tws.into_iter().map(|tw| tw.unwrap()).all(|tw| tw.intersects(shift_time))))
289}
290
291fn get_shift_time_window(shift: &VehicleShift) -> Option<TimeWindow> {
292    get_time_window(
293        &shift.start.earliest,
294        &shift.end.clone().map_or_else(|| "2200-07-04T00:00:00Z".to_string(), |end| end.latest),
295    )
296}
297
298/// Validates vehicles from the fleet.
299pub fn validate_vehicles(ctx: &ValidationContext) -> Result<(), MultiFormatError> {
300    combine_error_results(&[
301        check_e1300_no_vehicle_types_with_duplicate_type_ids(ctx),
302        check_e1301_no_vehicle_types_with_duplicate_ids(ctx),
303        check_e1302_vehicle_shift_time(ctx),
304        check_e1303_vehicle_breaks_time_is_correct(ctx),
305        check_e1304_vehicle_reload_time_is_correct(ctx),
306        check_e1306_vehicle_has_no_zero_costs(ctx),
307        check_e1307_vehicle_offset_break_rescheduling(ctx),
308        check_e1308_vehicle_reload_resources(ctx),
309    ])
310    .map_err(From::from)
311}