1fn build_leaf_selector<S, V, DM, IDM>(
2 config: Option<&MoveSelectorConfig>,
3 model: &ModelContext<S, V, DM, IDM>,
4 random_seed: Option<u64>,
5) -> LeafSelector<S, V, DM, IDM>
6where
7 S: PlanningSolution + 'static,
8 V: Clone + PartialEq + Send + Sync + Debug + 'static,
9 DM: CrossEntityDistanceMeter<S> + Clone + 'static,
10 IDM: CrossEntityDistanceMeter<S> + Clone + 'static,
11{
12 let mut leaves = Vec::new();
13 match config {
14 None => unreachable!("default neighborhoods must be resolved before leaf selection"),
15 Some(MoveSelectorConfig::ChangeMoveSelector(_))
16 | Some(MoveSelectorConfig::SwapMoveSelector(_))
17 | Some(MoveSelectorConfig::NearbyChangeMoveSelector(_))
18 | Some(MoveSelectorConfig::NearbySwapMoveSelector(_))
19 | Some(MoveSelectorConfig::PillarChangeMoveSelector(_))
20 | Some(MoveSelectorConfig::PillarSwapMoveSelector(_))
21 | Some(MoveSelectorConfig::RuinRecreateMoveSelector(_)) => {
22 push_scalar_selector(config, model, random_seed, &mut leaves);
23 }
24 Some(MoveSelectorConfig::GroupedScalarMoveSelector(config)) => {
25 push_grouped_scalar_selector(config, model, &mut leaves);
26 }
27 Some(MoveSelectorConfig::ListChangeMoveSelector(_))
28 | Some(MoveSelectorConfig::NearbyListChangeMoveSelector(_))
29 | Some(MoveSelectorConfig::ListSwapMoveSelector(_))
30 | Some(MoveSelectorConfig::NearbyListSwapMoveSelector(_))
31 | Some(MoveSelectorConfig::SublistChangeMoveSelector(_))
32 | Some(MoveSelectorConfig::SublistSwapMoveSelector(_))
33 | Some(MoveSelectorConfig::ListReverseMoveSelector(_))
34 | Some(MoveSelectorConfig::KOptMoveSelector(_))
35 | Some(MoveSelectorConfig::ListRuinMoveSelector(_)) => {
36 push_list_selector(config, model, random_seed, &mut leaves);
37 }
38 Some(MoveSelectorConfig::UnionMoveSelector(union)) => {
39 for child in &union.selectors {
40 match child {
41 MoveSelectorConfig::GroupedScalarMoveSelector(config) => {
42 push_grouped_scalar_selector(config, model, &mut leaves);
43 continue;
44 }
45 MoveSelectorConfig::ConflictRepairMoveSelector(config) => {
46 push_conflict_repair_selector(config, model, &mut leaves);
47 continue;
48 }
49 MoveSelectorConfig::CompoundConflictRepairMoveSelector(config) => {
50 push_compound_conflict_repair_selector(config, model, &mut leaves);
51 continue;
52 }
53 _ => {}
54 }
55 match selector_family(child) {
56 SelectorFamily::Scalar => {
57 push_scalar_selector(Some(child), model, random_seed, &mut leaves);
58 }
59 SelectorFamily::List => {
60 push_list_selector(Some(child), model, random_seed, &mut leaves);
61 }
62 SelectorFamily::Mixed => {
63 let nested = build_leaf_selector(Some(child), model, random_seed);
64 leaves.extend(nested.into_selectors());
65 }
66 SelectorFamily::Unsupported => {
67 panic!(
68 "cartesian_product move selectors are not supported in the runtime selector graph"
69 );
70 }
71 }
72 }
73 }
74 Some(MoveSelectorConfig::LimitedNeighborhood(_)) => {
75 panic!("limited_neighborhood must be wrapped at the neighborhood level");
76 }
77 Some(MoveSelectorConfig::CartesianProductMoveSelector(_)) => {
78 panic!(
79 "cartesian_product move selectors are not supported in the runtime selector graph"
80 );
81 }
82 Some(MoveSelectorConfig::ConflictRepairMoveSelector(config)) => {
83 push_conflict_repair_selector(config, model, &mut leaves);
84 }
85 Some(MoveSelectorConfig::CompoundConflictRepairMoveSelector(config)) => {
86 push_compound_conflict_repair_selector(config, model, &mut leaves);
87 }
88 }
89 assert!(
90 !leaves.is_empty(),
91 "move selector configuration produced no neighborhoods \
92 (scalar_contexts_present={}, list_contexts_present={}, requested_selector_family={})",
93 model.scalar_variables().next().is_some(),
94 model.has_list_variables(),
95 selector_family_name(config),
96 );
97 let selection_order = match config {
98 Some(MoveSelectorConfig::UnionMoveSelector(union)) => union.selection_order,
99 _ => solverforge_config::UnionSelectionOrder::Sequential,
100 };
101 VecUnionSelector::with_selection_order(leaves, selection_order)
102}
103
104fn build_cartesian_child_selector<S, V, DM, IDM>(
105 config: &MoveSelectorConfig,
106 model: &ModelContext<S, V, DM, IDM>,
107 random_seed: Option<u64>,
108) -> CartesianChildSelector<S, V, DM, IDM>
109where
110 S: PlanningSolution + 'static,
111 V: Clone + PartialEq + Send + Sync + Debug + 'static,
112 DM: CrossEntityDistanceMeter<S> + Clone + Debug + 'static,
113 IDM: CrossEntityDistanceMeter<S> + Clone + Debug + 'static,
114{
115 match config {
116 MoveSelectorConfig::LimitedNeighborhood(limit) => CartesianChildSelector::Limited {
117 selector: build_leaf_selector(Some(limit.selector.as_ref()), model, random_seed),
118 selected_count_limit: limit.selected_count_limit,
119 },
120 MoveSelectorConfig::CartesianProductMoveSelector(_) => {
121 panic!("nested cartesian_product move selectors are not supported")
122 }
123 other => CartesianChildSelector::Flat(build_leaf_selector(Some(other), model, random_seed)),
124 }
125}
126
127fn wrap_neighborhood_composite<S, V>(
128 mov: SequentialCompositeMove<S, NeighborhoodMove<S, V>>,
129) -> NeighborhoodMove<S, V>
130where
131 S: PlanningSolution,
132 V: Clone + PartialEq + Send + Sync + Debug + 'static,
133{
134 NeighborhoodMove::Composite(mov)
135}
136
137fn default_scalar_change_selector() -> MoveSelectorConfig {
138 MoveSelectorConfig::ChangeMoveSelector(ChangeMoveConfig {
139 value_candidate_limit: None,
140 target: VariableTargetConfig::default(),
141 })
142}
143
144fn default_scalar_swap_selector() -> MoveSelectorConfig {
145 MoveSelectorConfig::SwapMoveSelector(solverforge_config::SwapMoveConfig {
146 target: VariableTargetConfig::default(),
147 })
148}
149
150fn default_nearby_list_change_selector() -> MoveSelectorConfig {
151 MoveSelectorConfig::NearbyListChangeMoveSelector(NearbyListChangeMoveConfig {
152 max_nearby: 20,
153 target: VariableTargetConfig::default(),
154 })
155}
156
157fn default_nearby_list_swap_selector() -> MoveSelectorConfig {
158 MoveSelectorConfig::NearbyListSwapMoveSelector(NearbyListSwapMoveConfig {
159 max_nearby: 20,
160 target: VariableTargetConfig::default(),
161 })
162}
163
164fn default_list_reverse_selector() -> MoveSelectorConfig {
165 MoveSelectorConfig::ListReverseMoveSelector(ListReverseMoveConfig {
166 target: VariableTargetConfig::default(),
167 })
168}
169
170fn collect_default_neighborhoods<S, V, DM, IDM>(
171 model: &ModelContext<S, V, DM, IDM>,
172 random_seed: Option<u64>,
173 out: &mut Vec<Neighborhood<S, V, DM, IDM>>,
174) where
175 S: PlanningSolution + 'static,
176 V: Clone + PartialEq + Send + Sync + Debug + 'static,
177 DM: CrossEntityDistanceMeter<S> + Clone + Debug + 'static,
178 IDM: CrossEntityDistanceMeter<S> + Clone + Debug + 'static,
179{
180 if model.has_list_variables() {
181 let list_change = default_nearby_list_change_selector();
182 out.push(Neighborhood::Flat(build_leaf_selector(
183 Some(&list_change),
184 model,
185 random_seed,
186 )));
187
188 let list_swap = default_nearby_list_swap_selector();
189 out.push(Neighborhood::Flat(build_leaf_selector(
190 Some(&list_swap),
191 model,
192 random_seed,
193 )));
194
195 let list_reverse = default_list_reverse_selector();
196 out.push(Neighborhood::Flat(build_leaf_selector(
197 Some(&list_reverse),
198 model,
199 random_seed,
200 )));
201 }
202
203 if model.scalar_variables().next().is_some() {
204 let scalar_change = default_scalar_change_selector();
205 out.push(Neighborhood::Flat(build_leaf_selector(
206 Some(&scalar_change),
207 model,
208 random_seed,
209 )));
210
211 let scalar_swap = default_scalar_swap_selector();
212 out.push(Neighborhood::Flat(build_leaf_selector(
213 Some(&scalar_swap),
214 model,
215 random_seed,
216 )));
217 }
218}
219
220fn collect_neighborhoods<S, V, DM, IDM>(
221 config: Option<&MoveSelectorConfig>,
222 model: &ModelContext<S, V, DM, IDM>,
223 random_seed: Option<u64>,
224 out: &mut Vec<Neighborhood<S, V, DM, IDM>>,
225) where
226 S: PlanningSolution + 'static,
227 V: Clone + PartialEq + Send + Sync + Debug + 'static,
228 DM: CrossEntityDistanceMeter<S> + Clone + Debug + 'static,
229 IDM: CrossEntityDistanceMeter<S> + Clone + Debug + 'static,
230{
231 match config {
232 None => collect_default_neighborhoods(model, random_seed, out),
233 Some(MoveSelectorConfig::UnionMoveSelector(union)) => {
234 for child in &union.selectors {
235 collect_neighborhoods(Some(child), model, random_seed, out);
236 }
237 }
238 Some(MoveSelectorConfig::LimitedNeighborhood(limit)) => {
239 let selector = build_leaf_selector(Some(limit.selector.as_ref()), model, random_seed);
240 out.push(Neighborhood::Limited {
241 selector,
242 selected_count_limit: limit.selected_count_limit,
243 });
244 }
245 Some(MoveSelectorConfig::ConflictRepairMoveSelector(config)) => {
246 let mut leaves = Vec::new();
247 push_conflict_repair_selector(config, model, &mut leaves);
248 out.push(Neighborhood::Flat(VecUnionSelector::new(leaves)));
249 }
250 Some(MoveSelectorConfig::CompoundConflictRepairMoveSelector(config)) => {
251 let mut leaves = Vec::new();
252 push_compound_conflict_repair_selector(config, model, &mut leaves);
253 out.push(Neighborhood::Flat(VecUnionSelector::new(leaves)));
254 }
255 Some(MoveSelectorConfig::GroupedScalarMoveSelector(config)) => {
256 let mut leaves = Vec::new();
257 push_grouped_scalar_selector(config, model, &mut leaves);
258 out.push(Neighborhood::Flat(VecUnionSelector::new(leaves)));
259 }
260 Some(MoveSelectorConfig::CartesianProductMoveSelector(cartesian)) => {
261 assert_eq!(
262 cartesian.selectors.len(),
263 2,
264 "cartesian_product move selector requires exactly two child selectors"
265 );
266 assert_cartesian_left_preview_safe(&cartesian.selectors[0]);
267 let left = build_cartesian_child_selector(&cartesian.selectors[0], model, random_seed);
268 let right = build_cartesian_child_selector(&cartesian.selectors[1], model, random_seed);
269 out.push(Neighborhood::Cartesian(
270 CartesianProductSelector::new(left, right, wrap_neighborhood_composite::<S, V>)
271 .with_require_hard_improvement(cartesian.require_hard_improvement),
272 ));
273 }
274 Some(other) => out.push(Neighborhood::Flat(build_leaf_selector(
275 Some(other),
276 model,
277 random_seed,
278 ))),
279 }
280}
281
282pub fn build_move_selector<S, V, DM, IDM>(
283 config: Option<&MoveSelectorConfig>,
284 model: &ModelContext<S, V, DM, IDM>,
285 random_seed: Option<u64>,
286) -> Selector<S, V, DM, IDM>
287where
288 S: PlanningSolution + 'static,
289 V: Clone + PartialEq + Send + Sync + Debug + 'static,
290 DM: CrossEntityDistanceMeter<S> + Clone + Debug + 'static,
291 IDM: CrossEntityDistanceMeter<S> + Clone + Debug + 'static,
292{
293 let mut neighborhoods = Vec::new();
294 collect_neighborhoods(config, model, random_seed, &mut neighborhoods);
295 assert!(
296 !neighborhoods.is_empty(),
297 "move selector configuration produced no neighborhoods \
298 (scalar_contexts_present={}, list_contexts_present={}, requested_selector_family={})",
299 model.scalar_variables().next().is_some(),
300 model.has_list_variables(),
301 selector_family_name(config),
302 );
303 let selection_order = match config {
304 Some(MoveSelectorConfig::UnionMoveSelector(union)) => union.selection_order,
305 _ => solverforge_config::UnionSelectionOrder::Sequential,
306 };
307 VecUnionSelector::with_selection_order(neighborhoods, selection_order)
308}
309
310pub fn build_local_search<S, V, DM, IDM>(
311 config: Option<&LocalSearchConfig>,
312 model: &ModelContext<S, V, DM, IDM>,
313 random_seed: Option<u64>,
314) -> LocalSearch<S, V, DM, IDM>
315where
316 S: PlanningSolution + 'static,
317 S::Score: Score + ParseableScore,
318 V: Clone + PartialEq + Send + Sync + Debug + 'static,
319 DM: CrossEntityDistanceMeter<S> + Clone + Debug + 'static,
320 IDM: CrossEntityDistanceMeter<S> + Clone + Debug + 'static,
321{
322 let acceptor = config
323 .and_then(|ls| ls.acceptor.as_ref())
324 .map(|cfg| AcceptorBuilder::build_with_seed::<S>(cfg, random_seed))
325 .unwrap_or_else(|| {
326 if model.has_list_variables() {
327 AnyAcceptor::LateAcceptance(
328 crate::phase::localsearch::LateAcceptanceAcceptor::<S>::new(400),
329 )
330 } else {
331 match random_seed {
332 Some(seed) => AnyAcceptor::SimulatedAnnealing(
333 SimulatedAnnealingAcceptor::auto_calibrate_with_seed(0.999985, seed),
334 ),
335 None => AnyAcceptor::SimulatedAnnealing(SimulatedAnnealingAcceptor::default()),
336 }
337 }
338 });
339 let forager = config
340 .and_then(|ls| ls.forager.as_ref())
341 .map(|cfg| ForagerBuilder::build::<S>(Some(cfg)))
342 .unwrap_or_else(|| {
343 let is_tabu = config
344 .and_then(|ls| ls.acceptor.as_ref())
345 .is_some_and(|acceptor| matches!(acceptor, AcceptorConfig::TabuSearch(_)));
346 if is_tabu {
347 AnyForager::BestScore(crate::phase::localsearch::BestScoreForager::new())
348 } else {
349 let accepted = if model.has_list_variables() { 4 } else { 1 };
350 AnyForager::AcceptedCount(AcceptedCountForager::new(accepted))
351 }
352 });
353 let move_selector = build_move_selector(
354 config.and_then(|ls| ls.move_selector.as_ref()),
355 model,
356 random_seed,
357 );
358 let step_limit = config
359 .and_then(|ls| ls.termination.as_ref())
360 .and_then(|termination| termination.step_count_limit);
361
362 LocalSearchPhase::new(move_selector, acceptor, forager, step_limit)
363}
364
365pub fn build_vnd<S, V, DM, IDM>(
366 config: &VndConfig,
367 model: &ModelContext<S, V, DM, IDM>,
368 random_seed: Option<u64>,
369) -> Vnd<S, V, DM, IDM>
370where
371 S: PlanningSolution + 'static,
372 S::Score: Score + ParseableScore,
373 V: Clone + PartialEq + Send + Sync + Debug + 'static,
374 DM: CrossEntityDistanceMeter<S> + Clone + Debug + 'static,
375 IDM: CrossEntityDistanceMeter<S> + Clone + Debug + 'static,
376{
377 let neighborhoods = if config.neighborhoods.is_empty() {
378 let mut neighborhoods = Vec::new();
379 collect_neighborhoods(None, model, random_seed, &mut neighborhoods);
380 neighborhoods
381 } else {
382 config
383 .neighborhoods
384 .iter()
385 .flat_map(|selector| {
386 let mut neighborhoods = Vec::new();
387 collect_neighborhoods(Some(selector), model, random_seed, &mut neighborhoods);
388 neighborhoods
389 })
390 .collect()
391 };
392
393 DynamicVndPhase::new(neighborhoods)
394}
395
396#[cfg(test)]
397mod tests;