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