Skip to main content

u_nesting_core/
alns.rs

1//! # Adaptive Large Neighborhood Search (ALNS) Framework
2//!
3//! Implementation of the ALNS metaheuristic based on Ropke & Pisinger (2006).
4//!
5//! # Architecture
6//!
7//! This module maintains its own ALNS loop rather than delegating to u-metaheur's
8//! ALNS runner. Key design differences:
9//! - u-metaheur uses functional operators (`destroy(&self, &S) -> S`) as separate
10//!   trait objects; u-nesting integrates operators into `AlnsProblem` with mutable
11//!   access (`destroy(&mut self, &mut Solution)`) for stateful placement caches.
12//! - u-metaheur uses `cost(&self, &S) -> f64`; u-nesting uses `AlnsSolution::fitness()`.
13//! - u-nesting provides `relatedness()` for Shaw-based removal, not in u-metaheur.
14//!
15//! The rand 0.9 API is shared with u-metaheur for ecosystem compatibility.
16//!
17//! # Algorithm
18//!
19//! ALNS combines:
20//! - **Destroy operators**: Remove items from current solution (multiple strategies)
21//! - **Repair operators**: Reinsert items to improve solution (multiple strategies)
22//! - **Adaptive weights**: Operators are selected based on past performance
23//! - **Simulated Annealing acceptance**: Probabilistic acceptance of worse solutions
24//!
25//! ## Key Features
26//!
27//! - Multiple destroy/repair operators with adaptive selection
28//! - Segment-based weight updates
29//! - Configurable acceptance criteria (SA, Hill-Climbing)
30//! - Progress callbacks for monitoring
31//!
32//! ## Usage
33//!
34//! ```rust,ignore
35//! use u_nesting_core::alns::{AlnsConfig, AlnsRunner, AlnsProblem};
36//!
37//! let config = AlnsConfig::default();
38//! let runner = AlnsRunner::new(config)?;
39//! let result = runner.run(&mut problem, progress_callback);
40//! ```
41
42use crate::timing::Timer;
43use std::fmt::Debug;
44
45/// Configuration for the ALNS algorithm.
46#[derive(Debug, Clone)]
47pub struct AlnsConfig {
48    /// Maximum number of iterations
49    pub max_iterations: usize,
50    /// Time limit in milliseconds (0 = no limit)
51    pub time_limit_ms: u64,
52    /// Number of iterations per segment (for weight updates)
53    pub segment_size: usize,
54    /// Score for finding new best solution
55    pub score_best: f64,
56    /// Score for finding better solution than current
57    pub score_better: f64,
58    /// Score for accepting worse solution
59    pub score_accepted: f64,
60    /// Reaction factor (how quickly weights adapt, 0-1)
61    pub reaction_factor: f64,
62    /// Minimum operator weight
63    pub min_weight: f64,
64    /// Initial temperature for SA acceptance
65    pub initial_temperature: f64,
66    /// Cooling rate for SA acceptance (0-1)
67    pub cooling_rate: f64,
68    /// Final temperature threshold
69    pub final_temperature: f64,
70    /// Random seed for reproducibility (None = random)
71    pub seed: Option<u64>,
72}
73
74impl Default for AlnsConfig {
75    fn default() -> Self {
76        Self {
77            max_iterations: 10000,
78            time_limit_ms: 60000, // 1 minute
79            segment_size: 100,
80            score_best: 33.0,
81            score_better: 9.0,
82            score_accepted: 3.0,
83            reaction_factor: 0.1,
84            min_weight: 0.1,
85            initial_temperature: 100.0,
86            cooling_rate: 0.9995,
87            final_temperature: 0.01,
88            seed: None,
89        }
90    }
91}
92
93impl AlnsConfig {
94    /// Create a new configuration with default values.
95    pub fn new() -> Self {
96        Self::default()
97    }
98
99    /// Set maximum iterations.
100    pub fn with_max_iterations(mut self, iterations: usize) -> Self {
101        self.max_iterations = iterations;
102        self
103    }
104
105    /// Set time limit in milliseconds.
106    pub fn with_time_limit_ms(mut self, ms: u64) -> Self {
107        self.time_limit_ms = ms;
108        self
109    }
110
111    /// Set segment size for weight updates.
112    pub fn with_segment_size(mut self, size: usize) -> Self {
113        self.segment_size = size;
114        self
115    }
116
117    /// Set scoring parameters.
118    pub fn with_scores(mut self, best: f64, better: f64, accepted: f64) -> Self {
119        self.score_best = best;
120        self.score_better = better;
121        self.score_accepted = accepted;
122        self
123    }
124
125    /// Set reaction factor.
126    pub fn with_reaction_factor(mut self, factor: f64) -> Self {
127        self.reaction_factor = factor;
128        self
129    }
130
131    /// Set temperature parameters for SA acceptance.
132    pub fn with_temperature(mut self, initial: f64, cooling_rate: f64, final_temp: f64) -> Self {
133        self.initial_temperature = initial;
134        self.cooling_rate = cooling_rate;
135        self.final_temperature = final_temp;
136        self
137    }
138
139    /// Checks every field against its range; the builders store what they are
140    /// given.
141    ///
142    /// # Errors
143    /// `ConfigError` naming the first field out of range: `reaction_factor`
144    /// outside `[0, 1]`, `cooling_rate` outside `[0.9, 1]`, `initial_temperature`
145    /// below 0.01, `final_temperature` below 0.001, `min_weight` negative,
146    /// `segment_size` below 1.
147    pub fn validate(&self) -> crate::error::Result<()> {
148        use crate::error::{check_at_least, check_range};
149        check_at_least("segment_size", self.segment_size as f64, 1.0)?;
150        check_range("reaction_factor", self.reaction_factor, 0.0, 1.0)?;
151        check_range("cooling_rate", self.cooling_rate, 0.9, 1.0)?;
152        check_at_least("initial_temperature", self.initial_temperature, 0.01)?;
153        check_at_least("final_temperature", self.final_temperature, 0.001)?;
154        check_at_least("min_weight", self.min_weight, 0.0)?;
155        Ok(())
156    }
157
158    /// Set random seed for reproducibility.
159    pub fn with_seed(mut self, seed: u64) -> Self {
160        self.seed = Some(seed);
161        self
162    }
163}
164
165/// Statistics for an operator.
166#[derive(Debug, Clone)]
167pub struct OperatorStats {
168    /// Current weight
169    pub weight: f64,
170    /// Number of times used
171    pub times_used: usize,
172    /// Total score accumulated
173    pub total_score: f64,
174    /// Score in current segment
175    pub segment_score: f64,
176    /// Uses in current segment
177    pub segment_uses: usize,
178}
179
180impl Default for OperatorStats {
181    fn default() -> Self {
182        Self {
183            weight: 1.0,
184            times_used: 0,
185            total_score: 0.0,
186            segment_score: 0.0,
187            segment_uses: 0,
188        }
189    }
190}
191
192impl OperatorStats {
193    /// Create new stats with initial weight.
194    pub fn new(initial_weight: f64) -> Self {
195        Self {
196            weight: initial_weight,
197            ..Default::default()
198        }
199    }
200
201    /// Record operator usage with a score.
202    pub fn record_use(&mut self, score: f64) {
203        self.times_used += 1;
204        self.total_score += score;
205        self.segment_score += score;
206        self.segment_uses += 1;
207    }
208
209    /// Update weight at end of segment.
210    pub fn update_weight(&mut self, reaction_factor: f64, min_weight: f64) {
211        if self.segment_uses > 0 {
212            let segment_avg = self.segment_score / self.segment_uses as f64;
213            self.weight = self.weight * (1.0 - reaction_factor) + segment_avg * reaction_factor;
214            self.weight = self.weight.max(min_weight);
215        }
216        // Reset segment counters
217        self.segment_score = 0.0;
218        self.segment_uses = 0;
219    }
220}
221
222/// Identifies a destroy operator.
223#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
224pub enum DestroyOperatorId {
225    /// Random removal
226    Random,
227    /// Worst removal (highest cost items)
228    Worst,
229    /// Related removal (similar/nearby items)
230    Related,
231    /// Shaw removal (clustering-based)
232    Shaw,
233    /// Custom operator by index
234    Custom(usize),
235}
236
237/// Identifies a repair operator.
238#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
239pub enum RepairOperatorId {
240    /// Greedy repair (best position)
241    Greedy,
242    /// Regret-based repair
243    Regret,
244    /// Random repair
245    Random,
246    /// BLF-based repair
247    BottomLeftFill,
248    /// Custom operator by index
249    Custom(usize),
250}
251
252/// Result of a destroy operation.
253#[derive(Debug, Clone)]
254pub struct DestroyResult {
255    /// Indices of removed items
256    pub removed_indices: Vec<usize>,
257    /// Operator used
258    pub operator: DestroyOperatorId,
259}
260
261/// Result of a repair operation.
262#[derive(Debug, Clone)]
263pub struct RepairResult {
264    /// Number of items successfully placed
265    pub placed_count: usize,
266    /// Number of items that could not be placed
267    pub unplaced_count: usize,
268    /// Operator used
269    pub operator: RepairOperatorId,
270}
271
272/// Progress information for ALNS callbacks.
273#[derive(Debug, Clone)]
274pub struct AlnsProgress {
275    /// Current iteration number
276    pub iteration: usize,
277    /// Best fitness found so far
278    pub best_fitness: f64,
279    /// Current fitness
280    pub current_fitness: f64,
281    /// Current temperature
282    pub temperature: f64,
283    /// Current segment number
284    pub segment: usize,
285    /// Elapsed time in milliseconds
286    pub elapsed_ms: u64,
287    /// Acceptance rate in current segment
288    pub acceptance_rate: f64,
289    /// Best destroy operator (by weight)
290    pub best_destroy: DestroyOperatorId,
291    /// Best repair operator (by weight)
292    pub best_repair: RepairOperatorId,
293}
294
295/// Result of ALNS optimization.
296#[derive(Debug, Clone)]
297pub struct AlnsResult<S> {
298    /// Best solution found
299    pub best_solution: S,
300    /// Best fitness value
301    pub best_fitness: f64,
302    /// Total iterations performed
303    pub iterations: usize,
304    /// Total time elapsed in milliseconds
305    pub elapsed_ms: u64,
306    /// Number of improvements found
307    pub improvements: usize,
308    /// Final temperature
309    pub final_temperature: f64,
310    /// Final operator weights
311    pub destroy_weights: Vec<(DestroyOperatorId, f64)>,
312    /// Final repair operator weights
313    pub repair_weights: Vec<(RepairOperatorId, f64)>,
314}
315
316/// Trait for solutions that can be optimized by ALNS.
317pub trait AlnsSolution: Clone + Debug {
318    /// Get the fitness value (lower is better, 0 = optimal).
319    fn fitness(&self) -> f64;
320
321    /// Get the number of placed items.
322    fn placed_count(&self) -> usize;
323
324    /// Get the total number of items.
325    fn total_count(&self) -> usize;
326}
327
328/// Trait for problems that can be solved by ALNS.
329pub trait AlnsProblem {
330    /// Solution type
331    type Solution: AlnsSolution;
332
333    /// Create an initial solution.
334    fn create_initial_solution(&mut self) -> Self::Solution;
335
336    /// Clone a solution.
337    fn clone_solution(&self, solution: &Self::Solution) -> Self::Solution;
338
339    /// Get available destroy operators.
340    fn destroy_operators(&self) -> Vec<DestroyOperatorId>;
341
342    /// Get available repair operators.
343    fn repair_operators(&self) -> Vec<RepairOperatorId>;
344
345    /// Apply a destroy operator.
346    fn destroy(
347        &mut self,
348        solution: &mut Self::Solution,
349        operator: DestroyOperatorId,
350        degree: f64,
351        rng: &mut rand::rngs::StdRng,
352    ) -> DestroyResult;
353
354    /// Apply a repair operator. A repair that makes random choices draws them
355    /// from `rng`, the run's generator, so a seeded run is reproduced.
356    fn repair(
357        &mut self,
358        solution: &mut Self::Solution,
359        destroyed: &DestroyResult,
360        operator: RepairOperatorId,
361        rng: &mut rand::rngs::StdRng,
362    ) -> RepairResult;
363
364    /// Calculate relatedness between two items (for Shaw/Related removal).
365    fn relatedness(&self, solution: &Self::Solution, i: usize, j: usize) -> f64 {
366        // Default: no relatedness information
367        let _ = (solution, i, j);
368        0.0
369    }
370}
371
372/// The ALNS algorithm runner.
373pub struct AlnsRunner {
374    config: AlnsConfig,
375}
376
377impl AlnsRunner {
378    /// Create a new ALNS runner with the given configuration.
379    ///
380    /// # Errors
381    /// As [`AlnsConfig::validate`].
382    pub fn new(config: AlnsConfig) -> crate::error::Result<Self> {
383        config.validate()?;
384        Ok(Self { config })
385    }
386
387    /// Run the ALNS algorithm on the given problem.
388    pub fn run<P, F>(&self, problem: &mut P, mut progress_callback: F) -> AlnsResult<P::Solution>
389    where
390        P: AlnsProblem,
391        F: FnMut(&AlnsProgress),
392    {
393        use rand::prelude::*;
394        use rand::SeedableRng;
395
396        // Initialize RNG
397        let mut rng = match self.config.seed {
398            Some(seed) => rand::rngs::StdRng::seed_from_u64(seed),
399            None => rand::make_rng::<rand::rngs::StdRng>(),
400        };
401
402        let start_time = Timer::now();
403
404        // Create initial solution
405        let mut current = problem.create_initial_solution();
406        let mut best = problem.clone_solution(&current);
407        let mut best_fitness = best.fitness();
408
409        // Get operators
410        let destroy_ops = problem.destroy_operators();
411        let repair_ops = problem.repair_operators();
412
413        // Initialize operator statistics
414        let mut destroy_stats: Vec<(DestroyOperatorId, OperatorStats)> = destroy_ops
415            .iter()
416            .map(|&op| (op, OperatorStats::new(1.0)))
417            .collect();
418
419        let mut repair_stats: Vec<(RepairOperatorId, OperatorStats)> = repair_ops
420            .iter()
421            .map(|&op| (op, OperatorStats::new(1.0)))
422            .collect();
423
424        // Initialize temperature
425        let mut temperature = self.config.initial_temperature;
426
427        // Statistics
428        let mut iteration = 0;
429        let mut segment = 0;
430        let mut improvements = 0;
431        let mut segment_accepts = 0;
432        let mut segment_total = 0;
433
434        // Main loop
435        loop {
436            // Check termination conditions
437            let elapsed = start_time.elapsed();
438            let elapsed_ms = elapsed.as_millis() as u64;
439
440            if iteration >= self.config.max_iterations {
441                break;
442            }
443
444            if self.config.time_limit_ms > 0 && elapsed_ms >= self.config.time_limit_ms {
445                break;
446            }
447
448            // Select destroy operator using roulette wheel
449            let destroy_idx = self.select_operator_by_weight(&destroy_stats, &mut rng);
450            let destroy_op = destroy_stats[destroy_idx].0;
451
452            // Select repair operator using roulette wheel
453            let repair_idx = self.select_operator_by_weight(&repair_stats, &mut rng);
454            let repair_op = repair_stats[repair_idx].0;
455
456            // Clone current solution
457            let mut candidate = problem.clone_solution(&current);
458
459            // Apply destroy (remove 10-40% of items)
460            let degree = rng.random_range(0.1..=0.4);
461            let destroy_result = problem.destroy(&mut candidate, destroy_op, degree, &mut rng);
462
463            // Apply repair
464            let _repair_result =
465                problem.repair(&mut candidate, &destroy_result, repair_op, &mut rng);
466
467            let candidate_fitness = candidate.fitness();
468            let current_fitness = current.fitness();
469
470            // Determine acceptance and score
471            let (accepted, score) = if candidate_fitness < best_fitness {
472                // New best solution
473                best = problem.clone_solution(&candidate);
474                best_fitness = candidate_fitness;
475                improvements += 1;
476                (true, self.config.score_best)
477            } else if candidate_fitness < current_fitness {
478                // Better than current
479                (true, self.config.score_better)
480            } else {
481                // SA acceptance criterion
482                let delta = candidate_fitness - current_fitness;
483                let accept_prob = (-delta / temperature).exp();
484                if rng.random::<f64>() < accept_prob {
485                    (true, self.config.score_accepted)
486                } else {
487                    (false, 0.0)
488                }
489            };
490
491            // Update current if accepted
492            if accepted {
493                current = candidate;
494                segment_accepts += 1;
495            }
496
497            // Record operator usage
498            destroy_stats[destroy_idx].1.record_use(score);
499            repair_stats[repair_idx].1.record_use(score);
500
501            segment_total += 1;
502
503            // Update temperature
504            temperature *= self.config.cooling_rate;
505            temperature = temperature.max(self.config.final_temperature);
506
507            // Check if segment is complete
508            if iteration > 0 && iteration % self.config.segment_size == 0 {
509                // Update weights
510                for (_, stats) in &mut destroy_stats {
511                    stats.update_weight(self.config.reaction_factor, self.config.min_weight);
512                }
513                for (_, stats) in &mut repair_stats {
514                    stats.update_weight(self.config.reaction_factor, self.config.min_weight);
515                }
516
517                segment += 1;
518                segment_accepts = 0;
519                segment_total = 0;
520            }
521
522            // Progress callback
523            let acceptance_rate = if segment_total > 0 {
524                segment_accepts as f64 / segment_total as f64
525            } else {
526                0.0
527            };
528
529            let best_destroy = destroy_stats
530                .iter()
531                .max_by(|a, b| {
532                    a.1.weight
533                        .partial_cmp(&b.1.weight)
534                        .unwrap_or(std::cmp::Ordering::Equal)
535                })
536                .map(|(op, _)| *op)
537                .unwrap_or(DestroyOperatorId::Random);
538
539            let best_repair = repair_stats
540                .iter()
541                .max_by(|a, b| {
542                    a.1.weight
543                        .partial_cmp(&b.1.weight)
544                        .unwrap_or(std::cmp::Ordering::Equal)
545                })
546                .map(|(op, _)| *op)
547                .unwrap_or(RepairOperatorId::Greedy);
548
549            let progress = AlnsProgress {
550                iteration,
551                best_fitness,
552                current_fitness: current.fitness(),
553                temperature,
554                segment,
555                elapsed_ms,
556                acceptance_rate,
557                best_destroy,
558                best_repair,
559            };
560
561            progress_callback(&progress);
562
563            iteration += 1;
564        }
565
566        let elapsed_ms = start_time.elapsed_ms();
567
568        AlnsResult {
569            best_solution: best,
570            best_fitness,
571            iterations: iteration,
572            elapsed_ms,
573            improvements,
574            final_temperature: temperature,
575            destroy_weights: destroy_stats
576                .iter()
577                .map(|(op, stats)| (*op, stats.weight))
578                .collect(),
579            repair_weights: repair_stats
580                .iter()
581                .map(|(op, stats)| (*op, stats.weight))
582                .collect(),
583        }
584    }
585
586    /// Select an operator index using roulette wheel selection.
587    fn select_operator_by_weight<T>(
588        &self,
589        stats: &[(T, OperatorStats)],
590        rng: &mut rand::rngs::StdRng,
591    ) -> usize {
592        use rand::prelude::*;
593
594        let total_weight: f64 = stats.iter().map(|(_, s)| s.weight).sum();
595        if total_weight <= 0.0 || stats.is_empty() {
596            return 0;
597        }
598
599        let mut roll = rng.random::<f64>() * total_weight;
600        for (i, (_, stat)) in stats.iter().enumerate() {
601            roll -= stat.weight;
602            if roll <= 0.0 {
603                return i;
604            }
605        }
606
607        stats.len() - 1
608    }
609}
610
611#[cfg(test)]
612mod tests {
613    use super::*;
614
615    #[test]
616    fn test_alns_config_default() {
617        let config = AlnsConfig::default();
618        assert_eq!(config.max_iterations, 10000);
619        assert_eq!(config.time_limit_ms, 60000);
620        assert_eq!(config.segment_size, 100);
621        assert!((config.score_best - 33.0).abs() < 1e-9);
622    }
623
624    #[test]
625    fn test_alns_config_builder() {
626        let config = AlnsConfig::new()
627            .with_max_iterations(5000)
628            .with_time_limit_ms(30000)
629            .with_segment_size(50)
630            .with_scores(10.0, 5.0, 1.0)
631            .with_reaction_factor(0.2)
632            .with_temperature(50.0, 0.999, 0.001)
633            .with_seed(42);
634
635        assert_eq!(config.max_iterations, 5000);
636        assert_eq!(config.time_limit_ms, 30000);
637        assert_eq!(config.segment_size, 50);
638        assert!((config.score_best - 10.0).abs() < 1e-9);
639        assert!((config.score_better - 5.0).abs() < 1e-9);
640        assert!((config.score_accepted - 1.0).abs() < 1e-9);
641        assert!((config.reaction_factor - 0.2).abs() < 1e-9);
642        assert!((config.initial_temperature - 50.0).abs() < 1e-9);
643        assert!((config.cooling_rate - 0.999).abs() < 1e-9);
644        assert!((config.final_temperature - 0.001).abs() < 1e-9);
645        assert_eq!(config.seed, Some(42));
646    }
647
648    #[test]
649    fn test_operator_stats() {
650        let mut stats = OperatorStats::new(1.0);
651
652        stats.record_use(10.0);
653        stats.record_use(20.0);
654
655        assert_eq!(stats.times_used, 2);
656        assert!((stats.total_score - 30.0).abs() < 1e-9);
657        assert!((stats.segment_score - 30.0).abs() < 1e-9);
658        assert_eq!(stats.segment_uses, 2);
659
660        stats.update_weight(0.5, 0.1);
661
662        // New weight = 1.0 * 0.5 + 15.0 * 0.5 = 8.0
663        assert!((stats.weight - 8.0).abs() < 1e-9);
664        assert!((stats.segment_score - 0.0).abs() < 1e-9);
665        assert_eq!(stats.segment_uses, 0);
666    }
667
668    #[test]
669    fn test_destroy_operator_ids() {
670        let ops = [
671            DestroyOperatorId::Random,
672            DestroyOperatorId::Worst,
673            DestroyOperatorId::Related,
674            DestroyOperatorId::Shaw,
675            DestroyOperatorId::Custom(0),
676        ];
677
678        assert_eq!(ops.len(), 5);
679        assert_eq!(DestroyOperatorId::Random, DestroyOperatorId::Random);
680        assert_ne!(DestroyOperatorId::Random, DestroyOperatorId::Worst);
681    }
682
683    #[test]
684    fn test_repair_operator_ids() {
685        let ops = [
686            RepairOperatorId::Greedy,
687            RepairOperatorId::Regret,
688            RepairOperatorId::Random,
689            RepairOperatorId::BottomLeftFill,
690            RepairOperatorId::Custom(0),
691        ];
692
693        assert_eq!(ops.len(), 5);
694        assert_eq!(RepairOperatorId::Greedy, RepairOperatorId::Greedy);
695        assert_ne!(RepairOperatorId::Greedy, RepairOperatorId::Regret);
696    }
697
698    #[test]
699    fn test_destroy_result() {
700        let result = DestroyResult {
701            removed_indices: vec![0, 3, 5],
702            operator: DestroyOperatorId::Random,
703        };
704
705        assert_eq!(result.removed_indices.len(), 3);
706        assert_eq!(result.operator, DestroyOperatorId::Random);
707    }
708
709    #[test]
710    fn test_repair_result() {
711        let result = RepairResult {
712            placed_count: 8,
713            unplaced_count: 2,
714            operator: RepairOperatorId::Greedy,
715        };
716
717        assert_eq!(result.placed_count, 8);
718        assert_eq!(result.unplaced_count, 2);
719        assert_eq!(result.operator, RepairOperatorId::Greedy);
720    }
721
722    #[test]
723    fn test_alns_progress() {
724        let progress = AlnsProgress {
725            iteration: 100,
726            best_fitness: 0.85,
727            current_fitness: 0.90,
728            temperature: 50.0,
729            segment: 1,
730            elapsed_ms: 5000,
731            acceptance_rate: 0.45,
732            best_destroy: DestroyOperatorId::Worst,
733            best_repair: RepairOperatorId::Greedy,
734        };
735
736        assert_eq!(progress.iteration, 100);
737        assert!((progress.best_fitness - 0.85).abs() < 1e-9);
738        assert_eq!(progress.segment, 1);
739        assert_eq!(progress.best_destroy, DestroyOperatorId::Worst);
740        assert_eq!(progress.best_repair, RepairOperatorId::Greedy);
741    }
742
743    // Mock implementation for testing the runner
744    #[derive(Clone, Debug)]
745    struct MockSolution {
746        fitness: f64,
747        placed: usize,
748        total: usize,
749    }
750
751    impl AlnsSolution for MockSolution {
752        fn fitness(&self) -> f64 {
753            self.fitness
754        }
755
756        fn placed_count(&self) -> usize {
757            self.placed
758        }
759
760        fn total_count(&self) -> usize {
761            self.total
762        }
763    }
764
765    struct MockProblem {
766        improvement_per_iteration: f64,
767    }
768
769    impl AlnsProblem for MockProblem {
770        type Solution = MockSolution;
771
772        fn create_initial_solution(&mut self) -> MockSolution {
773            MockSolution {
774                fitness: 1.0,
775                placed: 8,
776                total: 10,
777            }
778        }
779
780        fn clone_solution(&self, solution: &MockSolution) -> MockSolution {
781            solution.clone()
782        }
783
784        fn destroy_operators(&self) -> Vec<DestroyOperatorId> {
785            vec![DestroyOperatorId::Random, DestroyOperatorId::Worst]
786        }
787
788        fn repair_operators(&self) -> Vec<RepairOperatorId> {
789            vec![RepairOperatorId::Greedy, RepairOperatorId::BottomLeftFill]
790        }
791
792        fn destroy(
793            &mut self,
794            _solution: &mut MockSolution,
795            operator: DestroyOperatorId,
796            _degree: f64,
797            _rng: &mut rand::rngs::StdRng,
798        ) -> DestroyResult {
799            DestroyResult {
800                removed_indices: vec![0, 1, 2],
801                operator,
802            }
803        }
804
805        fn repair(
806            &mut self,
807            solution: &mut MockSolution,
808            _destroyed: &DestroyResult,
809            operator: RepairOperatorId,
810            _rng: &mut rand::rngs::StdRng,
811        ) -> RepairResult {
812            // Simulate improvement
813            solution.fitness -= self.improvement_per_iteration;
814            solution.fitness = solution.fitness.max(0.1);
815            RepairResult {
816                placed_count: solution.placed,
817                unplaced_count: 0,
818                operator,
819            }
820        }
821    }
822
823    #[test]
824    fn test_alns_runner_basic() {
825        let config = AlnsConfig::new()
826            .with_max_iterations(100)
827            .with_time_limit_ms(5000)
828            .with_seed(42);
829
830        let mut problem = MockProblem {
831            improvement_per_iteration: 0.01,
832        };
833
834        let runner = AlnsRunner::new(config).expect("valid config");
835        let mut last_progress: Option<AlnsProgress> = None;
836
837        let result = runner.run(&mut problem, |progress| {
838            last_progress = Some(progress.clone());
839        });
840
841        assert!(result.best_fitness <= 1.0);
842        assert_eq!(result.iterations, 100);
843        assert!(last_progress.is_some());
844        assert!(!result.destroy_weights.is_empty());
845        assert!(!result.repair_weights.is_empty());
846    }
847
848    #[test]
849    fn test_alns_runner_time_limit() {
850        let config = AlnsConfig::new()
851            // High enough that no machine reaches it inside the limit below:
852            // a mock iteration is a few instructions, and a million of them take
853            // well under 100 ms in release, which used to end this run on the
854            // iteration cap and fail the assertion about the time limit.
855            .with_max_iterations(usize::MAX)
856            .with_time_limit_ms(100)
857            .with_seed(42);
858
859        let mut problem = MockProblem {
860            improvement_per_iteration: 0.001,
861        };
862
863        let runner = AlnsRunner::new(config).expect("valid config");
864        let result = runner.run(&mut problem, |_| {});
865
866        // Should have terminated due to time limit
867        assert!(result.iterations < usize::MAX);
868        assert!(result.elapsed_ms >= 100);
869    }
870
871    #[test]
872    fn test_alns_weight_adaptation() {
873        let config = AlnsConfig::new()
874            .with_max_iterations(200)
875            .with_segment_size(50)
876            .with_reaction_factor(0.5)
877            .with_seed(42);
878
879        let mut problem = MockProblem {
880            improvement_per_iteration: 0.01,
881        };
882
883        let runner = AlnsRunner::new(config).expect("valid config");
884        let result = runner.run(&mut problem, |_| {});
885
886        // Weights should have changed from initial values
887        let _initial_weight_sum: f64 = 2.0; // 2 destroy operators, each starting at 1.0
888        let _final_destroy_sum: f64 = result.destroy_weights.iter().map(|(_, w)| *w).sum();
889
890        // At least some adaptation should have occurred
891        // (weights won't all be exactly 1.0 anymore)
892        let max_destroy_weight = result
893            .destroy_weights
894            .iter()
895            .map(|(_, w)| *w)
896            .fold(0.0, f64::max);
897
898        assert!(max_destroy_weight >= 0.1); // At least min_weight
899        assert!(result.iterations == 200);
900    }
901}