solverforge_solver/heuristic/selector/
k_opt.rs

1//! K-opt move selector for tour optimization.
2//!
3//! Generates k-opt moves by enumerating all valid cut point combinations
4//! within selected entities and applying reconnection patterns.
5//!
6//! # Complexity
7//!
8//! For a route of length n and k-opt:
9//! - Full enumeration: O(n^k) cut combinations × reconnection patterns
10//! - Use `NearbyKOptMoveSelector` to reduce to O(n × m^(k-1)) with nearby selection
11//!
12//! # Example
13//!
14//! ```
15//! use solverforge_solver::heuristic::selector::k_opt::{KOptMoveSelector, KOptConfig};
16//! use solverforge_solver::heuristic::selector::entity::FromSolutionEntitySelector;
17//! use solverforge_core::domain::PlanningSolution;
18//! use solverforge_core::score::SimpleScore;
19//!
20//! #[derive(Clone, Debug)]
21//! struct Tour { cities: Vec<i32>, score: Option<SimpleScore> }
22//!
23//! impl PlanningSolution for Tour {
24//!     type Score = SimpleScore;
25//!     fn score(&self) -> Option<Self::Score> { self.score }
26//!     fn set_score(&mut self, score: Option<Self::Score>) { self.score = score; }
27//! }
28//!
29//! fn list_len(s: &Tour, _: usize) -> usize { s.cities.len() }
30//! fn sublist_remove(s: &mut Tour, _: usize, start: usize, end: usize) -> Vec<i32> {
31//!     s.cities.drain(start..end).collect()
32//! }
33//! fn sublist_insert(s: &mut Tour, _: usize, pos: usize, items: Vec<i32>) {
34//!     for (i, item) in items.into_iter().enumerate() {
35//!         s.cities.insert(pos + i, item);
36//!     }
37//! }
38//!
39//! let config = KOptConfig::new(3); // 3-opt
40//!
41//! let selector = KOptMoveSelector::<Tour, i32, _>::new(
42//!     FromSolutionEntitySelector::new(0),
43//!     config,
44//!     list_len,
45//!     sublist_remove,
46//!     sublist_insert,
47//!     "cities",
48//!     0,
49//! );
50//! ```
51
52use std::fmt::Debug;
53use std::marker::PhantomData;
54
55use solverforge_core::domain::PlanningSolution;
56use solverforge_scoring::ScoreDirector;
57
58use crate::heuristic::r#move::k_opt_reconnection::{
59    enumerate_reconnections, KOptReconnection, THREE_OPT_RECONNECTIONS,
60};
61use crate::heuristic::r#move::{CutPoint, KOptMove};
62
63use super::entity::EntitySelector;
64use super::typed_move_selector::MoveSelector;
65
66/// Configuration for k-opt move generation.
67#[derive(Debug, Clone)]
68pub struct KOptConfig {
69    /// The k value (2-5).
70    pub k: usize,
71    /// Minimum segment length between cuts (default: 1).
72    pub min_segment_len: usize,
73    /// Whether to use only a subset of reconnection patterns.
74    pub limited_patterns: bool,
75}
76
77impl KOptConfig {
78    /// Creates a new k-opt configuration.
79    ///
80    /// # Panics
81    ///
82    /// Panics if k < 2 or k > 5.
83    pub fn new(k: usize) -> Self {
84        assert!((2..=5).contains(&k), "k must be between 2 and 5");
85        Self {
86            k,
87            min_segment_len: 1,
88            limited_patterns: false,
89        }
90    }
91
92    /// Sets minimum segment length between cuts.
93    pub fn with_min_segment_len(mut self, len: usize) -> Self {
94        self.min_segment_len = len;
95        self
96    }
97
98    /// Enables limited pattern mode (faster but less thorough).
99    pub fn with_limited_patterns(mut self, limited: bool) -> Self {
100        self.limited_patterns = limited;
101        self
102    }
103}
104
105/// A move selector that generates k-opt moves.
106///
107/// Enumerates all valid cut point combinations for each selected entity
108/// and generates moves for each reconnection pattern.
109pub struct KOptMoveSelector<S, V, ES> {
110    /// Selects entities (routes) to apply k-opt to.
111    entity_selector: ES,
112    /// K-opt configuration.
113    config: KOptConfig,
114    /// Reconnection patterns to use.
115    patterns: Vec<&'static KOptReconnection>,
116    /// Get list length for an entity.
117    list_len: fn(&S, usize) -> usize,
118    /// Remove sublist [start, end).
119    sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
120    /// Insert elements at position.
121    sublist_insert: fn(&mut S, usize, usize, Vec<V>),
122    /// Variable name.
123    variable_name: &'static str,
124    /// Descriptor index.
125    descriptor_index: usize,
126    _phantom: PhantomData<(S, V)>,
127}
128
129impl<S, V: Debug, ES: Debug> Debug for KOptMoveSelector<S, V, ES> {
130    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131        f.debug_struct("KOptMoveSelector")
132            .field("entity_selector", &self.entity_selector)
133            .field("config", &self.config)
134            .field("pattern_count", &self.patterns.len())
135            .field("variable_name", &self.variable_name)
136            .finish()
137    }
138}
139
140impl<S: PlanningSolution, V, ES> KOptMoveSelector<S, V, ES> {
141    /// Creates a new k-opt move selector.
142    #[allow(clippy::too_many_arguments)]
143    pub fn new(
144        entity_selector: ES,
145        config: KOptConfig,
146        list_len: fn(&S, usize) -> usize,
147        sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
148        sublist_insert: fn(&mut S, usize, usize, Vec<V>),
149        variable_name: &'static str,
150        descriptor_index: usize,
151    ) -> Self {
152        // Get static patterns for k=3, generate for others
153        let patterns: Vec<&'static KOptReconnection> = if config.k == 3 {
154            THREE_OPT_RECONNECTIONS.iter().collect()
155        } else {
156            // For other k values, we need to leak the patterns to get 'static lifetime
157            // This is a one-time allocation per selector creation
158            let generated = enumerate_reconnections(config.k);
159            let leaked: &'static [KOptReconnection] = Box::leak(generated.into_boxed_slice());
160            leaked.iter().collect()
161        };
162
163        Self {
164            entity_selector,
165            config,
166            patterns,
167            list_len,
168            sublist_remove,
169            sublist_insert,
170            variable_name,
171            descriptor_index,
172            _phantom: PhantomData,
173        }
174    }
175}
176
177impl<S, V, ES> MoveSelector<S, KOptMove<S, V>> for KOptMoveSelector<S, V, ES>
178where
179    S: PlanningSolution,
180    ES: EntitySelector<S>,
181    V: Clone + Send + Sync + Debug + 'static,
182{
183    fn iter_moves<'a, D: ScoreDirector<S>>(
184        &'a self,
185        score_director: &'a D,
186    ) -> Box<dyn Iterator<Item = KOptMove<S, V>> + 'a> {
187        let k = self.config.k;
188        let min_seg = self.config.min_segment_len;
189        let patterns = &self.patterns;
190        let list_len = self.list_len;
191        let sublist_remove = self.sublist_remove;
192        let sublist_insert = self.sublist_insert;
193        let variable_name = self.variable_name;
194        let descriptor_index = self.descriptor_index;
195
196        let iter = self
197            .entity_selector
198            .iter(score_director)
199            .flat_map(move |entity_ref| {
200                let entity_idx = entity_ref.entity_index;
201                let solution = score_director.working_solution();
202                let len = list_len(solution, entity_idx);
203
204                // Generate all valid cut combinations
205                let cuts_iter = CutCombinationIterator::new(k, len, min_seg, entity_idx);
206
207                cuts_iter.flat_map(move |cuts| {
208                    // For each cut combination, generate moves for each pattern
209                    patterns.iter().map(move |&pattern| {
210                        KOptMove::new(
211                            &cuts,
212                            pattern,
213                            list_len,
214                            sublist_remove,
215                            sublist_insert,
216                            variable_name,
217                            descriptor_index,
218                        )
219                    })
220                })
221            });
222
223        Box::new(iter)
224    }
225
226    fn size<D: ScoreDirector<S>>(&self, score_director: &D) -> usize {
227        let k = self.config.k;
228        let min_seg = self.config.min_segment_len;
229        let pattern_count = self.patterns.len();
230
231        self.entity_selector
232            .iter(score_director)
233            .map(|entity_ref| {
234                let solution = score_director.working_solution();
235                let len = (self.list_len)(solution, entity_ref.entity_index);
236                count_cut_combinations(k, len, min_seg) * pattern_count
237            })
238            .sum()
239    }
240}
241
242/// Iterator over all valid k-cut combinations for a route of given length.
243struct CutCombinationIterator {
244    k: usize,
245    len: usize,
246    min_seg: usize,
247    entity_idx: usize,
248    /// Current cut positions.
249    positions: Vec<usize>,
250    /// Whether we've exhausted all combinations.
251    done: bool,
252}
253
254impl CutCombinationIterator {
255    fn new(k: usize, len: usize, min_seg: usize, entity_idx: usize) -> Self {
256        // Minimum length required: k cuts need k+1 segments of min_seg each
257        let min_len = (k + 1) * min_seg;
258
259        if len < min_len {
260            return Self {
261                k,
262                len,
263                min_seg,
264                entity_idx,
265                positions: vec![],
266                done: true,
267            };
268        }
269
270        // Initialize with first valid combination
271        // Cuts must be at positions that leave min_seg elements between them
272        let mut positions = Vec::with_capacity(k);
273        for i in 0..k {
274            positions.push(min_seg * (i + 1));
275        }
276
277        Self {
278            k,
279            len,
280            min_seg,
281            entity_idx,
282            positions,
283            done: false,
284        }
285    }
286
287    fn advance(&mut self) -> bool {
288        if self.done || self.positions.is_empty() {
289            return false;
290        }
291
292        // Find the rightmost position that can be incremented
293        let k = self.k;
294        let len = self.len;
295        let min_seg = self.min_seg;
296
297        for i in (0..k).rev() {
298            // Maximum position for cut i:
299            // Need to leave room for (k - i - 1) more cuts after this one,
300            // each separated by min_seg, plus min_seg at the end
301            let max_pos = len - min_seg * (k - i);
302
303            if self.positions[i] < max_pos {
304                self.positions[i] += 1;
305                // Reset all positions after i
306                for j in (i + 1)..k {
307                    self.positions[j] = self.positions[j - 1] + min_seg;
308                }
309                return true;
310            }
311        }
312
313        self.done = true;
314        false
315    }
316}
317
318impl Iterator for CutCombinationIterator {
319    type Item = Vec<CutPoint>;
320
321    fn next(&mut self) -> Option<Self::Item> {
322        if self.done {
323            return None;
324        }
325
326        let cuts: Vec<CutPoint> = self
327            .positions
328            .iter()
329            .map(|&pos| CutPoint::new(self.entity_idx, pos))
330            .collect();
331
332        self.advance();
333
334        Some(cuts)
335    }
336}
337
338/// Counts the number of valid k-cut combinations for a route of length len.
339fn count_cut_combinations(k: usize, len: usize, min_seg: usize) -> usize {
340    // This is equivalent to C(n - (k+1)*min_seg + k, k)
341    // where we're choosing k positions from the "free" slots
342    let min_len = (k + 1) * min_seg;
343    if len < min_len {
344        return 0;
345    }
346
347    let free_slots = len - min_len + k;
348    binomial(free_slots, k)
349}
350
351/// Compute binomial coefficient C(n, k).
352fn binomial(n: usize, k: usize) -> usize {
353    if k > n {
354        return 0;
355    }
356    if k == 0 || k == n {
357        return 1;
358    }
359
360    let k = k.min(n - k); // Use symmetry
361    let mut result = 1;
362    for i in 0..k {
363        result = result * (n - i) / (i + 1);
364    }
365    result
366}
367
368/// A distance meter for list element positions.
369///
370/// Measures distance between elements at two positions in a list.
371/// Used by `NearbyKOptMoveSelector` to limit k-opt search space.
372pub trait ListPositionDistanceMeter<S>: Send + Sync + Debug {
373    /// Measures distance between elements at two positions in the same entity.
374    fn distance(&self, solution: &S, entity_idx: usize, pos_a: usize, pos_b: usize) -> f64;
375}
376
377/// Default distance meter using position difference.
378#[derive(Debug, Clone, Copy)]
379pub struct DefaultDistanceMeter;
380
381impl<S> ListPositionDistanceMeter<S> for DefaultDistanceMeter {
382    fn distance(&self, _solution: &S, _entity_idx: usize, pos_a: usize, pos_b: usize) -> f64 {
383        (pos_a as f64 - pos_b as f64).abs()
384    }
385}
386
387/// A k-opt move selector with nearby selection for improved performance.
388///
389/// Instead of enumerating all O(n^k) cut combinations, uses distance-based
390/// pruning to reduce to O(n * m^(k-1)) where m = max_nearby_size.
391///
392/// # How It Works
393///
394/// 1. First cut: all positions in the route
395/// 2. Second cut: only positions nearby (by element distance) to first cut
396/// 3. Third cut: only positions nearby to second cut
397/// 4. etc.
398///
399/// This dramatically reduces the search space for large routes.
400pub struct NearbyKOptMoveSelector<S, V, D: ListPositionDistanceMeter<S>, ES> {
401    /// Selects entities (routes) to apply k-opt to.
402    entity_selector: ES,
403    /// Distance meter for nearby selection.
404    distance_meter: D,
405    /// Maximum nearby positions to consider.
406    max_nearby: usize,
407    /// K-opt configuration.
408    config: KOptConfig,
409    /// Reconnection patterns.
410    patterns: Vec<&'static KOptReconnection>,
411    /// Get list length for an entity.
412    list_len: fn(&S, usize) -> usize,
413    /// Remove sublist.
414    sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
415    /// Insert sublist.
416    sublist_insert: fn(&mut S, usize, usize, Vec<V>),
417    /// Variable name.
418    variable_name: &'static str,
419    /// Descriptor index.
420    descriptor_index: usize,
421    _phantom: PhantomData<(S, V)>,
422}
423
424impl<S, V: Debug, D: ListPositionDistanceMeter<S>, ES: Debug> Debug
425    for NearbyKOptMoveSelector<S, V, D, ES>
426{
427    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
428        f.debug_struct("NearbyKOptMoveSelector")
429            .field("entity_selector", &self.entity_selector)
430            .field("max_nearby", &self.max_nearby)
431            .field("config", &self.config)
432            .field("pattern_count", &self.patterns.len())
433            .finish()
434    }
435}
436
437impl<S: PlanningSolution, V, D: ListPositionDistanceMeter<S>, ES>
438    NearbyKOptMoveSelector<S, V, D, ES>
439{
440    /// Creates a new nearby k-opt move selector.
441    #[allow(clippy::too_many_arguments)]
442    pub fn new(
443        entity_selector: ES,
444        distance_meter: D,
445        max_nearby: usize,
446        config: KOptConfig,
447        list_len: fn(&S, usize) -> usize,
448        sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
449        sublist_insert: fn(&mut S, usize, usize, Vec<V>),
450        variable_name: &'static str,
451        descriptor_index: usize,
452    ) -> Self {
453        let patterns: Vec<&'static KOptReconnection> = if config.k == 3 {
454            THREE_OPT_RECONNECTIONS.iter().collect()
455        } else {
456            let generated = enumerate_reconnections(config.k);
457            let leaked: &'static [KOptReconnection] = Box::leak(generated.into_boxed_slice());
458            leaked.iter().collect()
459        };
460
461        Self {
462            entity_selector,
463            distance_meter,
464            max_nearby,
465            config,
466            patterns,
467            list_len,
468            sublist_remove,
469            sublist_insert,
470            variable_name,
471            descriptor_index,
472            _phantom: PhantomData,
473        }
474    }
475
476    /// Finds the m nearest positions to a given position.
477    fn nearby_positions(
478        &self,
479        solution: &S,
480        entity_idx: usize,
481        origin: usize,
482        len: usize,
483    ) -> Vec<usize> {
484        let mut positions: Vec<(usize, f64)> = (0..len)
485            .filter(|&p| p != origin)
486            .map(|p| {
487                let dist = self
488                    .distance_meter
489                    .distance(solution, entity_idx, origin, p);
490                (p, dist)
491            })
492            .collect();
493
494        positions.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
495        positions.truncate(self.max_nearby);
496        positions.into_iter().map(|(p, _)| p).collect()
497    }
498}
499
500impl<S, V, DM, ES> MoveSelector<S, KOptMove<S, V>> for NearbyKOptMoveSelector<S, V, DM, ES>
501where
502    S: PlanningSolution,
503    V: Clone + Send + Sync + Debug + 'static,
504    DM: ListPositionDistanceMeter<S> + 'static,
505    ES: EntitySelector<S>,
506{
507    fn iter_moves<'a, SD: ScoreDirector<S>>(
508        &'a self,
509        score_director: &'a SD,
510    ) -> Box<dyn Iterator<Item = KOptMove<S, V>> + 'a> {
511        let k = self.config.k;
512        let min_seg = self.config.min_segment_len;
513        let patterns = &self.patterns;
514        let list_len_fn = self.list_len;
515        let sublist_remove = self.sublist_remove;
516        let sublist_insert = self.sublist_insert;
517        let variable_name = self.variable_name;
518        let descriptor_index = self.descriptor_index;
519
520        let iter = self
521            .entity_selector
522            .iter(score_director)
523            .flat_map(move |entity_ref| {
524                let entity_idx = entity_ref.entity_index;
525                let solution = score_director.working_solution();
526                let len = list_len_fn(solution, entity_idx);
527
528                // Generate nearby cut combinations
529                let cuts_iter = NearbyCutIterator::new(self, solution, entity_idx, k, len, min_seg);
530
531                cuts_iter.flat_map(move |cuts| {
532                    patterns.iter().map(move |&pattern| {
533                        // Validate cuts are sorted for intra-route
534                        let mut sorted_cuts = cuts.clone();
535                        sorted_cuts.sort_by_key(|c| c.position());
536
537                        KOptMove::new(
538                            &sorted_cuts,
539                            pattern,
540                            list_len_fn,
541                            sublist_remove,
542                            sublist_insert,
543                            variable_name,
544                            descriptor_index,
545                        )
546                    })
547                })
548            });
549
550        Box::new(iter)
551    }
552
553    fn size<SD: ScoreDirector<S>>(&self, score_director: &SD) -> usize {
554        // Approximate size: n * m^(k-1) * patterns
555        let k = self.config.k;
556        let m = self.max_nearby;
557        let pattern_count = self.patterns.len();
558
559        self.entity_selector
560            .iter(score_director)
561            .map(|entity_ref| {
562                let solution = score_director.working_solution();
563                let len = (self.list_len)(solution, entity_ref.entity_index);
564                if len < (k + 1) * self.config.min_segment_len {
565                    0
566                } else {
567                    // Approximate: first cut has ~len choices, others have ~m choices
568                    len.saturating_sub(k) * m.pow((k - 1) as u32) * pattern_count
569                }
570            })
571            .sum()
572    }
573}
574
575/// Iterator for nearby k-cut combinations.
576struct NearbyCutIterator<'a, S, V, D: ListPositionDistanceMeter<S>, ES> {
577    selector: &'a NearbyKOptMoveSelector<S, V, D, ES>,
578    solution: &'a S,
579    entity_idx: usize,
580    k: usize,
581    len: usize,
582    min_seg: usize,
583    /// Stack of (position, nearby_iterator_index)
584    stack: Vec<(usize, usize)>,
585    /// Nearby positions for each level
586    nearby_cache: Vec<Vec<usize>>,
587    done: bool,
588}
589
590impl<'a, S: PlanningSolution, V, D: ListPositionDistanceMeter<S>, ES>
591    NearbyCutIterator<'a, S, V, D, ES>
592{
593    fn new(
594        selector: &'a NearbyKOptMoveSelector<S, V, D, ES>,
595        solution: &'a S,
596        entity_idx: usize,
597        k: usize,
598        len: usize,
599        min_seg: usize,
600    ) -> Self {
601        let min_len = (k + 1) * min_seg;
602        if len < min_len {
603            return Self {
604                selector,
605                solution,
606                entity_idx,
607                k,
608                len,
609                min_seg,
610                stack: vec![],
611                nearby_cache: vec![],
612                done: true,
613            };
614        }
615
616        // Start with first valid position
617        let mut iter = Self {
618            selector,
619            solution,
620            entity_idx,
621            k,
622            len,
623            min_seg,
624            stack: vec![(min_seg, 0)],
625            nearby_cache: vec![vec![]],
626            done: false,
627        };
628
629        // Build initial stack to depth k
630        iter.extend_stack();
631        iter
632    }
633
634    fn extend_stack(&mut self) {
635        while self.stack.len() < self.k && !self.done {
636            let (last_pos, _) = *self.stack.last().unwrap();
637
638            // Get nearby positions for next cut
639            let nearby =
640                self.selector
641                    .nearby_positions(self.solution, self.entity_idx, last_pos, self.len);
642
643            // Filter to valid positions (must leave room for remaining cuts)
644            let remaining_cuts = self.k - self.stack.len();
645            let min_pos = last_pos + self.min_seg;
646            let max_pos = self.len - self.min_seg * remaining_cuts;
647
648            let valid: Vec<usize> = nearby
649                .into_iter()
650                .filter(|&p| p >= min_pos && p <= max_pos)
651                .collect();
652
653            if valid.is_empty() {
654                // No valid positions, backtrack
655                if !self.backtrack() {
656                    self.done = true;
657                    return;
658                }
659            } else {
660                self.nearby_cache.push(valid);
661                let next_pos = self.nearby_cache.last().unwrap()[0];
662                self.stack.push((next_pos, 0));
663            }
664        }
665    }
666
667    fn backtrack(&mut self) -> bool {
668        while let Some((popped_pos, _idx)) = self.stack.pop() {
669            self.nearby_cache.pop();
670
671            if let Some((_, last_idx)) = self.stack.last_mut() {
672                let cache_idx = self.nearby_cache.len();
673                if cache_idx > 0 {
674                    let cache = &self.nearby_cache[cache_idx - 1];
675                    let next_idx = *last_idx + 1;
676                    if next_idx < cache.len() {
677                        *last_idx = next_idx;
678                        let (pos, _) = self.stack.last().unwrap();
679                        let new_pos = cache[next_idx];
680                        if new_pos > *pos {
681                            self.stack.pop();
682                            self.stack.push((new_pos, next_idx));
683                            return true;
684                        }
685                    }
686                }
687            } else {
688                // Stack is empty - use the popped position to find next first position
689                let next_first = popped_pos + 1;
690                let max_first = self.len - self.min_seg * self.k;
691                if next_first <= max_first {
692                    self.stack.push((next_first, 0));
693                    self.nearby_cache.push(vec![]);
694                    return true;
695                }
696            }
697        }
698        false
699    }
700
701    fn advance(&mut self) {
702        if self.done || self.stack.is_empty() {
703            self.done = true;
704            return;
705        }
706
707        // Try to advance at current depth
708        if let Some((_, idx)) = self.stack.last_mut() {
709            let cache_idx = self.nearby_cache.len() - 1;
710            let cache = &self.nearby_cache[cache_idx];
711            let next_idx = *idx + 1;
712            if next_idx < cache.len() {
713                *idx = next_idx;
714                let new_pos = cache[next_idx];
715                self.stack.pop();
716                self.stack.push((new_pos, next_idx));
717                return;
718            }
719        }
720
721        // Backtrack and extend
722        if self.backtrack() {
723            self.extend_stack();
724        } else {
725            self.done = true;
726        }
727    }
728}
729
730impl<'a, S: PlanningSolution, V, D: ListPositionDistanceMeter<S>, ES> Iterator
731    for NearbyCutIterator<'a, S, V, D, ES>
732{
733    type Item = Vec<CutPoint>;
734
735    fn next(&mut self) -> Option<Self::Item> {
736        if self.done || self.stack.len() != self.k {
737            return None;
738        }
739
740        let cuts: Vec<CutPoint> = self
741            .stack
742            .iter()
743            .map(|(pos, _)| CutPoint::new(self.entity_idx, *pos))
744            .collect();
745
746        self.advance();
747
748        Some(cuts)
749    }
750}
751
752#[cfg(test)]
753mod tests {
754    use super::*;
755    use crate::heuristic::r#move::Move;
756    use crate::heuristic::selector::entity::FromSolutionEntitySelector;
757    use solverforge_core::domain::{EntityDescriptor, SolutionDescriptor, TypedEntityExtractor};
758    use solverforge_core::score::SimpleScore;
759    use solverforge_scoring::SimpleScoreDirector;
760    use std::any::TypeId;
761
762    #[derive(Clone, Debug)]
763    struct Tour {
764        cities: Vec<i32>,
765    }
766
767    #[derive(Clone, Debug)]
768    struct TspSolution {
769        tours: Vec<Tour>,
770        score: Option<SimpleScore>,
771    }
772
773    impl PlanningSolution for TspSolution {
774        type Score = SimpleScore;
775        fn score(&self) -> Option<Self::Score> {
776            self.score
777        }
778        fn set_score(&mut self, score: Option<Self::Score>) {
779            self.score = score;
780        }
781    }
782
783    fn get_tours(s: &TspSolution) -> &Vec<Tour> {
784        &s.tours
785    }
786    fn get_tours_mut(s: &mut TspSolution) -> &mut Vec<Tour> {
787        &mut s.tours
788    }
789    fn list_len(s: &TspSolution, entity_idx: usize) -> usize {
790        s.tours.get(entity_idx).map_or(0, |t| t.cities.len())
791    }
792    fn sublist_remove(
793        s: &mut TspSolution,
794        entity_idx: usize,
795        start: usize,
796        end: usize,
797    ) -> Vec<i32> {
798        s.tours
799            .get_mut(entity_idx)
800            .map(|t| t.cities.drain(start..end).collect())
801            .unwrap_or_default()
802    }
803    fn sublist_insert(s: &mut TspSolution, entity_idx: usize, pos: usize, items: Vec<i32>) {
804        if let Some(t) = s.tours.get_mut(entity_idx) {
805            for (i, item) in items.into_iter().enumerate() {
806                t.cities.insert(pos + i, item);
807            }
808        }
809    }
810
811    fn create_director(
812        tours: Vec<Tour>,
813    ) -> SimpleScoreDirector<TspSolution, impl Fn(&TspSolution) -> SimpleScore> {
814        let solution = TspSolution { tours, score: None };
815        let extractor = Box::new(TypedEntityExtractor::new(
816            "Tour",
817            "tours",
818            get_tours,
819            get_tours_mut,
820        ));
821        let entity_desc =
822            EntityDescriptor::new("Tour", TypeId::of::<Tour>(), "tours").with_extractor(extractor);
823        let descriptor = SolutionDescriptor::new("TspSolution", TypeId::of::<TspSolution>())
824            .with_entity(entity_desc);
825        SimpleScoreDirector::with_calculator(solution, descriptor, |_| SimpleScore::of(0))
826    }
827
828    #[test]
829    fn cut_combination_iterator_basic() {
830        // For k=3, len=8, min_seg=1:
831        // We need 4 segments of length >= 1
832        // Cuts can be at positions 1-7 (not 0 or 8)
833        // First combination: [1, 2, 3]
834        let mut iter = CutCombinationIterator::new(3, 8, 1, 0);
835
836        let first = iter.next().unwrap();
837        assert_eq!(first.len(), 3);
838        assert_eq!(first[0].position(), 1);
839        assert_eq!(first[1].position(), 2);
840        assert_eq!(first[2].position(), 3);
841
842        // Count total combinations
843        let count = 1 + iter.count(); // +1 for first we already took
844                                      // C(8 - 4 + 3, 3) = C(7, 3) = 35
845        assert_eq!(count, 35);
846    }
847
848    #[test]
849    fn cut_combination_too_short() {
850        // Route too short for 3 cuts with min_seg=2
851        // Need 4 segments * 2 = 8 elements minimum
852        let mut iter = CutCombinationIterator::new(3, 6, 2, 0);
853        assert!(iter.next().is_none());
854    }
855
856    #[test]
857    fn binomial_coefficient() {
858        assert_eq!(binomial(5, 2), 10);
859        assert_eq!(binomial(7, 3), 35);
860        assert_eq!(binomial(10, 5), 252);
861    }
862
863    #[test]
864    fn selector_generates_moves() {
865        let tours = vec![Tour {
866            cities: vec![1, 2, 3, 4, 5, 6, 7, 8],
867        }];
868        let director = create_director(tours);
869
870        let config = KOptConfig::new(3);
871
872        let selector = KOptMoveSelector::<TspSolution, i32, _>::new(
873            FromSolutionEntitySelector::new(0),
874            config,
875            list_len,
876            sublist_remove,
877            sublist_insert,
878            "cities",
879            0,
880        );
881
882        let moves: Vec<_> = selector.iter_moves(&director).collect();
883
884        // 35 cut combinations × 7 patterns = 245 moves
885        assert_eq!(moves.len(), 245);
886
887        // All moves should be doable
888        for m in &moves {
889            assert!(m.is_doable(&director), "Move not doable: {:?}", m);
890        }
891    }
892
893    #[test]
894    fn selector_size_matches_iteration() {
895        let tours = vec![Tour {
896            cities: vec![1, 2, 3, 4, 5, 6, 7, 8],
897        }];
898        let director = create_director(tours);
899
900        let config = KOptConfig::new(3);
901
902        let selector = KOptMoveSelector::<TspSolution, i32, _>::new(
903            FromSolutionEntitySelector::new(0),
904            config,
905            list_len,
906            sublist_remove,
907            sublist_insert,
908            "cities",
909            0,
910        );
911
912        let size = selector.size(&director);
913        let actual_count = selector.iter_moves(&director).count();
914
915        assert_eq!(size, actual_count);
916    }
917}