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::{expired, 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    /// Creates a runner that stops when `cancelled` is set — the caller's own
281    /// flag, observed directly, so no thread is needed to forward it.
282    pub fn with_cancellation(config: SaConfig, problem: P, cancelled: Arc<AtomicBool>) -> Self {
283        Self {
284            config,
285            problem,
286            cancelled,
287        }
288    }
289
290    /// Returns a handle to cancel the algorithm.
291    pub fn cancel_handle(&self) -> Arc<AtomicBool> {
292        self.cancelled.clone()
293    }
294
295    /// Runs the Simulated Annealing algorithm.
296    pub fn run(&self) -> SaResult<P::Solution> {
297        self.run_with_rng(&mut rand::rng())
298    }
299
300    /// Runs the Simulated Annealing algorithm with a specific RNG.
301    pub fn run_with_rng<R: Rng>(&self, rng: &mut R) -> SaResult<P::Solution> {
302        let start = Timer::now();
303        let mut history = Vec::new();
304
305        // Initialize
306        let mut current = self.problem.initial_solution(rng);
307        self.problem.evaluate(&mut current);
308        let mut best = current.clone();
309        let mut best_fitness = best.objective();
310
311        let mut temperature = self.config.initial_temp;
312        let mut iteration = 0u64;
313        let mut target_reached = false;
314        let mut reheat_count = 0u32;
315        let mut stagnation_count = 0u64;
316
317        let operators = self.problem.available_operators();
318        let temp_delta = if matches!(self.config.cooling_schedule, CoolingSchedule::Linear) {
319            (self.config.initial_temp - self.config.final_temp)
320                / (self.config.max_iterations.unwrap_or(10000) as f64
321                    / self.config.iterations_per_temp as f64)
322        } else {
323            0.0
324        };
325
326        // For adaptive cooling
327        let mut accepted_count = 0usize;
328        let mut total_count = 0usize;
329
330        while temperature > self.config.final_temp {
331            // Check cancellation
332            if self.cancelled.load(Ordering::Relaxed) {
333                break;
334            }
335
336            // Check time limit
337            if expired(&start, self.config.time_limit) {
338                break;
339            }
340
341            // Check max iterations
342            if let Some(max) = self.config.max_iterations {
343                if iteration >= max {
344                    break;
345                }
346            }
347
348            // Check target fitness
349            if let Some(target) = self.config.target_fitness {
350                if best_fitness >= target {
351                    target_reached = true;
352                    break;
353                }
354            }
355
356            // Iterations at this temperature — each evaluation can be a full
357            // placement, so the time limit is checked per iteration, not per step
358            for _ in 0..self.config.iterations_per_temp {
359                if expired(&start, self.config.time_limit) {
360                    break;
361                }
362                iteration += 1;
363                total_count += 1;
364
365                // Select random operator
366                let operator = operators[rng.random_range(0..operators.len())];
367
368                // Generate neighbor
369                let mut neighbor = self.problem.neighbor(&current, operator, rng);
370                self.problem.evaluate(&mut neighbor);
371
372                let current_obj = current.objective();
373                let neighbor_obj = neighbor.objective();
374                let delta = neighbor_obj - current_obj;
375
376                // Accept or reject
377                let accept = if delta >= 0.0 {
378                    // Better solution - always accept
379                    true
380                } else {
381                    // Worse solution - accept with probability exp(delta/T)
382                    let probability = (delta / temperature).exp();
383                    rng.random::<f64>() < probability
384                };
385
386                if accept {
387                    accepted_count += 1;
388                    current = neighbor;
389
390                    // Update best
391                    if current.objective() > best_fitness {
392                        best = current.clone();
393                        best_fitness = best.objective();
394                        stagnation_count = 0;
395                    } else {
396                        stagnation_count += 1;
397                    }
398                } else {
399                    stagnation_count += 1;
400                }
401
402                // Check max iterations inside inner loop
403                if let Some(max) = self.config.max_iterations {
404                    if iteration >= max {
405                        break;
406                    }
407                }
408            }
409
410            // Record history
411            history.push(best_fitness);
412
413            // Callback
414            self.problem
415                .on_temperature_change(temperature, iteration, &best, &current);
416
417            // Reheating check
418            if self.config.enable_reheating && stagnation_count >= self.config.reheat_threshold {
419                temperature *= self.config.reheat_factor;
420                temperature = temperature.min(self.config.initial_temp);
421                stagnation_count = 0;
422                reheat_count += 1;
423            }
424
425            // Cool down
426            temperature = self.cool_down(temperature, temp_delta, accepted_count, total_count);
427
428            // Reset adaptive counters
429            accepted_count = 0;
430            total_count = 0;
431        }
432
433        // Final history entry
434        history.push(best_fitness);
435
436        SaResult {
437            best,
438            final_temperature: temperature,
439            iterations: iteration,
440            elapsed: start.elapsed(),
441            target_reached,
442            reheat_count,
443            history,
444        }
445    }
446
447    /// Apply cooling schedule.
448    fn cool_down(&self, current_temp: f64, delta: f64, accepted: usize, total: usize) -> f64 {
449        match self.config.cooling_schedule {
450            CoolingSchedule::Geometric => current_temp * self.config.cooling_rate,
451            CoolingSchedule::Linear => (current_temp - delta).max(self.config.final_temp),
452            CoolingSchedule::Adaptive => {
453                // Adjust cooling rate based on acceptance rate
454                let acceptance_rate = if total > 0 {
455                    accepted as f64 / total as f64
456                } else {
457                    0.5
458                };
459
460                // If acceptance rate is high, cool faster; if low, cool slower
461                let adjusted_rate = if acceptance_rate > 0.5 {
462                    self.config.cooling_rate * 0.95 // Cool faster
463                } else if acceptance_rate < 0.1 {
464                    self.config.cooling_rate.powf(0.5) // Cool slower (sqrt)
465                } else {
466                    self.config.cooling_rate
467                };
468
469                current_temp * adjusted_rate
470            }
471            CoolingSchedule::LundyMees => {
472                // T_new = T / (1 + beta * T)
473                current_temp / (1.0 + self.config.cooling_rate * current_temp)
474            }
475        }
476    }
477
478    /// Runs multiple SA instances in parallel and returns the best result.
479    ///
480    /// This is useful for escaping local optima by exploring different regions
481    /// of the solution space simultaneously.
482    ///
483    /// # Arguments
484    /// * `num_restarts` - Number of parallel SA runs to perform
485    ///
486    /// # Returns
487    /// The best result among all parallel runs
488    ///
489    /// Requires the `parallel` feature to be enabled.
490    #[cfg(feature = "parallel")]
491    pub fn run_parallel(&self, num_restarts: usize) -> SaResult<P::Solution>
492    where
493        P: Clone,
494    {
495        let num_restarts = num_restarts.max(1);
496
497        // Run SA instances in parallel
498        let results: Vec<SaResult<P::Solution>> = (0..num_restarts)
499            .into_par_iter()
500            .map(|_| {
501                let mut rng = rand::rng();
502                self.run_with_rng(&mut rng)
503            })
504            .collect();
505
506        // Find the best result
507        results
508            .into_iter()
509            .max_by(|a, b| {
510                a.best
511                    .objective()
512                    .partial_cmp(&b.best.objective())
513                    .unwrap_or(std::cmp::Ordering::Equal)
514            })
515            .expect("At least one result should exist")
516    }
517}
518
519/// Permutation-based solution for SA.
520#[derive(Debug, Clone)]
521pub struct PermutationSolution {
522    /// The permutation (indices).
523    pub sequence: Vec<usize>,
524    /// Additional rotation/orientation values.
525    pub rotations: Vec<usize>,
526    /// Number of rotation options per item.
527    pub rotation_options: usize,
528    /// Mirror flag per item (`allow_flip` support, 2D nesting only — see
529    /// [`NeighborhoodOperator::MirrorFlip`]).
530    pub mirrors: Vec<bool>,
531    /// Cached objective value.
532    objective: f64,
533}
534
535impl PermutationSolution {
536    /// Creates a new permutation solution.
537    pub fn new(size: usize, rotation_options: usize) -> Self {
538        Self {
539            sequence: (0..size).collect(),
540            rotations: vec![0; size],
541            rotation_options,
542            mirrors: vec![false; size],
543            objective: f64::NEG_INFINITY,
544        }
545    }
546
547    /// Creates a random permutation solution.
548    pub fn random<R: Rng>(size: usize, rotation_options: usize, rng: &mut R) -> Self {
549        let mut sequence: Vec<usize> = (0..size).collect();
550        sequence.shuffle(rng);
551
552        let rotations: Vec<usize> = (0..size)
553            .map(|_| rng.random_range(0..rotation_options.max(1)))
554            .collect();
555
556        let mirrors: Vec<bool> = (0..size).map(|_| rng.random()).collect();
557
558        Self {
559            sequence,
560            rotations,
561            rotation_options,
562            mirrors,
563            objective: f64::NEG_INFINITY,
564        }
565    }
566
567    /// Returns the length of the sequence.
568    pub fn len(&self) -> usize {
569        self.sequence.len()
570    }
571
572    /// Returns true if empty.
573    pub fn is_empty(&self) -> bool {
574        self.sequence.is_empty()
575    }
576
577    /// Applies swap operator: swaps two elements in sequence.
578    pub fn apply_swap<R: Rng>(&self, rng: &mut R) -> Self {
579        let mut result = self.clone();
580        if result.sequence.len() < 2 {
581            return result;
582        }
583
584        let i = rng.random_range(0..result.sequence.len());
585        let j = rng.random_range(0..result.sequence.len());
586        result.sequence.swap(i, j);
587        result.objective = f64::NEG_INFINITY;
588        result
589    }
590
591    /// Applies relocate operator: moves an element to a new position.
592    pub fn apply_relocate<R: Rng>(&self, rng: &mut R) -> Self {
593        let mut result = self.clone();
594        if result.sequence.len() < 2 {
595            return result;
596        }
597
598        let from = rng.random_range(0..result.sequence.len());
599        let to = rng.random_range(0..result.sequence.len());
600
601        if from != to {
602            let elem = result.sequence.remove(from);
603            let insert_pos = if to > from { to - 1 } else { to };
604            result
605                .sequence
606                .insert(insert_pos.min(result.sequence.len()), elem);
607        }
608
609        result.objective = f64::NEG_INFINITY;
610        result
611    }
612
613    /// Applies inversion operator: reverses a segment.
614    pub fn apply_inversion<R: Rng>(&self, rng: &mut R) -> Self {
615        let mut result = self.clone();
616        let n = result.sequence.len();
617        if n < 2 {
618            return result;
619        }
620
621        let (mut p1, mut p2) = (rng.random_range(0..n), rng.random_range(0..n));
622        if p1 > p2 {
623            std::mem::swap(&mut p1, &mut p2);
624        }
625
626        result.sequence[p1..=p2].reverse();
627        result.objective = f64::NEG_INFINITY;
628        result
629    }
630
631    /// Applies rotation operator: changes rotation of one element.
632    pub fn apply_rotation<R: Rng>(&self, rng: &mut R) -> Self {
633        let mut result = self.clone();
634        if result.rotations.is_empty() || result.rotation_options <= 1 {
635            return result;
636        }
637
638        let idx = rng.random_range(0..result.rotations.len());
639        result.rotations[idx] = rng.random_range(0..result.rotation_options);
640        result.objective = f64::NEG_INFINITY;
641        result
642    }
643
644    /// Applies mirror-flip operator: flips one element's mirror bit
645    /// (`allow_flip` support). Same shape as `apply_rotation` — unconditional
646    /// on the gene, the consuming problem masks it against its own
647    /// per-item mirror-eligibility (e.g. `Geometry2D::allow_flip()`).
648    pub fn apply_mirror_flip<R: Rng>(&self, rng: &mut R) -> Self {
649        let mut result = self.clone();
650        if result.mirrors.is_empty() {
651            return result;
652        }
653
654        let idx = rng.random_range(0..result.mirrors.len());
655        result.mirrors[idx] = !result.mirrors[idx];
656        result.objective = f64::NEG_INFINITY;
657        result
658    }
659
660    /// Applies chain operator: 3-opt style move.
661    pub fn apply_chain<R: Rng>(&self, rng: &mut R) -> Self {
662        let mut result = self.clone();
663        let n = result.sequence.len();
664        if n < 4 {
665            // Fall back to swap for small sequences
666            return self.apply_swap(rng);
667        }
668
669        // Select three distinct positions
670        let mut positions: Vec<usize> = (0..n).collect();
671        positions.shuffle(rng);
672        let mut selected: Vec<usize> = positions.into_iter().take(3).collect();
673        selected.sort();
674
675        let (p1, p2, p3) = (selected[0], selected[1], selected[2]);
676
677        // Rotate segments: [0..p1] [p1..p2] [p2..p3] [p3..n]
678        // New order: [0..p1] [p2..p3] [p1..p2] [p3..n]
679        let seg1: Vec<usize> = result.sequence[..p1].to_vec();
680        let seg2: Vec<usize> = result.sequence[p1..p2].to_vec();
681        let seg3: Vec<usize> = result.sequence[p2..p3].to_vec();
682        let seg4: Vec<usize> = result.sequence[p3..].to_vec();
683
684        result.sequence = [seg1, seg3, seg2, seg4].concat();
685        result.objective = f64::NEG_INFINITY;
686        result
687    }
688}
689
690impl SaSolution for PermutationSolution {
691    fn objective(&self) -> f64 {
692        self.objective
693    }
694
695    fn set_objective(&mut self, value: f64) {
696        self.objective = value;
697    }
698}
699
700#[cfg(test)]
701mod tests {
702    use super::*;
703
704    struct SimpleMaxProblem {
705        size: usize,
706    }
707
708    impl SaProblem for SimpleMaxProblem {
709        type Solution = PermutationSolution;
710
711        fn initial_solution<R: Rng>(&self, rng: &mut R) -> Self::Solution {
712            PermutationSolution::random(self.size, 1, rng)
713        }
714
715        fn neighbor<R: Rng>(
716            &self,
717            solution: &Self::Solution,
718            operator: NeighborhoodOperator,
719            rng: &mut R,
720        ) -> Self::Solution {
721            match operator {
722                NeighborhoodOperator::Swap => solution.apply_swap(rng),
723                NeighborhoodOperator::Relocate => solution.apply_relocate(rng),
724                NeighborhoodOperator::Inversion => solution.apply_inversion(rng),
725                NeighborhoodOperator::Rotation => solution.apply_rotation(rng),
726                NeighborhoodOperator::Chain => solution.apply_chain(rng),
727                NeighborhoodOperator::MirrorFlip => solution.apply_mirror_flip(rng),
728            }
729        }
730
731        fn evaluate(&self, solution: &mut Self::Solution) {
732            // Maximize: sequence should be in ascending order
733            // Fitness = negative sum of inversions
734            let mut inversions = 0i64;
735            for i in 0..solution.sequence.len() {
736                for j in (i + 1)..solution.sequence.len() {
737                    if solution.sequence[i] > solution.sequence[j] {
738                        inversions += 1;
739                    }
740                }
741            }
742            solution.set_objective(-inversions as f64);
743        }
744    }
745
746    #[test]
747    fn test_sa_basic() {
748        let config = SaConfig::default()
749            .with_initial_temp(100.0)
750            .with_final_temp(0.1)
751            .with_cooling_rate(0.9)
752            .with_iterations_per_temp(50)
753            .with_max_iterations(5000);
754
755        let problem = SimpleMaxProblem { size: 10 };
756        let runner = SaRunner::new(config, problem);
757        let result = runner.run();
758
759        // Should find something reasonably good (fewer inversions)
760        assert!(result.best.objective() > -20.0);
761        assert!(result.iterations > 0);
762    }
763
764    #[test]
765    fn test_cooling_schedules() {
766        let problem = SimpleMaxProblem { size: 5 };
767
768        for schedule in [
769            CoolingSchedule::Geometric,
770            CoolingSchedule::Linear,
771            CoolingSchedule::Adaptive,
772            CoolingSchedule::LundyMees,
773        ] {
774            let config = SaConfig::default()
775                .with_cooling_schedule(schedule)
776                .with_max_iterations(1000);
777
778            let runner = SaRunner::new(config, problem.clone());
779            let result = runner.run();
780
781            // Should complete without panic
782            assert!(result.iterations > 0);
783        }
784    }
785
786    #[test]
787    fn test_neighborhood_operators() {
788        let mut rng = rand::rng();
789        let solution = PermutationSolution::random(10, 4, &mut rng);
790
791        // Test all operators produce valid permutations
792        let swap = solution.apply_swap(&mut rng);
793        let relocate = solution.apply_relocate(&mut rng);
794        let inversion = solution.apply_inversion(&mut rng);
795        let rotation = solution.apply_rotation(&mut rng);
796        let chain = solution.apply_chain(&mut rng);
797
798        for sol in [&swap, &relocate, &inversion, &rotation, &chain] {
799            let mut sorted = sol.sequence.clone();
800            sorted.sort();
801            assert_eq!(sorted, (0..10).collect::<Vec<_>>());
802        }
803    }
804
805    #[test]
806    fn test_reheating() {
807        let config = SaConfig::default()
808            .with_initial_temp(10.0)
809            .with_final_temp(0.1)
810            .with_max_iterations(500)
811            .with_reheating(50, 1.5);
812
813        let problem = SimpleMaxProblem { size: 8 };
814        let runner = SaRunner::new(config, problem);
815        let result = runner.run();
816
817        // Should complete (reheating may or may not trigger)
818        assert!(result.iterations > 0);
819    }
820
821    impl Clone for SimpleMaxProblem {
822        fn clone(&self) -> Self {
823            Self { size: self.size }
824        }
825    }
826}