Skip to main content

vrp_core/construction/heuristics/
evaluators.rs

1#[cfg(test)]
2#[path = "../../../tests/unit/construction/heuristics/evaluators_test.rs"]
3mod evaluators_test;
4
5use rosomaxa::prelude::UnwrapValue;
6use std::ops::ControlFlow;
7use std::sync::Arc;
8
9use crate::construction::heuristics::*;
10use crate::models::common::Timestamp;
11use crate::models::problem::{Job, Multi, Single};
12use crate::models::solution::{Activity, Leg, Place};
13use crate::models::{ConstraintViolation, GoalContext, ViolationCode};
14use crate::utils::Either;
15
16/// Specifies an evaluation context data.
17pub struct EvaluationContext<'a> {
18    /// An actual optimization goal context.
19    pub goal: &'a GoalContext,
20    /// A job which is about to be inserted.
21    pub job: &'a Job,
22    /// A leg selection mode.
23    pub leg_selection: &'a LegSelection,
24    /// A result selector.
25    pub result_selector: &'a (dyn ResultSelector),
26}
27
28/// Specifies allowed insertion position in route for the job.
29#[derive(Copy, Clone)]
30pub enum InsertionPosition {
31    /// Job can be inserted anywhere in the route.
32    Any,
33    /// Job can be inserted only at the leg with the concrete index.
34    Concrete(usize),
35    /// Job can be inserted only to the end of the route.
36    Last,
37}
38
39/// Evaluates possibility to preform insertion from given insertion context in given route
40/// at given position constraint.
41pub fn eval_job_insertion_in_route(
42    insertion_ctx: &InsertionContext,
43    eval_ctx: &EvaluationContext,
44    route_ctx: &RouteContext,
45    position: InsertionPosition,
46    alternative: InsertionResult,
47) -> InsertionResult {
48    // NOTE do not evaluate unassigned job in unmodified route if it has a concrete code
49    match (route_ctx.is_stale(), insertion_ctx.solution.unassigned.get(eval_ctx.job)) {
50        (false, Some(UnassignmentInfo::Simple(_))) | (false, Some(UnassignmentInfo::Detailed(_))) => {
51            return alternative
52        }
53        _ => {}
54    }
55
56    let goal = &insertion_ctx.problem.goal;
57
58    if let Some(violation) = goal.evaluate(&MoveContext::route(&insertion_ctx.solution, route_ctx, eval_ctx.job)) {
59        return eval_ctx.result_selector.select_insertion(
60            insertion_ctx,
61            alternative,
62            InsertionResult::make_failure_with_code(violation.code, true, Some(eval_ctx.job.clone())),
63        );
64    }
65
66    let route_costs = goal.estimate(&MoveContext::route(&insertion_ctx.solution, route_ctx, eval_ctx.job));
67
68    // analyze alternative and return it if it looks better based on routing cost comparison
69    let (route_costs, best_known_cost) = if let Some(success) = alternative.as_success() {
70        match eval_ctx.result_selector.select_cost(&success.cost, &route_costs) {
71            Either::Left(_) => return alternative,
72            Either::Right(_) => (route_costs, Some(success.cost.clone())),
73        }
74    } else {
75        (route_costs, None)
76    };
77
78    eval_ctx.result_selector.select_insertion(
79        insertion_ctx,
80        alternative,
81        eval_job_constraint_in_route(eval_ctx, route_ctx, position, route_costs, best_known_cost),
82    )
83}
84
85/// Evaluates possibility to preform insertion in route context only.
86/// NOTE: doesn't evaluate constraints on route level.
87pub fn eval_job_constraint_in_route(
88    eval_ctx: &EvaluationContext,
89    route_ctx: &RouteContext,
90    position: InsertionPosition,
91    route_costs: InsertionCost,
92    best_known_cost: Option<InsertionCost>,
93) -> InsertionResult {
94    match eval_ctx.job {
95        Job::Single(single) => eval_single(eval_ctx, route_ctx, single, position, route_costs, best_known_cost),
96        Job::Multi(multi) => eval_multi(eval_ctx, route_ctx, multi, position, route_costs, best_known_cost),
97    }
98}
99
100pub(crate) fn eval_single_constraint_in_route(
101    insertion_ctx: &InsertionContext,
102    eval_ctx: &EvaluationContext,
103    route_ctx: &RouteContext,
104    single: &Arc<Single>,
105    position: InsertionPosition,
106    route_costs: InsertionCost,
107    best_known_cost: Option<InsertionCost>,
108) -> InsertionResult {
109    if let Some(violation) =
110        eval_ctx.goal.evaluate(&MoveContext::route(&insertion_ctx.solution, route_ctx, eval_ctx.job))
111    {
112        InsertionResult::Failure(InsertionFailure {
113            constraint: violation.code,
114            stopped: true,
115            job: Some(eval_ctx.job.clone()),
116        })
117    } else {
118        eval_single(eval_ctx, route_ctx, single, position, route_costs, best_known_cost)
119    }
120}
121
122fn eval_single(
123    eval_ctx: &EvaluationContext,
124    route_ctx: &RouteContext,
125    single: &Arc<Single>,
126    position: InsertionPosition,
127    route_costs: InsertionCost,
128    best_known_cost: Option<InsertionCost>,
129) -> InsertionResult {
130    let insertion_idx = get_insertion_index(route_ctx, position);
131    let mut activity = Activity::new_with_job(single.clone());
132
133    let result = analyze_insertion_in_route(
134        eval_ctx,
135        route_ctx,
136        insertion_idx,
137        single,
138        &mut activity,
139        route_costs,
140        SingleContext::new(best_known_cost, 0),
141    );
142
143    let job = eval_ctx.job.clone();
144    if let Some(place) = result.place {
145        activity.place = place;
146        let activities = vec![(activity, result.index)];
147        InsertionResult::make_success(result.cost.unwrap_or_default(), job, activities, route_ctx)
148    } else {
149        let (code, stopped) = result.violation.map_or((ViolationCode::unknown(), false), |v| (v.code, v.stopped));
150        InsertionResult::make_failure_with_code(code, stopped, Some(job))
151    }
152}
153
154fn eval_multi(
155    eval_ctx: &EvaluationContext,
156    route_ctx: &RouteContext,
157    multi: &Arc<Multi>,
158    position: InsertionPosition,
159    route_costs: InsertionCost,
160    best_known_cost: Option<InsertionCost>,
161) -> InsertionResult {
162    let insertion_idx = get_insertion_index(route_ctx, position).unwrap_or(0);
163    // 1. analyze permutations
164    let result = multi
165        .permutations()
166        .into_iter()
167        .try_fold(MultiContext::new(best_known_cost, insertion_idx), |acc_res, services| {
168            let mut shadow = ShadowContext::new(eval_ctx.goal, route_ctx);
169            let perm_res = (0..)
170                .try_fold(MultiContext::new(None, insertion_idx), |out, _| {
171                    if out.is_failure(route_ctx.route().tour.job_activity_count()) {
172                        return ControlFlow::Break(out);
173                    }
174                    shadow.restore(route_ctx);
175
176                    // 2. analyze inner jobs
177                    let sq_res = services
178                        .iter()
179                        .try_fold(out.next(), |in1, service| {
180                            if in1.violation.is_some() {
181                                return ControlFlow::Break(in1);
182                            }
183                            let mut activity = Activity::new_with_job(service.clone());
184                            // 3. analyze legs
185                            let srv_res = analyze_insertion_in_route(
186                                eval_ctx,
187                                shadow.route_ctx(),
188                                None,
189                                service,
190                                &mut activity,
191                                Default::default(),
192                                SingleContext::new(None, in1.next_index),
193                            );
194
195                            if let Some(place) = srv_res.place {
196                                activity.place = place;
197                                let activity = shadow.insert(activity, srv_res.index);
198                                let activities = concat_activities(in1.activities, (activity, srv_res.index));
199                                return MultiContext::success(
200                                    in1.cost.unwrap_or_else(|| route_costs.clone()) + srv_res.cost.unwrap_or_default(),
201                                    activities,
202                                );
203                            }
204
205                            MultiContext::fail(srv_res, in1)
206                        })
207                        .unwrap_value();
208
209                    MultiContext::promote(sq_res, out)
210                })
211                .unwrap_value();
212
213            MultiContext::promote(perm_res, acc_res)
214        })
215        .unwrap_value();
216
217    let job = eval_ctx.job.clone();
218    if result.is_success() {
219        let activities = result.activities.unwrap_or_default();
220        InsertionResult::make_success(result.cost.unwrap_or_default(), job, activities, route_ctx)
221    } else {
222        let (code, stopped) = result.violation.map_or((ViolationCode::unknown(), false), |v| (v.code, v.stopped));
223        InsertionResult::make_failure_with_code(code, stopped, Some(job))
224    }
225}
226
227fn analyze_insertion_in_route(
228    eval_ctx: &EvaluationContext,
229    route_ctx: &RouteContext,
230    insertion_idx: Option<usize>,
231    single: &Single,
232    target: &mut Activity,
233    route_costs: InsertionCost,
234    init: SingleContext,
235) -> SingleContext {
236    let mut analyze_leg_insertion = |leg: Leg<'_>, init| {
237        analyze_insertion_in_route_leg(eval_ctx, route_ctx, leg, single, target, route_costs.clone(), init)
238    };
239
240    match insertion_idx {
241        Some(idx) => {
242            if let Some(leg) = route_ctx.route().tour.legs().nth(idx) {
243                analyze_leg_insertion(leg, init).unwrap_value()
244            } else {
245                init
246            }
247        }
248        None => eval_ctx.leg_selection.sample_best(
249            route_ctx,
250            eval_ctx.job,
251            init.index,
252            init,
253            &mut |leg: Leg<'_>, init| analyze_leg_insertion(leg, init),
254            {
255                let max_value = InsertionCost::max_value();
256                move |lhs: &SingleContext, rhs: &SingleContext| {
257                    eval_ctx
258                        .result_selector
259                        .select_cost(lhs.cost.as_ref().unwrap_or(max_value), rhs.cost.as_ref().unwrap_or(max_value))
260                        .is_left()
261                }
262            },
263        ),
264    }
265}
266
267fn analyze_insertion_in_route_leg(
268    eval_ctx: &EvaluationContext,
269    route_ctx: &RouteContext,
270    leg: Leg,
271    single: &Single,
272    target: &mut Activity,
273    route_costs: InsertionCost,
274    mut single_ctx: SingleContext,
275) -> ControlFlow<SingleContext, SingleContext> {
276    let (items, index) = leg;
277    let (prev, next) = match items {
278        [prev] => (prev, None),
279        [prev, next] => (prev, Some(next)),
280        _ => return ControlFlow::Break(single_ctx),
281    };
282    let start_time = route_ctx.route().tour.start().map_or(Timestamp::default(), |act| act.schedule.departure);
283
284    // iterate over places and times to find the next best insertion point
285    for (place_idx, place) in single.places.iter().enumerate() {
286        target.place.idx = place_idx;
287        target.place.location = place.location.unwrap_or(prev.place.location);
288        target.place.duration = place.duration;
289
290        // iterate over time windows of the place
291        for time in place.times.iter() {
292            target.place.time = time.to_time_window(start_time);
293
294            let activity_ctx = ActivityContext { index, prev, target, next };
295            let move_ctx = MoveContext::activity(route_ctx, &activity_ctx);
296
297            if let Some(violation) = eval_ctx.goal.evaluate(&move_ctx) {
298                let is_stopped = violation.stopped;
299                single_ctx.violation = Some(violation);
300                if is_stopped {
301                    // should stop processing this leg and next ones
302                    return ControlFlow::Break(single_ctx);
303                } else {
304                    // can continue within the next place
305                    continue;
306                }
307            }
308
309            let costs = eval_ctx.goal.estimate(&move_ctx) + &route_costs;
310            let other_costs = single_ctx.cost.as_ref().unwrap_or(InsertionCost::max_value());
311
312            match eval_ctx.result_selector.select_cost(&costs, other_costs) {
313                // found better insertion
314                Either::Left(_) => {
315                    single_ctx.violation = None;
316                    single_ctx.index = index;
317                    single_ctx.cost = Some(costs);
318                    single_ctx.place = Some(target.place.clone());
319                }
320                Either::Right(_) => continue,
321            }
322        }
323    }
324
325    ControlFlow::Continue(single_ctx)
326}
327
328fn get_insertion_index(route_ctx: &RouteContext, position: InsertionPosition) -> Option<usize> {
329    match position {
330        InsertionPosition::Any => None,
331        InsertionPosition::Concrete(idx) => Some(idx),
332        InsertionPosition::Last => Some(route_ctx.route().tour.legs().count().max(1) - 1),
333    }
334}
335
336/// Stores information needed for single insertion.
337#[derive(Clone, Debug, Default)]
338struct SingleContext {
339    /// Constraint violation.
340    pub violation: Option<ConstraintViolation>,
341    /// Insertion index.
342    pub index: usize,
343    /// Best cost.
344    pub cost: Option<InsertionCost>,
345    /// Activity place.
346    pub place: Option<Place>,
347}
348
349impl SingleContext {
350    /// Creates a new empty context with given cost.
351    fn new(cost: Option<InsertionCost>, index: usize) -> Self {
352        Self { violation: None, index, cost, place: None }
353    }
354}
355
356/// Stores information needed for multi job insertion.
357struct MultiContext {
358    /// Constraint violation.
359    pub violation: Option<ConstraintViolation>,
360    /// Insertion index for first service.
361    pub start_index: usize,
362    /// Insertion index for next service.
363    pub next_index: usize,
364    /// Cost accumulator.
365    pub cost: Option<InsertionCost>,
366    /// Activities with their indices.
367    pub activities: Option<Vec<(Activity, usize)>>,
368}
369
370impl MultiContext {
371    /// Creates new empty insertion context.
372    fn new(cost: Option<InsertionCost>, index: usize) -> Self {
373        Self { violation: None, start_index: index, next_index: index, cost, activities: None }
374    }
375
376    /// Promotes insertion context by best price.
377    fn promote(left: Self, right: Self) -> ControlFlow<Self, Self> {
378        let index = left.start_index.max(right.start_index) + 1;
379        let best = match (&left.cost, &right.cost) {
380            (Some(left_cost), Some(right_cost)) => {
381                if left_cost < right_cost {
382                    left
383                } else {
384                    right
385                }
386            }
387            (Some(_), None) => left,
388            (None, Some(_)) => right,
389            // NOTE: no costs means failure, let's provide one which has violation field populated
390            (None, None) => {
391                if left.violation.is_some() {
392                    left
393                } else {
394                    right
395                }
396            }
397        };
398
399        let result = Self {
400            violation: best.violation,
401            start_index: index,
402            next_index: index,
403            cost: best.cost,
404            activities: best.activities,
405        };
406
407        if result.violation.as_ref().map_or(false, |v| v.stopped) {
408            ControlFlow::Break(result)
409        } else {
410            ControlFlow::Continue(result)
411        }
412    }
413
414    /// Creates failed insertion context within reason code.
415    #[inline]
416    fn fail(err_ctx: SingleContext, other_ctx: MultiContext) -> ControlFlow<Self, Self> {
417        let (code, stopped) = err_ctx
418            .violation
419            .map_or((ViolationCode::unknown(), false), |v| (v.code, v.stopped && other_ctx.activities.is_none()));
420
421        ControlFlow::Break(Self {
422            violation: Some(ConstraintViolation { code, stopped }),
423            start_index: other_ctx.start_index,
424            next_index: other_ctx.start_index,
425            cost: None,
426            activities: None,
427        })
428    }
429
430    /// Creates successful insertion context.
431    #[inline]
432    fn success(cost: InsertionCost, activities: Vec<(Activity, usize)>) -> ControlFlow<Self, Self> {
433        // NOTE avoid stack unwinding
434        let start_index = activities.first().map_or(0, |act| act.1);
435        let next_index = activities.last().map_or(0, |act| act.1) + 1;
436
437        ControlFlow::Continue(Self {
438            violation: None,
439            start_index,
440            next_index,
441            cost: Some(cost),
442            activities: Some(activities),
443        })
444    }
445
446    /// Creates next insertion context from existing one.
447    #[inline]
448    fn next(&self) -> Self {
449        Self {
450            violation: None,
451            start_index: self.start_index,
452            next_index: self.start_index,
453            cost: None,
454            activities: None,
455        }
456    }
457
458    /// Checks whether insertion is found.
459    #[inline]
460    fn is_success(&self) -> bool {
461        self.violation.is_none() && self.cost.is_some() && self.activities.is_some()
462    }
463
464    /// Checks whether insertion is failed.
465    #[inline]
466    fn is_failure(&self, index: usize) -> bool {
467        self.violation.as_ref().map_or(false, |v| v.stopped) || (self.start_index > index)
468    }
469}
470
471/// Provides the way to use copy on write strategy within route state context.
472struct ShadowContext<'a> {
473    goal: &'a GoalContext,
474    // NOTE Cow might be a better fit, but it would require RouteContext to implement Clone trait.
475    //      However we want to avoid this as it might lead to unnecessary clones and, as result,
476    //      performance degradation.
477    ctx: Either<&'a RouteContext, RouteContext>,
478}
479
480impl<'a> ShadowContext<'a> {
481    fn new(goal: &'a GoalContext, ctx: &'a RouteContext) -> Self {
482        Self { goal, ctx: Either::Left(ctx) }
483    }
484
485    fn route_ctx(&self) -> &'_ RouteContext {
486        match &self.ctx {
487            Either::Left(route_ctx) => route_ctx,
488            Either::Right(route_ctx) => route_ctx,
489        }
490    }
491
492    fn insert(&mut self, activity: Activity, index: usize) -> Activity {
493        if let Either::Left(route_ctx) = &self.ctx {
494            self.ctx = Either::Right(route_ctx.deep_copy());
495        }
496
497        if let Either::Right(ref mut route_ctx) = self.ctx {
498            route_ctx.route_mut().tour.insert_at(activity.deep_copy(), index + 1);
499            self.goal.accept_route_state(route_ctx);
500        }
501
502        activity
503    }
504
505    fn restore(&mut self, original: &'a RouteContext) {
506        if let Either::Right(_) = &self.ctx {
507            self.ctx = Either::Left(original)
508        }
509    }
510}
511
512fn concat_activities(
513    activities: Option<Vec<(Activity, usize)>>,
514    activity: (Activity, usize),
515) -> Vec<(Activity, usize)> {
516    let mut activities = activities.unwrap_or_default();
517    activities.push((activity.0, activity.1));
518
519    activities
520}