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::ListPermuteMoveSelector(_)
251            | MoveSelectorConfig::ListPrecedenceMoveSelector(_)
252            | MoveSelectorConfig::NearbyListSwapMoveSelector(_)
253            | MoveSelectorConfig::SublistChangeMoveSelector(_)
254            | MoveSelectorConfig::SublistSwapMoveSelector(_)
255            | MoveSelectorConfig::ListReverseMoveSelector(_)
256            | MoveSelectorConfig::KOptMoveSelector(_)
257            | MoveSelectorConfig::ListRuinMoveSelector(_) => {
258                panic!("list move selector configured against a scalar-variable model");
259            }
260            MoveSelectorConfig::CartesianProductMoveSelector(_) => {
261                panic!(
262                    "nested cartesian_product move selectors are not supported inside descriptor cartesian children"
263                );
264            }
265            MoveSelectorConfig::ConflictRepairMoveSelector(_) => {
266                panic!("conflict_repair_move_selector must be handled by the canonical runtime");
267            }
268            MoveSelectorConfig::CompoundConflictRepairMoveSelector(_) => {
269                panic!(
270                    "compound_conflict_repair_move_selector must be handled by the canonical runtime"
271                );
272            }
273            MoveSelectorConfig::GroupedScalarMoveSelector(_) => {
274                panic!("grouped_scalar_move_selector must be handled by the canonical runtime");
275            }
276        }
277    }
278
279    match config {
280        Some(cfg) => collect::<S>(cfg, descriptor, &bindings, random_seed, &mut leaves),
281        None => {
282            for binding in bindings {
283                leaves.push(DescriptorLeafSelector::Change(
284                    DescriptorChangeMoveSelector::new(binding.clone(), descriptor.clone(), None),
285                ));
286                leaves.push(DescriptorLeafSelector::Swap(
287                    DescriptorSwapMoveSelector::new(binding, descriptor.clone()),
288                ));
289            }
290        }
291    }
292
293    assert!(
294        !leaves.is_empty(),
295        "move selector configuration produced no scalar neighborhoods"
296    );
297
298    let selection_order = match config {
299        Some(MoveSelectorConfig::UnionMoveSelector(union)) => union.selection_order,
300        _ => solverforge_config::UnionSelectionOrder::Sequential,
301    };
302    VecUnionSelector::with_selection_order(leaves, selection_order)
303}
304
305pub fn build_descriptor_move_selector<S>(
306    config: Option<&MoveSelectorConfig>,
307    descriptor: &SolutionDescriptor,
308    random_seed: Option<u64>,
309) -> DescriptorSelector<S>
310where
311    S: PlanningSolution + 'static,
312    S::Score: Score,
313{
314    fn selector_requires_score_during_move(config: &MoveSelectorConfig) -> bool {
315        match config {
316            MoveSelectorConfig::RuinRecreateMoveSelector(_) => true,
317            MoveSelectorConfig::LimitedNeighborhood(limit) => {
318                selector_requires_score_during_move(limit.selector.as_ref())
319            }
320            MoveSelectorConfig::UnionMoveSelector(union) => union
321                .selectors
322                .iter()
323                .any(selector_requires_score_during_move),
324            MoveSelectorConfig::CartesianProductMoveSelector(_) => true,
325            _ => false,
326        }
327    }
328
329    fn assert_cartesian_left_preview_safe(config: &MoveSelectorConfig) {
330        assert!(
331            !selector_requires_score_during_move(config),
332            "cartesian_product left child cannot contain ruin_recreate_move_selector because preview directors do not calculate scores",
333        );
334    }
335
336    fn collect_nodes<S>(
337        config: Option<&MoveSelectorConfig>,
338        descriptor: &SolutionDescriptor,
339        random_seed: Option<u64>,
340        nodes: &mut Vec<DescriptorSelectorNode<S>>,
341    ) where
342        S: PlanningSolution + 'static,
343        S::Score: Score,
344    {
345        match config {
346            Some(MoveSelectorConfig::UnionMoveSelector(union)) => {
347                for child in &union.selectors {
348                    collect_nodes::<S>(Some(child), descriptor, random_seed, nodes);
349                }
350            }
351            Some(MoveSelectorConfig::CartesianProductMoveSelector(cartesian)) => {
352                assert_eq!(
353                    cartesian.selectors.len(),
354                    2,
355                    "cartesian_product move selector requires exactly two child selectors"
356                );
357                assert_cartesian_left_preview_safe(&cartesian.selectors[0]);
358                let left = build_descriptor_flat_selector::<S>(
359                    Some(&cartesian.selectors[0]),
360                    descriptor,
361                    random_seed,
362                );
363                let right = build_descriptor_flat_selector::<S>(
364                    Some(&cartesian.selectors[1]),
365                    descriptor,
366                    random_seed,
367                );
368                nodes.push(DescriptorSelectorNode::Cartesian(
369                    CartesianProductSelector::new(left, right)
370                        .with_require_hard_improvement(cartesian.require_hard_improvement),
371                ));
372            }
373            other => {
374                let flat = build_descriptor_flat_selector::<S>(other, descriptor, random_seed);
375                nodes.extend(
376                    flat.into_selectors()
377                        .into_iter()
378                        .map(DescriptorSelectorNode::Leaf),
379                );
380            }
381        }
382    }
383
384    let mut nodes = Vec::new();
385    collect_nodes::<S>(config, descriptor, random_seed, &mut nodes);
386    assert!(
387        !nodes.is_empty(),
388        "move selector configuration produced no scalar neighborhoods"
389    );
390    let selection_order = match config {
391        Some(MoveSelectorConfig::UnionMoveSelector(union)) => union.selection_order,
392        _ => solverforge_config::UnionSelectionOrder::Sequential,
393    };
394    VecUnionSelector::with_selection_order(nodes, selection_order)
395}