vrp_pragmatic/validation/
relations.rs1#[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
10fn 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
35fn 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
57fn 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
72fn 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
110fn 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
166fn 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
255pub 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}