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}