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