vrp_pragmatic/validation/
objectives.rs1#[cfg(test)]
2#[path = "../../tests/unit/validation/objectives_test.rs"]
3mod objectives_test;
4
5use super::*;
6use crate::format::problem::Objective::*;
7use crate::utils::combine_error_results;
8use std::collections::HashSet;
9use vrp_core::utils::Either;
10
11fn check_e1600_empty_objective(objectives: &[&Objective]) -> Result<(), FormatError> {
13 if objectives.is_empty() {
14 Err(FormatError::new(
15 "E1600".to_string(),
16 "an empty objective specified".to_string(),
17 "remove objectives property completely to use default".to_string(),
18 ))
19 } else {
20 Ok(())
21 }
22}
23
24fn check_e1601_duplicate_objectives(objectives: &[&Objective]) -> Result<(), FormatError> {
26 let original_count = get_objectives_flattened(objectives).count();
27 let unique = get_objectives_flattened(objectives).map(std::mem::discriminant).collect::<HashSet<_>>();
28
29 if unique.len() == original_count {
30 Ok(())
31 } else {
32 Err(FormatError::new(
33 "E1601".to_string(),
34 "duplicate objective specified".to_string(),
35 "remove duplicate objectives".to_string(),
36 ))
37 }
38}
39
40fn check_e1602_no_cost_objective(objectives: &[&Objective]) -> Result<(), FormatError> {
42 let no_min_cost = !get_objectives_flattened(objectives)
43 .any(|objective| matches!(objective, MinimizeCost | MinimizeDistance | MinimizeDuration));
44
45 if no_min_cost {
46 Err(FormatError::new(
47 "E1602".to_string(),
48 "missing one of cost objectives".to_string(),
49 "specify 'minimize-cost', 'minimize-duration' or 'minimize-distance' objective".to_string(),
50 ))
51 } else {
52 Ok(())
53 }
54}
55
56fn check_e1603_no_jobs_with_value_objective(
58 ctx: &ValidationContext,
59 objectives: &[&Objective],
60) -> Result<(), FormatError> {
61 let has_value_objective = objectives.iter().any(|objective| matches!(objective, MaximizeValue { .. }));
62 let has_no_jobs_with_value = !ctx.problem.plan.jobs.iter().filter_map(|job| job.value).any(|value| value > 0.);
63
64 if has_value_objective && has_no_jobs_with_value {
65 Err(FormatError::new(
66 "E1603".to_string(),
67 "redundant value objective".to_string(),
68 "specify at least one non-zero valued job or delete 'maximize-value' objective".to_string(),
69 ))
70 } else {
71 Ok(())
72 }
73}
74
75fn check_e1604_no_jobs_with_order_objective(
77 ctx: &ValidationContext,
78 objectives: &[&Objective],
79) -> Result<(), FormatError> {
80 let has_order_objective = objectives.iter().any(|objective| matches!(objective, TourOrder { .. }));
81 let has_no_jobs_with_order = !ctx
82 .problem
83 .plan
84 .jobs
85 .iter()
86 .flat_map(|job| job.all_tasks_iter())
87 .filter_map(|job| job.order)
88 .any(|value| value > 0);
89
90 if has_order_objective && has_no_jobs_with_order {
91 Err(FormatError::new(
92 "E1604".to_string(),
93 "redundant tour order objective".to_string(),
94 "specify at least one job with non-zero order or delete 'tour-order' objective".to_string(),
95 ))
96 } else {
97 Ok(())
98 }
99}
100
101fn check_e1605_check_positive_value_and_order(ctx: &ValidationContext) -> Result<(), FormatError> {
102 let job_ids = ctx
103 .problem
104 .plan
105 .jobs
106 .iter()
107 .filter(|job| {
108 let has_invalid_order = job.all_tasks_iter().filter_map(|task| task.order).any(|value| value < 1);
109 let has_invalid_value = job.value.map_or(false, |v| v < 1.);
110
111 has_invalid_order || has_invalid_value
112 })
113 .map(|job| job.id.as_str())
114 .collect::<Vec<_>>();
115
116 if job_ids.is_empty() {
117 Ok(())
118 } else {
119 Err(FormatError::new(
120 "E1605".to_string(),
121 "value or order of a job should be greater than zero".to_string(),
122 format!("change value or order of jobs to be greater than zero: '{}'", job_ids.join(", ")),
123 ))
124 }
125}
126
127fn check_e1606_check_multiple_cost_objectives(objectives: &[&Objective]) -> Result<(), FormatError> {
129 let cost_objectives = objectives
130 .iter()
131 .filter(|objective| matches!(objective, MinimizeCost | MinimizeDistance | MinimizeDuration))
132 .count();
133
134 if cost_objectives > 1 {
135 Err(FormatError::new(
136 "E1606".to_string(),
137 "multiple cost objectives specified".to_string(),
138 format!("keep only one cost objective: was specified: '{cost_objectives}'"),
139 ))
140 } else {
141 Ok(())
142 }
143}
144
145fn check_e1607_jobs_with_value_but_no_objective(
147 ctx: &ValidationContext,
148 objectives: &[&Objective],
149) -> Result<(), FormatError> {
150 if objectives.is_empty() {
151 return Ok(());
152 }
153
154 let has_no_value_objective = !objectives.iter().any(|objective| matches!(objective, MaximizeValue { .. }));
155 let has_jobs_with_vlue = ctx.problem.plan.jobs.iter().filter_map(|job| job.value).any(|value| value > 0.);
156
157 if has_no_value_objective && has_jobs_with_vlue {
158 Err(FormatError::new(
159 "E1607".to_string(),
160 "missing value objective".to_string(),
161 "specify 'maximize-value' objective, remove objectives property or remove value property from jobs"
162 .to_string(),
163 ))
164 } else {
165 Ok(())
166 }
167}
168
169fn get_objectives<'a>(ctx: &'a ValidationContext) -> Option<Vec<&'a Objective>> {
170 ctx.problem.objectives.as_ref().map(|objectives| objectives.iter().collect())
171}
172
173fn get_objectives_flattened<'a>(objectives: &'a [&Objective]) -> impl Iterator<Item = &'a Objective> + 'a {
174 objectives.iter().flat_map(|&o| match o {
175 MultiObjective { objectives, .. } => Either::Left(objectives.iter()),
176 _ => Either::Right(std::iter::once(o)),
177 })
178}
179
180pub fn validate_objectives(ctx: &ValidationContext) -> Result<(), MultiFormatError> {
181 if let Some(objectives) = get_objectives(ctx) {
182 combine_error_results(&[
183 check_e1600_empty_objective(&objectives),
184 check_e1601_duplicate_objectives(&objectives),
185 check_e1602_no_cost_objective(&objectives),
186 check_e1603_no_jobs_with_value_objective(ctx, &objectives),
187 check_e1604_no_jobs_with_order_objective(ctx, &objectives),
188 check_e1605_check_positive_value_and_order(ctx),
189 check_e1606_check_multiple_cost_objectives(&objectives),
190 check_e1607_jobs_with_value_but_no_objective(ctx, &objectives),
191 ])
192 .map_err(From::from)
193 } else {
194 Ok(())
195 }
196}