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