1use rand::prelude::*;
19use std::sync::atomic::{AtomicBool, Ordering};
20use std::sync::Arc;
21use std::time::Duration;
22use u_metaheur::TemperatureScale;
23
24use crate::error::{check_above, check_at_least, check_range, Result};
25use crate::result::StopReason;
26use crate::timing::{expired, Timer};
27
28#[cfg(feature = "serde")]
29use serde::{Deserialize, Serialize};
30
31#[derive(Debug, Clone, Copy, Default)]
33#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
34pub enum CoolingSchedule {
35 #[default]
37 Geometric,
38 Linear,
40 Adaptive,
42 LundyMees,
44}
45
46#[derive(Debug, Clone)]
48#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
49pub struct SaConfig {
50 #[cfg_attr(feature = "serde", serde(default))]
57 pub temperature_scale: TemperatureScale,
58 pub initial_temp: f64,
60 pub final_temp: f64,
62 pub cooling_rate: f64,
64 pub iterations_per_temp: usize,
66 pub max_iterations: Option<u64>,
68 pub cooling_schedule: CoolingSchedule,
70 pub time_limit: Option<Duration>,
72 pub target_fitness: Option<f64>,
74 pub enable_reheating: bool,
76 pub reheat_threshold: u64,
78 pub reheat_factor: f64,
80}
81
82impl Default for SaConfig {
83 fn default() -> Self {
84 Self {
85 temperature_scale: TemperatureScale::default(),
86 initial_temp: 1.0,
87 final_temp: 0.001,
88 cooling_rate: 0.95,
89 iterations_per_temp: 100,
90 max_iterations: Some(100_000),
91 cooling_schedule: CoolingSchedule::Geometric,
92 time_limit: None,
93 target_fitness: None,
94 enable_reheating: false,
95 reheat_threshold: 1000,
96 reheat_factor: 2.0,
97 }
98 }
99}
100
101impl SaConfig {
102 pub fn new() -> Self {
104 Self::default()
105 }
106
107 pub fn with_initial_temp(mut self, temp: f64) -> Self {
110 self.temperature_scale = TemperatureScale::Absolute;
111 self.initial_temp = temp;
112 self
113 }
114
115 pub fn with_relative_temperature(
121 mut self,
122 initial: f64,
123 final_temp: f64,
124 samples: usize,
125 ) -> Self {
126 self.temperature_scale = TemperatureScale::Calibrated { samples };
127 self.initial_temp = initial;
128 self.final_temp = final_temp;
129 self
130 }
131
132 pub fn with_final_temp(mut self, temp: f64) -> Self {
134 self.final_temp = temp;
135 self
136 }
137
138 pub fn with_cooling_rate(mut self, rate: f64) -> Self {
140 self.cooling_rate = rate;
141 self
142 }
143
144 pub fn validate(&self) -> Result<()> {
153 check_range("cooling_rate", self.cooling_rate, 0.001, 0.9999)?;
154 check_above("initial_temp", self.initial_temp, 0.0)?;
155 check_above("final_temp", self.final_temp, 0.0)?;
156 check_range("final_temp", self.final_temp, 0.0, self.initial_temp)?;
157 check_at_least("iterations_per_temp", self.iterations_per_temp as f64, 1.0)?;
158 check_at_least("reheat_factor", self.reheat_factor, 1.0)?;
159 if let TemperatureScale::Calibrated { samples } = self.temperature_scale {
160 check_at_least("temperature_scale.samples", samples as f64, 1.0)?;
161 }
162 Ok(())
163 }
164
165 pub fn with_iterations_per_temp(mut self, iterations: usize) -> Self {
167 self.iterations_per_temp = iterations;
168 self
169 }
170
171 pub fn with_max_iterations(mut self, iterations: u64) -> Self {
173 self.max_iterations = Some(iterations);
174 self
175 }
176
177 pub fn with_cooling_schedule(mut self, schedule: CoolingSchedule) -> Self {
179 self.cooling_schedule = schedule;
180 self
181 }
182
183 pub fn with_time_limit(mut self, duration: Duration) -> Self {
185 self.time_limit = Some(duration);
186 self
187 }
188
189 pub fn with_target_fitness(mut self, fitness: f64) -> Self {
191 self.target_fitness = Some(fitness);
192 self
193 }
194
195 pub fn with_reheating(mut self, threshold: u64, factor: f64) -> Self {
197 self.enable_reheating = true;
198 self.reheat_threshold = threshold;
199 self.reheat_factor = factor;
200 self
201 }
202}
203
204pub trait SaSolution: Clone + Send + Sync {
206 fn objective(&self) -> f64;
209
210 fn set_objective(&mut self, value: f64);
212}
213
214#[derive(Debug, Clone, Copy, PartialEq, Eq)]
216pub enum NeighborhoodOperator {
217 Swap,
219 Relocate,
221 Inversion,
223 Rotation,
225 Chain,
227 MirrorFlip,
232}
233
234pub trait SaProblem: Send + Sync {
236 type Solution: SaSolution;
238
239 fn initial_solution<R: Rng>(&self, rng: &mut R) -> Self::Solution;
241
242 fn neighbor<R: Rng>(
244 &self,
245 solution: &Self::Solution,
246 operator: NeighborhoodOperator,
247 rng: &mut R,
248 ) -> Self::Solution;
249
250 fn evaluate(&self, solution: &mut Self::Solution);
252
253 fn available_operators(&self) -> Vec<NeighborhoodOperator> {
255 vec![
256 NeighborhoodOperator::Swap,
257 NeighborhoodOperator::Relocate,
258 NeighborhoodOperator::Inversion,
259 ]
260 }
261
262 fn on_temperature_change(
264 &self,
265 _temperature: f64,
266 _iteration: u64,
267 _best: &Self::Solution,
268 _current: &Self::Solution,
269 ) {
270 }
272}
273
274#[derive(Debug, Clone)]
276pub struct SaProgress {
277 pub temperature: f64,
279 pub iteration: u64,
281 pub best_fitness: f64,
283 pub current_fitness: f64,
285 pub acceptance_rate: f64,
287 pub elapsed: Duration,
289 pub running: bool,
291}
292
293#[derive(Debug, Clone)]
295pub struct SaResult<S: SaSolution> {
296 pub best: S,
298 pub final_temperature: f64,
300 pub temperature_unit: f64,
303 pub iterations: u64,
305 pub elapsed: Duration,
307 pub stop_reason: StopReason,
310 pub reheat_count: u32,
312 pub history: Vec<f64>,
314}
315
316pub struct SaRunner<P: SaProblem> {
318 config: SaConfig,
319 problem: P,
320 cancelled: Arc<AtomicBool>,
321}
322
323impl<P: SaProblem> SaRunner<P> {
324 pub fn new(config: SaConfig, problem: P) -> Result<Self> {
329 Self::with_cancellation(config, problem, Arc::new(AtomicBool::new(false)))
330 }
331
332 pub fn with_cancellation(
338 config: SaConfig,
339 problem: P,
340 cancelled: Arc<AtomicBool>,
341 ) -> Result<Self> {
342 config.validate()?;
343 Ok(Self {
344 config,
345 problem,
346 cancelled,
347 })
348 }
349
350 pub fn cancel_handle(&self) -> Arc<AtomicBool> {
352 self.cancelled.clone()
353 }
354
355 pub fn run(&self) -> SaResult<P::Solution> {
357 self.run_with_rng(&mut rand::rng())
358 }
359
360 pub fn run_with_rng<R: Rng>(&self, rng: &mut R) -> SaResult<P::Solution> {
362 self.run_with_rng_and_progress::<R, fn(SaProgress)>(rng, None)
363 }
364
365 pub fn run_with_rng_and_progress<R: Rng, F>(
369 &self,
370 rng: &mut R,
371 progress_callback: Option<F>,
372 ) -> SaResult<P::Solution>
373 where
374 F: Fn(SaProgress),
375 {
376 let start = Timer::now();
377 let report = |progress: SaProgress| {
378 if let Some(ref callback) = progress_callback {
379 callback(progress);
380 }
381 };
382 let mut history = Vec::new();
383
384 let mut current = self.problem.initial_solution(rng);
386 self.problem.evaluate(&mut current);
387 let mut best = current.clone();
388 let mut best_fitness = best.objective();
389
390 let operators = self.problem.available_operators();
394 let scale = match self.config.temperature_scale {
395 TemperatureScale::Absolute => 1.0,
396 TemperatureScale::Calibrated { samples } => {
397 let from = current.objective();
398 let mut worsenings = Vec::with_capacity(samples);
399 for _ in 0..samples {
400 if expired(&start, self.config.time_limit)
401 || self.cancelled.load(Ordering::Relaxed)
402 {
403 break;
404 }
405 let operator = operators[rng.random_range(0..operators.len())];
406 let mut neighbor = self.problem.neighbor(¤t, operator, rng);
407 self.problem.evaluate(&mut neighbor);
408 worsenings.push(from - neighbor.objective());
409 }
410 TemperatureScale::cost_scale(worsenings)
411 }
412 };
413 let initial_temp = self.config.initial_temp * scale;
414 let final_temp = self.config.final_temp * scale;
415 let mut temperature = initial_temp;
416 let mut iteration = 0u64;
417 let reached = |fitness: f64| self.config.target_fitness.is_some_and(|t| fitness >= t);
418 let mut reheat_count = 0u32;
419 let mut stagnation_count = 0u64;
420
421 let temp_delta = if matches!(self.config.cooling_schedule, CoolingSchedule::Linear) {
422 (initial_temp - final_temp)
423 / (self.config.max_iterations.unwrap_or(10000) as f64
424 / self.config.iterations_per_temp as f64)
425 } else {
426 0.0
427 };
428
429 let mut accepted_count = 0usize;
431 let mut total_count = 0usize;
432
433 let stop_reason = loop {
436 if reached(best_fitness) {
437 break StopReason::TargetReached;
438 }
439 if temperature <= final_temp {
440 break StopReason::Completed;
441 }
442 if self
443 .config
444 .max_iterations
445 .is_some_and(|max| iteration >= max)
446 {
447 break StopReason::IterationLimit;
448 }
449 if self.cancelled.load(Ordering::Relaxed) {
450 break StopReason::Cancelled;
451 }
452 if expired(&start, self.config.time_limit) {
453 break StopReason::TimeLimit;
454 }
455
456 for _ in 0..self.config.iterations_per_temp {
459 if expired(&start, self.config.time_limit) {
460 break;
461 }
462 iteration += 1;
463 total_count += 1;
464
465 let operator = operators[rng.random_range(0..operators.len())];
467
468 let mut neighbor = self.problem.neighbor(¤t, operator, rng);
470 self.problem.evaluate(&mut neighbor);
471
472 let current_obj = current.objective();
473 let neighbor_obj = neighbor.objective();
474 let delta = neighbor_obj - current_obj;
475
476 let accept = if delta >= 0.0 {
478 true
480 } else {
481 let probability = (delta / temperature).exp();
483 rng.random::<f64>() < probability
484 };
485
486 if accept {
487 accepted_count += 1;
488 current = neighbor;
489
490 if current.objective() > best_fitness {
492 best = current.clone();
493 best_fitness = best.objective();
494 stagnation_count = 0;
495 } else {
496 stagnation_count += 1;
497 }
498 } else {
499 stagnation_count += 1;
500 }
501
502 report(SaProgress {
506 temperature,
507 iteration,
508 best_fitness,
509 current_fitness: current.objective(),
510 acceptance_rate: accepted_count as f64 / total_count.max(1) as f64,
511 elapsed: start.elapsed(),
512 running: true,
513 });
514
515 if let Some(max) = self.config.max_iterations {
517 if iteration >= max {
518 break;
519 }
520 }
521 }
522
523 history.push(best_fitness);
525
526 self.problem
528 .on_temperature_change(temperature, iteration, &best, ¤t);
529
530 if self.config.enable_reheating && stagnation_count >= self.config.reheat_threshold {
532 temperature *= self.config.reheat_factor;
533 temperature = temperature.min(initial_temp);
534 stagnation_count = 0;
535 reheat_count += 1;
536 }
537
538 temperature = self.cool_down(
540 temperature,
541 temp_delta,
542 (final_temp, scale),
543 accepted_count,
544 total_count,
545 );
546
547 accepted_count = 0;
549 total_count = 0;
550 };
551
552 history.push(best_fitness);
554 report(SaProgress {
555 temperature,
556 iteration,
557 best_fitness,
558 current_fitness: current.objective(),
559 acceptance_rate: 0.0,
560 elapsed: start.elapsed(),
561 running: false,
562 });
563
564 SaResult {
565 best,
566 final_temperature: temperature,
567 temperature_unit: scale,
568 iterations: iteration,
569 elapsed: start.elapsed(),
570 stop_reason,
571 reheat_count,
572 history,
573 }
574 }
575
576 fn cool_down(
578 &self,
579 current_temp: f64,
580 delta: f64,
581 (final_temp, scale): (f64, f64),
582 accepted: usize,
583 total: usize,
584 ) -> f64 {
585 match self.config.cooling_schedule {
586 CoolingSchedule::Geometric => current_temp * self.config.cooling_rate,
587 CoolingSchedule::Linear => (current_temp - delta).max(final_temp),
588 CoolingSchedule::Adaptive => {
589 let acceptance_rate = if total > 0 {
591 accepted as f64 / total as f64
592 } else {
593 0.5
594 };
595
596 let adjusted_rate = if acceptance_rate > 0.5 {
598 self.config.cooling_rate * 0.95 } else if acceptance_rate < 0.1 {
600 self.config.cooling_rate.powf(0.5) } else {
602 self.config.cooling_rate
603 };
604
605 current_temp * adjusted_rate
606 }
607 CoolingSchedule::LundyMees => {
608 current_temp / (1.0 + self.config.cooling_rate / scale * current_temp)
611 }
612 }
613 }
614}
615
616pub fn another_option<R: Rng>(current: usize, count: usize, rng: &mut R) -> usize {
618 let pick = rng.random_range(0..count - 1);
619 if pick >= current.min(count - 1) {
620 pick + 1
621 } else {
622 pick
623 }
624}
625
626#[derive(Debug, Clone)]
628pub struct PermutationSolution {
629 pub sequence: Vec<usize>,
631 pub rotations: Vec<usize>,
634 pub rotation_counts: std::sync::Arc<[usize]>,
637 pub mirrors: Vec<bool>,
640 objective: f64,
642}
643
644impl PermutationSolution {
645 pub fn new(rotation_counts: impl Into<std::sync::Arc<[usize]>>) -> Self {
648 let rotation_counts = rotation_counts.into();
649 let size = rotation_counts.len();
650 Self {
651 sequence: (0..size).collect(),
652 rotations: vec![0; size],
653 rotation_counts,
654 mirrors: vec![false; size],
655 objective: f64::NEG_INFINITY,
656 }
657 }
658
659 pub fn random<R: Rng>(
662 rotation_counts: impl Into<std::sync::Arc<[usize]>>,
663 rng: &mut R,
664 ) -> Self {
665 let rotation_counts = rotation_counts.into();
666 let size = rotation_counts.len();
667 let mut sequence: Vec<usize> = (0..size).collect();
668 sequence.shuffle(rng);
669
670 let rotations: Vec<usize> = rotation_counts
671 .iter()
672 .map(|&k| rng.random_range(0..k.max(1)))
673 .collect();
674
675 let mirrors: Vec<bool> = (0..size).map(|_| rng.random()).collect();
676
677 Self {
678 sequence,
679 rotations,
680 rotation_counts,
681 mirrors,
682 objective: f64::NEG_INFINITY,
683 }
684 }
685
686 pub fn len(&self) -> usize {
688 self.sequence.len()
689 }
690
691 pub fn is_empty(&self) -> bool {
693 self.sequence.is_empty()
694 }
695
696 pub fn apply_swap<R: Rng>(&self, rng: &mut R) -> Self {
698 let mut result = self.clone();
699 if result.sequence.len() < 2 {
700 return result;
701 }
702
703 let i = rng.random_range(0..result.sequence.len());
704 let j = rng.random_range(0..result.sequence.len());
705 result.sequence.swap(i, j);
706 result.objective = f64::NEG_INFINITY;
707 result
708 }
709
710 pub fn apply_relocate<R: Rng>(&self, rng: &mut R) -> Self {
712 let mut result = self.clone();
713 if result.sequence.len() < 2 {
714 return result;
715 }
716
717 let from = rng.random_range(0..result.sequence.len());
718 let to = rng.random_range(0..result.sequence.len());
719
720 if from != to {
721 let elem = result.sequence.remove(from);
722 let insert_pos = if to > from { to - 1 } else { to };
723 result
724 .sequence
725 .insert(insert_pos.min(result.sequence.len()), elem);
726 }
727
728 result.objective = f64::NEG_INFINITY;
729 result
730 }
731
732 pub fn apply_inversion<R: Rng>(&self, rng: &mut R) -> Self {
734 let mut result = self.clone();
735 let n = result.sequence.len();
736 if n < 2 {
737 return result;
738 }
739
740 let (mut p1, mut p2) = (rng.random_range(0..n), rng.random_range(0..n));
741 if p1 > p2 {
742 std::mem::swap(&mut p1, &mut p2);
743 }
744
745 result.sequence[p1..=p2].reverse();
746 result.objective = f64::NEG_INFINITY;
747 result
748 }
749
750 pub fn apply_rotation<R: Rng>(&self, rng: &mut R) -> Self {
754 let mut result = self.clone();
755 let turnable: Vec<usize> = (0..result.rotations.len())
756 .filter(|&i| result.rotation_counts.get(i).copied().unwrap_or(1) > 1)
757 .collect();
758 if turnable.is_empty() {
759 return result;
760 }
761
762 let idx = turnable[rng.random_range(0..turnable.len())];
763 result.rotations[idx] =
764 another_option(result.rotations[idx], result.rotation_counts[idx], rng);
765 result.objective = f64::NEG_INFINITY;
766 result
767 }
768
769 pub fn apply_mirror_flip<R: Rng>(&self, rng: &mut R) -> Self {
774 let mut result = self.clone();
775 if result.mirrors.is_empty() {
776 return result;
777 }
778
779 let idx = rng.random_range(0..result.mirrors.len());
780 result.mirrors[idx] = !result.mirrors[idx];
781 result.objective = f64::NEG_INFINITY;
782 result
783 }
784
785 pub fn apply_chain<R: Rng>(&self, rng: &mut R) -> Self {
787 let mut result = self.clone();
788 let n = result.sequence.len();
789 if n < 4 {
790 return self.apply_swap(rng);
792 }
793
794 let mut positions: Vec<usize> = (0..n).collect();
796 positions.shuffle(rng);
797 let mut selected: Vec<usize> = positions.into_iter().take(3).collect();
798 selected.sort();
799
800 let (p1, p2, p3) = (selected[0], selected[1], selected[2]);
801
802 let seg1: Vec<usize> = result.sequence[..p1].to_vec();
805 let seg2: Vec<usize> = result.sequence[p1..p2].to_vec();
806 let seg3: Vec<usize> = result.sequence[p2..p3].to_vec();
807 let seg4: Vec<usize> = result.sequence[p3..].to_vec();
808
809 result.sequence = [seg1, seg3, seg2, seg4].concat();
810 result.objective = f64::NEG_INFINITY;
811 result
812 }
813}
814
815impl SaSolution for PermutationSolution {
816 fn objective(&self) -> f64 {
817 self.objective
818 }
819
820 fn set_objective(&mut self, value: f64) {
821 self.objective = value;
822 }
823}
824
825#[cfg(test)]
826mod tests {
827 use super::*;
828
829 #[test]
830 fn a_rotation_move_turns_only_items_with_options_within_their_own() {
831 use rand::rngs::StdRng;
832 use rand::SeedableRng;
833 let mut rng = StdRng::seed_from_u64(7);
834 let counts: Vec<usize> = vec![1, 3, 1, 8];
835 let start = PermutationSolution::random(counts.clone(), &mut rng);
836 for (r, k) in start.rotations.iter().zip(&counts) {
837 assert!(r < k);
838 }
839 let mut reached = vec![std::collections::HashSet::new(); counts.len()];
840 let mut current = start;
841 for _ in 0..2000 {
842 let next = current.apply_rotation(&mut rng);
843 let changed: Vec<usize> = (0..counts.len())
844 .filter(|&i| next.rotations[i] != current.rotations[i])
845 .collect();
846 assert_eq!(changed.len(), 1, "a move changes exactly one item");
847 assert!(counts[changed[0]] > 1, "a fixed item was turned");
848 for (i, &r) in next.rotations.iter().enumerate() {
849 assert!(r < counts[i]);
850 reached[i].insert(r);
851 }
852 current = next;
853 }
854 assert_eq!(reached[1].len(), 3);
856 assert_eq!(reached[3].len(), 8);
857 assert_eq!(reached[0].len(), 1);
858 let fixed = PermutationSolution::new(vec![1; 4]);
860 assert_eq!(fixed.apply_rotation(&mut rng).rotations, vec![0; 4]);
861 }
862
863 struct SimpleMaxProblem {
864 size: usize,
865 }
866
867 impl SaProblem for SimpleMaxProblem {
868 type Solution = PermutationSolution;
869
870 fn initial_solution<R: Rng>(&self, rng: &mut R) -> Self::Solution {
871 PermutationSolution::random(vec![1; self.size], rng)
872 }
873
874 fn neighbor<R: Rng>(
875 &self,
876 solution: &Self::Solution,
877 operator: NeighborhoodOperator,
878 rng: &mut R,
879 ) -> Self::Solution {
880 match operator {
881 NeighborhoodOperator::Swap => solution.apply_swap(rng),
882 NeighborhoodOperator::Relocate => solution.apply_relocate(rng),
883 NeighborhoodOperator::Inversion => solution.apply_inversion(rng),
884 NeighborhoodOperator::Rotation => solution.apply_rotation(rng),
885 NeighborhoodOperator::Chain => solution.apply_chain(rng),
886 NeighborhoodOperator::MirrorFlip => solution.apply_mirror_flip(rng),
887 }
888 }
889
890 fn evaluate(&self, solution: &mut Self::Solution) {
891 let mut inversions = 0i64;
894 for i in 0..solution.sequence.len() {
895 for j in (i + 1)..solution.sequence.len() {
896 if solution.sequence[i] > solution.sequence[j] {
897 inversions += 1;
898 }
899 }
900 }
901 solution.set_objective(-inversions as f64);
902 }
903 }
904
905 #[test]
906 fn test_sa_basic() {
907 let config = SaConfig::default()
908 .with_initial_temp(100.0)
909 .with_final_temp(0.1)
910 .with_cooling_rate(0.9)
911 .with_iterations_per_temp(50)
912 .with_max_iterations(5000);
913
914 let problem = SimpleMaxProblem { size: 10 };
915 let runner = SaRunner::new(config, problem).expect("valid config");
916 let result = runner.run();
917
918 assert!(result.best.objective() > -20.0);
920 assert!(result.iterations > 0);
921 }
922
923 struct ScaledMax {
925 inner: SimpleMaxProblem,
926 unit: f64,
927 }
928
929 impl SaProblem for ScaledMax {
930 type Solution = PermutationSolution;
931
932 fn initial_solution<R: Rng>(&self, rng: &mut R) -> Self::Solution {
933 self.inner.initial_solution(rng)
934 }
935
936 fn neighbor<R: Rng>(
937 &self,
938 solution: &Self::Solution,
939 operator: NeighborhoodOperator,
940 rng: &mut R,
941 ) -> Self::Solution {
942 self.inner.neighbor(solution, operator, rng)
943 }
944
945 fn evaluate(&self, solution: &mut Self::Solution) {
946 self.inner.evaluate(solution);
947 solution.set_objective(solution.objective() * self.unit);
948 }
949 }
950
951 #[test]
956 fn a_calibrated_run_does_not_depend_on_the_unit_of_the_objective() {
957 use rand::rngs::StdRng;
958 let config = SaConfig::default()
959 .with_relative_temperature(1.0, 0.01, 20)
960 .with_iterations_per_temp(10)
961 .with_max_iterations(1500);
962 let run = |unit: f64| {
963 let problem = ScaledMax {
964 inner: SimpleMaxProblem { size: 12 },
965 unit,
966 };
967 let runner = SaRunner::new(config.clone(), problem).expect("valid config");
968 runner.run_with_rng(&mut StdRng::seed_from_u64(9))
969 };
970 let (small, large) = (run(1.0), run(1024.0));
971 assert_eq!(small.best.sequence, large.best.sequence);
972 assert_eq!(small.iterations, large.iterations);
973 assert_eq!(small.best.objective() * 1024.0, large.best.objective());
974 assert_eq!(small.temperature_unit * 1024.0, large.temperature_unit);
975 }
976
977 #[test]
979 fn an_absolute_temperature_is_in_the_unit_of_the_objective() {
980 let config = SaConfig::default()
981 .with_initial_temp(10.0)
982 .with_final_temp(0.5)
983 .with_max_iterations(200);
984 let runner = SaRunner::new(config, SimpleMaxProblem { size: 6 }).expect("valid config");
985 let result = runner.run();
986 assert_eq!(result.temperature_unit, 1.0);
987 assert!(result.final_temperature <= 10.0);
988 }
989
990 #[test]
991 fn test_cooling_schedules() {
992 let problem = SimpleMaxProblem { size: 5 };
993
994 for schedule in [
995 CoolingSchedule::Geometric,
996 CoolingSchedule::Linear,
997 CoolingSchedule::Adaptive,
998 CoolingSchedule::LundyMees,
999 ] {
1000 let config = SaConfig::default()
1001 .with_cooling_schedule(schedule)
1002 .with_max_iterations(1000);
1003
1004 let runner = SaRunner::new(config, problem.clone()).expect("valid config");
1005 let result = runner.run();
1006
1007 assert!(result.iterations > 0);
1009 }
1010 }
1011
1012 #[test]
1013 fn test_neighborhood_operators() {
1014 let mut rng = rand::rng();
1015 let solution = PermutationSolution::random(vec![4; 10], &mut rng);
1016
1017 let swap = solution.apply_swap(&mut rng);
1019 let relocate = solution.apply_relocate(&mut rng);
1020 let inversion = solution.apply_inversion(&mut rng);
1021 let rotation = solution.apply_rotation(&mut rng);
1022 let chain = solution.apply_chain(&mut rng);
1023
1024 for sol in [&swap, &relocate, &inversion, &rotation, &chain] {
1025 let mut sorted = sol.sequence.clone();
1026 sorted.sort();
1027 assert_eq!(sorted, (0..10).collect::<Vec<_>>());
1028 }
1029 }
1030
1031 #[test]
1032 fn test_reheating() {
1033 let config = SaConfig::default()
1034 .with_initial_temp(10.0)
1035 .with_final_temp(0.1)
1036 .with_max_iterations(500)
1037 .with_reheating(50, 1.5);
1038
1039 let problem = SimpleMaxProblem { size: 8 };
1040 let runner = SaRunner::new(config, problem).expect("valid config");
1041 let result = runner.run();
1042
1043 assert!(result.iterations > 0);
1045 }
1046
1047 impl Clone for SimpleMaxProblem {
1048 fn clone(&self) -> Self {
1049 Self { size: self.size }
1050 }
1051 }
1052}