Skip to main content

u_nesting_core/
sa.rs

1//! Simulated Annealing framework for optimization.
2//!
3//! # Architecture
4//!
5//! This module maintains its own SA loop rather than delegating to u-metaheur's
6//! SA runner. u-metaheur uses `cost(&self, &Solution) -> f64` (immutable),
7//! while u-nesting uses `evaluate(&self, &mut Solution)` (mutable) for
8//! consistency with the GA/BRKGA evaluation pattern.
9//!
10//! Additionally, this module provides features not available in u-metaheur SA:
11//! - `NeighborhoodOperator` enum (5 operators) with `available_operators()`
12//! - `PermutationSolution` with built-in swap/relocate/inversion/rotation/chain
13//! - Reheating with configurable threshold and factor
14//! - Adaptive cooling schedule
15//! - Parallel multi-restart via `run_parallel()`
16//!
17//! The rand 0.9 API is shared with u-metaheur for ecosystem compatibility.
18
19use rand::prelude::*;
20#[cfg(feature = "parallel")]
21use rayon::prelude::*;
22use std::sync::atomic::{AtomicBool, Ordering};
23use std::sync::Arc;
24use std::time::Duration;
25
26use crate::timing::Timer;
27
28#[cfg(feature = "serde")]
29use serde::{Deserialize, Serialize};
30
31/// Cooling schedule types for Simulated Annealing.
32#[derive(Debug, Clone, Copy, Default)]
33#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
34pub enum CoolingSchedule {
35    /// Geometric cooling: T_new = T * alpha (alpha typically 0.95-0.99).
36    #[default]
37    Geometric,
38    /// Linear cooling: T_new = T - delta.
39    Linear,
40    /// Adaptive cooling: adjusts based on acceptance rate.
41    Adaptive,
42    /// Lundy-Mees: T_new = T / (1 + beta * T).
43    LundyMees,
44}
45
46/// Configuration for Simulated Annealing.
47#[derive(Debug, Clone)]
48#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
49pub struct SaConfig {
50    /// Initial temperature.
51    pub initial_temp: f64,
52    /// Final (minimum) temperature.
53    pub final_temp: f64,
54    /// Cooling rate (alpha for Geometric, delta for Linear, beta for LundyMees).
55    pub cooling_rate: f64,
56    /// Number of iterations at each temperature level.
57    pub iterations_per_temp: usize,
58    /// Maximum total iterations (None = temperature-based stopping only).
59    pub max_iterations: Option<u64>,
60    /// Cooling schedule type.
61    pub cooling_schedule: CoolingSchedule,
62    /// Maximum time limit (None = unlimited).
63    pub time_limit: Option<Duration>,
64    /// Target fitness to stop early (None = run until temperature limit).
65    pub target_fitness: Option<f64>,
66    /// Enable reheating when stagnation detected.
67    pub enable_reheating: bool,
68    /// Stagnation threshold for reheating.
69    pub reheat_threshold: u64,
70    /// Reheat factor (multiplier for current temperature).
71    pub reheat_factor: f64,
72}
73
74impl Default for SaConfig {
75    fn default() -> Self {
76        Self {
77            initial_temp: 1000.0,
78            final_temp: 0.001,
79            cooling_rate: 0.95,
80            iterations_per_temp: 100,
81            max_iterations: Some(100_000),
82            cooling_schedule: CoolingSchedule::Geometric,
83            time_limit: None,
84            target_fitness: None,
85            enable_reheating: false,
86            reheat_threshold: 1000,
87            reheat_factor: 2.0,
88        }
89    }
90}
91
92impl SaConfig {
93    /// Creates a new configuration with default values.
94    pub fn new() -> Self {
95        Self::default()
96    }
97
98    /// Sets the initial temperature.
99    pub fn with_initial_temp(mut self, temp: f64) -> Self {
100        self.initial_temp = temp.max(0.001);
101        self
102    }
103
104    /// Sets the final temperature.
105    pub fn with_final_temp(mut self, temp: f64) -> Self {
106        self.final_temp = temp.max(0.0001);
107        self
108    }
109
110    /// Sets the cooling rate.
111    pub fn with_cooling_rate(mut self, rate: f64) -> Self {
112        self.cooling_rate = rate.clamp(0.001, 0.9999);
113        self
114    }
115
116    /// Sets the iterations per temperature level.
117    pub fn with_iterations_per_temp(mut self, iterations: usize) -> Self {
118        self.iterations_per_temp = iterations.max(1);
119        self
120    }
121
122    /// Sets the maximum iterations.
123    pub fn with_max_iterations(mut self, iterations: u64) -> Self {
124        self.max_iterations = Some(iterations);
125        self
126    }
127
128    /// Sets the cooling schedule.
129    pub fn with_cooling_schedule(mut self, schedule: CoolingSchedule) -> Self {
130        self.cooling_schedule = schedule;
131        self
132    }
133
134    /// Sets the time limit.
135    pub fn with_time_limit(mut self, duration: Duration) -> Self {
136        self.time_limit = Some(duration);
137        self
138    }
139
140    /// Sets the target fitness.
141    pub fn with_target_fitness(mut self, fitness: f64) -> Self {
142        self.target_fitness = Some(fitness);
143        self
144    }
145
146    /// Enables reheating.
147    pub fn with_reheating(mut self, threshold: u64, factor: f64) -> Self {
148        self.enable_reheating = true;
149        self.reheat_threshold = threshold;
150        self.reheat_factor = factor.max(1.1);
151        self
152    }
153}
154
155/// Trait for solutions in Simulated Annealing.
156pub trait SaSolution: Clone + Send + Sync {
157    /// Returns the objective value (fitness) of this solution.
158    /// Higher values are better (maximization).
159    fn objective(&self) -> f64;
160
161    /// Sets the objective value.
162    fn set_objective(&mut self, value: f64);
163}
164
165/// Neighborhood operator types.
166#[derive(Debug, Clone, Copy, PartialEq, Eq)]
167pub enum NeighborhoodOperator {
168    /// Swap two elements.
169    Swap,
170    /// Relocate an element to a new position.
171    Relocate,
172    /// Reverse a segment (2-opt style).
173    Inversion,
174    /// Rotate/change orientation of an element.
175    Rotation,
176    /// Chain swap (3-opt style).
177    Chain,
178    /// Flip an element's mirror flag (`allow_flip` support, 2D nesting only
179    /// — a solution's `mirrors` vector is harmless-if-unused by problems
180    /// with no mirror concept, same as an all-`false` `rotations` vector is
181    /// for a problem with a single rotation option).
182    MirrorFlip,
183}
184
185/// Trait for problem-specific SA operations.
186pub trait SaProblem: Send + Sync {
187    /// The solution type for this problem.
188    type Solution: SaSolution;
189
190    /// Creates an initial solution.
191    fn initial_solution<R: Rng>(&self, rng: &mut R) -> Self::Solution;
192
193    /// Generates a neighbor solution using the specified operator.
194    fn neighbor<R: Rng>(
195        &self,
196        solution: &Self::Solution,
197        operator: NeighborhoodOperator,
198        rng: &mut R,
199    ) -> Self::Solution;
200
201    /// Evaluates the objective of a solution.
202    fn evaluate(&self, solution: &mut Self::Solution);
203
204    /// Returns available neighborhood operators for this problem.
205    fn available_operators(&self) -> Vec<NeighborhoodOperator> {
206        vec![
207            NeighborhoodOperator::Swap,
208            NeighborhoodOperator::Relocate,
209            NeighborhoodOperator::Inversion,
210        ]
211    }
212
213    /// Called after each temperature level (for progress reporting).
214    fn on_temperature_change(
215        &self,
216        _temperature: f64,
217        _iteration: u64,
218        _best: &Self::Solution,
219        _current: &Self::Solution,
220    ) {
221        // Default: do nothing
222    }
223}
224
225/// Progress information during SA execution.
226#[derive(Debug, Clone)]
227pub struct SaProgress {
228    /// Current temperature.
229    pub temperature: f64,
230    /// Current iteration number.
231    pub iteration: u64,
232    /// Best fitness so far.
233    pub best_fitness: f64,
234    /// Current fitness.
235    pub current_fitness: f64,
236    /// Acceptance rate at current temperature.
237    pub acceptance_rate: f64,
238    /// Elapsed time since start.
239    pub elapsed: Duration,
240    /// Whether the algorithm is still running.
241    pub running: bool,
242}
243
244/// Result of a SA run.
245#[derive(Debug, Clone)]
246pub struct SaResult<S: SaSolution> {
247    /// The best solution found.
248    pub best: S,
249    /// Final temperature reached.
250    pub final_temperature: f64,
251    /// Total iterations performed.
252    pub iterations: u64,
253    /// Total elapsed time.
254    pub elapsed: Duration,
255    /// Whether the target fitness was reached.
256    pub target_reached: bool,
257    /// Number of reheats performed.
258    pub reheat_count: u32,
259    /// Fitness history (sampled at temperature changes).
260    pub history: Vec<f64>,
261}
262
263/// Simulated Annealing runner.
264pub struct SaRunner<P: SaProblem> {
265    config: SaConfig,
266    problem: P,
267    cancelled: Arc<AtomicBool>,
268}
269
270impl<P: SaProblem> SaRunner<P> {
271    /// Creates a new SA runner.
272    pub fn new(config: SaConfig, problem: P) -> Self {
273        Self {
274            config,
275            problem,
276            cancelled: Arc::new(AtomicBool::new(false)),
277        }
278    }
279
280    /// Returns a handle to cancel the algorithm.
281    pub fn cancel_handle(&self) -> Arc<AtomicBool> {
282        self.cancelled.clone()
283    }
284
285    /// Runs the Simulated Annealing algorithm.
286    pub fn run(&self) -> SaResult<P::Solution> {
287        self.run_with_rng(&mut rand::rng())
288    }
289
290    /// Runs the Simulated Annealing algorithm with a specific RNG.
291    pub fn run_with_rng<R: Rng>(&self, rng: &mut R) -> SaResult<P::Solution> {
292        let start = Timer::now();
293        let mut history = Vec::new();
294
295        // Initialize
296        let mut current = self.problem.initial_solution(rng);
297        self.problem.evaluate(&mut current);
298        let mut best = current.clone();
299        let mut best_fitness = best.objective();
300
301        let mut temperature = self.config.initial_temp;
302        let mut iteration = 0u64;
303        let mut target_reached = false;
304        let mut reheat_count = 0u32;
305        let mut stagnation_count = 0u64;
306
307        let operators = self.problem.available_operators();
308        let temp_delta = if matches!(self.config.cooling_schedule, CoolingSchedule::Linear) {
309            (self.config.initial_temp - self.config.final_temp)
310                / (self.config.max_iterations.unwrap_or(10000) as f64
311                    / self.config.iterations_per_temp as f64)
312        } else {
313            0.0
314        };
315
316        // For adaptive cooling
317        let mut accepted_count = 0usize;
318        let mut total_count = 0usize;
319
320        while temperature > self.config.final_temp {
321            // Check cancellation
322            if self.cancelled.load(Ordering::Relaxed) {
323                break;
324            }
325
326            // Check time limit
327            if let Some(limit) = self.config.time_limit {
328                if start.elapsed() > limit {
329                    break;
330                }
331            }
332
333            // Check max iterations
334            if let Some(max) = self.config.max_iterations {
335                if iteration >= max {
336                    break;
337                }
338            }
339
340            // Check target fitness
341            if let Some(target) = self.config.target_fitness {
342                if best_fitness >= target {
343                    target_reached = true;
344                    break;
345                }
346            }
347
348            // Iterations at this temperature
349            for _ in 0..self.config.iterations_per_temp {
350                iteration += 1;
351                total_count += 1;
352
353                // Select random operator
354                let operator = operators[rng.random_range(0..operators.len())];
355
356                // Generate neighbor
357                let mut neighbor = self.problem.neighbor(&current, operator, rng);
358                self.problem.evaluate(&mut neighbor);
359
360                let current_obj = current.objective();
361                let neighbor_obj = neighbor.objective();
362                let delta = neighbor_obj - current_obj;
363
364                // Accept or reject
365                let accept = if delta >= 0.0 {
366                    // Better solution - always accept
367                    true
368                } else {
369                    // Worse solution - accept with probability exp(delta/T)
370                    let probability = (delta / temperature).exp();
371                    rng.random::<f64>() < probability
372                };
373
374                if accept {
375                    accepted_count += 1;
376                    current = neighbor;
377
378                    // Update best
379                    if current.objective() > best_fitness {
380                        best = current.clone();
381                        best_fitness = best.objective();
382                        stagnation_count = 0;
383                    } else {
384                        stagnation_count += 1;
385                    }
386                } else {
387                    stagnation_count += 1;
388                }
389
390                // Check max iterations inside inner loop
391                if let Some(max) = self.config.max_iterations {
392                    if iteration >= max {
393                        break;
394                    }
395                }
396            }
397
398            // Record history
399            history.push(best_fitness);
400
401            // Callback
402            self.problem
403                .on_temperature_change(temperature, iteration, &best, &current);
404
405            // Reheating check
406            if self.config.enable_reheating && stagnation_count >= self.config.reheat_threshold {
407                temperature *= self.config.reheat_factor;
408                temperature = temperature.min(self.config.initial_temp);
409                stagnation_count = 0;
410                reheat_count += 1;
411            }
412
413            // Cool down
414            temperature = self.cool_down(temperature, temp_delta, accepted_count, total_count);
415
416            // Reset adaptive counters
417            accepted_count = 0;
418            total_count = 0;
419        }
420
421        // Final history entry
422        history.push(best_fitness);
423
424        SaResult {
425            best,
426            final_temperature: temperature,
427            iterations: iteration,
428            elapsed: start.elapsed(),
429            target_reached,
430            reheat_count,
431            history,
432        }
433    }
434
435    /// Apply cooling schedule.
436    fn cool_down(&self, current_temp: f64, delta: f64, accepted: usize, total: usize) -> f64 {
437        match self.config.cooling_schedule {
438            CoolingSchedule::Geometric => current_temp * self.config.cooling_rate,
439            CoolingSchedule::Linear => (current_temp - delta).max(self.config.final_temp),
440            CoolingSchedule::Adaptive => {
441                // Adjust cooling rate based on acceptance rate
442                let acceptance_rate = if total > 0 {
443                    accepted as f64 / total as f64
444                } else {
445                    0.5
446                };
447
448                // If acceptance rate is high, cool faster; if low, cool slower
449                let adjusted_rate = if acceptance_rate > 0.5 {
450                    self.config.cooling_rate * 0.95 // Cool faster
451                } else if acceptance_rate < 0.1 {
452                    self.config.cooling_rate.powf(0.5) // Cool slower (sqrt)
453                } else {
454                    self.config.cooling_rate
455                };
456
457                current_temp * adjusted_rate
458            }
459            CoolingSchedule::LundyMees => {
460                // T_new = T / (1 + beta * T)
461                current_temp / (1.0 + self.config.cooling_rate * current_temp)
462            }
463        }
464    }
465
466    /// Runs multiple SA instances in parallel and returns the best result.
467    ///
468    /// This is useful for escaping local optima by exploring different regions
469    /// of the solution space simultaneously.
470    ///
471    /// # Arguments
472    /// * `num_restarts` - Number of parallel SA runs to perform
473    ///
474    /// # Returns
475    /// The best result among all parallel runs
476    ///
477    /// Requires the `parallel` feature to be enabled.
478    #[cfg(feature = "parallel")]
479    pub fn run_parallel(&self, num_restarts: usize) -> SaResult<P::Solution>
480    where
481        P: Clone,
482    {
483        let num_restarts = num_restarts.max(1);
484
485        // Run SA instances in parallel
486        let results: Vec<SaResult<P::Solution>> = (0..num_restarts)
487            .into_par_iter()
488            .map(|_| {
489                let mut rng = rand::rng();
490                self.run_with_rng(&mut rng)
491            })
492            .collect();
493
494        // Find the best result
495        results
496            .into_iter()
497            .max_by(|a, b| {
498                a.best
499                    .objective()
500                    .partial_cmp(&b.best.objective())
501                    .unwrap_or(std::cmp::Ordering::Equal)
502            })
503            .expect("At least one result should exist")
504    }
505}
506
507/// Permutation-based solution for SA.
508#[derive(Debug, Clone)]
509pub struct PermutationSolution {
510    /// The permutation (indices).
511    pub sequence: Vec<usize>,
512    /// Additional rotation/orientation values.
513    pub rotations: Vec<usize>,
514    /// Number of rotation options per item.
515    pub rotation_options: usize,
516    /// Mirror flag per item (`allow_flip` support, 2D nesting only — see
517    /// [`NeighborhoodOperator::MirrorFlip`]).
518    pub mirrors: Vec<bool>,
519    /// Cached objective value.
520    objective: f64,
521}
522
523impl PermutationSolution {
524    /// Creates a new permutation solution.
525    pub fn new(size: usize, rotation_options: usize) -> Self {
526        Self {
527            sequence: (0..size).collect(),
528            rotations: vec![0; size],
529            rotation_options,
530            mirrors: vec![false; size],
531            objective: f64::NEG_INFINITY,
532        }
533    }
534
535    /// Creates a random permutation solution.
536    pub fn random<R: Rng>(size: usize, rotation_options: usize, rng: &mut R) -> Self {
537        let mut sequence: Vec<usize> = (0..size).collect();
538        sequence.shuffle(rng);
539
540        let rotations: Vec<usize> = (0..size)
541            .map(|_| rng.random_range(0..rotation_options.max(1)))
542            .collect();
543
544        let mirrors: Vec<bool> = (0..size).map(|_| rng.random()).collect();
545
546        Self {
547            sequence,
548            rotations,
549            rotation_options,
550            mirrors,
551            objective: f64::NEG_INFINITY,
552        }
553    }
554
555    /// Returns the length of the sequence.
556    pub fn len(&self) -> usize {
557        self.sequence.len()
558    }
559
560    /// Returns true if empty.
561    pub fn is_empty(&self) -> bool {
562        self.sequence.is_empty()
563    }
564
565    /// Applies swap operator: swaps two elements in sequence.
566    pub fn apply_swap<R: Rng>(&self, rng: &mut R) -> Self {
567        let mut result = self.clone();
568        if result.sequence.len() < 2 {
569            return result;
570        }
571
572        let i = rng.random_range(0..result.sequence.len());
573        let j = rng.random_range(0..result.sequence.len());
574        result.sequence.swap(i, j);
575        result.objective = f64::NEG_INFINITY;
576        result
577    }
578
579    /// Applies relocate operator: moves an element to a new position.
580    pub fn apply_relocate<R: Rng>(&self, rng: &mut R) -> Self {
581        let mut result = self.clone();
582        if result.sequence.len() < 2 {
583            return result;
584        }
585
586        let from = rng.random_range(0..result.sequence.len());
587        let to = rng.random_range(0..result.sequence.len());
588
589        if from != to {
590            let elem = result.sequence.remove(from);
591            let insert_pos = if to > from { to - 1 } else { to };
592            result
593                .sequence
594                .insert(insert_pos.min(result.sequence.len()), elem);
595        }
596
597        result.objective = f64::NEG_INFINITY;
598        result
599    }
600
601    /// Applies inversion operator: reverses a segment.
602    pub fn apply_inversion<R: Rng>(&self, rng: &mut R) -> Self {
603        let mut result = self.clone();
604        let n = result.sequence.len();
605        if n < 2 {
606            return result;
607        }
608
609        let (mut p1, mut p2) = (rng.random_range(0..n), rng.random_range(0..n));
610        if p1 > p2 {
611            std::mem::swap(&mut p1, &mut p2);
612        }
613
614        result.sequence[p1..=p2].reverse();
615        result.objective = f64::NEG_INFINITY;
616        result
617    }
618
619    /// Applies rotation operator: changes rotation of one element.
620    pub fn apply_rotation<R: Rng>(&self, rng: &mut R) -> Self {
621        let mut result = self.clone();
622        if result.rotations.is_empty() || result.rotation_options <= 1 {
623            return result;
624        }
625
626        let idx = rng.random_range(0..result.rotations.len());
627        result.rotations[idx] = rng.random_range(0..result.rotation_options);
628        result.objective = f64::NEG_INFINITY;
629        result
630    }
631
632    /// Applies mirror-flip operator: flips one element's mirror bit
633    /// (`allow_flip` support). Same shape as `apply_rotation` — unconditional
634    /// on the gene, the consuming problem masks it against its own
635    /// per-item mirror-eligibility (e.g. `Geometry2D::allow_flip()`).
636    pub fn apply_mirror_flip<R: Rng>(&self, rng: &mut R) -> Self {
637        let mut result = self.clone();
638        if result.mirrors.is_empty() {
639            return result;
640        }
641
642        let idx = rng.random_range(0..result.mirrors.len());
643        result.mirrors[idx] = !result.mirrors[idx];
644        result.objective = f64::NEG_INFINITY;
645        result
646    }
647
648    /// Applies chain operator: 3-opt style move.
649    pub fn apply_chain<R: Rng>(&self, rng: &mut R) -> Self {
650        let mut result = self.clone();
651        let n = result.sequence.len();
652        if n < 4 {
653            // Fall back to swap for small sequences
654            return self.apply_swap(rng);
655        }
656
657        // Select three distinct positions
658        let mut positions: Vec<usize> = (0..n).collect();
659        positions.shuffle(rng);
660        let mut selected: Vec<usize> = positions.into_iter().take(3).collect();
661        selected.sort();
662
663        let (p1, p2, p3) = (selected[0], selected[1], selected[2]);
664
665        // Rotate segments: [0..p1] [p1..p2] [p2..p3] [p3..n]
666        // New order: [0..p1] [p2..p3] [p1..p2] [p3..n]
667        let seg1: Vec<usize> = result.sequence[..p1].to_vec();
668        let seg2: Vec<usize> = result.sequence[p1..p2].to_vec();
669        let seg3: Vec<usize> = result.sequence[p2..p3].to_vec();
670        let seg4: Vec<usize> = result.sequence[p3..].to_vec();
671
672        result.sequence = [seg1, seg3, seg2, seg4].concat();
673        result.objective = f64::NEG_INFINITY;
674        result
675    }
676}
677
678impl SaSolution for PermutationSolution {
679    fn objective(&self) -> f64 {
680        self.objective
681    }
682
683    fn set_objective(&mut self, value: f64) {
684        self.objective = value;
685    }
686}
687
688#[cfg(test)]
689mod tests {
690    use super::*;
691
692    struct SimpleMaxProblem {
693        size: usize,
694    }
695
696    impl SaProblem for SimpleMaxProblem {
697        type Solution = PermutationSolution;
698
699        fn initial_solution<R: Rng>(&self, rng: &mut R) -> Self::Solution {
700            PermutationSolution::random(self.size, 1, rng)
701        }
702
703        fn neighbor<R: Rng>(
704            &self,
705            solution: &Self::Solution,
706            operator: NeighborhoodOperator,
707            rng: &mut R,
708        ) -> Self::Solution {
709            match operator {
710                NeighborhoodOperator::Swap => solution.apply_swap(rng),
711                NeighborhoodOperator::Relocate => solution.apply_relocate(rng),
712                NeighborhoodOperator::Inversion => solution.apply_inversion(rng),
713                NeighborhoodOperator::Rotation => solution.apply_rotation(rng),
714                NeighborhoodOperator::Chain => solution.apply_chain(rng),
715                NeighborhoodOperator::MirrorFlip => solution.apply_mirror_flip(rng),
716            }
717        }
718
719        fn evaluate(&self, solution: &mut Self::Solution) {
720            // Maximize: sequence should be in ascending order
721            // Fitness = negative sum of inversions
722            let mut inversions = 0i64;
723            for i in 0..solution.sequence.len() {
724                for j in (i + 1)..solution.sequence.len() {
725                    if solution.sequence[i] > solution.sequence[j] {
726                        inversions += 1;
727                    }
728                }
729            }
730            solution.set_objective(-inversions as f64);
731        }
732    }
733
734    #[test]
735    fn test_sa_basic() {
736        let config = SaConfig::default()
737            .with_initial_temp(100.0)
738            .with_final_temp(0.1)
739            .with_cooling_rate(0.9)
740            .with_iterations_per_temp(50)
741            .with_max_iterations(5000);
742
743        let problem = SimpleMaxProblem { size: 10 };
744        let runner = SaRunner::new(config, problem);
745        let result = runner.run();
746
747        // Should find something reasonably good (fewer inversions)
748        assert!(result.best.objective() > -20.0);
749        assert!(result.iterations > 0);
750    }
751
752    #[test]
753    fn test_cooling_schedules() {
754        let problem = SimpleMaxProblem { size: 5 };
755
756        for schedule in [
757            CoolingSchedule::Geometric,
758            CoolingSchedule::Linear,
759            CoolingSchedule::Adaptive,
760            CoolingSchedule::LundyMees,
761        ] {
762            let config = SaConfig::default()
763                .with_cooling_schedule(schedule)
764                .with_max_iterations(1000);
765
766            let runner = SaRunner::new(config, problem.clone());
767            let result = runner.run();
768
769            // Should complete without panic
770            assert!(result.iterations > 0);
771        }
772    }
773
774    #[test]
775    fn test_neighborhood_operators() {
776        let mut rng = rand::rng();
777        let solution = PermutationSolution::random(10, 4, &mut rng);
778
779        // Test all operators produce valid permutations
780        let swap = solution.apply_swap(&mut rng);
781        let relocate = solution.apply_relocate(&mut rng);
782        let inversion = solution.apply_inversion(&mut rng);
783        let rotation = solution.apply_rotation(&mut rng);
784        let chain = solution.apply_chain(&mut rng);
785
786        for sol in [&swap, &relocate, &inversion, &rotation, &chain] {
787            let mut sorted = sol.sequence.clone();
788            sorted.sort();
789            assert_eq!(sorted, (0..10).collect::<Vec<_>>());
790        }
791    }
792
793    #[test]
794    fn test_reheating() {
795        let config = SaConfig::default()
796            .with_initial_temp(10.0)
797            .with_final_temp(0.1)
798            .with_max_iterations(500)
799            .with_reheating(50, 1.5);
800
801        let problem = SimpleMaxProblem { size: 8 };
802        let runner = SaRunner::new(config, problem);
803        let result = runner.run();
804
805        // Should complete (reheating may or may not trigger)
806        assert!(result.iterations > 0);
807    }
808
809    impl Clone for SimpleMaxProblem {
810        fn clone(&self) -> Self {
811            Self { size: self.size }
812        }
813    }
814}