Skip to main content

vrp_core/models/
goal.rs

1#[cfg(test)]
2#[path = "../../tests/unit/models/goal_test.rs"]
3mod goal_test;
4
5use crate::construction::enablers::*;
6use crate::construction::heuristics::*;
7use crate::models::common::Cost;
8use crate::models::problem::Job;
9use rand::prelude::SliceRandom;
10use rosomaxa::population::Shuffled;
11use rosomaxa::prelude::*;
12use std::cmp::Ordering;
13use std::collections::HashSet;
14use std::fmt::{Debug, Display, Formatter};
15use std::ops::ControlFlow;
16use std::sync::Arc;
17
18/// Defines Vehicle Routing Problem variant by global and local objectives:
19/// A **global objective** defines the way two VRP solutions are compared to select better one:
20/// for example, given the same number of assigned jobs, prefer fewer tours used instead of total
21/// solution cost.
22///
23/// A **local objective** defines how a single VRP solution is created/modified. It specifies hard
24/// constraints such as vehicle capacity, time windows, skills, etc. Also, it defines soft constraints
25/// which are used to guide search in preferred by global objective direction: reduce the number of tours
26/// served, maximize the total value of assigned jobs, etc.
27///
28/// Both, global and local objectives, are specified by individual **features**. In general, a **Feature**
29/// encapsulates a single VRP aspect, such as capacity constraint for job's demand, time limitations
30/// for vehicles/jobs, etc.
31#[derive(Clone)]
32pub struct GoalContext {
33    goal: Goal,
34    alternative_goals: Vec<(Goal, Float)>,
35    constraints: Vec<Arc<dyn FeatureConstraint>>,
36    states: Vec<Arc<dyn FeatureState>>,
37}
38
39impl GoalContext {
40    /// Creates a new instance of `GoalContext` with given feature constraints.
41    pub fn with_constraints<Iter>(&self, constraints: Iter) -> Self
42    where
43        Iter: Iterator<Item = Arc<dyn FeatureConstraint>>,
44    {
45        GoalContext { constraints: constraints.collect(), ..self.clone() }
46    }
47
48    /// Returns an iterator over internal feature constraints.
49    pub fn constraints(&self) -> impl Iterator<Item = Arc<dyn FeatureConstraint>> + '_ {
50        self.constraints.iter().cloned()
51    }
52}
53
54impl Debug for GoalContext {
55    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
56        f.debug_struct(short_type_name::<Self>())
57            .field("goal layers", &self.goal.layers.len())
58            .field("alternatives", &self.alternative_goals.len())
59            .field("constraints", &self.constraints.len())
60            .field("states", &self.states.len())
61            .finish()
62    }
63}
64
65/// Provides a customizable way to build goal context.
66pub struct GoalContextBuilder {
67    main_goal: Option<Goal>,
68    alternative_goals: Vec<(Goal, Float)>,
69    features: Vec<Feature>,
70}
71
72impl GoalContextBuilder {
73    /// Creates a `GoalContextBuilder` with the given list of features.
74    pub fn with_features(features: &[Feature]) -> GenericResult<Self> {
75        let features = features.to_vec();
76        let ids_all = features.iter().map(|feature| feature.name.as_str()).collect::<Vec<_>>();
77        let ids_unique = ids_all.iter().collect::<HashSet<_>>();
78
79        if ids_unique.len() != ids_all.len() {
80            return Err(format!(
81                "some of the features are defined more than once, check ids list: {}",
82                ids_all.join(",")
83            )
84            .into());
85        }
86
87        let goal = Goal::simple(&features)?;
88
89        Ok(Self { main_goal: Some(goal), alternative_goals: Vec::default(), features })
90    }
91
92    /// Sets a main goal of optimization.
93    pub fn set_main_goal(mut self, goal: Goal) -> Self {
94        self.main_goal = Some(goal);
95        self
96    }
97
98    /// Sets an alternative goal of optimization.
99    pub fn add_alternative_goal(mut self, goal: Goal, weight: Float) -> Self {
100        self.alternative_goals.push((goal, weight));
101        self
102    }
103
104    /// Builds goal context.
105    pub fn build(self) -> GenericResult<GoalContext> {
106        let goal = self.main_goal.ok_or_else(|| GenericError::from("missing goal of optimization"))?;
107        let alternative_goals = self.alternative_goals;
108        let states = self.features.iter().filter_map(|feature| feature.state.clone()).collect();
109        let constraints = self.features.iter().filter_map(|feature| feature.constraint.clone()).collect();
110
111        Ok(GoalContext { goal, alternative_goals, constraints, states })
112    }
113}
114
115type TotalOrderFn =
116    Arc<dyn Fn(&[Arc<dyn FeatureObjective>], &InsertionContext, &InsertionContext) -> Ordering + Send + Sync>;
117type CostEstimateFn = Arc<dyn Fn(&[Arc<dyn FeatureObjective>], &MoveContext<'_>) -> Cost + Send + Sync>;
118type ObjectiveLayer = (TotalOrderFn, CostEstimateFn, Vec<Arc<dyn FeatureObjective>>);
119
120/// Specifies a goal of optimization as a list of `Feature` objectives with rules in lexicographical order.
121#[derive(Clone)]
122pub struct Goal {
123    layers: Vec<ObjectiveLayer>,
124}
125
126impl Goal {
127    /// Creates a goal using objectives from given list of features in lexicographical order.
128    /// See [GoalBuilder] for more complex options.
129    pub fn simple(features: &[Feature]) -> GenericResult<Self> {
130        let mut builder = GoalBuilder::default();
131        let names = features.iter().filter(|f| f.objective.is_some()).map(|f| &f.name);
132
133        for name in names {
134            builder = Self::add_with_name(builder, features, name)?;
135        }
136
137        builder.build()
138    }
139
140    /// Creates a goal using feature names from the given list. Objectives are defined in lexicographical order.
141    pub fn subset_of<S: AsRef<str>>(features: &[Feature], names: &[S]) -> GenericResult<Self> {
142        let mut builder = GoalBuilder::default();
143
144        for name in names {
145            builder = Self::add_with_name(builder, features, name.as_ref())?;
146        }
147
148        builder.build()
149    }
150
151    fn add_with_name(builder: GoalBuilder, features: &[Feature], name: &str) -> GenericResult<GoalBuilder> {
152        let feature = features
153            .iter()
154            .find(|f| f.name == name)
155            .ok_or_else(|| GenericError::from(format!("cannot find a feature with given name: '{name}'")))?;
156
157        let objective = feature
158            .objective
159            .clone()
160            .take()
161            .ok_or_else(|| GenericError::from(format!("feature '{name}' has no objective")))?;
162
163        Ok(builder.add_single(objective))
164    }
165}
166
167impl Goal {
168    /// Compares two solutions from optimization goal point of view and returns their comparison order.
169    pub fn total_order(&self, a: &InsertionContext, b: &InsertionContext) -> Ordering {
170        self.layers
171            .iter()
172            .try_fold(Ordering::Equal, |_, (total_order_fn, _, objectives)| {
173                match (total_order_fn)(objectives.as_slice(), a, b) {
174                    Ordering::Equal => ControlFlow::Continue(Ordering::Equal),
175                    order => ControlFlow::Break(order),
176                }
177            })
178            .unwrap_value()
179    }
180
181    /// Estimates insertion cost (penalty) of the refinement move.
182    pub fn estimate(&self, move_ctx: &MoveContext<'_>) -> InsertionCost {
183        self.layers.iter().map(|(_, estimate_fn, objectives)| (estimate_fn)(objectives.as_slice(), move_ctx)).collect()
184    }
185
186    /// Calculates solution's fitness.
187    pub fn fitness<'a>(&'a self, solution: &'a InsertionContext) -> impl Iterator<Item = Float> + 'a {
188        self.layers.iter().flat_map(|(_, _, objectives)| objectives.iter()).map(|objective| objective.fitness(solution))
189    }
190}
191
192/// Builds a [Goal] - a goal of optimization - composing multiple layers from objective functions
193/// in lexicographical order.
194#[derive(Default)]
195pub struct GoalBuilder {
196    layers: Vec<ObjectiveLayer>,
197}
198
199impl GoalBuilder {
200    /// Add a layer which consists of one objective function with a given feature name.
201    pub fn add_single(mut self, objective: Arc<dyn FeatureObjective>) -> Self {
202        // NOTE: indices are controlled internally
203        self.layers.push((
204            Arc::new(|objectives, a, b| {
205                let fitness_a = objectives[0].fitness(a);
206                let fitness_b = objectives[0].fitness(b);
207
208                // NOTE total_cmp distinguishes between positive zero and negative zero while
209                // logically they are the same in this context
210                if fitness_a == 0. && fitness_b == 0. {
211                    Ordering::Equal
212                } else {
213                    fitness_a.total_cmp(&fitness_b)
214                }
215            }),
216            Arc::new(|objectives, move_ctx| objectives[0].estimate(move_ctx)),
217            vec![objective],
218        ));
219        self
220    }
221
222    /// Add a layer which consists of one or many objective function with a given feature name and
223    /// a custom `GoalResolver`.
224    pub fn add_multi<TO, CE>(
225        mut self,
226        objectives: &[Arc<dyn FeatureObjective>],
227        total_order_fn: TO,
228        cost_estimate_fn: CE,
229    ) -> Self
230    where
231        TO: Fn(&[Arc<dyn FeatureObjective>], &InsertionContext, &InsertionContext) -> Ordering + Send + Sync + 'static,
232        CE: Fn(&[Arc<dyn FeatureObjective>], &MoveContext<'_>) -> Cost + Send + Sync + 'static,
233    {
234        self.layers.push((Arc::new(total_order_fn), Arc::new(cost_estimate_fn), objectives.to_vec()));
235        self
236    }
237
238    /// Builds a [Goal] of optimization using features provided.
239    pub fn build(self) -> GenericResult<Goal> {
240        if self.layers.is_empty() {
241            return Err(GenericError::from("no objectives specified in the goal"));
242        }
243
244        Ok(Goal { layers: self.layers })
245    }
246}
247
248/// An individual feature which is used to build a specific VRP variant, e.g., capacity restriction,
249/// job values, etc. Each feature consists of three optional parts (but at least one should be defined):
250///
251/// * **constraint**: an invariant which should be hold to have a feasible VRP solution in the end.
252///   A good examples are hard constraints such as capacity, time, travel limits, etc.
253///
254/// * **objective**: an objective of the optimization such as minimization of unassigned jobs or tours.
255///   All objectives form together a hierarchy which describes a goal of optimization, including
256///   various soft constraints: assignment of preferred jobs, optional breaks, etc. This helps to
257///   guide the search on the global objective level (e.g. comparison of various solutions in order to
258///   find out which one is "better") and local objective level (e.g. which job should be inserted next
259///   into specific solution).
260///
261/// * **state**: the corresponding cached data of constraint/objective to speed up/control their evaluations.
262///
263/// As mentioned above, at least one part should be defined. Some rules of thumb:
264/// * each soft constraint requires an objective so that the goal of optimization is reflected on global
265///   and local levels
266/// * hard constraint can be defined without objective as this is an invariant
267/// * state should be used to avoid expensive calculations during insertion evaluation phase.
268///   `FeatureObjective::estimate` and `FeatureConstraint::evaluate` methods are called during this phase.
269///   Additionally, it can be used to do some solution modifications at `FeatureState::accept_solution_state`.
270#[derive(Clone, Default)]
271pub struct Feature {
272    /// An unique id of the feature.
273    pub name: String,
274    /// A hard constraint.
275    pub constraint: Option<Arc<dyn FeatureConstraint>>,
276    /// An objective which models soft constraints.
277    pub objective: Option<Arc<dyn FeatureObjective>>,
278    /// A state change handler.
279    pub state: Option<Arc<dyn FeatureState>>,
280}
281
282/// Specifies a result of hard route constraint check.
283#[derive(Clone, Debug, Eq, PartialEq)]
284pub struct ConstraintViolation {
285    /// Violation code which is used as marker of specific constraint violated.
286    pub code: ViolationCode,
287    /// True if further insertions should not be attempted.
288    pub stopped: bool,
289}
290
291impl ConstraintViolation {
292    /// A constraint violation failure with stopped set to true.
293    pub fn fail(code: ViolationCode) -> Option<Self> {
294        Some(ConstraintViolation { code, stopped: true })
295    }
296
297    /// A constraint violation failure with stopped set to false.
298    pub fn skip(code: ViolationCode) -> Option<Self> {
299        Some(ConstraintViolation { code, stopped: false })
300    }
301
302    /// No constraint violation.
303    pub fn success() -> Option<Self> {
304        None
305    }
306}
307
308/// Specifies a type for constraint violation code.
309#[derive(Clone, Copy, Debug, Default, Hash, Eq, PartialEq)]
310pub struct ViolationCode(pub i32);
311
312impl ViolationCode {
313    /// Returns an unknown violation code.
314    pub fn unknown() -> Self {
315        Self(-1)
316    }
317
318    /// Checks whether violation code is unknown.
319    pub fn is_unknown(&self) -> bool {
320        self.0 == -1
321    }
322}
323
324impl From<i32> for ViolationCode {
325    fn from(value: i32) -> Self {
326        Self(value)
327    }
328}
329
330impl Display for ViolationCode {
331    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
332        f.write_fmt(format_args!("{}", self.0))
333    }
334}
335
336/// Provides a way to build feature with some checks.
337#[derive(Default)]
338pub struct FeatureBuilder(Feature);
339
340impl FeatureBuilder {
341    /// Creates a builder from another feature.
342    pub fn from_feature(feature: Feature) -> Self {
343        Self(feature)
344    }
345
346    /// Sets given name.
347    pub fn with_name(mut self, name: &str) -> Self {
348        self.0.name = name.to_string();
349        self
350    }
351
352    /// Adds given constraint.
353    pub fn with_constraint<T: FeatureConstraint + 'static>(mut self, constraint: T) -> Self {
354        self.0.constraint = Some(Arc::new(constraint));
355        self
356    }
357
358    /// Adds given objective.
359    pub fn with_objective<T: FeatureObjective + 'static>(mut self, objective: T) -> Self {
360        self.0.objective = Some(Arc::new(objective));
361        self
362    }
363
364    /// Adds given state.
365    pub fn with_state<T: FeatureState + 'static>(mut self, state: T) -> Self {
366        self.0.state = Some(Arc::new(state));
367        self
368    }
369
370    /// Tries to build a feature.
371    pub fn build(self) -> Result<Feature, GenericError> {
372        let feature = self.0;
373
374        if feature.name == String::default() {
375            return Err("features with default id are not allowed".into());
376        }
377
378        if feature.constraint.is_none() && feature.objective.is_none() {
379            Err("empty feature is not allowed".into())
380        } else {
381            Ok(feature)
382        }
383    }
384}
385
386/// Provides the way to modify solution state when the search is performed.
387pub trait FeatureState: Send + Sync {
388    /// Notifies a state that given routes (indices) and jobs cannot be assigned due to constraint violations.
389    /// This method can be used to modify solution context to help resolve some limitations imposed by
390    /// constraints and, generally, can modify solution context.
391    /// If some action was taken which might help to assign given jobs to given routes, then true
392    /// should be returned. **Please note**, if this method wrongly returns true, it might cause infinite
393    /// loops in an insertion evaluation process.
394    /// The default implementation returns false, which is safe and ok for most of the features.
395    fn notify_failure(&self, _solution_ctx: &mut SolutionContext, _route_indices: &[usize], _jobs: &[Job]) -> bool {
396        false
397    }
398
399    /// Accept insertion of a specific job into the route.
400    /// Called once a job has been inserted into a solution represented via `solution_ctx`.
401    /// Target route is defined by `route_index` which refers to `routes` collection in solution context.
402    /// Inserted job is `job`.
403    /// This method can call `accept_route_state` internally.
404    /// This method should NOT modify the number of job activities in the tour.
405    fn accept_insertion(&self, solution_ctx: &mut SolutionContext, route_index: usize, job: &Job);
406
407    /// Accept route and updates its state to allow more efficient constraint checks.
408    /// This method should NOT modify the number of job activities in the tour.
409    fn accept_route_state(&self, route_ctx: &mut RouteContext);
410
411    /// Accepts insertion solution context allowing to update job insertion data.
412    /// This method called twice: before insertion of all jobs starts and when it ends.
413    /// Please note that it is important to update only stale routes as this allows avoiding
414    /// update of non-changed route states.
415    fn accept_solution_state(&self, solution_ctx: &mut SolutionContext);
416}
417
418/// Defines feature constraint behavior.
419pub trait FeatureConstraint: Send + Sync {
420    /// Evaluates hard constraints violations.
421    fn evaluate(&self, move_ctx: &MoveContext<'_>) -> Option<ConstraintViolation>;
422
423    /// Tries to merge two jobs taking into account common constraints.
424    /// Returns a new job, if it is possible to merge them having a theoretically assignable
425    /// job. Otherwise, returns violation error code.
426    /// Default implementation returns an error with default [ViolationCode].
427    fn merge(&self, _source: Job, _candidate: Job) -> Result<Job, ViolationCode> {
428        Err(ViolationCode::default())
429    }
430}
431
432/// Defines feature's objective function behavior.
433pub trait FeatureObjective: Send + Sync {
434    /// An objective fitness value for the given `solution`.
435    fn fitness(&self, solution: &InsertionContext) -> Cost;
436
437    /// Estimates the cost of insertion.
438    fn estimate(&self, move_ctx: &MoveContext<'_>) -> Cost;
439}
440
441impl HeuristicObjective for GoalContext {
442    type Solution = InsertionContext;
443
444    fn total_order(&self, a: &Self::Solution, b: &Self::Solution) -> Ordering {
445        self.goal.total_order(a, b)
446    }
447}
448
449impl Shuffled for GoalContext {
450    fn get_shuffled(&self, random: &(dyn Random)) -> Self {
451        const RANDOM_ALTERNATIVE_PROBABILITY: Float = 0.05;
452        const RANDOM_SHUFFLE_PROBABILITY: Float = 0.001;
453
454        if !self.alternative_goals.is_empty() && random.is_hit(RANDOM_ALTERNATIVE_PROBABILITY) {
455            let idx = random.uniform_int(0, self.alternative_goals.len() as i32 - 1) as usize;
456            return self.get_alternative(idx);
457        }
458
459        if random.is_hit(RANDOM_SHUFFLE_PROBABILITY) {
460            self.get_shuffled(random)
461        } else {
462            self.clone()
463        }
464    }
465}
466
467impl GoalContext {
468    fn get_alternative(&self, idx: usize) -> Self {
469        let (goal, _) = self.alternative_goals[idx].clone();
470
471        Self { goal, ..self.clone() }
472    }
473
474    fn get_shuffled(&self, random: &(dyn Random)) -> Self {
475        let instance = self.clone();
476
477        let mut layers = self.goal.layers.clone();
478        layers.shuffle(&mut random.get_rng());
479
480        Self { goal: Goal { layers }, ..instance }
481    }
482
483    /// Returns goals with alternative objectives.
484    pub(crate) fn get_alternatives(&self) -> impl Iterator<Item = Self> + '_ {
485        self.alternative_goals.iter().enumerate().map(|(idx, _)| self.get_alternative(idx))
486    }
487
488    /// Accepts job insertion.
489    pub fn accept_insertion(&self, solution_ctx: &mut SolutionContext, route_index: usize, job: &Job) {
490        accept_insertion_with_states(&self.states, solution_ctx, route_index, job)
491    }
492
493    /// Accepts route state.
494    pub fn accept_route_state(&self, route_ctx: &mut RouteContext) {
495        accept_route_state_with_states(&self.states, route_ctx)
496    }
497
498    /// Accepts solution state.
499    pub fn accept_solution_state(&self, solution_ctx: &mut SolutionContext) {
500        accept_solution_state_with_states(&self.states, solution_ctx);
501    }
502
503    /// Notifies about a failed attempt to insert given jobs into given routes (indices).
504    /// Returns true if failure is some attempt to handle failure was performed and retry can be
505    /// performed.
506    pub fn notify_failure(&self, solution_ctx: &mut SolutionContext, route_indices: &[usize], jobs: &[Job]) -> bool {
507        notify_failure_with_states(&self.states, solution_ctx, route_indices, jobs)
508    }
509
510    /// Tries to merge two jobs taking into account common constraints.
511    /// Returns a new job, if it is possible to merge them having a theoretically assignable
512    /// job. Otherwise returns violation error code.
513    pub fn merge(&self, source: Job, candidate: Job) -> Result<Job, ViolationCode> {
514        merge_with_constraints(&self.constraints, source, candidate)
515    }
516
517    /// Evaluates feasibility of the refinement move.
518    pub fn evaluate(&self, move_ctx: &MoveContext<'_>) -> Option<ConstraintViolation> {
519        evaluate_with_constraints(&self.constraints, move_ctx)
520    }
521
522    /// Estimates insertion cost (penalty) of the refinement move.
523    pub fn estimate(&self, move_ctx: &MoveContext<'_>) -> InsertionCost {
524        self.goal.estimate(move_ctx)
525    }
526
527    /// Calculates solution's fitness.
528    pub fn fitness<'a>(&'a self, solution: &'a InsertionContext) -> impl Iterator<Item = Float> + 'a {
529        self.goal.fitness(solution)
530    }
531}
532
533impl Debug for Feature {
534    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
535        f.debug_struct(short_type_name::<Self>())
536            .field("name", &self.name)
537            .field("constraint", &self.constraint.is_some())
538            .field("objective", &self.objective.is_some())
539            .field("state", &self.state.is_some())
540            .finish()
541    }
542}