1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
#[cfg(test)]
#[path = "../../../tests/unit/construction/constraints/conditional_test.rs"]
mod conditional_test;

use crate::construction::constraints::{ConstraintModule, ConstraintVariant};
use crate::construction::heuristics::{RouteContext, SolutionContext};
use crate::models::problem::Job;
use hashbrown::HashSet;
use std::slice::Iter;

/// Defines how jobs are moved in solution context. Index of original affected route context is passed.
pub trait JobContextTransition {
    /// Returns true if job is moved from required to ignored.
    fn remove_from_required(&self, solution_ctx: &SolutionContext, route_index: Option<usize>, job: &Job) -> bool;

    /// Returns true if job is moved from ignored to required.
    fn promote_to_required(&self, solution_ctx: &SolutionContext, route_index: Option<usize>, job: &Job) -> bool;

    /// Returns true if job is removed from locked.
    fn remove_from_locked(&self, solution_ctx: &SolutionContext, route_index: Option<usize>, job: &Job) -> bool;

    /// Returns true if job is moved to locked.
    fn promote_to_locked(&self, solution_ctx: &SolutionContext, route_index: Option<usize>, job: &Job) -> bool;
}

/// A concrete implementation of `JobContextTransition` which allows to use lambdas.
pub struct ConcreteJobContextTransition<FRemoveRequired, FPromoteRequired, FRemoveLocked, FPromoteLocked>
where
    FRemoveRequired: Fn(&SolutionContext, Option<usize>, &Job) -> bool,
    FPromoteRequired: Fn(&SolutionContext, Option<usize>, &Job) -> bool,
    FRemoveLocked: Fn(&SolutionContext, Option<usize>, &Job) -> bool,
    FPromoteLocked: Fn(&SolutionContext, Option<usize>, &Job) -> bool,
{
    /// A function which removes job from required list.
    pub remove_required: FRemoveRequired,
    /// A function which promotes job to required jobs.
    pub promote_required: FPromoteRequired,
    /// A function which removes job from locked list.
    pub remove_locked: FRemoveLocked,
    /// A function which promotes job to locked jobs.
    pub promote_locked: FPromoteLocked,
}

impl<FRemoveRequired, FPromoteRequired, FRemoveLocked, FPromoteLocked> JobContextTransition
    for ConcreteJobContextTransition<FRemoveRequired, FPromoteRequired, FRemoveLocked, FPromoteLocked>
where
    FRemoveRequired: Fn(&SolutionContext, Option<usize>, &Job) -> bool,
    FPromoteRequired: Fn(&SolutionContext, Option<usize>, &Job) -> bool,
    FRemoveLocked: Fn(&SolutionContext, Option<usize>, &Job) -> bool,
    FPromoteLocked: Fn(&SolutionContext, Option<usize>, &Job) -> bool,
{
    fn remove_from_required(&self, solution_ctx: &SolutionContext, route_index: Option<usize>, job: &Job) -> bool {
        (self.remove_required)(solution_ctx, route_index, job)
    }

    fn promote_to_required(&self, solution_ctx: &SolutionContext, route_index: Option<usize>, job: &Job) -> bool {
        (self.promote_required)(solution_ctx, route_index, job)
    }

    fn remove_from_locked(&self, solution_ctx: &SolutionContext, route_index: Option<usize>, job: &Job) -> bool {
        (self.remove_locked)(solution_ctx, route_index, job)
    }

    fn promote_to_locked(&self, solution_ctx: &SolutionContext, route_index: Option<usize>, job: &Job) -> bool {
        (self.promote_locked)(solution_ctx, route_index, job)
    }
}

/// A module which allows to promote jobs between required and ignored collection using some condition.
/// Useful to model some optional/conditional activities, e.g. breaks, refueling, etc.
pub struct ConditionalJobModule {
    context_transition: Box<dyn JobContextTransition + Send + Sync>,
    state_keys: Vec<i32>,
    constraints: Vec<ConstraintVariant>,
}

impl ConditionalJobModule {
    /// Creates a new instance of `ConditionalJobModule`.
    pub fn new(context_transition: Box<dyn JobContextTransition + Send + Sync>) -> Self {
        Self { context_transition, state_keys: vec![], constraints: vec![] }
    }
}

impl ConstraintModule for ConditionalJobModule {
    fn accept_insertion(&self, solution_ctx: &mut SolutionContext, route_index: usize, _job: &Job) {
        // TODO avoid calling this on each insertion as it is expensive.
        analyze_and_perform_transitions(solution_ctx, Some(route_index), self.context_transition.as_ref());
    }

    fn accept_route_state(&self, _ctx: &mut RouteContext) {}

    fn accept_solution_state(&self, solution_ctx: &mut SolutionContext) {
        analyze_and_perform_transitions(solution_ctx, None, self.context_transition.as_ref());
    }

    fn state_keys(&self) -> Iter<i32> {
        self.state_keys.iter()
    }

    fn get_constraints(&self) -> Iter<ConstraintVariant> {
        self.constraints.iter()
    }
}

fn analyze_and_perform_transitions(
    solution_ctx: &mut SolutionContext,
    route_index: Option<usize>,
    context_transition: &(dyn JobContextTransition + Send + Sync),
) {
    // analyzed required/ignored
    let ignored: HashSet<Job> = solution_ctx
        .required
        .iter()
        .filter(|job| context_transition.remove_from_required(solution_ctx, route_index, job))
        .cloned()
        .collect();
    solution_ctx.required.retain(|job| !ignored.contains(job));
    solution_ctx.unassigned.retain(|job, _| !ignored.contains(job));

    // identify required inside ignored
    let required: HashSet<Job> = solution_ctx
        .ignored
        .iter()
        .filter(|job| context_transition.promote_to_required(solution_ctx, route_index, job))
        .cloned()
        .collect();
    solution_ctx.ignored.retain(|job| !required.contains(job));

    solution_ctx.required.extend(required);
    solution_ctx.ignored.extend(ignored);

    // analyze locked
    let not_locked: HashSet<Job> = solution_ctx
        .locked
        .iter()
        .filter(|job| context_transition.remove_from_locked(solution_ctx, route_index, job))
        .cloned()
        .collect();
    solution_ctx.locked.retain(|job| !not_locked.contains(job));

    let locked: HashSet<Job> = solution_ctx
        .required
        .iter()
        .chain(solution_ctx.ignored.iter())
        .filter(|job| context_transition.promote_to_locked(solution_ctx, route_index, job))
        .cloned()
        .collect();

    solution_ctx.locked.extend(locked);
}