vrp_pragmatic/validation/
objectives.rs

1#[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
11/// Checks that objective is not empty when specified.
12fn 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
24/// Checks that each objective type specified only once.
25fn 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
40/// Checks that cost objective is specified.
41fn 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
56/// Checks that value objective can be specified only when job with value is used.
57fn 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
75/// Checks that order objective can be specified only when job with order is used.
76fn 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
127/// Checks that only one cost objective is specified.
128fn 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
145/// Checks that value objective is specified when some jobs have value property set.
146fn 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}