1use std::fmt::{self, Debug};
2
3use solverforge_config::{
4 ChangeMoveConfig, ListReverseMoveConfig, LocalSearchConfig, MoveSelectorConfig,
5 NearbyListChangeMoveConfig, NearbyListSwapMoveConfig, VariableTargetConfig, VndConfig,
6};
7use solverforge_core::domain::PlanningSolution;
8use solverforge_core::score::{ParseableScore, Score};
9
10use crate::heuristic::r#move::{EitherMove, ListMoveImpl, Move, MoveArena};
11use crate::heuristic::selector::decorator::{SelectedCountLimitMoveSelector, VecUnionSelector};
12use crate::heuristic::selector::move_selector::MoveSelector;
13use crate::heuristic::selector::nearby_list_change::CrossEntityDistanceMeter;
14use crate::phase::dynamic_vnd::DynamicVndPhase;
15use crate::phase::localsearch::{
16 AcceptedCountForager, LocalSearchPhase, SimulatedAnnealingAcceptor,
17};
18
19use super::acceptor::{AcceptorBuilder, AnyAcceptor};
20use super::context::ModelContext;
21use super::forager::{AnyForager, ForagerBuilder};
22use super::list_selector::{ListLeafSelector, ListMoveSelectorBuilder};
23use super::standard_selector::{build_standard_move_selector, StandardLeafSelector};
24
25type LeafSelector<S, V, DM, IDM> =
26 VecUnionSelector<S, NeighborhoodMove<S, V>, NeighborhoodLeaf<S, V, DM, IDM>>;
27
28type LimitedNeighborhood<S, V, DM, IDM> =
29 SelectedCountLimitMoveSelector<S, NeighborhoodMove<S, V>, LeafSelector<S, V, DM, IDM>>;
30
31pub enum NeighborhoodMove<S, V> {
32 Scalar(EitherMove<S, usize>),
33 List(ListMoveImpl<S, V>),
34}
35
36impl<S, V> Debug for NeighborhoodMove<S, V>
37where
38 S: PlanningSolution + 'static,
39 V: Clone + PartialEq + Send + Sync + Debug + 'static,
40{
41 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42 match self {
43 Self::Scalar(m) => write!(f, "NeighborhoodMove::Scalar({m:?})"),
44 Self::List(m) => write!(f, "NeighborhoodMove::List({m:?})"),
45 }
46 }
47}
48
49impl<S, V> Move<S> for NeighborhoodMove<S, V>
50where
51 S: PlanningSolution + 'static,
52 V: Clone + PartialEq + Send + Sync + Debug + 'static,
53{
54 fn is_doable<D: solverforge_scoring::Director<S>>(&self, score_director: &D) -> bool {
55 match self {
56 Self::Scalar(m) => m.is_doable(score_director),
57 Self::List(m) => m.is_doable(score_director),
58 }
59 }
60
61 fn do_move<D: solverforge_scoring::Director<S>>(&self, score_director: &mut D) {
62 match self {
63 Self::Scalar(m) => m.do_move(score_director),
64 Self::List(m) => m.do_move(score_director),
65 }
66 }
67
68 fn descriptor_index(&self) -> usize {
69 match self {
70 Self::Scalar(m) => m.descriptor_index(),
71 Self::List(m) => m.descriptor_index(),
72 }
73 }
74
75 fn entity_indices(&self) -> &[usize] {
76 match self {
77 Self::Scalar(m) => m.entity_indices(),
78 Self::List(m) => m.entity_indices(),
79 }
80 }
81
82 fn variable_name(&self) -> &str {
83 match self {
84 Self::Scalar(m) => m.variable_name(),
85 Self::List(m) => m.variable_name(),
86 }
87 }
88}
89
90pub enum NeighborhoodLeaf<S, V, DM, IDM>
91where
92 S: PlanningSolution + 'static,
93 V: Clone + PartialEq + Send + Sync + Debug + 'static,
94 DM: CrossEntityDistanceMeter<S> + Clone,
95 IDM: CrossEntityDistanceMeter<S> + Clone,
96{
97 Scalar(StandardLeafSelector<S>),
98 List(ListLeafSelector<S, V, DM, IDM>),
99}
100
101impl<S, V, DM, IDM> Debug for NeighborhoodLeaf<S, V, DM, IDM>
102where
103 S: PlanningSolution + 'static,
104 V: Clone + PartialEq + Send + Sync + Debug + 'static,
105 DM: CrossEntityDistanceMeter<S> + Clone + Debug,
106 IDM: CrossEntityDistanceMeter<S> + Clone + Debug,
107{
108 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109 match self {
110 Self::Scalar(selector) => write!(f, "NeighborhoodLeaf::Scalar({selector:?})"),
111 Self::List(selector) => write!(f, "NeighborhoodLeaf::List({selector:?})"),
112 }
113 }
114}
115
116impl<S, V, DM, IDM> MoveSelector<S, NeighborhoodMove<S, V>> for NeighborhoodLeaf<S, V, DM, IDM>
117where
118 S: PlanningSolution + 'static,
119 V: Clone + PartialEq + Send + Sync + Debug + 'static,
120 DM: CrossEntityDistanceMeter<S> + Clone + Debug + 'static,
121 IDM: CrossEntityDistanceMeter<S> + Clone + Debug + 'static,
122{
123 fn open_cursor<'a, D: solverforge_scoring::Director<S>>(
124 &'a self,
125 score_director: &D,
126 ) -> impl Iterator<Item = NeighborhoodMove<S, V>> + 'a {
127 enum LeafIter<A, B> {
128 Scalar(A),
129 List(B),
130 }
131
132 impl<T, A, B> Iterator for LeafIter<A, B>
133 where
134 A: Iterator<Item = T>,
135 B: Iterator<Item = T>,
136 {
137 type Item = T;
138
139 fn next(&mut self) -> Option<Self::Item> {
140 match self {
141 Self::Scalar(iter) => iter.next(),
142 Self::List(iter) => iter.next(),
143 }
144 }
145 }
146
147 match self {
148 Self::Scalar(selector) => LeafIter::Scalar(
149 selector
150 .open_cursor(score_director)
151 .map(NeighborhoodMove::Scalar),
152 ),
153 Self::List(selector) => LeafIter::List(
154 selector
155 .open_cursor(score_director)
156 .map(NeighborhoodMove::List),
157 ),
158 }
159 }
160
161 fn size<D: solverforge_scoring::Director<S>>(&self, score_director: &D) -> usize {
162 match self {
163 Self::Scalar(selector) => selector.size(score_director),
164 Self::List(selector) => selector.size(score_director),
165 }
166 }
167
168 fn append_moves<D: solverforge_scoring::Director<S>>(
169 &self,
170 score_director: &D,
171 arena: &mut MoveArena<NeighborhoodMove<S, V>>,
172 ) {
173 match self {
174 Self::Scalar(selector) => {
175 arena.extend(
176 selector
177 .open_cursor(score_director)
178 .map(NeighborhoodMove::Scalar),
179 );
180 }
181 Self::List(selector) => {
182 arena.extend(
183 selector
184 .open_cursor(score_director)
185 .map(NeighborhoodMove::List),
186 );
187 }
188 }
189 }
190}
191
192pub enum Neighborhood<S, V, DM, IDM>
193where
194 S: PlanningSolution + 'static,
195 V: Clone + PartialEq + Send + Sync + Debug + 'static,
196 DM: CrossEntityDistanceMeter<S> + Clone,
197 IDM: CrossEntityDistanceMeter<S> + Clone,
198{
199 Flat(LeafSelector<S, V, DM, IDM>),
200 Limited(LimitedNeighborhood<S, V, DM, IDM>),
201}
202
203impl<S, V, DM, IDM> Debug for Neighborhood<S, V, DM, IDM>
204where
205 S: PlanningSolution + 'static,
206 V: Clone + PartialEq + Send + Sync + Debug + 'static,
207 DM: CrossEntityDistanceMeter<S> + Clone + Debug,
208 IDM: CrossEntityDistanceMeter<S> + Clone + Debug,
209{
210 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211 match self {
212 Self::Flat(selector) => write!(f, "Neighborhood::Flat({selector:?})"),
213 Self::Limited(selector) => write!(f, "Neighborhood::Limited({selector:?})"),
214 }
215 }
216}
217
218impl<S, V, DM, IDM> MoveSelector<S, NeighborhoodMove<S, V>> for Neighborhood<S, V, DM, IDM>
219where
220 S: PlanningSolution + 'static,
221 V: Clone + PartialEq + Send + Sync + Debug + 'static,
222 DM: CrossEntityDistanceMeter<S> + Clone + Debug + 'static,
223 IDM: CrossEntityDistanceMeter<S> + Clone + Debug + 'static,
224{
225 fn open_cursor<'a, D: solverforge_scoring::Director<S>>(
226 &'a self,
227 score_director: &D,
228 ) -> impl Iterator<Item = NeighborhoodMove<S, V>> + 'a {
229 enum NeighborhoodIter<A, B> {
230 Flat(A),
231 Limited(B),
232 }
233
234 impl<T, A, B> Iterator for NeighborhoodIter<A, B>
235 where
236 A: Iterator<Item = T>,
237 B: Iterator<Item = T>,
238 {
239 type Item = T;
240
241 fn next(&mut self) -> Option<Self::Item> {
242 match self {
243 Self::Flat(iter) => iter.next(),
244 Self::Limited(iter) => iter.next(),
245 }
246 }
247 }
248
249 match self {
250 Self::Flat(selector) => NeighborhoodIter::Flat(selector.open_cursor(score_director)),
251 Self::Limited(selector) => {
252 NeighborhoodIter::Limited(selector.open_cursor(score_director))
253 }
254 }
255 }
256
257 fn size<D: solverforge_scoring::Director<S>>(&self, score_director: &D) -> usize {
258 match self {
259 Self::Flat(selector) => selector.size(score_director),
260 Self::Limited(selector) => selector.size(score_director),
261 }
262 }
263}
264
265pub type Selector<S, V, DM, IDM> =
266 VecUnionSelector<S, NeighborhoodMove<S, V>, Neighborhood<S, V, DM, IDM>>;
267
268pub type LocalSearch<S, V, DM, IDM> = LocalSearchPhase<
269 S,
270 NeighborhoodMove<S, V>,
271 Selector<S, V, DM, IDM>,
272 AnyAcceptor<S>,
273 AnyForager<S>,
274>;
275
276pub type Vnd<S, V, DM, IDM> =
277 DynamicVndPhase<S, NeighborhoodMove<S, V>, Neighborhood<S, V, DM, IDM>>;
278
279#[derive(Clone, Copy, Debug, PartialEq, Eq)]
280enum SelectorFamily {
281 Scalar,
282 List,
283 Mixed,
284 Unsupported,
285}
286
287fn selector_family(config: &MoveSelectorConfig) -> SelectorFamily {
288 match config {
289 MoveSelectorConfig::ChangeMoveSelector(_) | MoveSelectorConfig::SwapMoveSelector(_) => {
290 SelectorFamily::Scalar
291 }
292 MoveSelectorConfig::ListChangeMoveSelector(_)
293 | MoveSelectorConfig::NearbyListChangeMoveSelector(_)
294 | MoveSelectorConfig::ListSwapMoveSelector(_)
295 | MoveSelectorConfig::NearbyListSwapMoveSelector(_)
296 | MoveSelectorConfig::SubListChangeMoveSelector(_)
297 | MoveSelectorConfig::SubListSwapMoveSelector(_)
298 | MoveSelectorConfig::ListReverseMoveSelector(_)
299 | MoveSelectorConfig::KOptMoveSelector(_)
300 | MoveSelectorConfig::ListRuinMoveSelector(_) => SelectorFamily::List,
301 MoveSelectorConfig::SelectedCountLimitMoveSelector(limit) => {
302 selector_family(limit.selector.as_ref())
303 }
304 MoveSelectorConfig::UnionMoveSelector(union) => {
305 let mut family = None;
306 for child in &union.selectors {
307 let child_family = selector_family(child);
308 if child_family == SelectorFamily::Unsupported {
309 return SelectorFamily::Unsupported;
310 }
311 family = Some(match family {
312 None => child_family,
313 Some(current) if current == child_family => current,
314 Some(_) => SelectorFamily::Mixed,
315 });
316 if family == Some(SelectorFamily::Mixed) {
317 return SelectorFamily::Mixed;
318 }
319 }
320 family.unwrap_or(SelectorFamily::Mixed)
321 }
322 MoveSelectorConfig::CartesianProductMoveSelector(_) => SelectorFamily::Unsupported,
323 }
324}
325
326fn push_scalar_selector<S, V, DM, IDM>(
327 config: Option<&MoveSelectorConfig>,
328 model: &ModelContext<S, V, DM, IDM>,
329 out: &mut Vec<NeighborhoodLeaf<S, V, DM, IDM>>,
330) where
331 S: PlanningSolution + 'static,
332 V: Clone + PartialEq + Send + Sync + Debug + 'static,
333 DM: CrossEntityDistanceMeter<S> + Clone + 'static,
334 IDM: CrossEntityDistanceMeter<S> + Clone + 'static,
335{
336 let scalar_variables: Vec<_> = model.scalar_variables().copied().collect();
337 if scalar_variables.is_empty() {
338 return;
339 }
340 let selector = build_standard_move_selector(config, &scalar_variables);
341 out.extend(
342 selector
343 .into_selectors()
344 .into_iter()
345 .map(NeighborhoodLeaf::Scalar),
346 );
347}
348
349fn push_list_selector<S, V, DM, IDM>(
350 config: Option<&MoveSelectorConfig>,
351 model: &ModelContext<S, V, DM, IDM>,
352 random_seed: Option<u64>,
353 out: &mut Vec<NeighborhoodLeaf<S, V, DM, IDM>>,
354) where
355 S: PlanningSolution + 'static,
356 V: Clone + PartialEq + Send + Sync + Debug + 'static,
357 DM: CrossEntityDistanceMeter<S> + Clone + 'static,
358 IDM: CrossEntityDistanceMeter<S> + Clone + 'static,
359{
360 for variable in model.list_variables() {
361 let selector = ListMoveSelectorBuilder::build(config, variable, random_seed);
362 out.extend(
363 selector
364 .into_selectors()
365 .into_iter()
366 .map(NeighborhoodLeaf::List),
367 );
368 }
369}
370
371fn build_leaf_selector<S, V, DM, IDM>(
372 config: Option<&MoveSelectorConfig>,
373 model: &ModelContext<S, V, DM, IDM>,
374 random_seed: Option<u64>,
375) -> LeafSelector<S, V, DM, IDM>
376where
377 S: PlanningSolution + 'static,
378 V: Clone + PartialEq + Send + Sync + Debug + 'static,
379 DM: CrossEntityDistanceMeter<S> + Clone + 'static,
380 IDM: CrossEntityDistanceMeter<S> + Clone + 'static,
381{
382 let mut leaves = Vec::new();
383 match config {
384 None => unreachable!("default neighborhoods must be resolved before leaf selection"),
385 Some(MoveSelectorConfig::ChangeMoveSelector(_))
386 | Some(MoveSelectorConfig::SwapMoveSelector(_)) => {
387 push_scalar_selector(config, model, &mut leaves);
388 }
389 Some(MoveSelectorConfig::ListChangeMoveSelector(_))
390 | Some(MoveSelectorConfig::NearbyListChangeMoveSelector(_))
391 | Some(MoveSelectorConfig::ListSwapMoveSelector(_))
392 | Some(MoveSelectorConfig::NearbyListSwapMoveSelector(_))
393 | Some(MoveSelectorConfig::SubListChangeMoveSelector(_))
394 | Some(MoveSelectorConfig::SubListSwapMoveSelector(_))
395 | Some(MoveSelectorConfig::ListReverseMoveSelector(_))
396 | Some(MoveSelectorConfig::KOptMoveSelector(_))
397 | Some(MoveSelectorConfig::ListRuinMoveSelector(_)) => {
398 push_list_selector(config, model, random_seed, &mut leaves);
399 }
400 Some(MoveSelectorConfig::UnionMoveSelector(union)) => {
401 for child in &union.selectors {
402 match selector_family(child) {
403 SelectorFamily::Scalar => {
404 push_scalar_selector(Some(child), model, &mut leaves);
405 }
406 SelectorFamily::List => {
407 push_list_selector(Some(child), model, random_seed, &mut leaves);
408 }
409 SelectorFamily::Mixed => {
410 let nested = build_leaf_selector(Some(child), model, random_seed);
411 leaves.extend(nested.into_selectors());
412 }
413 SelectorFamily::Unsupported => {
414 panic!(
415 "cartesian_product move selectors are not supported in the runtime selector graph"
416 );
417 }
418 }
419 }
420 }
421 Some(MoveSelectorConfig::SelectedCountLimitMoveSelector(_)) => {
422 panic!("selected_count_limit_move_selector must be wrapped at the neighborhood level");
423 }
424 Some(MoveSelectorConfig::CartesianProductMoveSelector(_)) => {
425 panic!(
426 "cartesian_product move selectors are not supported in the runtime selector graph"
427 );
428 }
429 }
430 assert!(
431 !leaves.is_empty(),
432 "move selector configuration produced no neighborhoods",
433 );
434 VecUnionSelector::new(leaves)
435}
436
437fn default_scalar_change_selector() -> MoveSelectorConfig {
438 MoveSelectorConfig::ChangeMoveSelector(ChangeMoveConfig {
439 target: VariableTargetConfig::default(),
440 })
441}
442
443fn default_nearby_list_change_selector() -> MoveSelectorConfig {
444 MoveSelectorConfig::NearbyListChangeMoveSelector(NearbyListChangeMoveConfig {
445 max_nearby: 20,
446 target: VariableTargetConfig::default(),
447 })
448}
449
450fn default_nearby_list_swap_selector() -> MoveSelectorConfig {
451 MoveSelectorConfig::NearbyListSwapMoveSelector(NearbyListSwapMoveConfig {
452 max_nearby: 20,
453 target: VariableTargetConfig::default(),
454 })
455}
456
457fn default_list_reverse_selector() -> MoveSelectorConfig {
458 MoveSelectorConfig::ListReverseMoveSelector(ListReverseMoveConfig {
459 target: VariableTargetConfig::default(),
460 })
461}
462
463fn collect_default_neighborhoods<S, V, DM, IDM>(
464 model: &ModelContext<S, V, DM, IDM>,
465 random_seed: Option<u64>,
466 out: &mut Vec<Neighborhood<S, V, DM, IDM>>,
467) where
468 S: PlanningSolution + 'static,
469 V: Clone + PartialEq + Send + Sync + Debug + 'static,
470 DM: CrossEntityDistanceMeter<S> + Clone + Debug + 'static,
471 IDM: CrossEntityDistanceMeter<S> + Clone + Debug + 'static,
472{
473 if model.has_list_variables() {
474 let list_change = default_nearby_list_change_selector();
475 out.push(Neighborhood::Flat(build_leaf_selector(
476 Some(&list_change),
477 model,
478 random_seed,
479 )));
480
481 let list_swap = default_nearby_list_swap_selector();
482 out.push(Neighborhood::Flat(build_leaf_selector(
483 Some(&list_swap),
484 model,
485 random_seed,
486 )));
487
488 let list_reverse = default_list_reverse_selector();
489 out.push(Neighborhood::Flat(build_leaf_selector(
490 Some(&list_reverse),
491 model,
492 random_seed,
493 )));
494 }
495
496 if model.scalar_variables().next().is_some() {
497 let scalar_change = default_scalar_change_selector();
498 out.push(Neighborhood::Flat(build_leaf_selector(
499 Some(&scalar_change),
500 model,
501 random_seed,
502 )));
503 }
504}
505
506fn collect_neighborhoods<S, V, DM, IDM>(
507 config: Option<&MoveSelectorConfig>,
508 model: &ModelContext<S, V, DM, IDM>,
509 random_seed: Option<u64>,
510 out: &mut Vec<Neighborhood<S, V, DM, IDM>>,
511) where
512 S: PlanningSolution + 'static,
513 V: Clone + PartialEq + Send + Sync + Debug + 'static,
514 DM: CrossEntityDistanceMeter<S> + Clone + Debug + 'static,
515 IDM: CrossEntityDistanceMeter<S> + Clone + Debug + 'static,
516{
517 match config {
518 None => collect_default_neighborhoods(model, random_seed, out),
519 Some(MoveSelectorConfig::UnionMoveSelector(union)) => {
520 for child in &union.selectors {
521 collect_neighborhoods(Some(child), model, random_seed, out);
522 }
523 }
524 Some(MoveSelectorConfig::SelectedCountLimitMoveSelector(limit)) => {
525 let selector = build_leaf_selector(Some(limit.selector.as_ref()), model, random_seed);
526 out.push(Neighborhood::Limited(SelectedCountLimitMoveSelector::new(
527 selector,
528 limit.selected_count_limit,
529 )));
530 }
531 Some(MoveSelectorConfig::CartesianProductMoveSelector(_)) => {
532 panic!(
533 "cartesian_product move selectors are not supported in the runtime selector graph"
534 );
535 }
536 Some(other) => out.push(Neighborhood::Flat(build_leaf_selector(
537 Some(other),
538 model,
539 random_seed,
540 ))),
541 }
542}
543
544pub fn build_move_selector<S, V, DM, IDM>(
545 config: Option<&MoveSelectorConfig>,
546 model: &ModelContext<S, V, DM, IDM>,
547 random_seed: Option<u64>,
548) -> Selector<S, V, DM, IDM>
549where
550 S: PlanningSolution + 'static,
551 V: Clone + PartialEq + Send + Sync + Debug + 'static,
552 DM: CrossEntityDistanceMeter<S> + Clone + Debug + 'static,
553 IDM: CrossEntityDistanceMeter<S> + Clone + Debug + 'static,
554{
555 let mut neighborhoods = Vec::new();
556 collect_neighborhoods(config, model, random_seed, &mut neighborhoods);
557 assert!(
558 !neighborhoods.is_empty(),
559 "move selector configuration produced no neighborhoods",
560 );
561 VecUnionSelector::new(neighborhoods)
562}
563
564pub fn build_local_search<S, V, DM, IDM>(
565 config: Option<&LocalSearchConfig>,
566 model: &ModelContext<S, V, DM, IDM>,
567 random_seed: Option<u64>,
568) -> LocalSearch<S, V, DM, IDM>
569where
570 S: PlanningSolution + 'static,
571 S::Score: Score + ParseableScore,
572 V: Clone + PartialEq + Send + Sync + Debug + 'static,
573 DM: CrossEntityDistanceMeter<S> + Clone + Debug + 'static,
574 IDM: CrossEntityDistanceMeter<S> + Clone + Debug + 'static,
575{
576 let acceptor = config
577 .and_then(|ls| ls.acceptor.as_ref())
578 .map(|cfg| AcceptorBuilder::build_with_seed::<S>(cfg, random_seed))
579 .unwrap_or_else(|| {
580 if model.has_list_variables() {
581 AnyAcceptor::LateAcceptance(
582 crate::phase::localsearch::LateAcceptanceAcceptor::<S>::new(400),
583 )
584 } else {
585 match random_seed {
586 Some(seed) => AnyAcceptor::SimulatedAnnealing(
587 SimulatedAnnealingAcceptor::auto_calibrate_with_seed(0.999985, seed),
588 ),
589 None => AnyAcceptor::SimulatedAnnealing(SimulatedAnnealingAcceptor::default()),
590 }
591 }
592 });
593 let forager = config
594 .and_then(|ls| ls.forager.as_ref())
595 .map(|cfg| ForagerBuilder::build::<S>(Some(cfg)))
596 .unwrap_or_else(|| {
597 let accepted = if model.has_list_variables() { 4 } else { 1 };
598 AnyForager::AcceptedCount(AcceptedCountForager::new(accepted))
599 });
600 let move_selector = build_move_selector(
601 config.and_then(|ls| ls.move_selector.as_ref()),
602 model,
603 random_seed,
604 );
605
606 LocalSearchPhase::new(move_selector, acceptor, forager, None)
607}
608
609pub fn build_vnd<S, V, DM, IDM>(
610 config: &VndConfig,
611 model: &ModelContext<S, V, DM, IDM>,
612 random_seed: Option<u64>,
613) -> Vnd<S, V, DM, IDM>
614where
615 S: PlanningSolution + 'static,
616 S::Score: Score + ParseableScore,
617 V: Clone + PartialEq + Send + Sync + Debug + 'static,
618 DM: CrossEntityDistanceMeter<S> + Clone + Debug + 'static,
619 IDM: CrossEntityDistanceMeter<S> + Clone + Debug + 'static,
620{
621 let neighborhoods = if config.neighborhoods.is_empty() {
622 let mut neighborhoods = Vec::new();
623 collect_neighborhoods(None, model, random_seed, &mut neighborhoods);
624 neighborhoods
625 } else {
626 config
627 .neighborhoods
628 .iter()
629 .flat_map(|selector| {
630 let mut neighborhoods = Vec::new();
631 collect_neighborhoods(Some(selector), model, random_seed, &mut neighborhoods);
632 neighborhoods
633 })
634 .collect()
635 };
636
637 DynamicVndPhase::new(neighborhoods)
638}
639
640#[cfg(test)]
641#[path = "selector_tests.rs"]
642mod tests;