1use crate::error::{OptimizeError, OptimizeResult};
10use scirs2_core::random::rngs::StdRng;
11use scirs2_core::random::{rng, Rng, SeedableRng};
12use scirs2_core::RngExt;
13
14pub trait CombinatorialProblem {
24 fn num_nodes(&self) -> usize;
26
27 fn heuristic(&self, i: usize, j: usize) -> f64;
30
31 fn evaluate(&self, solution: &[usize]) -> f64;
34
35 fn is_feasible(&self, _current: usize, _next: usize, _visited: &[bool]) -> bool {
38 true
39 }
40}
41
42#[derive(Debug, Clone)]
44pub struct TspProblem {
45 distances: Vec<Vec<f64>>,
47 n: usize,
48}
49
50impl TspProblem {
51 pub fn new(distances: Vec<Vec<f64>>) -> OptimizeResult<Self> {
53 let n = distances.len();
54 if n == 0 {
55 return Err(OptimizeError::InvalidInput(
56 "Distance matrix must not be empty".to_string(),
57 ));
58 }
59 for row in &distances {
60 if row.len() != n {
61 return Err(OptimizeError::InvalidInput(
62 "Distance matrix must be square".to_string(),
63 ));
64 }
65 }
66 Ok(Self { distances, n })
67 }
68
69 pub fn from_coordinates(coords: &[(f64, f64)]) -> OptimizeResult<Self> {
71 let n = coords.len();
72 if n == 0 {
73 return Err(OptimizeError::InvalidInput(
74 "Coordinates must not be empty".to_string(),
75 ));
76 }
77 let mut distances = vec![vec![0.0; n]; n];
78 for i in 0..n {
79 for j in (i + 1)..n {
80 let dx = coords[i].0 - coords[j].0;
81 let dy = coords[i].1 - coords[j].1;
82 let d = (dx * dx + dy * dy).sqrt();
83 distances[i][j] = d;
84 distances[j][i] = d;
85 }
86 }
87 Ok(Self { distances, n })
88 }
89
90 pub fn num_cities(&self) -> usize {
92 self.n
93 }
94
95 pub fn distance(&self, i: usize, j: usize) -> f64 {
97 self.distances[i][j]
98 }
99}
100
101impl CombinatorialProblem for TspProblem {
102 fn num_nodes(&self) -> usize {
103 self.n
104 }
105
106 fn heuristic(&self, i: usize, j: usize) -> f64 {
107 let d = self.distances[i][j];
108 if d > 0.0 {
109 1.0 / d
110 } else {
111 1e10 }
113 }
114
115 fn evaluate(&self, solution: &[usize]) -> f64 {
116 if solution.len() < 2 {
117 return 0.0;
118 }
119 let mut total = 0.0;
120 for i in 0..solution.len() - 1 {
121 total += self.distances[solution[i]][solution[i + 1]];
122 }
123 total += self.distances[*solution.last().unwrap_or(&0)][solution[0]];
125 total
126 }
127}
128
129#[derive(Clone)]
131pub struct PermutationProblem {
132 n: usize,
133 heuristic_matrix: Vec<Vec<f64>>,
134 eval_fn: std::sync::Arc<dyn Fn(&[usize]) -> f64 + Send + Sync>,
135}
136
137impl std::fmt::Debug for PermutationProblem {
138 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139 f.debug_struct("PermutationProblem")
140 .field("n", &self.n)
141 .field("heuristic_matrix_size", &self.heuristic_matrix.len())
142 .finish()
143 }
144}
145
146impl PermutationProblem {
147 pub fn new<F>(n: usize, heuristic_matrix: Vec<Vec<f64>>, eval_fn: F) -> OptimizeResult<Self>
153 where
154 F: Fn(&[usize]) -> f64 + Send + Sync + 'static,
155 {
156 if n == 0 {
157 return Err(OptimizeError::InvalidInput(
158 "Problem size must be > 0".to_string(),
159 ));
160 }
161 if heuristic_matrix.len() != n {
162 return Err(OptimizeError::InvalidInput(
163 "Heuristic matrix size must match n".to_string(),
164 ));
165 }
166 Ok(Self {
167 n,
168 heuristic_matrix,
169 eval_fn: std::sync::Arc::new(eval_fn),
170 })
171 }
172
173 pub fn with_uniform_heuristic<F>(n: usize, eval_fn: F) -> OptimizeResult<Self>
175 where
176 F: Fn(&[usize]) -> f64 + Send + Sync + 'static,
177 {
178 if n == 0 {
179 return Err(OptimizeError::InvalidInput(
180 "Problem size must be > 0".to_string(),
181 ));
182 }
183 let heuristic_matrix = vec![vec![1.0; n]; n];
184 Ok(Self {
185 n,
186 heuristic_matrix,
187 eval_fn: std::sync::Arc::new(eval_fn),
188 })
189 }
190}
191
192impl CombinatorialProblem for PermutationProblem {
193 fn num_nodes(&self) -> usize {
194 self.n
195 }
196
197 fn heuristic(&self, i: usize, j: usize) -> f64 {
198 self.heuristic_matrix[i][j]
199 }
200
201 fn evaluate(&self, solution: &[usize]) -> f64 {
202 (self.eval_fn)(solution)
203 }
204}
205
206#[derive(Debug, Clone)]
212pub struct AcoResult {
213 pub best_solution: Vec<usize>,
215 pub best_cost: f64,
217 pub iterations: usize,
219 pub nfev: usize,
221 pub converged: bool,
223 pub cost_history: Vec<f64>,
225 pub message: String,
227}
228
229#[derive(Debug, Clone)]
235pub struct AntSystemOptions {
236 pub num_ants: usize,
238 pub max_iter: usize,
240 pub alpha: f64,
242 pub beta: f64,
244 pub evaporation_rate: f64,
246 pub initial_pheromone: f64,
248 pub q_factor: f64,
250 pub seed: Option<u64>,
252 pub tol: f64,
254 pub patience: usize,
256}
257
258impl Default for AntSystemOptions {
259 fn default() -> Self {
260 Self {
261 num_ants: 20,
262 max_iter: 200,
263 alpha: 1.0,
264 beta: 2.0,
265 evaporation_rate: 0.5,
266 initial_pheromone: 1.0,
267 q_factor: 100.0,
268 seed: None,
269 tol: 1e-8,
270 patience: 50,
271 }
272 }
273}
274
275pub struct AntColonyOptimizer {
277 options: AntSystemOptions,
278 rng: StdRng,
279}
280
281impl AntColonyOptimizer {
282 pub fn new(options: AntSystemOptions) -> Self {
284 let seed = options.seed.unwrap_or_else(|| rng().random());
285 Self {
286 options,
287 rng: StdRng::seed_from_u64(seed),
288 }
289 }
290
291 pub fn solve<P: CombinatorialProblem>(&mut self, problem: &P) -> OptimizeResult<AcoResult> {
293 let n = problem.num_nodes();
294 if n == 0 {
295 return Err(OptimizeError::InvalidInput(
296 "Problem has 0 nodes".to_string(),
297 ));
298 }
299
300 let mut pheromone = vec![vec![self.options.initial_pheromone; n]; n];
302 let mut best_solution: Vec<usize> = Vec::new();
303 let mut best_cost = f64::INFINITY;
304 let mut nfev: usize = 0;
305 let mut cost_history = Vec::with_capacity(self.options.max_iter);
306 let mut no_improve_count: usize = 0;
307
308 for iteration in 0..self.options.max_iter {
309 let mut iteration_solutions = Vec::with_capacity(self.options.num_ants);
310 let mut iteration_costs = Vec::with_capacity(self.options.num_ants);
311
312 for _ in 0..self.options.num_ants {
314 let solution = self.construct_solution(problem, &pheromone, n);
315 let cost = problem.evaluate(&solution);
316 nfev += 1;
317
318 if cost < best_cost {
319 best_cost = cost;
320 best_solution = solution.clone();
321 no_improve_count = 0;
322 }
323
324 iteration_solutions.push(solution);
325 iteration_costs.push(cost);
326 }
327
328 cost_history.push(best_cost);
329
330 for row in &mut pheromone {
332 for val in row.iter_mut() {
333 *val *= 1.0 - self.options.evaporation_rate;
334 if *val < 1e-15 {
335 *val = 1e-15; }
337 }
338 }
339
340 for (solution, cost) in iteration_solutions.iter().zip(iteration_costs.iter()) {
342 if *cost > 0.0 {
343 let deposit = self.options.q_factor / cost;
344 for i in 0..solution.len() {
345 let from = solution[i];
346 let to = solution[(i + 1) % solution.len()];
347 pheromone[from][to] += deposit;
348 pheromone[to][from] += deposit;
349 }
350 }
351 }
352
353 no_improve_count += 1;
354 if no_improve_count >= self.options.patience {
355 return Ok(AcoResult {
356 best_solution,
357 best_cost,
358 iterations: iteration + 1,
359 nfev,
360 converged: true,
361 cost_history,
362 message: format!(
363 "Converged after {} iterations (no improvement for {} iterations)",
364 iteration + 1,
365 self.options.patience
366 ),
367 });
368 }
369 }
370
371 Ok(AcoResult {
372 best_solution,
373 best_cost,
374 iterations: self.options.max_iter,
375 nfev,
376 converged: false,
377 cost_history,
378 message: format!("Completed {} iterations", self.options.max_iter),
379 })
380 }
381
382 fn construct_solution<P: CombinatorialProblem>(
384 &mut self,
385 problem: &P,
386 pheromone: &[Vec<f64>],
387 n: usize,
388 ) -> Vec<usize> {
389 let mut visited = vec![false; n];
390 let mut solution = Vec::with_capacity(n);
391
392 let start = self.rng.random_range(0..n);
394 solution.push(start);
395 visited[start] = true;
396
397 for _ in 1..n {
398 let current = *solution.last().unwrap_or(&0);
399 let next = self.select_next_node(problem, pheromone, current, &visited, n);
400 solution.push(next);
401 visited[next] = true;
402 }
403
404 solution
405 }
406
407 fn select_next_node<P: CombinatorialProblem>(
409 &mut self,
410 problem: &P,
411 pheromone: &[Vec<f64>],
412 current: usize,
413 visited: &[bool],
414 n: usize,
415 ) -> usize {
416 let mut probabilities = Vec::with_capacity(n);
418 let mut total = 0.0;
419
420 for j in 0..n {
421 if visited[j] || !problem.is_feasible(current, j, visited) {
422 probabilities.push(0.0);
423 continue;
424 }
425 let tau = pheromone[current][j].max(1e-15);
426 let eta = problem.heuristic(current, j).max(1e-15);
427 let p = tau.powf(self.options.alpha) * eta.powf(self.options.beta);
428 probabilities.push(p);
429 total += p;
430 }
431
432 if total <= 0.0 {
433 for j in 0..n {
435 if !visited[j] {
436 return j;
437 }
438 }
439 return 0; }
441
442 let threshold = self.rng.random::<f64>() * total;
444 let mut cumulative = 0.0;
445 for j in 0..n {
446 cumulative += probabilities[j];
447 if cumulative >= threshold {
448 return j;
449 }
450 }
451
452 for j in (0..n).rev() {
454 if !visited[j] {
455 return j;
456 }
457 }
458 0
459 }
460}
461
462#[derive(Debug, Clone)]
468pub struct MaxMinAntSystemOptions {
469 pub base: AntSystemOptions,
471 pub tau_min: f64,
473 pub tau_max: f64,
475 pub use_iteration_best: bool,
477 pub smooth_init: bool,
479}
480
481impl Default for MaxMinAntSystemOptions {
482 fn default() -> Self {
483 Self {
484 base: AntSystemOptions::default(),
485 tau_min: 0.01,
486 tau_max: 10.0,
487 use_iteration_best: true,
488 smooth_init: true,
489 }
490 }
491}
492
493pub struct MaxMinAntSystem {
497 options: MaxMinAntSystemOptions,
498 rng: StdRng,
499}
500
501impl MaxMinAntSystem {
502 pub fn new(options: MaxMinAntSystemOptions) -> Self {
504 let seed = options.base.seed.unwrap_or_else(|| rng().random());
505 Self {
506 options,
507 rng: StdRng::seed_from_u64(seed),
508 }
509 }
510
511 pub fn solve<P: CombinatorialProblem>(&mut self, problem: &P) -> OptimizeResult<AcoResult> {
513 let n = problem.num_nodes();
514 if n == 0 {
515 return Err(OptimizeError::InvalidInput(
516 "Problem has 0 nodes".to_string(),
517 ));
518 }
519
520 let init_val = if self.options.smooth_init {
521 self.options.tau_max
522 } else {
523 (self.options.tau_min + self.options.tau_max) / 2.0
524 };
525
526 let mut pheromone = vec![vec![init_val; n]; n];
527 let mut best_solution: Vec<usize> = Vec::new();
528 let mut best_cost = f64::INFINITY;
529 let mut nfev: usize = 0;
530
531 let max_iter = self.options.base.max_iter;
533 let num_ants = self.options.base.num_ants;
534 let evaporation_rate = self.options.base.evaporation_rate;
535 let q_factor = self.options.base.q_factor;
536 let patience = self.options.base.patience;
537 let tau_min = self.options.tau_min;
538 let tau_max = self.options.tau_max;
539 let use_iteration_best = self.options.use_iteration_best;
540
541 let mut cost_history = Vec::with_capacity(max_iter);
542 let mut no_improve_count: usize = 0;
543
544 for iteration in 0..max_iter {
545 let mut iter_best_solution: Vec<usize> = Vec::new();
546 let mut iter_best_cost = f64::INFINITY;
547
548 for _ in 0..num_ants {
550 let solution = self.construct_solution_mmas(problem, &pheromone, n);
551 let cost = problem.evaluate(&solution);
552 nfev += 1;
553
554 if cost < iter_best_cost {
555 iter_best_cost = cost;
556 iter_best_solution = solution.clone();
557 }
558
559 if cost < best_cost {
560 best_cost = cost;
561 best_solution = solution;
562 no_improve_count = 0;
563 }
564 }
565
566 cost_history.push(best_cost);
567
568 for row in &mut pheromone {
570 for val in row.iter_mut() {
571 *val *= 1.0 - evaporation_rate;
572 }
573 }
574
575 let (update_sol, update_cost) = if use_iteration_best {
577 (&iter_best_solution, iter_best_cost)
578 } else {
579 (&best_solution, best_cost)
580 };
581
582 if update_cost > 0.0 && !update_sol.is_empty() {
583 let deposit = q_factor / update_cost;
584 for i in 0..update_sol.len() {
585 let from = update_sol[i];
586 let to = update_sol[(i + 1) % update_sol.len()];
587 pheromone[from][to] += deposit;
588 pheromone[to][from] += deposit;
589 }
590 }
591
592 for row in &mut pheromone {
594 for val in row.iter_mut() {
595 *val = val.clamp(tau_min, tau_max);
596 }
597 }
598
599 no_improve_count += 1;
600 if no_improve_count >= patience {
601 return Ok(AcoResult {
602 best_solution,
603 best_cost,
604 iterations: iteration + 1,
605 nfev,
606 converged: true,
607 cost_history,
608 message: format!("MMAS converged after {} iterations", iteration + 1),
609 });
610 }
611 }
612
613 Ok(AcoResult {
614 best_solution,
615 best_cost,
616 iterations: max_iter,
617 nfev,
618 converged: false,
619 cost_history,
620 message: format!("MMAS completed {} iterations", max_iter),
621 })
622 }
623
624 fn construct_solution_mmas<P: CombinatorialProblem>(
626 &mut self,
627 problem: &P,
628 pheromone: &[Vec<f64>],
629 n: usize,
630 ) -> Vec<usize> {
631 let mut visited = vec![false; n];
632 let mut solution = Vec::with_capacity(n);
633
634 let start = self.rng.random_range(0..n);
635 solution.push(start);
636 visited[start] = true;
637
638 for _ in 1..n {
639 let current = *solution.last().unwrap_or(&0);
640 let next = self.select_next_mmas(problem, pheromone, current, &visited, n);
641 solution.push(next);
642 visited[next] = true;
643 }
644
645 solution
646 }
647
648 fn select_next_mmas<P: CombinatorialProblem>(
650 &mut self,
651 problem: &P,
652 pheromone: &[Vec<f64>],
653 current: usize,
654 visited: &[bool],
655 n: usize,
656 ) -> usize {
657 let alpha = self.options.base.alpha;
658 let beta = self.options.base.beta;
659 let mut probabilities = Vec::with_capacity(n);
660 let mut total = 0.0;
661
662 for j in 0..n {
663 if visited[j] || !problem.is_feasible(current, j, visited) {
664 probabilities.push(0.0);
665 continue;
666 }
667 let tau = pheromone[current][j].max(1e-15);
668 let eta = problem.heuristic(current, j).max(1e-15);
669 let p = tau.powf(alpha) * eta.powf(beta);
670 probabilities.push(p);
671 total += p;
672 }
673
674 if total <= 0.0 {
675 for j in 0..n {
676 if !visited[j] {
677 return j;
678 }
679 }
680 return 0;
681 }
682
683 let threshold = self.rng.random::<f64>() * total;
684 let mut cumulative = 0.0;
685 for j in 0..n {
686 cumulative += probabilities[j];
687 if cumulative >= threshold {
688 return j;
689 }
690 }
691
692 for j in (0..n).rev() {
693 if !visited[j] {
694 return j;
695 }
696 }
697 0
698 }
699}
700
701#[cfg(test)]
706mod tests {
707 use super::*;
708
709 fn make_simple_tsp() -> TspProblem {
710 TspProblem::from_coordinates(&[(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)])
713 .expect("valid TSP")
714 }
715
716 fn make_line_tsp() -> TspProblem {
717 TspProblem::from_coordinates(&[(0.0, 0.0), (1.0, 0.0), (2.0, 0.0), (3.0, 0.0), (4.0, 0.0)])
720 .expect("valid TSP")
721 }
722
723 #[test]
726 fn test_tsp_from_coordinates() {
727 let tsp = make_simple_tsp();
728 assert_eq!(tsp.num_cities(), 4);
729 assert!((tsp.distance(0, 1) - 1.0).abs() < 1e-10);
730 let diag = (2.0_f64).sqrt();
731 assert!((tsp.distance(0, 2) - diag).abs() < 1e-10);
732 }
733
734 #[test]
735 fn test_tsp_evaluate() {
736 let tsp = make_simple_tsp();
737 let cost = tsp.evaluate(&[0, 1, 2, 3]);
739 assert!((cost - 4.0).abs() < 1e-10);
740 }
741
742 #[test]
743 fn test_tsp_heuristic() {
744 let tsp = make_simple_tsp();
745 let h = tsp.heuristic(0, 1);
747 assert!((h - 1.0).abs() < 1e-10);
748 }
749
750 #[test]
751 fn test_tsp_empty_error() {
752 let result = TspProblem::from_coordinates(&[]);
753 assert!(result.is_err());
754 }
755
756 #[test]
757 fn test_tsp_non_square_error() {
758 let result = TspProblem::new(vec![vec![0.0, 1.0], vec![1.0]]);
759 assert!(result.is_err());
760 }
761
762 #[test]
765 fn test_as_simple_tsp() {
766 let tsp = make_simple_tsp();
767 let opts = AntSystemOptions {
768 num_ants: 10,
769 max_iter: 100,
770 alpha: 1.0,
771 beta: 3.0,
772 evaporation_rate: 0.3,
773 q_factor: 100.0,
774 seed: Some(42),
775 patience: 50,
776 ..Default::default()
777 };
778
779 let mut aco = AntColonyOptimizer::new(opts);
780 let result = aco.solve(&tsp).expect("AS should solve simple TSP");
781
782 assert!(
784 result.best_cost <= 4.0 + 1.0,
785 "AS should find near-optimal: got {}",
786 result.best_cost
787 );
788 assert_eq!(result.best_solution.len(), 4);
789 assert!(result.nfev > 0);
790 }
791
792 #[test]
793 fn test_as_line_tsp() {
794 let tsp = make_line_tsp();
795 let opts = AntSystemOptions {
796 num_ants: 15,
797 max_iter: 150,
798 alpha: 1.0,
799 beta: 5.0,
800 evaporation_rate: 0.3,
801 q_factor: 100.0,
802 seed: Some(99),
803 patience: 80,
804 ..Default::default()
805 };
806
807 let mut aco = AntColonyOptimizer::new(opts);
808 let result = aco.solve(&tsp).expect("AS should solve line TSP");
809
810 assert_eq!(result.best_solution.len(), 5);
811 assert!(
813 result.best_cost <= 9.0,
814 "Line TSP best cost: {}",
815 result.best_cost
816 );
817 }
818
819 #[test]
820 fn test_as_convergence_history() {
821 let tsp = make_simple_tsp();
822 let opts = AntSystemOptions {
823 num_ants: 10,
824 max_iter: 50,
825 seed: Some(42),
826 patience: 100, ..Default::default()
828 };
829
830 let mut aco = AntColonyOptimizer::new(opts);
831 let result = aco.solve(&tsp).expect("AS should produce history");
832
833 assert!(!result.cost_history.is_empty());
834 for i in 1..result.cost_history.len() {
836 assert!(
837 result.cost_history[i] <= result.cost_history[i - 1] + 1e-10,
838 "Cost history should be non-increasing"
839 );
840 }
841 }
842
843 #[test]
844 fn test_as_zero_nodes_error() {
845 let tsp = TspProblem::new(vec![]).unwrap_or_else(|_| {
846 TspProblem::new(vec![vec![0.0]]).expect("valid")
848 });
849 struct EmptyProblem;
851 impl CombinatorialProblem for EmptyProblem {
852 fn num_nodes(&self) -> usize {
853 0
854 }
855 fn heuristic(&self, _i: usize, _j: usize) -> f64 {
856 0.0
857 }
858 fn evaluate(&self, _solution: &[usize]) -> f64 {
859 0.0
860 }
861 }
862
863 let opts = AntSystemOptions {
864 seed: Some(1),
865 ..Default::default()
866 };
867 let mut aco = AntColonyOptimizer::new(opts);
868 let result = aco.solve(&EmptyProblem);
869 assert!(result.is_err());
870 }
871
872 #[test]
875 fn test_mmas_simple_tsp() {
876 let tsp = make_simple_tsp();
877 let opts = MaxMinAntSystemOptions {
878 base: AntSystemOptions {
879 num_ants: 10,
880 max_iter: 100,
881 alpha: 1.0,
882 beta: 3.0,
883 evaporation_rate: 0.3,
884 q_factor: 100.0,
885 seed: Some(42),
886 patience: 60,
887 ..Default::default()
888 },
889 tau_min: 0.01,
890 tau_max: 10.0,
891 use_iteration_best: true,
892 smooth_init: true,
893 };
894
895 let mut mmas = MaxMinAntSystem::new(opts);
896 let result = mmas.solve(&tsp).expect("MMAS should solve simple TSP");
897
898 assert!(
899 result.best_cost <= 4.0 + 1.0,
900 "MMAS should find near-optimal: got {}",
901 result.best_cost
902 );
903 assert_eq!(result.best_solution.len(), 4);
904 }
905
906 #[test]
907 fn test_mmas_global_best_update() {
908 let tsp = make_simple_tsp();
909 let opts = MaxMinAntSystemOptions {
910 base: AntSystemOptions {
911 num_ants: 10,
912 max_iter: 80,
913 seed: Some(55),
914 patience: 60,
915 ..Default::default()
916 },
917 use_iteration_best: false, ..Default::default()
919 };
920
921 let mut mmas = MaxMinAntSystem::new(opts);
922 let result = mmas.solve(&tsp).expect("MMAS global-best should work");
923 assert!(result.best_cost < f64::INFINITY);
924 }
925
926 #[test]
927 fn test_mmas_non_smooth_init() {
928 let tsp = make_simple_tsp();
929 let opts = MaxMinAntSystemOptions {
930 base: AntSystemOptions {
931 num_ants: 10,
932 max_iter: 60,
933 seed: Some(77),
934 patience: 50,
935 ..Default::default()
936 },
937 smooth_init: false,
938 ..Default::default()
939 };
940
941 let mut mmas = MaxMinAntSystem::new(opts);
942 let result = mmas.solve(&tsp).expect("MMAS non-smooth init should work");
943 assert!(result.best_cost < f64::INFINITY);
944 }
945
946 #[test]
949 fn test_permutation_problem() {
950 let eval_fn = |perm: &[usize]| -> f64 {
952 perm.iter()
953 .enumerate()
954 .map(|(i, &v)| (i as f64) * (v as f64))
955 .sum()
956 };
957
958 let problem =
959 PermutationProblem::with_uniform_heuristic(5, eval_fn).expect("valid problem");
960
961 let opts = AntSystemOptions {
962 num_ants: 10,
963 max_iter: 100,
964 seed: Some(42),
965 patience: 50,
966 ..Default::default()
967 };
968
969 let mut aco = AntColonyOptimizer::new(opts);
970 let result = aco
971 .solve(&problem)
972 .expect("AS should solve permutation problem");
973
974 assert_eq!(result.best_solution.len(), 5);
975 let mut sorted = result.best_solution.clone();
977 sorted.sort();
978 assert_eq!(sorted, vec![0, 1, 2, 3, 4]);
979 }
980
981 #[test]
982 fn test_permutation_problem_with_heuristic() {
983 let n = 4;
984 let heuristic = vec![
985 vec![0.0, 1.0, 0.5, 0.2],
986 vec![1.0, 0.0, 0.8, 0.3],
987 vec![0.5, 0.8, 0.0, 1.0],
988 vec![0.2, 0.3, 1.0, 0.0],
989 ];
990
991 let eval_fn = |perm: &[usize]| -> f64 {
992 let mut cost = 0.0;
993 for i in 0..perm.len() - 1 {
994 cost += (perm[i] as f64 - perm[i + 1] as f64).abs();
995 }
996 cost
997 };
998
999 let problem = PermutationProblem::new(n, heuristic, eval_fn).expect("valid problem");
1000
1001 let opts = AntSystemOptions {
1002 num_ants: 10,
1003 max_iter: 50,
1004 seed: Some(42),
1005 ..Default::default()
1006 };
1007
1008 let mut aco = AntColonyOptimizer::new(opts);
1009 let result = aco.solve(&problem).expect("should solve");
1010 assert_eq!(result.best_solution.len(), 4);
1011 }
1012
1013 #[test]
1014 fn test_permutation_empty_error() {
1015 let result = PermutationProblem::with_uniform_heuristic(0, |_: &[usize]| 0.0);
1016 assert!(result.is_err());
1017 }
1018
1019 #[test]
1020 fn test_permutation_heuristic_size_mismatch() {
1021 let result = PermutationProblem::new(3, vec![vec![1.0; 3]; 2], |_: &[usize]| 0.0);
1022 assert!(result.is_err());
1023 }
1024
1025 #[test]
1026 fn test_mmas_patience_convergence() {
1027 let tsp = make_simple_tsp();
1028 let opts = MaxMinAntSystemOptions {
1029 base: AntSystemOptions {
1030 num_ants: 10,
1031 max_iter: 1000,
1032 seed: Some(42),
1033 patience: 10, ..Default::default()
1035 },
1036 ..Default::default()
1037 };
1038
1039 let mut mmas = MaxMinAntSystem::new(opts);
1040 let result = mmas.solve(&tsp).expect("should converge via patience");
1041
1042 assert!(result.iterations < 1000);
1044 assert!(result.converged);
1045 }
1046}