Skip to main content

solverforge_solver/
descriptor_standard.rs

1use std::any::Any;
2use std::fmt::{self, Debug};
3use std::marker::PhantomData;
4
5use solverforge_config::{
6    ConstructionHeuristicConfig, ConstructionHeuristicType, MoveSelectorConfig,
7};
8use solverforge_core::domain::{
9    SolutionDescriptor, UsizeEntityValueProvider, UsizeGetter, UsizeSetter, ValueRangeType,
10};
11use solverforge_scoring::Director;
12
13use crate::heuristic::r#move::Move;
14use crate::heuristic::selector::decorator::VecUnionSelector;
15use crate::heuristic::selector::move_selector::MoveSelector;
16use crate::heuristic::selector::EntityReference;
17use crate::phase::construction::{
18    BestFitForager, ConstructionHeuristicPhase, EntityPlacer, FirstFitForager, Placement,
19};
20use crate::scope::{ProgressCallback, SolverScope};
21
22#[derive(Clone)]
23struct VariableBinding {
24    descriptor_index: usize,
25    entity_type_name: &'static str,
26    variable_name: &'static str,
27    getter: UsizeGetter,
28    setter: UsizeSetter,
29    provider: Option<UsizeEntityValueProvider>,
30    range_type: ValueRangeType,
31}
32
33impl Debug for VariableBinding {
34    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35        f.debug_struct("VariableBinding")
36            .field("descriptor_index", &self.descriptor_index)
37            .field("entity_type_name", &self.entity_type_name)
38            .field("variable_name", &self.variable_name)
39            .field("range_type", &self.range_type)
40            .finish()
41    }
42}
43
44impl VariableBinding {
45    fn values_for_entity(&self, entity: &dyn Any) -> Vec<usize> {
46        match (&self.provider, &self.range_type) {
47            (Some(provider), _) => provider(entity),
48            (_, ValueRangeType::CountableRange { from, to }) => {
49                let start = *from;
50                let end = *to;
51                (start..end)
52                    .filter_map(|value| usize::try_from(value).ok())
53                    .collect()
54            }
55            _ => Vec::new(),
56        }
57    }
58}
59
60#[derive(Clone)]
61pub struct DescriptorChangeMove<S> {
62    binding: VariableBinding,
63    entity_index: usize,
64    to_value: Option<usize>,
65    solution_descriptor: SolutionDescriptor,
66    _phantom: PhantomData<fn() -> S>,
67}
68
69impl<S> Debug for DescriptorChangeMove<S> {
70    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71        f.debug_struct("DescriptorChangeMove")
72            .field("descriptor_index", &self.binding.descriptor_index)
73            .field("entity_index", &self.entity_index)
74            .field("variable_name", &self.binding.variable_name)
75            .field("to_value", &self.to_value)
76            .finish()
77    }
78}
79
80impl<S: 'static> DescriptorChangeMove<S> {
81    fn new(
82        binding: VariableBinding,
83        entity_index: usize,
84        to_value: Option<usize>,
85        solution_descriptor: SolutionDescriptor,
86    ) -> Self {
87        Self {
88            binding,
89            entity_index,
90            to_value,
91            solution_descriptor,
92            _phantom: PhantomData,
93        }
94    }
95
96    fn current_value(&self, solution: &S) -> Option<usize> {
97        let entity = self
98            .solution_descriptor
99            .get_entity(
100                solution as &dyn Any,
101                self.binding.descriptor_index,
102                self.entity_index,
103            )
104            .expect("entity lookup failed for descriptor change move");
105        (self.binding.getter)(entity)
106    }
107}
108
109impl<S> Move<S> for DescriptorChangeMove<S>
110where
111    S: solverforge_core::domain::PlanningSolution + 'static,
112{
113    fn is_doable<D: Director<S>>(&self, score_director: &D) -> bool {
114        self.current_value(score_director.working_solution()) != self.to_value
115    }
116
117    fn do_move<D: Director<S>>(&self, score_director: &mut D) {
118        let old_value = self.current_value(score_director.working_solution());
119        score_director.before_variable_changed(self.binding.descriptor_index, self.entity_index);
120        let entity = self
121            .solution_descriptor
122            .get_entity_mut(
123                score_director.working_solution_mut() as &mut dyn Any,
124                self.binding.descriptor_index,
125                self.entity_index,
126            )
127            .expect("entity lookup failed for descriptor change move");
128        (self.binding.setter)(entity, self.to_value);
129        score_director.after_variable_changed(self.binding.descriptor_index, self.entity_index);
130
131        let descriptor = self.solution_descriptor.clone();
132        let binding = self.binding.clone();
133        let entity_index = self.entity_index;
134        score_director.register_undo(Box::new(move |solution: &mut S| {
135            let entity = descriptor
136                .get_entity_mut(
137                    solution as &mut dyn Any,
138                    binding.descriptor_index,
139                    entity_index,
140                )
141                .expect("entity lookup failed for descriptor change undo");
142            (binding.setter)(entity, old_value);
143        }));
144    }
145
146    fn descriptor_index(&self) -> usize {
147        self.binding.descriptor_index
148    }
149
150    fn entity_indices(&self) -> &[usize] {
151        std::slice::from_ref(&self.entity_index)
152    }
153
154    fn variable_name(&self) -> &str {
155        self.binding.variable_name
156    }
157}
158
159#[derive(Clone)]
160pub struct DescriptorSwapMove<S> {
161    binding: VariableBinding,
162    left_entity_index: usize,
163    right_entity_index: usize,
164    indices: [usize; 2],
165    solution_descriptor: SolutionDescriptor,
166    _phantom: PhantomData<fn() -> S>,
167}
168
169impl<S> Debug for DescriptorSwapMove<S> {
170    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
171        f.debug_struct("DescriptorSwapMove")
172            .field("descriptor_index", &self.binding.descriptor_index)
173            .field("left_entity_index", &self.left_entity_index)
174            .field("right_entity_index", &self.right_entity_index)
175            .field("variable_name", &self.binding.variable_name)
176            .finish()
177    }
178}
179
180impl<S: 'static> DescriptorSwapMove<S> {
181    fn new(
182        binding: VariableBinding,
183        left_entity_index: usize,
184        right_entity_index: usize,
185        solution_descriptor: SolutionDescriptor,
186    ) -> Self {
187        Self {
188            binding,
189            left_entity_index,
190            right_entity_index,
191            indices: [left_entity_index, right_entity_index],
192            solution_descriptor,
193            _phantom: PhantomData,
194        }
195    }
196
197    fn current_value(&self, solution: &S, entity_index: usize) -> Option<usize> {
198        let entity = self
199            .solution_descriptor
200            .get_entity(
201                solution as &dyn Any,
202                self.binding.descriptor_index,
203                entity_index,
204            )
205            .expect("entity lookup failed for descriptor swap move");
206        (self.binding.getter)(entity)
207    }
208}
209
210impl<S> Move<S> for DescriptorSwapMove<S>
211where
212    S: solverforge_core::domain::PlanningSolution + 'static,
213{
214    fn is_doable<D: Director<S>>(&self, score_director: &D) -> bool {
215        self.left_entity_index != self.right_entity_index
216            && self.current_value(score_director.working_solution(), self.left_entity_index)
217                != self.current_value(score_director.working_solution(), self.right_entity_index)
218    }
219
220    fn do_move<D: Director<S>>(&self, score_director: &mut D) {
221        let left_value =
222            self.current_value(score_director.working_solution(), self.left_entity_index);
223        let right_value =
224            self.current_value(score_director.working_solution(), self.right_entity_index);
225
226        score_director
227            .before_variable_changed(self.binding.descriptor_index, self.left_entity_index);
228        score_director
229            .before_variable_changed(self.binding.descriptor_index, self.right_entity_index);
230
231        let left_entity = self
232            .solution_descriptor
233            .get_entity_mut(
234                score_director.working_solution_mut() as &mut dyn Any,
235                self.binding.descriptor_index,
236                self.left_entity_index,
237            )
238            .expect("entity lookup failed for descriptor swap move");
239        (self.binding.setter)(left_entity, right_value);
240
241        let right_entity = self
242            .solution_descriptor
243            .get_entity_mut(
244                score_director.working_solution_mut() as &mut dyn Any,
245                self.binding.descriptor_index,
246                self.right_entity_index,
247            )
248            .expect("entity lookup failed for descriptor swap move");
249        (self.binding.setter)(right_entity, left_value);
250
251        score_director
252            .after_variable_changed(self.binding.descriptor_index, self.left_entity_index);
253        score_director
254            .after_variable_changed(self.binding.descriptor_index, self.right_entity_index);
255
256        let descriptor = self.solution_descriptor.clone();
257        let binding = self.binding.clone();
258        let left_entity_index = self.left_entity_index;
259        let right_entity_index = self.right_entity_index;
260        score_director.register_undo(Box::new(move |solution: &mut S| {
261            let left_entity = descriptor
262                .get_entity_mut(
263                    solution as &mut dyn Any,
264                    binding.descriptor_index,
265                    left_entity_index,
266                )
267                .expect("entity lookup failed for descriptor swap undo");
268            (binding.setter)(left_entity, left_value);
269            let right_entity = descriptor
270                .get_entity_mut(
271                    solution as &mut dyn Any,
272                    binding.descriptor_index,
273                    right_entity_index,
274                )
275                .expect("entity lookup failed for descriptor swap undo");
276            (binding.setter)(right_entity, right_value);
277        }));
278    }
279
280    fn descriptor_index(&self) -> usize {
281        self.binding.descriptor_index
282    }
283
284    fn entity_indices(&self) -> &[usize] {
285        &self.indices
286    }
287
288    fn variable_name(&self) -> &str {
289        self.binding.variable_name
290    }
291}
292
293#[derive(Clone)]
294pub enum DescriptorEitherMove<S> {
295    Change(DescriptorChangeMove<S>),
296    Swap(DescriptorSwapMove<S>),
297}
298
299impl<S> Debug for DescriptorEitherMove<S> {
300    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
301        match self {
302            Self::Change(m) => m.fmt(f),
303            Self::Swap(m) => m.fmt(f),
304        }
305    }
306}
307
308impl<S> Move<S> for DescriptorEitherMove<S>
309where
310    S: solverforge_core::domain::PlanningSolution + 'static,
311{
312    fn is_doable<D: Director<S>>(&self, score_director: &D) -> bool {
313        match self {
314            Self::Change(m) => m.is_doable(score_director),
315            Self::Swap(m) => m.is_doable(score_director),
316        }
317    }
318
319    fn do_move<D: Director<S>>(&self, score_director: &mut D) {
320        match self {
321            Self::Change(m) => m.do_move(score_director),
322            Self::Swap(m) => m.do_move(score_director),
323        }
324    }
325
326    fn descriptor_index(&self) -> usize {
327        match self {
328            Self::Change(m) => m.descriptor_index(),
329            Self::Swap(m) => m.descriptor_index(),
330        }
331    }
332
333    fn entity_indices(&self) -> &[usize] {
334        match self {
335            Self::Change(m) => m.entity_indices(),
336            Self::Swap(m) => m.entity_indices(),
337        }
338    }
339
340    fn variable_name(&self) -> &str {
341        match self {
342            Self::Change(m) => m.variable_name(),
343            Self::Swap(m) => m.variable_name(),
344        }
345    }
346}
347
348#[derive(Clone)]
349pub struct DescriptorChangeMoveSelector<S> {
350    binding: VariableBinding,
351    solution_descriptor: SolutionDescriptor,
352    _phantom: PhantomData<fn() -> S>,
353}
354
355impl<S> Debug for DescriptorChangeMoveSelector<S> {
356    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
357        f.debug_struct("DescriptorChangeMoveSelector")
358            .field("binding", &self.binding)
359            .finish()
360    }
361}
362
363impl<S> DescriptorChangeMoveSelector<S> {
364    fn new(binding: VariableBinding, solution_descriptor: SolutionDescriptor) -> Self {
365        Self {
366            binding,
367            solution_descriptor,
368            _phantom: PhantomData,
369        }
370    }
371}
372
373impl<S> MoveSelector<S, DescriptorEitherMove<S>> for DescriptorChangeMoveSelector<S>
374where
375    S: solverforge_core::domain::PlanningSolution + 'static,
376{
377    fn iter_moves<'a, D: Director<S>>(
378        &'a self,
379        score_director: &'a D,
380    ) -> impl Iterator<Item = DescriptorEitherMove<S>> + 'a {
381        let count = score_director
382            .entity_count(self.binding.descriptor_index)
383            .unwrap_or(0);
384        let mut moves = Vec::new();
385        for entity_index in 0..count {
386            let entity = self
387                .solution_descriptor
388                .get_entity(
389                    score_director.working_solution() as &dyn Any,
390                    self.binding.descriptor_index,
391                    entity_index,
392                )
393                .expect("entity lookup failed for change selector");
394            for value in self.binding.values_for_entity(entity) {
395                moves.push(DescriptorEitherMove::Change(DescriptorChangeMove::new(
396                    self.binding.clone(),
397                    entity_index,
398                    Some(value),
399                    self.solution_descriptor.clone(),
400                )));
401            }
402        }
403        moves.into_iter()
404    }
405
406    fn size<D: Director<S>>(&self, score_director: &D) -> usize {
407        let count = score_director
408            .entity_count(self.binding.descriptor_index)
409            .unwrap_or(0);
410        let mut total = 0;
411        for entity_index in 0..count {
412            let entity = self
413                .solution_descriptor
414                .get_entity(
415                    score_director.working_solution() as &dyn Any,
416                    self.binding.descriptor_index,
417                    entity_index,
418                )
419                .expect("entity lookup failed for change selector");
420            total += self.binding.values_for_entity(entity).len();
421        }
422        total
423    }
424}
425
426#[derive(Clone)]
427pub struct DescriptorSwapMoveSelector<S> {
428    binding: VariableBinding,
429    solution_descriptor: SolutionDescriptor,
430    _phantom: PhantomData<fn() -> S>,
431}
432
433impl<S> Debug for DescriptorSwapMoveSelector<S> {
434    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
435        f.debug_struct("DescriptorSwapMoveSelector")
436            .field("binding", &self.binding)
437            .finish()
438    }
439}
440
441impl<S> DescriptorSwapMoveSelector<S> {
442    fn new(binding: VariableBinding, solution_descriptor: SolutionDescriptor) -> Self {
443        Self {
444            binding,
445            solution_descriptor,
446            _phantom: PhantomData,
447        }
448    }
449}
450
451impl<S> MoveSelector<S, DescriptorEitherMove<S>> for DescriptorSwapMoveSelector<S>
452where
453    S: solverforge_core::domain::PlanningSolution + 'static,
454{
455    fn iter_moves<'a, D: Director<S>>(
456        &'a self,
457        score_director: &'a D,
458    ) -> impl Iterator<Item = DescriptorEitherMove<S>> + 'a {
459        let count = score_director
460            .entity_count(self.binding.descriptor_index)
461            .unwrap_or(0);
462        let mut moves = Vec::new();
463        for left_entity_index in 0..count {
464            for right_entity_index in (left_entity_index + 1)..count {
465                moves.push(DescriptorEitherMove::Swap(DescriptorSwapMove::new(
466                    self.binding.clone(),
467                    left_entity_index,
468                    right_entity_index,
469                    self.solution_descriptor.clone(),
470                )));
471            }
472        }
473        moves.into_iter()
474    }
475
476    fn size<D: Director<S>>(&self, score_director: &D) -> usize {
477        let count = score_director
478            .entity_count(self.binding.descriptor_index)
479            .unwrap_or(0);
480        count.saturating_mul(count.saturating_sub(1)) / 2
481    }
482}
483
484#[derive(Clone)]
485pub enum DescriptorLeafSelector<S> {
486    Change(DescriptorChangeMoveSelector<S>),
487    Swap(DescriptorSwapMoveSelector<S>),
488}
489
490impl<S> Debug for DescriptorLeafSelector<S>
491where
492    S: solverforge_core::domain::PlanningSolution,
493{
494    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
495        match self {
496            Self::Change(selector) => selector.fmt(f),
497            Self::Swap(selector) => selector.fmt(f),
498        }
499    }
500}
501
502impl<S> MoveSelector<S, DescriptorEitherMove<S>> for DescriptorLeafSelector<S>
503where
504    S: solverforge_core::domain::PlanningSolution + 'static,
505{
506    fn iter_moves<'a, D: Director<S>>(
507        &'a self,
508        score_director: &'a D,
509    ) -> impl Iterator<Item = DescriptorEitherMove<S>> + 'a {
510        let moves: Vec<_> = match self {
511            Self::Change(selector) => selector.iter_moves(score_director).collect(),
512            Self::Swap(selector) => selector.iter_moves(score_director).collect(),
513        };
514        moves.into_iter()
515    }
516
517    fn size<D: Director<S>>(&self, score_director: &D) -> usize {
518        match self {
519            Self::Change(selector) => selector.size(score_director),
520            Self::Swap(selector) => selector.size(score_director),
521        }
522    }
523}
524
525pub enum DescriptorConstruction<S: solverforge_core::domain::PlanningSolution> {
526    FirstFit(
527        ConstructionHeuristicPhase<
528            S,
529            DescriptorEitherMove<S>,
530            DescriptorEntityPlacer<S>,
531            FirstFitForager<S, DescriptorEitherMove<S>>,
532        >,
533    ),
534    BestFit(
535        ConstructionHeuristicPhase<
536            S,
537            DescriptorEitherMove<S>,
538            DescriptorEntityPlacer<S>,
539            BestFitForager<S, DescriptorEitherMove<S>>,
540        >,
541    ),
542}
543
544impl<S: solverforge_core::domain::PlanningSolution> Debug for DescriptorConstruction<S> {
545    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
546        match self {
547            Self::FirstFit(phase) => write!(f, "DescriptorConstruction::FirstFit({phase:?})"),
548            Self::BestFit(phase) => write!(f, "DescriptorConstruction::BestFit({phase:?})"),
549        }
550    }
551}
552
553impl<S, D, ProgressCb> crate::phase::Phase<S, D, ProgressCb> for DescriptorConstruction<S>
554where
555    S: solverforge_core::domain::PlanningSolution + 'static,
556    D: Director<S>,
557    ProgressCb: ProgressCallback<S>,
558{
559    fn solve(&mut self, solver_scope: &mut SolverScope<'_, S, D, ProgressCb>) {
560        match self {
561            Self::FirstFit(phase) => phase.solve(solver_scope),
562            Self::BestFit(phase) => phase.solve(solver_scope),
563        }
564    }
565
566    fn phase_type_name(&self) -> &'static str {
567        "DescriptorConstruction"
568    }
569}
570
571#[derive(Clone)]
572pub struct DescriptorEntityPlacer<S> {
573    bindings: Vec<VariableBinding>,
574    solution_descriptor: SolutionDescriptor,
575    _phantom: PhantomData<fn() -> S>,
576}
577
578impl<S> Debug for DescriptorEntityPlacer<S> {
579    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
580        f.debug_struct("DescriptorEntityPlacer")
581            .field("bindings", &self.bindings)
582            .finish()
583    }
584}
585
586impl<S> DescriptorEntityPlacer<S> {
587    fn new(bindings: Vec<VariableBinding>, solution_descriptor: SolutionDescriptor) -> Self {
588        Self {
589            bindings,
590            solution_descriptor,
591            _phantom: PhantomData,
592        }
593    }
594}
595
596impl<S> EntityPlacer<S, DescriptorEitherMove<S>> for DescriptorEntityPlacer<S>
597where
598    S: solverforge_core::domain::PlanningSolution + 'static,
599{
600    fn get_placements<D: Director<S>>(
601        &self,
602        score_director: &D,
603    ) -> Vec<Placement<S, DescriptorEitherMove<S>>> {
604        let mut placements = Vec::new();
605
606        for binding in &self.bindings {
607            let count = score_director
608                .entity_count(binding.descriptor_index)
609                .unwrap_or(0);
610
611            for entity_index in 0..count {
612                let entity = self
613                    .solution_descriptor
614                    .get_entity(
615                        score_director.working_solution() as &dyn Any,
616                        binding.descriptor_index,
617                        entity_index,
618                    )
619                    .expect("entity lookup failed for descriptor construction");
620                let current_value = (binding.getter)(entity);
621                if current_value.is_some() {
622                    continue;
623                }
624
625                let moves = binding
626                    .values_for_entity(entity)
627                    .into_iter()
628                    .map(|value| {
629                        DescriptorEitherMove::Change(DescriptorChangeMove::new(
630                            binding.clone(),
631                            entity_index,
632                            Some(value),
633                            self.solution_descriptor.clone(),
634                        ))
635                    })
636                    .collect::<Vec<_>>();
637
638                if moves.is_empty() {
639                    continue;
640                }
641
642                placements.push(Placement::new(
643                    EntityReference::new(binding.descriptor_index, entity_index),
644                    moves,
645                ));
646            }
647        }
648
649        placements
650    }
651}
652
653fn collect_bindings(descriptor: &SolutionDescriptor) -> Vec<VariableBinding> {
654    let mut bindings = Vec::new();
655    for (descriptor_index, entity_descriptor) in descriptor.entity_descriptors.iter().enumerate() {
656        for variable in entity_descriptor.genuine_variable_descriptors() {
657            let Some(getter) = variable.usize_getter else {
658                continue;
659            };
660            let Some(setter) = variable.usize_setter else {
661                continue;
662            };
663            bindings.push(VariableBinding {
664                descriptor_index,
665                entity_type_name: entity_descriptor.type_name,
666                variable_name: variable.name,
667                getter,
668                setter,
669                provider: variable.entity_value_provider,
670                range_type: variable.value_range_type.clone(),
671            });
672        }
673    }
674    bindings
675}
676
677fn find_binding(
678    bindings: &[VariableBinding],
679    entity_class: Option<&str>,
680    variable_name: Option<&str>,
681) -> Vec<VariableBinding> {
682    bindings
683        .iter()
684        .filter(|binding| entity_class.is_none_or(|name| name == binding.entity_type_name))
685        .filter(|binding| variable_name.is_none_or(|name| name == binding.variable_name))
686        .cloned()
687        .collect()
688}
689
690pub fn descriptor_has_bindings(descriptor: &SolutionDescriptor) -> bool {
691    !collect_bindings(descriptor).is_empty()
692}
693
694pub fn standard_work_remaining<S>(
695    descriptor: &SolutionDescriptor,
696    entity_class: Option<&str>,
697    variable_name: Option<&str>,
698    solution: &S,
699) -> bool
700where
701    S: solverforge_core::domain::PlanningSolution + 'static,
702{
703    let bindings = find_binding(&collect_bindings(descriptor), entity_class, variable_name);
704    for binding in bindings {
705        let Some(entity_count) = descriptor
706            .entity_descriptors
707            .get(binding.descriptor_index)
708            .and_then(|entity| entity.entity_count(solution as &dyn Any))
709        else {
710            continue;
711        };
712        for entity_index in 0..entity_count {
713            let entity = descriptor
714                .get_entity(solution as &dyn Any, binding.descriptor_index, entity_index)
715                .expect("entity lookup failed while checking standard work");
716            if (binding.getter)(entity).is_none() && !binding.values_for_entity(entity).is_empty() {
717                return true;
718            }
719        }
720    }
721    false
722}
723
724pub fn standard_target_matches(
725    descriptor: &SolutionDescriptor,
726    entity_class: Option<&str>,
727    variable_name: Option<&str>,
728) -> bool {
729    !find_binding(&collect_bindings(descriptor), entity_class, variable_name).is_empty()
730}
731
732fn collect_descriptor_leaf_selectors<S>(
733    config: Option<&MoveSelectorConfig>,
734    descriptor: &SolutionDescriptor,
735) -> Vec<DescriptorLeafSelector<S>>
736where
737    S: solverforge_core::domain::PlanningSolution + 'static,
738{
739    let bindings = collect_bindings(descriptor);
740    let mut leaves = Vec::new();
741
742    fn collect<S>(
743        cfg: &MoveSelectorConfig,
744        descriptor: &SolutionDescriptor,
745        bindings: &[VariableBinding],
746        leaves: &mut Vec<DescriptorLeafSelector<S>>,
747    ) where
748        S: solverforge_core::domain::PlanningSolution + 'static,
749    {
750        match cfg {
751            MoveSelectorConfig::ChangeMoveSelector(change) => {
752                let matched = find_binding(
753                    bindings,
754                    change.target.entity_class.as_deref(),
755                    change.target.variable_name.as_deref(),
756                );
757                assert!(
758                    !matched.is_empty(),
759                    "change_move selector matched no standard planning variables for entity_class={:?} variable_name={:?}",
760                    change.target.entity_class,
761                    change.target.variable_name
762                );
763                for binding in matched {
764                    leaves.push(DescriptorLeafSelector::Change(
765                        DescriptorChangeMoveSelector::new(binding, descriptor.clone()),
766                    ));
767                }
768            }
769            MoveSelectorConfig::SwapMoveSelector(swap) => {
770                let matched = find_binding(
771                    bindings,
772                    swap.target.entity_class.as_deref(),
773                    swap.target.variable_name.as_deref(),
774                );
775                assert!(
776                    !matched.is_empty(),
777                    "swap_move selector matched no standard planning variables for entity_class={:?} variable_name={:?}",
778                    swap.target.entity_class,
779                    swap.target.variable_name
780                );
781                for binding in matched {
782                    leaves.push(DescriptorLeafSelector::Swap(
783                        DescriptorSwapMoveSelector::new(binding, descriptor.clone()),
784                    ));
785                }
786            }
787            MoveSelectorConfig::UnionMoveSelector(union) => {
788                for child in &union.selectors {
789                    collect(child, descriptor, bindings, leaves);
790                }
791            }
792            MoveSelectorConfig::ListChangeMoveSelector(_)
793            | MoveSelectorConfig::NearbyListChangeMoveSelector(_)
794            | MoveSelectorConfig::ListSwapMoveSelector(_)
795            | MoveSelectorConfig::NearbyListSwapMoveSelector(_)
796            | MoveSelectorConfig::SubListChangeMoveSelector(_)
797            | MoveSelectorConfig::SubListSwapMoveSelector(_)
798            | MoveSelectorConfig::ListReverseMoveSelector(_)
799            | MoveSelectorConfig::KOptMoveSelector(_)
800            | MoveSelectorConfig::ListRuinMoveSelector(_) => {
801                panic!("list move selector configured against a standard-variable stock context");
802            }
803            MoveSelectorConfig::CartesianProductMoveSelector(_) => {
804                panic!("cartesian_product move selectors are not supported in stock solving");
805            }
806        }
807    }
808
809    match config {
810        Some(cfg) => collect(cfg, descriptor, &bindings, &mut leaves),
811        None => {
812            for binding in bindings {
813                leaves.push(DescriptorLeafSelector::Change(
814                    DescriptorChangeMoveSelector::new(binding.clone(), descriptor.clone()),
815                ));
816                leaves.push(DescriptorLeafSelector::Swap(
817                    DescriptorSwapMoveSelector::new(binding, descriptor.clone()),
818                ));
819            }
820        }
821    }
822
823    assert!(
824        !leaves.is_empty(),
825        "stock move selector configuration produced no standard neighborhoods"
826    );
827
828    leaves
829}
830
831pub fn build_descriptor_move_selector<S>(
832    config: Option<&MoveSelectorConfig>,
833    descriptor: &SolutionDescriptor,
834) -> VecUnionSelector<S, DescriptorEitherMove<S>, DescriptorLeafSelector<S>>
835where
836    S: solverforge_core::domain::PlanningSolution + 'static,
837{
838    VecUnionSelector::new(collect_descriptor_leaf_selectors(config, descriptor))
839}
840
841pub fn build_descriptor_construction<S>(
842    config: Option<&ConstructionHeuristicConfig>,
843    descriptor: &SolutionDescriptor,
844) -> DescriptorConstruction<S>
845where
846    S: solverforge_core::domain::PlanningSolution + 'static,
847{
848    let bindings = config
849        .map(|cfg| {
850            let matched = find_binding(
851                &collect_bindings(descriptor),
852                cfg.target.entity_class.as_deref(),
853                cfg.target.variable_name.as_deref(),
854            );
855            assert!(
856                !matched.is_empty(),
857                "construction heuristic matched no standard planning variables for entity_class={:?} variable_name={:?}",
858                cfg.target.entity_class,
859                cfg.target.variable_name
860            );
861            matched
862        })
863        .unwrap_or_else(|| collect_bindings(descriptor));
864    let placer = DescriptorEntityPlacer::new(bindings, descriptor.clone());
865    let construction_type = config
866        .map(|cfg| cfg.construction_heuristic_type)
867        .unwrap_or(ConstructionHeuristicType::FirstFit);
868
869    match construction_type {
870        ConstructionHeuristicType::FirstFit => DescriptorConstruction::FirstFit(
871            ConstructionHeuristicPhase::new(placer, FirstFitForager::new()),
872        ),
873        ConstructionHeuristicType::CheapestInsertion => DescriptorConstruction::BestFit(
874            ConstructionHeuristicPhase::new(placer, BestFitForager::new()),
875        ),
876        ConstructionHeuristicType::FirstFitDecreasing
877        | ConstructionHeuristicType::WeakestFit
878        | ConstructionHeuristicType::WeakestFitDecreasing
879        | ConstructionHeuristicType::StrongestFit
880        | ConstructionHeuristicType::StrongestFitDecreasing
881        | ConstructionHeuristicType::AllocateEntityFromQueue
882        | ConstructionHeuristicType::AllocateToValueFromQueue
883        | ConstructionHeuristicType::ListRoundRobin
884        | ConstructionHeuristicType::ListCheapestInsertion
885        | ConstructionHeuristicType::ListRegretInsertion
886        | ConstructionHeuristicType::ListClarkeWright
887        | ConstructionHeuristicType::ListKOpt => {
888            panic!(
889                "descriptor standard construction does not support {:?}",
890                construction_type
891            );
892        }
893    }
894}