vrp_core/solver/search/
decompose_search.rs

1#[cfg(test)]
2#[path = "../../../tests/unit/solver/search/decompose_search_test.rs"]
3mod decompose_search_test;
4
5use crate::construction::heuristics::*;
6use crate::models::GoalContext;
7use crate::solver::search::create_environment_with_custom_quota;
8use crate::solver::*;
9use crate::utils::Either;
10use rosomaxa::utils::parallel_into_collect;
11use std::cell::RefCell;
12use std::cmp::Ordering;
13use std::collections::HashSet;
14use std::iter::{empty, once};
15
16/// A search operator which decomposes an original solution into multiple partial solutions,
17/// performs search independently, and then merges partial solutions back into one solution.
18pub struct DecomposeSearch {
19    inner_search: TargetSearchOperator,
20    max_routes_range: (i32, i32),
21    repeat_count: usize,
22    quota_limit: usize,
23}
24
25impl DecomposeSearch {
26    /// Create a new instance of `DecomposeSearch`.
27    pub fn new(
28        inner_search: TargetSearchOperator,
29        max_routes_range: (usize, usize),
30        repeat_count: usize,
31        quota_limit: usize,
32    ) -> Self {
33        assert!(max_routes_range.0 > 1);
34        let max_routes_range = (max_routes_range.0 as i32, max_routes_range.1 as i32);
35
36        Self { inner_search, max_routes_range, repeat_count, quota_limit }
37    }
38}
39
40impl HeuristicSearchOperator for DecomposeSearch {
41    type Context = RefinementContext;
42    type Objective = GoalContext;
43    type Solution = InsertionContext;
44
45    fn search(&self, heuristic_ctx: &Self::Context, solution: &Self::Solution) -> Self::Solution {
46        let refinement_ctx = heuristic_ctx;
47        let insertion_ctx = solution;
48
49        decompose_insertion_context(
50            refinement_ctx,
51            insertion_ctx,
52            self.max_routes_range,
53            self.repeat_count,
54            self.quota_limit,
55        )
56        .map(|contexts| self.refine_decomposed(refinement_ctx, insertion_ctx, contexts))
57        .unwrap_or_else(|| self.inner_search.search(heuristic_ctx, insertion_ctx))
58    }
59}
60
61const GREEDY_ERROR: &str = "greedy population has no insertion_ctxs";
62
63impl DecomposeSearch {
64    fn refine_decomposed(
65        &self,
66        refinement_ctx: &RefinementContext,
67        original_insertion_ctx: &InsertionContext,
68        decomposed: Vec<(RefinementContext, HashSet<usize>)>,
69    ) -> InsertionContext {
70        // NOTE: validate decomposition
71        decomposed.iter().enumerate().for_each(|(outer_ix, (_, outer))| {
72            decomposed.iter().enumerate().filter(|(inner_idx, _)| outer_ix != *inner_idx).for_each(
73                |(_, (_, inner))| {
74                    assert!(outer.intersection(inner).next().is_none());
75                },
76            );
77        });
78
79        // do actual refinement independently for each decomposed context
80        let decomposed = parallel_into_collect(decomposed, |(mut refinement_ctx, route_indices)| {
81            let _ = (0..self.repeat_count).try_for_each(|_| {
82                let insertion_ctx = refinement_ctx.selected().next().expect(GREEDY_ERROR);
83                let insertion_ctx = self.inner_search.search(&refinement_ctx, insertion_ctx);
84                let is_quota_reached =
85                    refinement_ctx.environment.quota.as_ref().map_or(false, |quota| quota.is_reached());
86                refinement_ctx.add_solution(insertion_ctx);
87
88                if is_quota_reached {
89                    Err(())
90                } else {
91                    Ok(())
92                }
93            });
94            (refinement_ctx, route_indices)
95        });
96
97        // merge evolution results into one insertion_ctx
98        let mut insertion_ctx = decomposed.into_iter().fold(
99            InsertionContext::new_empty(refinement_ctx.problem.clone(), refinement_ctx.environment.clone()),
100            |insertion_ctx, decomposed| merge_best(decomposed, original_insertion_ctx, insertion_ctx),
101        );
102
103        insertion_ctx.restore();
104        finalize_insertion_ctx(&mut insertion_ctx);
105
106        insertion_ctx
107    }
108}
109
110fn create_population(insertion_ctx: InsertionContext) -> TargetPopulation {
111    Box::new(GreedyPopulation::new(insertion_ctx.problem.goal.clone(), 1, Some(insertion_ctx)))
112}
113
114fn create_multiple_insertion_contexts(
115    insertion_ctx: &InsertionContext,
116    environment: Arc<Environment>,
117    max_routes_range: (i32, i32),
118) -> Option<Vec<(InsertionContext, HashSet<usize>)>> {
119    if insertion_ctx.solution.routes.is_empty() {
120        return None;
121    }
122
123    let route_groups = group_routes_by_proximity(insertion_ctx);
124    let (min, max) = max_routes_range;
125    let max = if insertion_ctx.solution.routes.len() < 4 { 2 } else { max };
126
127    // identify route groups and create contexts from them
128    let used_indices = RefCell::new(HashSet::new());
129    let insertion_ctxs = route_groups
130        .into_iter()
131        .enumerate()
132        .filter(|(outer_idx, _)| !used_indices.borrow().contains(outer_idx))
133        .map(|(outer_idx, route_group)| {
134            let group_size = environment.random.uniform_int(min, max) as usize;
135            let route_group = once(outer_idx)
136                .chain(route_group.into_iter().filter(|inner_idx| !used_indices.borrow().contains(inner_idx)))
137                .take(group_size)
138                .collect::<HashSet<_>>();
139
140            used_indices.borrow_mut().extend(route_group.iter().cloned());
141
142            create_partial_insertion_ctx(insertion_ctx, environment.clone(), route_group)
143        })
144        .chain(create_empty_insertion_ctxs(insertion_ctx, environment.clone()))
145        .collect();
146
147    Some(insertion_ctxs)
148}
149
150fn create_partial_insertion_ctx(
151    insertion_ctx: &InsertionContext,
152    environment: Arc<Environment>,
153    route_indices: HashSet<usize>,
154) -> (InsertionContext, HashSet<usize>) {
155    let solution = &insertion_ctx.solution;
156
157    let routes = route_indices.iter().map(|idx| solution.routes[*idx].deep_copy()).collect::<Vec<_>>();
158    let actors = routes.iter().map(|route_ctx| route_ctx.route().actor.clone()).collect::<HashSet<_>>();
159    let registry = solution.registry.deep_slice(|actor| actors.contains(actor));
160
161    (
162        InsertionContext {
163            problem: insertion_ctx.problem.clone(),
164            solution: SolutionContext {
165                // NOTE we need to handle empty route indices case differently
166                required: if route_indices.is_empty() { solution.required.clone() } else { Default::default() },
167                ignored: if route_indices.is_empty() { solution.ignored.clone() } else { Default::default() },
168                unassigned: if route_indices.is_empty() { solution.unassigned.clone() } else { Default::default() },
169                locked: if route_indices.is_empty() {
170                    let jobs = solution
171                        .routes
172                        .iter()
173                        .flat_map(|route_ctx| route_ctx.route().tour.jobs())
174                        .collect::<HashSet<_>>();
175                    solution.locked.iter().filter(|job| !jobs.contains(*job)).cloned().collect()
176                } else {
177                    let jobs =
178                        routes.iter().flat_map(|route_ctx| route_ctx.route().tour.jobs()).collect::<HashSet<_>>();
179                    solution.locked.iter().filter(|job| jobs.contains(*job)).cloned().collect()
180                },
181                routes,
182                registry,
183                state: Default::default(),
184            },
185            environment,
186        },
187        route_indices,
188    )
189}
190
191fn create_empty_insertion_ctxs(
192    insertion_ctx: &InsertionContext,
193    environment: Arc<Environment>,
194) -> impl Iterator<Item = (InsertionContext, HashSet<usize>)> {
195    let solution = &insertion_ctx.solution;
196
197    if solution.required.is_empty()
198        && solution.unassigned.is_empty()
199        && solution.ignored.is_empty()
200        && solution.locked.is_empty()
201    {
202        Either::Left(empty())
203    } else {
204        Either::Right(once((
205            InsertionContext {
206                problem: insertion_ctx.problem.clone(),
207                solution: SolutionContext {
208                    required: solution.required.clone(),
209                    ignored: solution.ignored.clone(),
210                    unassigned: solution.unassigned.clone(),
211                    locked: solution.locked.clone(),
212                    routes: Default::default(),
213                    registry: solution.registry.deep_copy(),
214                    state: Default::default(),
215                },
216                environment,
217            },
218            HashSet::default(),
219        )))
220    }
221}
222
223fn decompose_insertion_context(
224    refinement_ctx: &RefinementContext,
225    insertion_ctx: &InsertionContext,
226    max_routes_range: (i32, i32),
227    repeat: usize,
228    quota_limit: usize,
229) -> Option<Vec<(RefinementContext, HashSet<usize>)>> {
230    // NOTE make limit a bit higher than median
231    let median = refinement_ctx.statistics().speed.get_median();
232    let limit = median.map(|median| (median * repeat).max(quota_limit));
233    let environment = create_environment_with_custom_quota(limit, refinement_ctx.environment.as_ref());
234
235    create_multiple_insertion_contexts(insertion_ctx, environment.clone(), max_routes_range)
236        .map(|insertion_ctxs| {
237            insertion_ctxs
238                .into_iter()
239                .map(|(insertion_ctx, indices)| {
240                    (
241                        RefinementContext::new(
242                            refinement_ctx.problem.clone(),
243                            create_population(insertion_ctx),
244                            TelemetryMode::None,
245                            environment.clone(),
246                        ),
247                        indices,
248                    )
249                })
250                .collect::<Vec<_>>()
251        })
252        .and_then(|contexts| if contexts.len() > 1 { Some(contexts) } else { None })
253}
254
255fn merge_best(
256    decomposed: (RefinementContext, HashSet<usize>),
257    original_insertion_ctx: &InsertionContext,
258    accumulated: InsertionContext,
259) -> InsertionContext {
260    let (decomposed_ctx, route_indices) = decomposed;
261    let decomposed_insertion_ctx = decomposed_ctx.ranked().next().expect(GREEDY_ERROR);
262    let environment = original_insertion_ctx.environment.clone();
263
264    let (partial_insertion_ctx, _) = create_partial_insertion_ctx(original_insertion_ctx, environment, route_indices);
265    let goal = partial_insertion_ctx.problem.goal.as_ref();
266
267    let source_solution = if goal.total_order(decomposed_insertion_ctx, &partial_insertion_ctx) == Ordering::Less {
268        &decomposed_insertion_ctx.solution
269    } else {
270        &partial_insertion_ctx.solution
271    };
272
273    let mut accumulated = accumulated;
274    let dest_solution = &mut accumulated.solution;
275
276    // NOTE theoretically, we can avoid deep copy here, but this would require an extension in Population trait
277    dest_solution.routes.extend(source_solution.routes.iter().map(|route_ctx| route_ctx.deep_copy()));
278    dest_solution.ignored.extend(source_solution.ignored.iter().cloned());
279    dest_solution.required.extend(source_solution.required.iter().cloned());
280    dest_solution.locked.extend(source_solution.locked.iter().cloned());
281    dest_solution.unassigned.extend(source_solution.unassigned.iter().map(|(k, v)| (k.clone(), v.clone())));
282
283    source_solution.routes.iter().for_each(|route_ctx| {
284        assert!(dest_solution.registry.use_route(route_ctx), "attempt to use route more than once");
285    });
286
287    accumulated
288}