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 descriptor = self.solution_descriptor.clone();
410 let binding = self.binding.clone();
411 let solution = score_director.working_solution() as &dyn Any;
412 (0..count).flat_map(move |entity_index| {
413 let entity = descriptor
414 .get_entity(solution, binding.descriptor_index, entity_index)
415 .expect("entity lookup failed for change selector");
416 binding
417 .values_for_entity(&descriptor, solution, entity)
418 .into_iter()
419 .map({
420 let binding = binding.clone();
421 let descriptor = descriptor.clone();
422 move |value| {
423 DescriptorEitherMove::Change(DescriptorChangeMove::new(
424 binding.clone(),
425 entity_index,
426 Some(value),
427 descriptor.clone(),
428 ))
429 }
430 })
431 })
432 }
433
434 fn size<D: Director<S>>(&self, score_director: &D) -> usize {
435 let count = score_director
436 .entity_count(self.binding.descriptor_index)
437 .unwrap_or(0);
438 let mut total = 0;
439 for entity_index in 0..count {
440 let entity = self
441 .solution_descriptor
442 .get_entity(
443 score_director.working_solution() as &dyn Any,
444 self.binding.descriptor_index,
445 entity_index,
446 )
447 .expect("entity lookup failed for change selector");
448 total += self
449 .binding
450 .values_for_entity(
451 &self.solution_descriptor,
452 score_director.working_solution() as &dyn Any,
453 entity,
454 )
455 .len();
456 }
457 total
458 }
459}
460
461#[derive(Clone)]
462pub struct DescriptorSwapMoveSelector<S> {
463 binding: VariableBinding,
464 solution_descriptor: SolutionDescriptor,
465 _phantom: PhantomData<fn() -> S>,
466}
467
468impl<S> Debug for DescriptorSwapMoveSelector<S> {
469 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
470 f.debug_struct("DescriptorSwapMoveSelector")
471 .field("binding", &self.binding)
472 .finish()
473 }
474}
475
476impl<S> DescriptorSwapMoveSelector<S> {
477 fn new(binding: VariableBinding, solution_descriptor: SolutionDescriptor) -> Self {
478 Self {
479 binding,
480 solution_descriptor,
481 _phantom: PhantomData,
482 }
483 }
484}
485
486impl<S> MoveSelector<S, DescriptorEitherMove<S>> for DescriptorSwapMoveSelector<S>
487where
488 S: solverforge_core::domain::PlanningSolution + 'static,
489{
490 fn iter_moves<'a, D: Director<S>>(
491 &'a self,
492 score_director: &'a D,
493 ) -> impl Iterator<Item = DescriptorEitherMove<S>> + 'a {
494 let count = score_director
495 .entity_count(self.binding.descriptor_index)
496 .unwrap_or(0);
497 let binding = self.binding.clone();
498 let descriptor = self.solution_descriptor.clone();
499 (0..count).flat_map(move |left_entity_index| {
500 ((left_entity_index + 1)..count).map({
501 let binding = binding.clone();
502 let descriptor = descriptor.clone();
503 move |right_entity_index| {
504 DescriptorEitherMove::Swap(DescriptorSwapMove::new(
505 binding.clone(),
506 left_entity_index,
507 right_entity_index,
508 descriptor.clone(),
509 ))
510 }
511 })
512 })
513 }
514
515 fn size<D: Director<S>>(&self, score_director: &D) -> usize {
516 let count = score_director
517 .entity_count(self.binding.descriptor_index)
518 .unwrap_or(0);
519 count.saturating_mul(count.saturating_sub(1)) / 2
520 }
521}
522
523#[derive(Clone)]
524pub enum DescriptorLeafSelector<S> {
525 Change(DescriptorChangeMoveSelector<S>),
526 Swap(DescriptorSwapMoveSelector<S>),
527}
528
529impl<S> Debug for DescriptorLeafSelector<S>
530where
531 S: solverforge_core::domain::PlanningSolution,
532{
533 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
534 match self {
535 Self::Change(selector) => selector.fmt(f),
536 Self::Swap(selector) => selector.fmt(f),
537 }
538 }
539}
540
541impl<S> MoveSelector<S, DescriptorEitherMove<S>> for DescriptorLeafSelector<S>
542where
543 S: solverforge_core::domain::PlanningSolution + 'static,
544{
545 fn iter_moves<'a, D: Director<S>>(
546 &'a self,
547 score_director: &'a D,
548 ) -> impl Iterator<Item = DescriptorEitherMove<S>> + 'a {
549 enum DescriptorLeafIter<A, B> {
550 Change(A),
551 Swap(B),
552 }
553
554 impl<T, A, B> Iterator for DescriptorLeafIter<A, B>
555 where
556 A: Iterator<Item = T>,
557 B: Iterator<Item = T>,
558 {
559 type Item = T;
560
561 fn next(&mut self) -> Option<Self::Item> {
562 match self {
563 Self::Change(iter) => iter.next(),
564 Self::Swap(iter) => iter.next(),
565 }
566 }
567 }
568
569 match self {
570 Self::Change(selector) => {
571 DescriptorLeafIter::Change(selector.iter_moves(score_director))
572 }
573 Self::Swap(selector) => DescriptorLeafIter::Swap(selector.iter_moves(score_director)),
574 }
575 }
576
577 fn size<D: Director<S>>(&self, score_director: &D) -> usize {
578 match self {
579 Self::Change(selector) => selector.size(score_director),
580 Self::Swap(selector) => selector.size(score_director),
581 }
582 }
583}
584
585pub enum DescriptorConstruction<S: solverforge_core::domain::PlanningSolution> {
586 FirstFit(
587 ConstructionHeuristicPhase<
588 S,
589 DescriptorEitherMove<S>,
590 DescriptorEntityPlacer<S>,
591 FirstFitForager<S, DescriptorEitherMove<S>>,
592 >,
593 ),
594 BestFit(
595 ConstructionHeuristicPhase<
596 S,
597 DescriptorEitherMove<S>,
598 DescriptorEntityPlacer<S>,
599 BestFitForager<S, DescriptorEitherMove<S>>,
600 >,
601 ),
602}
603
604impl<S: solverforge_core::domain::PlanningSolution> Debug for DescriptorConstruction<S> {
605 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
606 match self {
607 Self::FirstFit(phase) => write!(f, "DescriptorConstruction::FirstFit({phase:?})"),
608 Self::BestFit(phase) => write!(f, "DescriptorConstruction::BestFit({phase:?})"),
609 }
610 }
611}
612
613impl<S, D, ProgressCb> crate::phase::Phase<S, D, ProgressCb> for DescriptorConstruction<S>
614where
615 S: solverforge_core::domain::PlanningSolution + 'static,
616 D: Director<S>,
617 ProgressCb: ProgressCallback<S>,
618{
619 fn solve(&mut self, solver_scope: &mut SolverScope<'_, S, D, ProgressCb>) {
620 match self {
621 Self::FirstFit(phase) => phase.solve(solver_scope),
622 Self::BestFit(phase) => phase.solve(solver_scope),
623 }
624 }
625
626 fn phase_type_name(&self) -> &'static str {
627 "DescriptorConstruction"
628 }
629}
630
631#[derive(Clone)]
632pub struct DescriptorEntityPlacer<S> {
633 bindings: Vec<VariableBinding>,
634 solution_descriptor: SolutionDescriptor,
635 _phantom: PhantomData<fn() -> S>,
636}
637
638impl<S> Debug for DescriptorEntityPlacer<S> {
639 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
640 f.debug_struct("DescriptorEntityPlacer")
641 .field("bindings", &self.bindings)
642 .finish()
643 }
644}
645
646impl<S> DescriptorEntityPlacer<S> {
647 fn new(bindings: Vec<VariableBinding>, solution_descriptor: SolutionDescriptor) -> Self {
648 Self {
649 bindings,
650 solution_descriptor,
651 _phantom: PhantomData,
652 }
653 }
654}
655
656impl<S> EntityPlacer<S, DescriptorEitherMove<S>> for DescriptorEntityPlacer<S>
657where
658 S: solverforge_core::domain::PlanningSolution + 'static,
659{
660 fn get_placements<D: Director<S>>(
661 &self,
662 score_director: &D,
663 ) -> Vec<Placement<S, DescriptorEitherMove<S>>> {
664 let mut placements = Vec::new();
665
666 for binding in &self.bindings {
667 let count = score_director
668 .entity_count(binding.descriptor_index)
669 .unwrap_or(0);
670
671 for entity_index in 0..count {
672 let entity = self
673 .solution_descriptor
674 .get_entity(
675 score_director.working_solution() as &dyn Any,
676 binding.descriptor_index,
677 entity_index,
678 )
679 .expect("entity lookup failed for descriptor construction");
680 let current_value = (binding.getter)(entity);
681 if current_value.is_some() {
682 continue;
683 }
684
685 let moves = binding
686 .values_for_entity(
687 &self.solution_descriptor,
688 score_director.working_solution() as &dyn Any,
689 entity,
690 )
691 .into_iter()
692 .map(|value| {
693 DescriptorEitherMove::Change(DescriptorChangeMove::new(
694 binding.clone(),
695 entity_index,
696 Some(value),
697 self.solution_descriptor.clone(),
698 ))
699 })
700 .collect::<Vec<_>>();
701
702 if moves.is_empty() {
703 continue;
704 }
705
706 placements.push(Placement::new(
707 EntityReference::new(binding.descriptor_index, entity_index),
708 moves,
709 ));
710 }
711 }
712
713 placements
714 }
715}
716
717fn collect_bindings(descriptor: &SolutionDescriptor) -> Vec<VariableBinding> {
718 let mut bindings = Vec::new();
719 for (descriptor_index, entity_descriptor) in descriptor.entity_descriptors.iter().enumerate() {
720 for variable in entity_descriptor.genuine_variable_descriptors() {
721 let Some(getter) = variable.usize_getter else {
722 continue;
723 };
724 let Some(setter) = variable.usize_setter else {
725 continue;
726 };
727 bindings.push(VariableBinding {
728 descriptor_index,
729 entity_type_name: entity_descriptor.type_name,
730 variable_name: variable.name,
731 getter,
732 setter,
733 value_range_provider: variable.value_range_provider,
734 provider: variable.entity_value_provider,
735 range_type: variable.value_range_type.clone(),
736 });
737 }
738 }
739 bindings
740}
741
742fn find_binding(
743 bindings: &[VariableBinding],
744 entity_class: Option<&str>,
745 variable_name: Option<&str>,
746) -> Vec<VariableBinding> {
747 bindings
748 .iter()
749 .filter(|binding| entity_class.is_none_or(|name| name == binding.entity_type_name))
750 .filter(|binding| variable_name.is_none_or(|name| name == binding.variable_name))
751 .cloned()
752 .collect()
753}
754
755pub fn descriptor_has_bindings(descriptor: &SolutionDescriptor) -> bool {
756 !collect_bindings(descriptor).is_empty()
757}
758
759pub fn standard_work_remaining<S>(
760 descriptor: &SolutionDescriptor,
761 entity_class: Option<&str>,
762 variable_name: Option<&str>,
763 solution: &S,
764) -> bool
765where
766 S: solverforge_core::domain::PlanningSolution + 'static,
767{
768 let bindings = find_binding(&collect_bindings(descriptor), entity_class, variable_name);
769 for binding in bindings {
770 let Some(entity_count) = descriptor
771 .entity_descriptors
772 .get(binding.descriptor_index)
773 .and_then(|entity| entity.entity_count(solution as &dyn Any))
774 else {
775 continue;
776 };
777 for entity_index in 0..entity_count {
778 let entity = descriptor
779 .get_entity(solution as &dyn Any, binding.descriptor_index, entity_index)
780 .expect("entity lookup failed while checking standard work");
781 if (binding.getter)(entity).is_none()
782 && !binding
783 .values_for_entity(descriptor, solution as &dyn Any, entity)
784 .is_empty()
785 {
786 return true;
787 }
788 }
789 }
790 false
791}
792
793pub fn standard_target_matches(
794 descriptor: &SolutionDescriptor,
795 entity_class: Option<&str>,
796 variable_name: Option<&str>,
797) -> bool {
798 !find_binding(&collect_bindings(descriptor), entity_class, variable_name).is_empty()
799}
800
801fn collect_descriptor_leaf_selectors<S>(
802 config: Option<&MoveSelectorConfig>,
803 descriptor: &SolutionDescriptor,
804) -> Vec<DescriptorLeafSelector<S>>
805where
806 S: solverforge_core::domain::PlanningSolution + 'static,
807{
808 let bindings = collect_bindings(descriptor);
809 let mut leaves = Vec::new();
810
811 fn collect<S>(
812 cfg: &MoveSelectorConfig,
813 descriptor: &SolutionDescriptor,
814 bindings: &[VariableBinding],
815 leaves: &mut Vec<DescriptorLeafSelector<S>>,
816 ) where
817 S: solverforge_core::domain::PlanningSolution + 'static,
818 {
819 match cfg {
820 MoveSelectorConfig::ChangeMoveSelector(change) => {
821 let matched = find_binding(
822 bindings,
823 change.target.entity_class.as_deref(),
824 change.target.variable_name.as_deref(),
825 );
826 assert!(
827 !matched.is_empty(),
828 "change_move selector matched no standard planning variables for entity_class={:?} variable_name={:?}",
829 change.target.entity_class,
830 change.target.variable_name
831 );
832 for binding in matched {
833 leaves.push(DescriptorLeafSelector::Change(
834 DescriptorChangeMoveSelector::new(binding, descriptor.clone()),
835 ));
836 }
837 }
838 MoveSelectorConfig::SwapMoveSelector(swap) => {
839 let matched = find_binding(
840 bindings,
841 swap.target.entity_class.as_deref(),
842 swap.target.variable_name.as_deref(),
843 );
844 assert!(
845 !matched.is_empty(),
846 "swap_move selector matched no standard planning variables for entity_class={:?} variable_name={:?}",
847 swap.target.entity_class,
848 swap.target.variable_name
849 );
850 for binding in matched {
851 leaves.push(DescriptorLeafSelector::Swap(
852 DescriptorSwapMoveSelector::new(binding, descriptor.clone()),
853 ));
854 }
855 }
856 MoveSelectorConfig::UnionMoveSelector(union) => {
857 for child in &union.selectors {
858 collect(child, descriptor, bindings, leaves);
859 }
860 }
861 MoveSelectorConfig::SelectedCountLimitMoveSelector(_) => {
862 panic!(
863 "selected_count_limit_move_selector must be handled by the unified stock runtime"
864 );
865 }
866 MoveSelectorConfig::ListChangeMoveSelector(_)
867 | MoveSelectorConfig::NearbyListChangeMoveSelector(_)
868 | MoveSelectorConfig::ListSwapMoveSelector(_)
869 | MoveSelectorConfig::NearbyListSwapMoveSelector(_)
870 | MoveSelectorConfig::SubListChangeMoveSelector(_)
871 | MoveSelectorConfig::SubListSwapMoveSelector(_)
872 | MoveSelectorConfig::ListReverseMoveSelector(_)
873 | MoveSelectorConfig::KOptMoveSelector(_)
874 | MoveSelectorConfig::ListRuinMoveSelector(_) => {
875 panic!("list move selector configured against a standard-variable stock context");
876 }
877 MoveSelectorConfig::CartesianProductMoveSelector(_) => {
878 panic!("cartesian_product move selectors are not supported in stock solving");
879 }
880 }
881 }
882
883 match config {
884 Some(cfg) => collect(cfg, descriptor, &bindings, &mut leaves),
885 None => {
886 for binding in bindings {
887 leaves.push(DescriptorLeafSelector::Change(
888 DescriptorChangeMoveSelector::new(binding.clone(), descriptor.clone()),
889 ));
890 leaves.push(DescriptorLeafSelector::Swap(
891 DescriptorSwapMoveSelector::new(binding, descriptor.clone()),
892 ));
893 }
894 }
895 }
896
897 assert!(
898 !leaves.is_empty(),
899 "stock move selector configuration produced no standard neighborhoods"
900 );
901
902 leaves
903}
904
905pub fn build_descriptor_move_selector<S>(
906 config: Option<&MoveSelectorConfig>,
907 descriptor: &SolutionDescriptor,
908) -> VecUnionSelector<S, DescriptorEitherMove<S>, DescriptorLeafSelector<S>>
909where
910 S: solverforge_core::domain::PlanningSolution + 'static,
911{
912 VecUnionSelector::new(collect_descriptor_leaf_selectors(config, descriptor))
913}
914
915pub fn build_descriptor_construction<S>(
916 config: Option<&ConstructionHeuristicConfig>,
917 descriptor: &SolutionDescriptor,
918) -> DescriptorConstruction<S>
919where
920 S: solverforge_core::domain::PlanningSolution + 'static,
921{
922 let bindings = config
923 .map(|cfg| {
924 let matched = find_binding(
925 &collect_bindings(descriptor),
926 cfg.target.entity_class.as_deref(),
927 cfg.target.variable_name.as_deref(),
928 );
929 assert!(
930 !matched.is_empty(),
931 "construction heuristic matched no standard planning variables for entity_class={:?} variable_name={:?}",
932 cfg.target.entity_class,
933 cfg.target.variable_name
934 );
935 matched
936 })
937 .unwrap_or_else(|| collect_bindings(descriptor));
938 let placer = DescriptorEntityPlacer::new(bindings, descriptor.clone());
939 let construction_type = config
940 .map(|cfg| cfg.construction_heuristic_type)
941 .unwrap_or(ConstructionHeuristicType::FirstFit);
942
943 match construction_type {
944 ConstructionHeuristicType::FirstFit => DescriptorConstruction::FirstFit(
945 ConstructionHeuristicPhase::new(placer, FirstFitForager::new()),
946 ),
947 ConstructionHeuristicType::CheapestInsertion => DescriptorConstruction::BestFit(
948 ConstructionHeuristicPhase::new(placer, BestFitForager::new()),
949 ),
950 ConstructionHeuristicType::FirstFitDecreasing
951 | ConstructionHeuristicType::WeakestFit
952 | ConstructionHeuristicType::WeakestFitDecreasing
953 | ConstructionHeuristicType::StrongestFit
954 | ConstructionHeuristicType::StrongestFitDecreasing
955 | ConstructionHeuristicType::AllocateEntityFromQueue
956 | ConstructionHeuristicType::AllocateToValueFromQueue
957 | ConstructionHeuristicType::ListRoundRobin
958 | ConstructionHeuristicType::ListCheapestInsertion
959 | ConstructionHeuristicType::ListRegretInsertion
960 | ConstructionHeuristicType::ListClarkeWright
961 | ConstructionHeuristicType::ListKOpt => {
962 panic!(
963 "descriptor standard construction does not support {:?}",
964 construction_type
965 );
966 }
967 }
968}
969
970#[cfg(test)]
971mod tests {
972 use super::*;
973 use solverforge_core::domain::{
974 EntityCollectionExtractor, EntityDescriptor, PlanningSolution, ProblemFactDescriptor,
975 VariableDescriptor,
976 };
977 use solverforge_core::score::SoftScore;
978 use solverforge_scoring::ScoreDirector;
979 use std::any::{Any, TypeId};
980
981 #[derive(Clone, Debug)]
982 struct Worker;
983
984 #[derive(Clone, Debug)]
985 struct Task {
986 worker_idx: Option<usize>,
987 }
988
989 #[derive(Clone, Debug)]
990 struct Plan {
991 workers: Vec<Worker>,
992 tasks: Vec<Task>,
993 score: Option<SoftScore>,
994 }
995
996 impl PlanningSolution for Plan {
997 type Score = SoftScore;
998
999 fn score(&self) -> Option<Self::Score> {
1000 self.score
1001 }
1002
1003 fn set_score(&mut self, score: Option<Self::Score>) {
1004 self.score = score;
1005 }
1006 }
1007
1008 fn get_worker_idx(entity: &dyn Any) -> Option<usize> {
1009 entity
1010 .downcast_ref::<Task>()
1011 .expect("task expected")
1012 .worker_idx
1013 }
1014
1015 fn set_worker_idx(entity: &mut dyn Any, value: Option<usize>) {
1016 entity
1017 .downcast_mut::<Task>()
1018 .expect("task expected")
1019 .worker_idx = value;
1020 }
1021
1022 fn descriptor() -> SolutionDescriptor {
1023 SolutionDescriptor::new("Plan", TypeId::of::<Plan>())
1024 .with_entity(
1025 EntityDescriptor::new("Task", TypeId::of::<Task>(), "tasks")
1026 .with_extractor(Box::new(EntityCollectionExtractor::new(
1027 "Task",
1028 "tasks",
1029 |s: &Plan| &s.tasks,
1030 |s: &mut Plan| &mut s.tasks,
1031 )))
1032 .with_variable(
1033 VariableDescriptor::genuine("worker_idx")
1034 .with_allows_unassigned(true)
1035 .with_value_range("workers")
1036 .with_usize_accessors(get_worker_idx, set_worker_idx),
1037 ),
1038 )
1039 .with_problem_fact(
1040 ProblemFactDescriptor::new("Worker", TypeId::of::<Worker>(), "workers")
1041 .with_extractor(Box::new(EntityCollectionExtractor::new(
1042 "Worker",
1043 "workers",
1044 |s: &Plan| &s.workers,
1045 |s: &mut Plan| &mut s.workers,
1046 ))),
1047 )
1048 }
1049
1050 #[test]
1051 fn solution_level_value_range_generates_standard_work() {
1052 let descriptor = descriptor();
1053 let plan = Plan {
1054 workers: vec![Worker, Worker, Worker],
1055 tasks: vec![Task { worker_idx: None }],
1056 score: None,
1057 };
1058
1059 assert!(standard_work_remaining(
1060 &descriptor,
1061 Some("Task"),
1062 Some("worker_idx"),
1063 &plan
1064 ));
1065 }
1066
1067 #[test]
1068 fn solution_level_value_range_builds_change_moves() {
1069 let descriptor = descriptor();
1070 let plan = Plan {
1071 workers: vec![Worker, Worker, Worker],
1072 tasks: vec![Task { worker_idx: None }],
1073 score: None,
1074 };
1075 let director = ScoreDirector::simple(plan, descriptor.clone(), |s, _| s.tasks.len());
1076 let selector = build_descriptor_move_selector::<Plan>(None, &descriptor);
1077
1078 assert_eq!(selector.size(&director), 3);
1080 }
1081}