Skip to main content

solverforge_solver/descriptor/selectors/dispatch/
build.rs

1fn build_descriptor_flat_selector<S>(
2    config: Option<&MoveSelectorConfig>,
3    descriptor: &SolutionDescriptor,
4    random_seed: Option<u64>,
5) -> DescriptorFlatSelector<S>
6where
7    S: PlanningSolution + 'static,
8    S::Score: Score,
9{
10    let bindings = collect_bindings(descriptor);
11    let mut leaves = Vec::new();
12
13    fn require_matches<S>(
14        label: &str,
15        entity_class: Option<&str>,
16        variable_name: Option<&str>,
17        matched: &[VariableBinding],
18    ) where
19        S: PlanningSolution + 'static,
20        S::Score: Score,
21    {
22        assert!(
23            !matched.is_empty(),
24            "{label} selector matched no scalar planning variables for entity_class={:?} variable_name={:?}",
25            entity_class,
26            variable_name,
27        );
28    }
29
30    fn collect<S>(
31        cfg: &MoveSelectorConfig,
32        descriptor: &SolutionDescriptor,
33        bindings: &[VariableBinding],
34        random_seed: Option<u64>,
35        leaves: &mut Vec<DescriptorLeafSelector<S>>,
36    ) where
37        S: PlanningSolution + 'static,
38        S::Score: Score,
39    {
40        match cfg {
41            MoveSelectorConfig::ChangeMoveSelector(change) => {
42                let matched = find_binding(
43                    bindings,
44                    change.target.entity_class.as_deref(),
45                    change.target.variable_name.as_deref(),
46                );
47                require_matches::<S>(
48                    "change_move",
49                    change.target.entity_class.as_deref(),
50                    change.target.variable_name.as_deref(),
51                    &matched,
52                );
53                for binding in matched {
54                    leaves.push(DescriptorLeafSelector::Change(
55                        DescriptorChangeMoveSelector::new(
56                            binding,
57                            descriptor.clone(),
58                            change.value_candidate_limit,
59                        ),
60                    ));
61                }
62            }
63            MoveSelectorConfig::SwapMoveSelector(swap) => {
64                let matched = find_binding(
65                    bindings,
66                    swap.target.entity_class.as_deref(),
67                    swap.target.variable_name.as_deref(),
68                );
69                require_matches::<S>(
70                    "swap_move",
71                    swap.target.entity_class.as_deref(),
72                    swap.target.variable_name.as_deref(),
73                    &matched,
74                );
75                for binding in matched {
76                    leaves.push(DescriptorLeafSelector::Swap(
77                        DescriptorSwapMoveSelector::new(binding, descriptor.clone()),
78                    ));
79                }
80            }
81            MoveSelectorConfig::NearbyChangeMoveSelector(nearby_change) => {
82                let matched = find_binding(
83                    bindings,
84                    nearby_change.target.entity_class.as_deref(),
85                    nearby_change.target.variable_name.as_deref(),
86                );
87                require_matches::<S>(
88                    "nearby_change_move",
89                    nearby_change.target.entity_class.as_deref(),
90                    nearby_change.target.variable_name.as_deref(),
91                    &matched,
92                );
93                for binding in matched {
94                    assert!(
95                        binding.nearby_value_candidates.is_some(),
96                        "nearby_change_move selector requires nearby_value_candidates for {}::{}",
97                        binding.entity_type_name,
98                        binding.variable_name,
99                    );
100                    leaves.push(DescriptorLeafSelector::NearbyChange(
101                        DescriptorNearbyChangeMoveSelector {
102                            binding,
103                            solution_descriptor: descriptor.clone(),
104                            max_nearby: nearby_change.max_nearby,
105                            value_candidate_limit: nearby_change.value_candidate_limit,
106                            _phantom: PhantomData,
107                        },
108                    ));
109                }
110            }
111            MoveSelectorConfig::NearbySwapMoveSelector(nearby_swap) => {
112                let matched = find_binding(
113                    bindings,
114                    nearby_swap.target.entity_class.as_deref(),
115                    nearby_swap.target.variable_name.as_deref(),
116                );
117                require_matches::<S>(
118                    "nearby_swap_move",
119                    nearby_swap.target.entity_class.as_deref(),
120                    nearby_swap.target.variable_name.as_deref(),
121                    &matched,
122                );
123                for binding in matched {
124                    assert!(
125                        binding.nearby_entity_candidates.is_some(),
126                        "nearby_swap_move selector requires nearby_entity_candidates for {}::{}",
127                        binding.entity_type_name,
128                        binding.variable_name,
129                    );
130                    leaves.push(DescriptorLeafSelector::NearbySwap(
131                        DescriptorNearbySwapMoveSelector {
132                            binding,
133                            solution_descriptor: descriptor.clone(),
134                            max_nearby: nearby_swap.max_nearby,
135                            _phantom: PhantomData,
136                        },
137                    ));
138                }
139            }
140            MoveSelectorConfig::PillarChangeMoveSelector(pillar_change) => {
141                let matched = find_binding(
142                    bindings,
143                    pillar_change.target.entity_class.as_deref(),
144                    pillar_change.target.variable_name.as_deref(),
145                );
146                require_matches::<S>(
147                    "pillar_change_move",
148                    pillar_change.target.entity_class.as_deref(),
149                    pillar_change.target.variable_name.as_deref(),
150                    &matched,
151                );
152                for binding in matched {
153                    leaves.push(DescriptorLeafSelector::PillarChange(
154                        DescriptorPillarChangeMoveSelector {
155                            binding,
156                            solution_descriptor: descriptor.clone(),
157                            minimum_sub_pillar_size: pillar_change.minimum_sub_pillar_size,
158                            maximum_sub_pillar_size: pillar_change.maximum_sub_pillar_size,
159                            value_candidate_limit: pillar_change.value_candidate_limit,
160                            _phantom: PhantomData,
161                        },
162                    ));
163                }
164            }
165            MoveSelectorConfig::PillarSwapMoveSelector(pillar_swap) => {
166                let matched = find_binding(
167                    bindings,
168                    pillar_swap.target.entity_class.as_deref(),
169                    pillar_swap.target.variable_name.as_deref(),
170                );
171                require_matches::<S>(
172                    "pillar_swap_move",
173                    pillar_swap.target.entity_class.as_deref(),
174                    pillar_swap.target.variable_name.as_deref(),
175                    &matched,
176                );
177                for binding in matched {
178                    leaves.push(DescriptorLeafSelector::PillarSwap(
179                        DescriptorPillarSwapMoveSelector {
180                            binding,
181                            solution_descriptor: descriptor.clone(),
182                            minimum_sub_pillar_size: pillar_swap.minimum_sub_pillar_size,
183                            maximum_sub_pillar_size: pillar_swap.maximum_sub_pillar_size,
184                            _phantom: PhantomData,
185                        },
186                    ));
187                }
188            }
189            MoveSelectorConfig::RuinRecreateMoveSelector(ruin_recreate) => {
190                validate_ruin_recreate_bounds(
191                    ruin_recreate.min_ruin_count,
192                    ruin_recreate.max_ruin_count,
193                );
194                let matched = find_binding(
195                    bindings,
196                    ruin_recreate.target.entity_class.as_deref(),
197                    ruin_recreate.target.variable_name.as_deref(),
198                );
199                require_matches::<S>(
200                    "ruin_recreate_move",
201                    ruin_recreate.target.entity_class.as_deref(),
202                    ruin_recreate.target.variable_name.as_deref(),
203                    &matched,
204                );
205                for binding in matched {
206                    if ruin_recreate.recreate_heuristic_type == RecreateHeuristicType::CheapestInsertion {
207                        assert!(
208                            binding.candidate_values.is_some()
209                                || ruin_recreate.value_candidate_limit.is_some(),
210                            "cheapest_insertion descriptor-driven ruin_recreate requires candidate_values or value_candidate_limit for {}::{}",
211                            binding.entity_type_name,
212                            binding.variable_name,
213                        );
214                    }
215                    let rng = match scoped_seed(
216                        random_seed,
217                        binding.descriptor_index,
218                        binding.variable_name,
219                        "descriptor_ruin_recreate_move_selector",
220                    ) {
221                        Some(seed) => SmallRng::seed_from_u64(seed),
222                        None => SmallRng::from_rng(&mut rand::rng()),
223                    };
224                    leaves.push(DescriptorLeafSelector::RuinRecreate(
225                        DescriptorRuinRecreateMoveSelector {
226                            binding,
227                            solution_descriptor: descriptor.clone(),
228                            min_ruin_count: ruin_recreate.min_ruin_count,
229                            max_ruin_count: ruin_recreate.max_ruin_count,
230                            moves_per_step: ruin_recreate.moves_per_step.unwrap_or(10).max(1),
231                            value_candidate_limit: ruin_recreate.value_candidate_limit,
232                            recreate_heuristic_type: ruin_recreate.recreate_heuristic_type,
233                            rng: RefCell::new(rng),
234                            _phantom: PhantomData,
235                        },
236                    ));
237                }
238            }
239            MoveSelectorConfig::UnionMoveSelector(union) => {
240                for child in &union.selectors {
241                    collect::<S>(child, descriptor, bindings, random_seed, leaves);
242                }
243            }
244            MoveSelectorConfig::LimitedNeighborhood(_) => {
245                panic!("limited_neighborhood must be handled by the canonical runtime");
246            }
247            MoveSelectorConfig::ListChangeMoveSelector(_)
248            | MoveSelectorConfig::NearbyListChangeMoveSelector(_)
249            | MoveSelectorConfig::ListSwapMoveSelector(_)
250            | MoveSelectorConfig::NearbyListSwapMoveSelector(_)
251            | MoveSelectorConfig::SublistChangeMoveSelector(_)
252            | MoveSelectorConfig::SublistSwapMoveSelector(_)
253            | MoveSelectorConfig::ListReverseMoveSelector(_)
254            | MoveSelectorConfig::KOptMoveSelector(_)
255            | MoveSelectorConfig::ListRuinMoveSelector(_) => {
256                panic!("list move selector configured against a scalar-variable model");
257            }
258            MoveSelectorConfig::CartesianProductMoveSelector(_) => {
259                panic!(
260                    "nested cartesian_product move selectors are not supported inside descriptor cartesian children"
261                );
262            }
263            MoveSelectorConfig::ConflictRepairMoveSelector(_) => {
264                panic!("conflict_repair_move_selector must be handled by the canonical runtime");
265            }
266            MoveSelectorConfig::CompoundConflictRepairMoveSelector(_) => {
267                panic!(
268                    "compound_conflict_repair_move_selector must be handled by the canonical runtime"
269                );
270            }
271            MoveSelectorConfig::GroupedScalarMoveSelector(_) => {
272                panic!("grouped_scalar_move_selector must be handled by the canonical runtime");
273            }
274            MoveSelectorConfig::CoverageRepairMoveSelector(_) => {
275                panic!("coverage_repair_move_selector must be handled by the canonical runtime");
276            }
277        }
278    }
279
280    match config {
281        Some(cfg) => collect::<S>(cfg, descriptor, &bindings, random_seed, &mut leaves),
282        None => {
283            for binding in bindings {
284                leaves.push(DescriptorLeafSelector::Change(
285                    DescriptorChangeMoveSelector::new(binding.clone(), descriptor.clone(), None),
286                ));
287                leaves.push(DescriptorLeafSelector::Swap(
288                    DescriptorSwapMoveSelector::new(binding, descriptor.clone()),
289                ));
290            }
291        }
292    }
293
294    assert!(
295        !leaves.is_empty(),
296        "move selector configuration produced no scalar neighborhoods"
297    );
298
299    let selection_order = match config {
300        Some(MoveSelectorConfig::UnionMoveSelector(union)) => union.selection_order,
301        _ => solverforge_config::UnionSelectionOrder::Sequential,
302    };
303    VecUnionSelector::with_selection_order(leaves, selection_order)
304}
305
306pub fn build_descriptor_move_selector<S>(
307    config: Option<&MoveSelectorConfig>,
308    descriptor: &SolutionDescriptor,
309    random_seed: Option<u64>,
310) -> DescriptorSelector<S>
311where
312    S: PlanningSolution + 'static,
313    S::Score: Score,
314{
315    fn selector_requires_score_during_move(config: &MoveSelectorConfig) -> bool {
316        match config {
317            MoveSelectorConfig::RuinRecreateMoveSelector(_) => true,
318            MoveSelectorConfig::LimitedNeighborhood(limit) => {
319                selector_requires_score_during_move(limit.selector.as_ref())
320            }
321            MoveSelectorConfig::UnionMoveSelector(union) => union
322                .selectors
323                .iter()
324                .any(selector_requires_score_during_move),
325            MoveSelectorConfig::CartesianProductMoveSelector(_) => true,
326            _ => false,
327        }
328    }
329
330    fn assert_cartesian_left_preview_safe(config: &MoveSelectorConfig) {
331        assert!(
332            !selector_requires_score_during_move(config),
333            "cartesian_product left child cannot contain ruin_recreate_move_selector because preview directors do not calculate scores",
334        );
335    }
336
337    fn collect_nodes<S>(
338        config: Option<&MoveSelectorConfig>,
339        descriptor: &SolutionDescriptor,
340        random_seed: Option<u64>,
341        nodes: &mut Vec<DescriptorSelectorNode<S>>,
342    ) where
343        S: PlanningSolution + 'static,
344        S::Score: Score,
345    {
346        match config {
347            Some(MoveSelectorConfig::UnionMoveSelector(union)) => {
348                for child in &union.selectors {
349                    collect_nodes::<S>(Some(child), descriptor, random_seed, nodes);
350                }
351            }
352            Some(MoveSelectorConfig::CartesianProductMoveSelector(cartesian)) => {
353                assert_eq!(
354                    cartesian.selectors.len(),
355                    2,
356                    "cartesian_product move selector requires exactly two child selectors"
357                );
358                assert_cartesian_left_preview_safe(&cartesian.selectors[0]);
359                let left = build_descriptor_flat_selector::<S>(
360                    Some(&cartesian.selectors[0]),
361                    descriptor,
362                    random_seed,
363                );
364                let right = build_descriptor_flat_selector::<S>(
365                    Some(&cartesian.selectors[1]),
366                    descriptor,
367                    random_seed,
368                );
369                nodes.push(DescriptorSelectorNode::Cartesian(
370                    CartesianProductSelector::new(left, right, wrap_descriptor_composite::<S>)
371                        .with_require_hard_improvement(cartesian.require_hard_improvement),
372                ));
373            }
374            other => {
375                let flat = build_descriptor_flat_selector::<S>(other, descriptor, random_seed);
376                nodes.extend(
377                    flat.into_selectors()
378                        .into_iter()
379                        .map(DescriptorSelectorNode::Leaf),
380                );
381            }
382        }
383    }
384
385    let mut nodes = Vec::new();
386    collect_nodes::<S>(config, descriptor, random_seed, &mut nodes);
387    assert!(
388        !nodes.is_empty(),
389        "move selector configuration produced no scalar neighborhoods"
390    );
391    let selection_order = match config {
392        Some(MoveSelectorConfig::UnionMoveSelector(union)) => union.selection_order,
393        _ => solverforge_config::UnionSelectionOrder::Sequential,
394    };
395    VecUnionSelector::with_selection_order(nodes, selection_order)
396}