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
16pub 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 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 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 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 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 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 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 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 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}