Skip to main content

solverforge_solver/builder/
selector.rs

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