vrp_pragmatic/validation/
relations.rs

1#[cfg(test)]
2#[path = "../../tests/unit/validation/relations_test.rs"]
3mod relations_test;
4
5use super::*;
6use crate::utils::combine_error_results;
7use std::collections::HashSet;
8use vrp_core::utils::CollectGroupBy;
9
10/// Checks that relation job ids are defined in plan.
11fn check_e1200_job_existence(ctx: &ValidationContext, relations: &[Relation]) -> Result<(), FormatError> {
12    let job_ids = relations
13        .iter()
14        .flat_map(|relation| {
15            relation
16                .jobs
17                .iter()
18                .filter(|&job_id| !is_reserved_job_id(job_id))
19                .filter(|&job_id| !ctx.job_index.contains_key(job_id))
20                .cloned()
21        })
22        .collect::<Vec<_>>();
23
24    if job_ids.is_empty() {
25        Ok(())
26    } else {
27        Err(FormatError::new(
28            "E1200".to_string(),
29            "relation has job id which does not present in the plan".to_string(),
30            format!("remove from relations or add jobs to the plan, ids: '{}'", job_ids.join(", ")),
31        ))
32    }
33}
34
35/// Checks that relation vehicle ids are defined in fleet.
36fn check_e1201_vehicle_existence(
37    relations: &[Relation],
38    vehicle_map: &HashMap<String, &VehicleType>,
39) -> Result<(), FormatError> {
40    let vehicle_ids = relations
41        .iter()
42        .map(|relation| relation.vehicle_id.clone())
43        .filter(|vehicle_id| !vehicle_map.contains_key(vehicle_id))
44        .collect::<Vec<_>>();
45
46    if vehicle_ids.is_empty() {
47        Ok(())
48    } else {
49        Err(FormatError::new(
50            "E1201".to_string(),
51            "relation has vehicle id which does not present in the fleet".to_string(),
52            format!("remove from relations or add vehicle types to the fleet, ids: '{}'", vehicle_ids.join(", ")),
53        ))
54    }
55}
56
57/// Checks that relation vehicle ids are defined in fleet.
58fn check_e1202_empty_job_list(relations: &[Relation]) -> Result<(), FormatError> {
59    let has_empty_relations = relations.iter().any(|relation| !relation.jobs.iter().any(|id| !is_reserved_job_id(id)));
60
61    if has_empty_relations {
62        Err(FormatError::new(
63            "E1202".to_string(),
64            "relation has empty job id list".to_string(),
65            "remove relation with empty jobs list or add job ids to them".to_string(),
66        ))
67    } else {
68        Ok(())
69    }
70}
71
72/// Checks that relation has no jobs with multiple places or time windows.
73fn check_e1203_no_multiple_places_times(ctx: &ValidationContext, relations: &[Relation]) -> Result<(), FormatError> {
74    let mut job_ids = relations
75        .iter()
76        .flat_map(|relation| {
77            relation
78                .jobs
79                .iter()
80                .filter(|&job_id| !is_reserved_job_id(job_id))
81                .filter_map(|job_id| ctx.job_index.get(job_id))
82                .filter(|&job| {
83                    ctx.tasks(job).into_iter().any(|task| {
84                        task.places.len() > 1
85                            || task.places.iter().any(|place| place.times.as_ref().map_or(false, |tw| tw.len() > 1))
86                    })
87                })
88                .map(|job| job.id.clone())
89        })
90        .collect::<HashSet<_>>()
91        .into_iter()
92        .collect::<Vec<_>>();
93
94    job_ids.sort();
95
96    if job_ids.is_empty() {
97        Ok(())
98    } else {
99        Err(FormatError::new(
100            "E1203".to_string(),
101            "strict or sequence relation has job with multiple places or time windows".to_string(),
102            format!(
103                "remove job from relation or specify only one place and time window, job ids: '{}'",
104                job_ids.join(", ")
105            ),
106        ))
107    }
108}
109
110/// Checks that relation job is assigned to one vehicle.
111fn check_e1204_job_assigned_to_multiple_vehicles(relations: &[Relation]) -> Result<(), FormatError> {
112    let mut job_vehicle_map = HashMap::<String, String>::new();
113    let job_ids: Vec<String> = relations
114        .iter()
115        .flat_map(|relation| {
116            relation
117                .jobs
118                .clone()
119                .into_iter()
120                .filter(|job_id| !is_reserved_job_id(job_id))
121                .filter(|job_id| {
122                    *job_vehicle_map.entry(job_id.clone()).or_insert_with(|| relation.vehicle_id.clone())
123                        != relation.vehicle_id
124                })
125                .collect::<Vec<String>>()
126                .into_iter()
127        })
128        .collect::<Vec<_>>();
129
130    if job_ids.is_empty() {
131        Ok(())
132    } else {
133        Err(FormatError::new(
134            "E1204".to_string(),
135            "job is assigned to different vehicles in relations".to_string(),
136            format!("assign jobs only to one vehicle, ids: '{}'", job_ids.join(", ")),
137        ))
138    }
139}
140
141fn check_e1205_relation_has_correct_shift_index(
142    relations: &[Relation],
143    vehicle_map: &HashMap<String, &VehicleType>,
144) -> Result<(), FormatError> {
145    let vehicle_ids: Vec<String> = relations
146        .iter()
147        .filter_map(|relation| vehicle_map.get(&relation.vehicle_id).map(|vehicle| (vehicle, relation)))
148        .filter(|(vehicle, relation)| vehicle.shifts.get(relation.shift_index.unwrap_or(0)).is_none())
149        .map(|(_, relation)| relation.vehicle_id.clone())
150        .collect::<Vec<_>>();
151
152    if vehicle_ids.is_empty() {
153        Ok(())
154    } else {
155        Err(FormatError::new(
156            "E1205".to_string(),
157            "relation has invalid shift index".to_string(),
158            format!(
159                "check that vehicle has enough shifts defined or correct relation, vehicle ids: '{}'",
160                vehicle_ids.join(", ")
161            ),
162        ))
163    }
164}
165
166/// Checks that relation has no reserved job ids for vehicle shift properties which are not used.
167fn check_e1206_relation_has_no_missing_shift_properties(
168    relations: &[Relation],
169    vehicle_map: &HashMap<String, &VehicleType>,
170) -> Result<(), FormatError> {
171    let vehicle_ids: Vec<String> = relations
172        .iter()
173        .filter_map(|relation| {
174            vehicle_map
175                .get(&relation.vehicle_id)
176                .and_then(|vehicle| vehicle.shifts.get(relation.shift_index.unwrap_or(0)))
177                .map(|vehicle_shift| (vehicle_shift, relation))
178        })
179        .filter(|(vehicle_shift, relation)| {
180            relation.jobs.iter().filter(|job_id| is_reserved_job_id(job_id)).any(|job_id| match job_id.as_str() {
181                "break" => vehicle_shift.breaks.is_none(),
182                "reload" => vehicle_shift.reloads.is_none(),
183                "arrival" => vehicle_shift.end.is_none(),
184                _ => false,
185            })
186        })
187        .map(|(_, relation)| relation.vehicle_id.clone())
188        .collect::<Vec<_>>();
189
190    if vehicle_ids.is_empty() {
191        Ok(())
192    } else {
193        Err(FormatError::new(
194            "E1206".to_string(),
195            "relation has special job id which is not defined on vehicle shift".to_string(),
196            format!(
197                "remove special job id or add vehicle shift property (e.g. break, reload), vehicle ids: '{}'",
198                vehicle_ids.join(", ")
199            ),
200        ))
201    }
202}
203
204fn check_e1207_no_incomplete_relation(ctx: &ValidationContext, relations: &[Relation]) -> Result<(), FormatError> {
205    let get_tasks_size = |tasks: &Option<Vec<JobTask>>| {
206        if let Some(tasks) = tasks {
207            tasks.len()
208        } else {
209            0
210        }
211    };
212
213    let ids = relations
214        .iter()
215        .filter_map(|relation| {
216            let job_frequencies = relation.jobs.iter().collect_group_by_key(|&job| job);
217            let ids = relation
218                .jobs
219                .iter()
220                .filter_map(|job_id| ctx.job_index.get(job_id))
221                .filter(|job| {
222                    let size = get_tasks_size(&job.pickups)
223                        + get_tasks_size(&job.deliveries)
224                        + get_tasks_size(&job.replacements)
225                        + get_tasks_size(&job.services);
226
227                    job_frequencies.get(&job.id).unwrap().len() != size
228                })
229                .map(|job| job.id.clone())
230                .collect::<Vec<_>>();
231
232            if ids.is_empty() {
233                None
234            } else {
235                Some(ids)
236            }
237        })
238        .flatten()
239        .collect::<Vec<_>>();
240
241    if ids.is_empty() {
242        Ok(())
243    } else {
244        Err(FormatError::new(
245            "E1207".to_string(),
246            "some relations have incomplete job definitions".to_string(),
247            format!(
248                "ensure that job id specified in relation as many times, as it has tasks, problematic job ids: '{}'",
249                ids.join(", ")
250            ),
251        ))
252    }
253}
254
255/// Validates relations in the plan.
256pub fn validate_relations(ctx: &ValidationContext) -> Result<(), MultiFormatError> {
257    let vehicle_map = ctx
258        .vehicles()
259        .flat_map(|v_type| v_type.vehicle_ids.iter().map(move |id| (id.clone(), v_type)))
260        .collect::<HashMap<_, _>>();
261
262    if let Some(relations) = ctx.problem.plan.relations.as_ref() {
263        combine_error_results(&[
264            check_e1200_job_existence(ctx, relations),
265            check_e1201_vehicle_existence(relations, &vehicle_map),
266            check_e1202_empty_job_list(relations),
267            check_e1203_no_multiple_places_times(ctx, relations),
268            check_e1204_job_assigned_to_multiple_vehicles(relations),
269            check_e1205_relation_has_correct_shift_index(relations, &vehicle_map),
270            check_e1206_relation_has_no_missing_shift_properties(relations, &vehicle_map),
271            check_e1207_no_incomplete_relation(ctx, relations),
272        ])
273        .map_err(From::from)
274    } else {
275        Ok(())
276    }
277}