Skip to main content

vrp_core/solver/search/local/
exchange_sequence.rs

1#[cfg(test)]
2#[path = "../../../../tests/unit/solver/search/local/exchange_sequence_test.rs"]
3mod exchange_sequence_test;
4
5use crate::construction::heuristics::*;
6use crate::models::problem::Job;
7use crate::solver::search::LocalOperator;
8use crate::solver::RefinementContext;
9use rand::prelude::SliceRandom;
10use rosomaxa::prelude::*;
11use std::collections::HashSet;
12use std::ops::ControlFlow;
13
14const MIN_JOBS: usize = 2;
15
16/// A local search operator which tries to exchange sequence of jobs between routes.
17pub struct ExchangeSequence {
18    max_sequence_size: usize,
19    reverse_prob: Float,
20    shuffle_prob: Float,
21}
22
23impl ExchangeSequence {
24    /// Creates a new instance of `ExchangeSequence`.
25    pub fn new(max_sequence_size: usize, reverse_prob: Float, shuffle_prob: Float) -> Self {
26        assert!(max_sequence_size >= MIN_JOBS);
27
28        Self { max_sequence_size, reverse_prob, shuffle_prob }
29    }
30}
31
32impl Default for ExchangeSequence {
33    fn default() -> Self {
34        Self::new(6, 0.5, 0.01)
35    }
36}
37
38impl LocalOperator for ExchangeSequence {
39    fn explore(&self, _: &RefinementContext, insertion_ctx: &InsertionContext) -> Option<InsertionContext> {
40        let route_indices = get_route_indices(insertion_ctx);
41
42        if route_indices.is_empty() {
43            return None;
44        }
45
46        let mut insertion_ctx = insertion_ctx.deep_copy();
47
48        exchange_jobs(
49            &mut insertion_ctx,
50            route_indices.as_slice(),
51            self.max_sequence_size,
52            self.reverse_prob,
53            self.shuffle_prob,
54        );
55
56        Some(insertion_ctx)
57    }
58}
59
60fn get_route_indices(insertion_ctx: &InsertionContext) -> Vec<usize> {
61    insertion_ctx
62        .solution
63        .routes
64        .iter()
65        .enumerate()
66        .filter_map(|(idx, route_ctx)| {
67            let locked_jobs =
68                route_ctx.route().tour.jobs().filter(|job| insertion_ctx.solution.locked.contains(*job)).count();
69            let has_enough_jobs = (route_ctx.route().tour.job_count() - locked_jobs) >= MIN_JOBS;
70
71            if has_enough_jobs {
72                Some(idx)
73            } else {
74                None
75            }
76        })
77        .collect()
78}
79
80fn exchange_jobs(
81    insertion_ctx: &mut InsertionContext,
82    route_indices: &[usize],
83    max_sequence_size: usize,
84    reverse_prob: Float,
85    shuffle_prob: Float,
86) {
87    let get_route_idx = |insertion_ctx: &InsertionContext| {
88        let idx = insertion_ctx.environment.random.uniform_int(0, route_indices.len() as i32 - 1) as usize;
89        route_indices.get(idx).cloned().unwrap()
90    };
91
92    let get_sequence_size = |insertion_ctx: &InsertionContext, route_idx: usize| {
93        let job_count = get_route_ctx(insertion_ctx, route_idx).route().tour.job_count().min(max_sequence_size);
94        insertion_ctx.environment.random.uniform_int(MIN_JOBS as i32, job_count as i32) as usize
95    };
96
97    let first_route_idx = get_route_idx(insertion_ctx);
98    let first_sequence_size = get_sequence_size(insertion_ctx, first_route_idx);
99    let first_jobs = extract_jobs(insertion_ctx, first_route_idx, first_sequence_size);
100
101    let second_route_idx = get_route_idx(insertion_ctx);
102
103    if first_route_idx != second_route_idx {
104        let second_sequence_size = get_sequence_size(insertion_ctx, second_route_idx);
105        let second_jobs = extract_jobs(insertion_ctx, second_route_idx, second_sequence_size);
106
107        insert_jobs(insertion_ctx, first_route_idx, second_jobs, reverse_prob, shuffle_prob);
108        insert_jobs(insertion_ctx, second_route_idx, first_jobs, reverse_prob, shuffle_prob);
109    } else {
110        insert_jobs(insertion_ctx, first_route_idx, first_jobs, reverse_prob, shuffle_prob);
111    }
112
113    finalize_insertion_ctx(insertion_ctx);
114}
115
116fn extract_jobs(insertion_ctx: &mut InsertionContext, route_idx: usize, sequence_size: usize) -> Vec<Job> {
117    let locked = &insertion_ctx.solution.locked;
118    let route_ctx = insertion_ctx.solution.routes.get_mut(route_idx).unwrap();
119    let job_count = route_ctx.route().tour.job_count();
120
121    assert!(job_count >= sequence_size);
122
123    // get jobs in the exact order as they appear first time in the tour
124    let (_, jobs) = route_ctx.route().tour.all_activities().filter_map(|activity| activity.retrieve_job()).fold(
125        (HashSet::<Job>::default(), Vec::with_capacity(job_count)),
126        |(mut set, mut vec), job| {
127            if !set.contains(&job) && !locked.contains(&job) {
128                vec.push(job.clone());
129                set.insert(job);
130            }
131
132            (set, vec)
133        },
134    );
135
136    let sequence_size = sequence_size.min(jobs.len());
137    let last_index = jobs.len() - sequence_size;
138    let start_index = insertion_ctx.environment.random.uniform_int(0, last_index as i32) as usize;
139
140    let removed =
141        (start_index..(start_index + sequence_size)).fold(Vec::with_capacity(sequence_size), |mut acc, index| {
142            let job = jobs.get(index).unwrap();
143            assert!(route_ctx.route_mut().tour.remove(job));
144            acc.push(job.clone());
145
146            acc
147        });
148
149    insertion_ctx.problem.goal.accept_route_state(route_ctx);
150
151    removed
152}
153
154fn insert_jobs(
155    insertion_ctx: &mut InsertionContext,
156    route_idx: usize,
157    jobs: Vec<Job>,
158    reverse_prob: Float,
159    shuffle_prob: Float,
160) {
161    let random = &insertion_ctx.environment.random;
162    let leg_selection = LegSelection::Stochastic(random.clone());
163    let result_selector = BestResultSelector::default();
164
165    let mut jobs = jobs;
166    match (random.is_hit(reverse_prob), random.is_hit(shuffle_prob)) {
167        (true, _) => {
168            jobs.reverse();
169        }
170        (_, true) => {
171            jobs.shuffle(&mut random.get_rng());
172        }
173        _ => {}
174    };
175
176    let start_index = random
177        .uniform_int(0, get_route_ctx(insertion_ctx, route_idx).route().tour.job_activity_count() as i32)
178        as usize;
179
180    let (failures, _) = jobs.into_iter().fold((Vec::new(), start_index), |(mut unassigned, start_index), job| {
181        let eval_ctx = EvaluationContext {
182            goal: &insertion_ctx.problem.goal,
183            job: &job,
184            leg_selection: &leg_selection,
185            result_selector: &result_selector,
186        };
187
188        // reevaluate last insertion point
189        let last_index = get_route_ctx(insertion_ctx, route_idx).route().tour.job_activity_count();
190        // try to find success insertion starting from given point
191        let (result, start_index) = (start_index..=last_index)
192            .try_fold((InsertionResult::make_failure(), start_index), |_, insertion_idx| {
193                let insertion = eval_job_insertion_in_route(
194                    insertion_ctx,
195                    &eval_ctx,
196                    get_route_ctx(insertion_ctx, route_idx),
197                    InsertionPosition::Concrete(insertion_idx),
198                    // NOTE we don't try to insert the best, so alternative is a failure
199                    InsertionResult::make_failure(),
200                );
201
202                match &insertion {
203                    InsertionResult::Failure(_) => ControlFlow::Continue((insertion, insertion_idx)),
204                    InsertionResult::Success(_) => ControlFlow::Break((insertion, insertion_idx)),
205                }
206            })
207            .unwrap_value();
208
209        match result {
210            InsertionResult::Success(success) => {
211                apply_insertion_success(insertion_ctx, success);
212            }
213            InsertionResult::Failure(failure) => unassigned.push((job, failure)),
214        }
215
216        (unassigned, start_index + 1)
217    });
218
219    insertion_ctx.solution.unassigned.extend(failures.into_iter().map(|(job, failure)| {
220        let code = UnassignmentInfo::Simple(failure.constraint);
221        let job = failure.job.unwrap_or(job);
222        (job, code)
223    }));
224}
225
226fn get_route_ctx(insertion_ctx: &InsertionContext, route_idx: usize) -> &RouteContext {
227    insertion_ctx.solution.routes.get(route_idx).unwrap()
228}