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}