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