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