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.solver_scope().should_terminate_construction() {
210 break;
211 }
212
213 let element =
214 (self.index_to_element)(phase_scope.score_director().working_solution(), elem_idx);
215 if assigned_set.contains(&element) {
216 continue;
217 }
218
219 let mut step_scope = StepScope::new(&mut phase_scope);
220
221 {
222 let sd = step_scope.score_director_mut();
223 let insert_pos = (self.list_len)(sd.working_solution(), entity_idx);
224 sd.before_variable_changed(self.descriptor_index, entity_idx);
225 (self.list_insert)(sd.working_solution_mut(), entity_idx, insert_pos, element);
226 sd.after_variable_changed(self.descriptor_index, entity_idx);
227 }
228
229 let step_score = step_scope.calculate_score();
230 step_scope.set_step_score(step_score);
231 step_scope.complete();
232
233 entity_idx = (entity_idx + 1) % n_entities;
234 }
235
236 phase_scope.update_best_solution();
237 }
238
239 fn phase_type_name(&self) -> &'static str {
240 "ListRoundRobin"
241 }
242}
243
244#[allow(clippy::too_many_arguments)]
246pub fn build_list_construction<S, V>(
247 config: Option<&ConstructionHeuristicConfig>,
248 element_count: fn(&S) -> usize,
249 get_assigned: fn(&S) -> Vec<V>,
250 entity_count: fn(&S) -> usize,
251 list_len: fn(&S, usize) -> usize,
252 list_insert: fn(&mut S, usize, usize, V),
253 list_remove: fn(&mut S, usize, usize) -> V,
254 index_to_element: fn(&S, usize) -> V,
255 descriptor_index: usize,
256 depot_fn: Option<fn(&S) -> usize>,
257 distance_fn: Option<fn(&S, usize, usize) -> i64>,
258 element_load_fn: Option<fn(&S, usize) -> i64>,
259 capacity_fn: Option<fn(&S) -> i64>,
260 assign_route_fn: Option<fn(&mut S, usize, Vec<V>)>,
261 merge_feasible_fn: Option<fn(&S, &[usize]) -> bool>,
262 k_opt_get_route: Option<fn(&S, usize) -> Vec<usize>>,
263 k_opt_set_route: Option<fn(&mut S, usize, Vec<usize>)>,
264 k_opt_depot_fn: Option<fn(&S, usize) -> usize>,
265 k_opt_distance_fn: Option<fn(&S, usize, usize) -> i64>,
266 k_opt_feasible_fn: Option<fn(&S, usize, &[usize]) -> bool>,
267) -> ListConstruction<S, V>
268where
269 S: PlanningSolution,
270 V: Copy + PartialEq + Eq + std::hash::Hash + Send + Sync + fmt::Debug + 'static,
271{
272 let Some((ch_type, k)) = config.map(|cfg| (cfg.construction_heuristic_type, cfg.k)) else {
273 return ListConstruction::CheapestInsertion(ListCheapestInsertionPhase::new(
274 element_count,
275 get_assigned,
276 entity_count,
277 list_len,
278 list_insert,
279 list_remove,
280 index_to_element,
281 descriptor_index,
282 ));
283 };
284
285 match ch_type {
286 ConstructionHeuristicType::ListRoundRobin => {
287 ListConstruction::RoundRobin(ListRoundRobinPhase {
288 element_count,
289 get_assigned,
290 entity_count,
291 list_len,
292 list_insert,
293 index_to_element,
294 descriptor_index,
295 _phantom: PhantomData,
296 })
297 }
298 ConstructionHeuristicType::ListRegretInsertion => {
299 ListConstruction::RegretInsertion(ListRegretInsertionPhase::new(
300 element_count,
301 get_assigned,
302 entity_count,
303 list_len,
304 list_insert,
305 list_remove,
306 index_to_element,
307 descriptor_index,
308 ))
309 }
310 ConstructionHeuristicType::ListClarkeWright => {
311 match (
312 depot_fn,
313 distance_fn,
314 element_load_fn,
315 capacity_fn,
316 assign_route_fn,
317 ) {
318 (Some(depot), Some(dist), Some(load), Some(cap), Some(assign)) => {
319 ListConstruction::ClarkeWright(ListClarkeWrightPhase::new(
320 element_count,
321 get_assigned,
322 entity_count,
323 assign,
324 index_to_element,
325 depot,
326 dist,
327 load,
328 cap,
329 merge_feasible_fn,
330 descriptor_index,
331 ))
332 }
333 _ => {
334 panic!(
335 "list_clarke_wright requires depot_fn, distance_fn, \
336 element_load_fn, capacity_fn, and assign_route_fn"
337 );
338 }
339 }
340 }
341 ConstructionHeuristicType::ListKOpt => {
342 match (
343 k_opt_get_route,
344 k_opt_set_route,
345 k_opt_depot_fn,
346 k_opt_distance_fn,
347 ) {
348 (Some(get_route), Some(set_route), Some(ko_depot), Some(ko_dist)) => {
349 ListConstruction::KOpt(ListKOptPhase::new(
350 k,
351 entity_count,
352 get_route,
353 set_route,
354 ko_depot,
355 ko_dist,
356 k_opt_feasible_fn,
357 descriptor_index,
358 ))
359 }
360 _ => {
361 panic!(
362 "list_k_opt requires k_opt_get_route, k_opt_set_route, \
363 k_opt_depot_fn, and k_opt_distance_fn"
364 );
365 }
366 }
367 }
368 ConstructionHeuristicType::ListCheapestInsertion => {
369 ListConstruction::CheapestInsertion(ListCheapestInsertionPhase::new(
370 element_count,
371 get_assigned,
372 entity_count,
373 list_len,
374 list_insert,
375 list_remove,
376 index_to_element,
377 descriptor_index,
378 ))
379 }
380 ConstructionHeuristicType::FirstFit | ConstructionHeuristicType::CheapestInsertion => {
381 panic!(
382 "generic construction heuristic {:?} must be normalized before list construction",
383 ch_type
384 );
385 }
386 ConstructionHeuristicType::FirstFitDecreasing
387 | ConstructionHeuristicType::WeakestFit
388 | ConstructionHeuristicType::WeakestFitDecreasing
389 | ConstructionHeuristicType::StrongestFit
390 | ConstructionHeuristicType::StrongestFitDecreasing
391 | ConstructionHeuristicType::AllocateEntityFromQueue
392 | ConstructionHeuristicType::AllocateToValueFromQueue => {
393 panic!(
394 "standard construction heuristic {:?} configured against a list variable",
395 ch_type
396 );
397 }
398 }
399}
400
401#[cfg(test)]
402mod tests {
403 use super::*;
404 use solverforge_config::VariableTargetConfig;
405 use solverforge_core::score::SoftScore;
406
407 #[derive(Clone, Debug)]
408 struct TestSolution {
409 score: Option<SoftScore>,
410 }
411
412 impl PlanningSolution for TestSolution {
413 type Score = SoftScore;
414
415 fn score(&self) -> Option<Self::Score> {
416 self.score
417 }
418
419 fn set_score(&mut self, score: Option<Self::Score>) {
420 self.score = score;
421 }
422 }
423
424 fn config(kind: ConstructionHeuristicType) -> ConstructionHeuristicConfig {
425 ConstructionHeuristicConfig {
426 construction_heuristic_type: kind,
427 target: VariableTargetConfig::default(),
428 k: 2,
429 termination: None,
430 }
431 }
432
433 #[test]
434 fn list_builder_rejects_unnormalized_generic_construction() {
435 let panic = std::panic::catch_unwind(|| {
436 let _ = build_list_construction::<TestSolution, usize>(
437 Some(&config(ConstructionHeuristicType::FirstFit)),
438 |_| 0,
439 |_| Vec::new(),
440 |_| 0,
441 |_, _| 0,
442 |_, _, _, _| {},
443 |_, _, _| 0,
444 |_, _| 0,
445 0,
446 None,
447 None,
448 None,
449 None,
450 None,
451 None,
452 None,
453 None,
454 None,
455 None,
456 None,
457 );
458 })
459 .expect_err("unnormalized generic construction should panic");
460
461 let message = panic
462 .downcast_ref::<String>()
463 .map(String::as_str)
464 .or_else(|| panic.downcast_ref::<&'static str>().copied())
465 .unwrap_or("");
466 assert!(message.contains("must be normalized before list construction"));
467 }
468}