u_nesting_core/
sa.rs

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