1use solverforge_config::{ConstructionHeuristicConfig, ConstructionHeuristicType};
15use solverforge_core::domain::PlanningSolution;
16use std::fmt;
17use std::marker::PhantomData;
18
19use crate::heuristic::selector::nearby_list_change::CrossEntityDistanceMeter;
20use crate::manager::{
21 ListCheapestInsertionPhase, ListClarkeWrightPhase, ListKOptPhase, ListRegretInsertionPhase,
22};
23use crate::scope::{PhaseScope, SolverScope, StepScope};
24
25pub struct ListVariableMetadata<S, DM, IDM> {
28 pub cross_distance_meter: DM,
29 pub intra_distance_meter: IDM,
30 pub merge_feasible_fn: Option<fn(&S, &[usize]) -> bool>,
31 pub cw_depot_fn: Option<fn(&S) -> usize>,
32 pub cw_distance_fn: Option<fn(&S, usize, usize) -> i64>,
33 pub cw_element_load_fn: Option<fn(&S, usize) -> i64>,
34 pub cw_capacity_fn: Option<fn(&S) -> i64>,
35 pub cw_assign_route_fn: Option<fn(&mut S, usize, Vec<usize>)>,
36 pub k_opt_get_route: Option<fn(&S, usize) -> Vec<usize>>,
37 pub k_opt_set_route: Option<fn(&mut S, usize, Vec<usize>)>,
38 pub k_opt_depot_fn: Option<fn(&S, usize) -> usize>,
39 pub k_opt_distance_fn: Option<fn(&S, usize, usize) -> i64>,
40 pub k_opt_feasible_fn: Option<fn(&S, usize, &[usize]) -> bool>,
41 _phantom: PhantomData<fn() -> S>,
42}
43
44pub trait ListVariableEntity<S> {
48 type CrossDistanceMeter: CrossEntityDistanceMeter<S> + Clone + fmt::Debug;
49 type IntraDistanceMeter: CrossEntityDistanceMeter<S> + Clone + fmt::Debug + 'static;
50
51 const HAS_STOCK_LIST_VARIABLE: bool;
52 const STOCK_LIST_VARIABLE_NAME: &'static str;
53 const STOCK_LIST_ELEMENT_SOURCE: Option<&'static str>;
54
55 fn list_field(entity: &Self) -> &[usize];
56 fn list_field_mut(entity: &mut Self) -> &mut Vec<usize>;
57 fn list_metadata() -> ListVariableMetadata<S, Self::CrossDistanceMeter, Self::IntraDistanceMeter>;
58}
59
60impl<S, DM, IDM> ListVariableMetadata<S, DM, IDM> {
61 #[allow(clippy::too_many_arguments)]
62 pub fn new(
63 cross_distance_meter: DM,
64 intra_distance_meter: IDM,
65 merge_feasible_fn: Option<fn(&S, &[usize]) -> bool>,
66 cw_depot_fn: Option<fn(&S) -> usize>,
67 cw_distance_fn: Option<fn(&S, usize, usize) -> i64>,
68 cw_element_load_fn: Option<fn(&S, usize) -> i64>,
69 cw_capacity_fn: Option<fn(&S) -> i64>,
70 cw_assign_route_fn: Option<fn(&mut S, usize, Vec<usize>)>,
71 k_opt_get_route: Option<fn(&S, usize) -> Vec<usize>>,
72 k_opt_set_route: Option<fn(&mut S, usize, Vec<usize>)>,
73 k_opt_depot_fn: Option<fn(&S, usize) -> usize>,
74 k_opt_distance_fn: Option<fn(&S, usize, usize) -> i64>,
75 k_opt_feasible_fn: Option<fn(&S, usize, &[usize]) -> bool>,
76 ) -> Self {
77 Self {
78 cross_distance_meter,
79 intra_distance_meter,
80 merge_feasible_fn,
81 cw_depot_fn,
82 cw_distance_fn,
83 cw_element_load_fn,
84 cw_capacity_fn,
85 cw_assign_route_fn,
86 k_opt_get_route,
87 k_opt_set_route,
88 k_opt_depot_fn,
89 k_opt_distance_fn,
90 k_opt_feasible_fn,
91 _phantom: PhantomData,
92 }
93 }
94}
95
96pub enum ListConstruction<S, V>
98where
99 S: PlanningSolution,
100 V: Copy + PartialEq + Eq + std::hash::Hash + Send + Sync + 'static,
101{
102 RoundRobin(ListRoundRobinPhase<S, V>),
103 CheapestInsertion(ListCheapestInsertionPhase<S, V>),
104 RegretInsertion(ListRegretInsertionPhase<S, V>),
105 ClarkeWright(ListClarkeWrightPhase<S, V>),
106 KOpt(ListKOptPhase<S, V>),
107}
108
109impl<S, V> fmt::Debug for ListConstruction<S, V>
110where
111 S: PlanningSolution,
112 V: Copy + PartialEq + Eq + std::hash::Hash + Send + Sync + fmt::Debug + 'static,
113{
114 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
115 match self {
116 Self::RoundRobin(phase) => write!(f, "ListConstruction::RoundRobin({phase:?})"),
117 Self::CheapestInsertion(phase) => {
118 write!(f, "ListConstruction::CheapestInsertion({phase:?})")
119 }
120 Self::RegretInsertion(phase) => {
121 write!(f, "ListConstruction::RegretInsertion({phase:?})")
122 }
123 Self::ClarkeWright(phase) => {
124 write!(f, "ListConstruction::ClarkeWright({phase:?})")
125 }
126 Self::KOpt(phase) => write!(f, "ListConstruction::KOpt({phase:?})"),
127 }
128 }
129}
130
131impl<S, V, D, ProgressCb> crate::phase::Phase<S, D, ProgressCb> for ListConstruction<S, V>
132where
133 S: PlanningSolution,
134 V: Copy + PartialEq + Eq + std::hash::Hash + Send + Sync + fmt::Debug + 'static,
135 D: solverforge_scoring::Director<S>,
136 ProgressCb: crate::scope::ProgressCallback<S>,
137{
138 fn solve(&mut self, solver_scope: &mut crate::scope::SolverScope<'_, S, D, ProgressCb>) {
139 match self {
140 Self::RoundRobin(phase) => phase.solve(solver_scope),
141 Self::CheapestInsertion(phase) => phase.solve(solver_scope),
142 Self::RegretInsertion(phase) => phase.solve(solver_scope),
143 Self::ClarkeWright(phase) => phase.solve(solver_scope),
144 Self::KOpt(phase) => phase.solve(solver_scope),
145 }
146 }
147
148 fn phase_type_name(&self) -> &'static str {
149 "ListConstruction"
150 }
151}
152
153pub struct ListRoundRobinPhase<S, V>
154where
155 S: PlanningSolution,
156 V: Copy + PartialEq + Eq + std::hash::Hash + Send + Sync + 'static,
157{
158 element_count: fn(&S) -> usize,
159 get_assigned: fn(&S) -> Vec<V>,
160 entity_count: fn(&S) -> usize,
161 list_len: fn(&S, usize) -> usize,
162 list_insert: fn(&mut S, usize, usize, V),
163 index_to_element: fn(&S, usize) -> V,
164 descriptor_index: usize,
165 _phantom: PhantomData<(fn() -> S, fn() -> V)>,
166}
167
168impl<S, V> fmt::Debug for ListRoundRobinPhase<S, V>
169where
170 S: PlanningSolution,
171 V: Copy + PartialEq + Eq + std::hash::Hash + Send + Sync + 'static,
172{
173 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
174 f.debug_struct("ListRoundRobinPhase").finish()
175 }
176}
177
178impl<S, V, D, ProgressCb> crate::phase::Phase<S, D, ProgressCb> for ListRoundRobinPhase<S, V>
179where
180 S: PlanningSolution,
181 V: Copy + PartialEq + Eq + std::hash::Hash + Send + Sync + fmt::Debug + 'static,
182 D: solverforge_scoring::Director<S>,
183 ProgressCb: crate::scope::ProgressCallback<S>,
184{
185 fn solve(&mut self, solver_scope: &mut SolverScope<'_, S, D, ProgressCb>) {
186 let mut phase_scope = PhaseScope::new(solver_scope, 0);
187
188 let solution = phase_scope.score_director().working_solution();
189 let n_elements = (self.element_count)(solution);
190 let n_entities = (self.entity_count)(solution);
191
192 if n_entities == 0 || n_elements == 0 {
193 let _score = phase_scope.score_director_mut().calculate_score();
194 phase_scope.update_best_solution();
195 return;
196 }
197
198 let assigned: Vec<V> = (self.get_assigned)(phase_scope.score_director().working_solution());
199 if assigned.len() >= n_elements {
200 let _score = phase_scope.score_director_mut().calculate_score();
201 phase_scope.update_best_solution();
202 return;
203 }
204
205 let assigned_set: std::collections::HashSet<V> = assigned.into_iter().collect();
206 let mut entity_idx = 0;
207
208 for elem_idx in 0..n_elements {
209 if phase_scope
210 .solver_scope_mut()
211 .should_terminate_construction()
212 {
213 break;
214 }
215
216 let element =
217 (self.index_to_element)(phase_scope.score_director().working_solution(), elem_idx);
218 if assigned_set.contains(&element) {
219 continue;
220 }
221
222 let mut step_scope = StepScope::new(&mut phase_scope);
223
224 {
225 let sd = step_scope.score_director_mut();
226 let insert_pos = (self.list_len)(sd.working_solution(), entity_idx);
227 sd.before_variable_changed(self.descriptor_index, entity_idx);
228 (self.list_insert)(sd.working_solution_mut(), entity_idx, insert_pos, element);
229 sd.after_variable_changed(self.descriptor_index, entity_idx);
230 }
231
232 let step_score = step_scope.calculate_score();
233 step_scope.set_step_score(step_score);
234 step_scope.complete();
235
236 entity_idx = (entity_idx + 1) % n_entities;
237 }
238
239 phase_scope.update_best_solution();
240 }
241
242 fn phase_type_name(&self) -> &'static str {
243 "ListRoundRobin"
244 }
245}
246
247#[allow(clippy::too_many_arguments)]
249pub fn build_list_construction<S, V>(
250 config: Option<&ConstructionHeuristicConfig>,
251 element_count: fn(&S) -> usize,
252 get_assigned: fn(&S) -> Vec<V>,
253 entity_count: fn(&S) -> usize,
254 list_len: fn(&S, usize) -> usize,
255 list_insert: fn(&mut S, usize, usize, V),
256 list_remove: fn(&mut S, usize, usize) -> V,
257 index_to_element: fn(&S, usize) -> V,
258 descriptor_index: usize,
259 depot_fn: Option<fn(&S) -> usize>,
260 distance_fn: Option<fn(&S, usize, usize) -> i64>,
261 element_load_fn: Option<fn(&S, usize) -> i64>,
262 capacity_fn: Option<fn(&S) -> i64>,
263 assign_route_fn: Option<fn(&mut S, usize, Vec<V>)>,
264 merge_feasible_fn: Option<fn(&S, &[usize]) -> bool>,
265 k_opt_get_route: Option<fn(&S, usize) -> Vec<usize>>,
266 k_opt_set_route: Option<fn(&mut S, usize, Vec<usize>)>,
267 k_opt_depot_fn: Option<fn(&S, usize) -> usize>,
268 k_opt_distance_fn: Option<fn(&S, usize, usize) -> i64>,
269 k_opt_feasible_fn: Option<fn(&S, usize, &[usize]) -> bool>,
270) -> ListConstruction<S, V>
271where
272 S: PlanningSolution,
273 V: Copy + PartialEq + Eq + std::hash::Hash + Send + Sync + fmt::Debug + 'static,
274{
275 let Some((ch_type, k)) = config.map(|cfg| (cfg.construction_heuristic_type, cfg.k)) else {
276 return ListConstruction::CheapestInsertion(ListCheapestInsertionPhase::new(
277 element_count,
278 get_assigned,
279 entity_count,
280 list_len,
281 list_insert,
282 list_remove,
283 index_to_element,
284 descriptor_index,
285 ));
286 };
287
288 match ch_type {
289 ConstructionHeuristicType::ListRoundRobin => {
290 ListConstruction::RoundRobin(ListRoundRobinPhase {
291 element_count,
292 get_assigned,
293 entity_count,
294 list_len,
295 list_insert,
296 index_to_element,
297 descriptor_index,
298 _phantom: PhantomData,
299 })
300 }
301 ConstructionHeuristicType::ListRegretInsertion => {
302 ListConstruction::RegretInsertion(ListRegretInsertionPhase::new(
303 element_count,
304 get_assigned,
305 entity_count,
306 list_len,
307 list_insert,
308 list_remove,
309 index_to_element,
310 descriptor_index,
311 ))
312 }
313 ConstructionHeuristicType::ListClarkeWright => {
314 match (
315 depot_fn,
316 distance_fn,
317 element_load_fn,
318 capacity_fn,
319 assign_route_fn,
320 ) {
321 (Some(depot), Some(dist), Some(load), Some(cap), Some(assign)) => {
322 ListConstruction::ClarkeWright(ListClarkeWrightPhase::new(
323 element_count,
324 get_assigned,
325 entity_count,
326 assign,
327 index_to_element,
328 depot,
329 dist,
330 load,
331 cap,
332 merge_feasible_fn,
333 descriptor_index,
334 ))
335 }
336 _ => {
337 panic!(
338 "list_clarke_wright requires depot_fn, distance_fn, \
339 element_load_fn, capacity_fn, and assign_route_fn"
340 );
341 }
342 }
343 }
344 ConstructionHeuristicType::ListKOpt => {
345 match (
346 k_opt_get_route,
347 k_opt_set_route,
348 k_opt_depot_fn,
349 k_opt_distance_fn,
350 ) {
351 (Some(get_route), Some(set_route), Some(ko_depot), Some(ko_dist)) => {
352 ListConstruction::KOpt(ListKOptPhase::new(
353 k,
354 entity_count,
355 get_route,
356 set_route,
357 ko_depot,
358 ko_dist,
359 k_opt_feasible_fn,
360 descriptor_index,
361 ))
362 }
363 _ => {
364 panic!(
365 "list_k_opt requires k_opt_get_route, k_opt_set_route, \
366 k_opt_depot_fn, and k_opt_distance_fn"
367 );
368 }
369 }
370 }
371 ConstructionHeuristicType::ListCheapestInsertion => {
372 ListConstruction::CheapestInsertion(ListCheapestInsertionPhase::new(
373 element_count,
374 get_assigned,
375 entity_count,
376 list_len,
377 list_insert,
378 list_remove,
379 index_to_element,
380 descriptor_index,
381 ))
382 }
383 ConstructionHeuristicType::FirstFit | ConstructionHeuristicType::CheapestInsertion => {
384 panic!(
385 "generic construction heuristic {:?} must be normalized before list construction",
386 ch_type
387 );
388 }
389 ConstructionHeuristicType::FirstFitDecreasing
390 | ConstructionHeuristicType::WeakestFit
391 | ConstructionHeuristicType::WeakestFitDecreasing
392 | ConstructionHeuristicType::StrongestFit
393 | ConstructionHeuristicType::StrongestFitDecreasing
394 | ConstructionHeuristicType::AllocateEntityFromQueue
395 | ConstructionHeuristicType::AllocateToValueFromQueue => {
396 panic!(
397 "standard construction heuristic {:?} configured against a list variable",
398 ch_type
399 );
400 }
401 }
402}
403
404#[cfg(test)]
405mod tests {
406 use super::*;
407 use solverforge_config::VariableTargetConfig;
408 use solverforge_core::score::SoftScore;
409
410 #[derive(Clone, Debug)]
411 struct TestSolution {
412 score: Option<SoftScore>,
413 }
414
415 impl PlanningSolution for TestSolution {
416 type Score = SoftScore;
417
418 fn score(&self) -> Option<Self::Score> {
419 self.score
420 }
421
422 fn set_score(&mut self, score: Option<Self::Score>) {
423 self.score = score;
424 }
425 }
426
427 fn config(kind: ConstructionHeuristicType) -> ConstructionHeuristicConfig {
428 ConstructionHeuristicConfig {
429 construction_heuristic_type: kind,
430 target: VariableTargetConfig::default(),
431 k: 2,
432 termination: None,
433 }
434 }
435
436 #[test]
437 fn list_builder_rejects_unnormalized_generic_construction() {
438 let panic = std::panic::catch_unwind(|| {
439 let _ = build_list_construction::<TestSolution, usize>(
440 Some(&config(ConstructionHeuristicType::FirstFit)),
441 |_| 0,
442 |_| Vec::new(),
443 |_| 0,
444 |_, _| 0,
445 |_, _, _, _| {},
446 |_, _, _| 0,
447 |_, _| 0,
448 0,
449 None,
450 None,
451 None,
452 None,
453 None,
454 None,
455 None,
456 None,
457 None,
458 None,
459 None,
460 );
461 })
462 .expect_err("unnormalized generic construction should panic");
463
464 let message = panic
465 .downcast_ref::<String>()
466 .map(String::as_str)
467 .or_else(|| panic.downcast_ref::<&'static str>().copied())
468 .unwrap_or("");
469 assert!(message.contains("must be normalized before list construction"));
470 }
471}