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