Skip to main content

scirs2_optimize/metaheuristics/
aco.rs

1//! # Ant Colony Optimization (ACO)
2//!
3//! Population-based metaheuristic for combinatorial optimization:
4//! - **Ant System (AS)**: Classic ant-based algorithm with pheromone trails
5//! - **Max-Min Ant System (MMAS)**: Bounded pheromone levels to avoid stagnation
6//! - **TSP solver interface**: Specialized for Travelling Salesman Problems
7//! - **General permutation problem interface**: Solve any permutation-based problem
8
9use crate::error::{OptimizeError, OptimizeResult};
10use scirs2_core::random::rngs::StdRng;
11use scirs2_core::random::{rng, Rng, SeedableRng};
12use scirs2_core::RngExt;
13
14// ---------------------------------------------------------------------------
15// Problem Traits
16// ---------------------------------------------------------------------------
17
18/// A combinatorial optimization problem over a set of discrete elements.
19///
20/// The ants construct solutions by building paths through a graph where
21/// each node represents a decision point and each edge has associated
22/// pheromone and heuristic information.
23pub trait CombinatorialProblem {
24    /// Number of nodes (cities, tasks, etc.)
25    fn num_nodes(&self) -> usize;
26
27    /// Heuristic desirability of going from node `i` to node `j`.
28    /// Higher is more desirable (typically 1/distance for TSP).
29    fn heuristic(&self, i: usize, j: usize) -> f64;
30
31    /// Evaluate the quality (cost) of a complete solution (permutation of node indices).
32    /// Lower is better.
33    fn evaluate(&self, solution: &[usize]) -> f64;
34
35    /// Check if moving from current node to next node is feasible.
36    /// Default returns true (all moves allowed).
37    fn is_feasible(&self, _current: usize, _next: usize, _visited: &[bool]) -> bool {
38        true
39    }
40}
41
42/// TSP problem definition
43#[derive(Debug, Clone)]
44pub struct TspProblem {
45    /// Distance matrix (n x n)
46    distances: Vec<Vec<f64>>,
47    n: usize,
48}
49
50impl TspProblem {
51    /// Create a TSP from a distance matrix
52    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    /// Create a TSP from 2D coordinates (Euclidean distances)
70    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    /// Get number of cities
91    pub fn num_cities(&self) -> usize {
92        self.n
93    }
94
95    /// Get distance between two cities
96    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 // very high desirability for zero distance (same city)
112        }
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        // Return to start
124        total += self.distances[*solution.last().unwrap_or(&0)][solution[0]];
125        total
126    }
127}
128
129/// A general permutation problem
130#[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    /// Create a new permutation problem
148    ///
149    /// - `n`: number of elements to permute
150    /// - `heuristic_matrix`: n x n matrix of heuristic values (higher = more desirable)
151    /// - `eval_fn`: function that evaluates a permutation (lower = better)
152    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    /// Create with uniform heuristic (all edges equally desirable)
174    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// ---------------------------------------------------------------------------
207// ACO Result
208// ---------------------------------------------------------------------------
209
210/// Result from ACO optimization
211#[derive(Debug, Clone)]
212pub struct AcoResult {
213    /// Best solution (permutation of node indices)
214    pub best_solution: Vec<usize>,
215    /// Cost of the best solution
216    pub best_cost: f64,
217    /// Number of iterations completed
218    pub iterations: usize,
219    /// Number of function evaluations
220    pub nfev: usize,
221    /// Whether the optimizer converged
222    pub converged: bool,
223    /// History of best cost per iteration (for convergence analysis)
224    pub cost_history: Vec<f64>,
225    /// Termination message
226    pub message: String,
227}
228
229// ---------------------------------------------------------------------------
230// Ant System (Classic AS)
231// ---------------------------------------------------------------------------
232
233/// Options for Ant System
234#[derive(Debug, Clone)]
235pub struct AntSystemOptions {
236    /// Number of ants
237    pub num_ants: usize,
238    /// Maximum iterations
239    pub max_iter: usize,
240    /// Pheromone importance (alpha)
241    pub alpha: f64,
242    /// Heuristic importance (beta)
243    pub beta: f64,
244    /// Evaporation rate (rho), in [0, 1]
245    pub evaporation_rate: f64,
246    /// Initial pheromone level
247    pub initial_pheromone: f64,
248    /// Pheromone deposit factor Q
249    pub q_factor: f64,
250    /// Random seed
251    pub seed: Option<u64>,
252    /// Convergence tolerance (stop if best cost doesn't improve by this fraction)
253    pub tol: f64,
254    /// Patience: number of iterations without improvement before stopping
255    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
275/// Classic Ant System optimizer
276pub struct AntColonyOptimizer {
277    options: AntSystemOptions,
278    rng: StdRng,
279}
280
281impl AntColonyOptimizer {
282    /// Create a new ACO optimizer
283    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    /// Solve a combinatorial problem
292    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        // Initialize pheromone matrix
301        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            // Each ant constructs a solution
313            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            // Evaporate pheromone
331            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; // prevent zero pheromone
336                    }
337                }
338            }
339
340            // Deposit pheromone from all ants
341            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    /// Construct a single ant solution using probabilistic selection
383    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        // Start from a random node
393        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    /// Select the next node using roulette wheel selection
408    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        // Compute probabilities for unvisited nodes
417        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            // Fallback: pick first unvisited node
434            for j in 0..n {
435                if !visited[j] {
436                    return j;
437                }
438            }
439            return 0; // shouldn't happen if problem is well-formed
440        }
441
442        // Roulette wheel
443        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        // Fallback
453        for j in (0..n).rev() {
454            if !visited[j] {
455                return j;
456            }
457        }
458        0
459    }
460}
461
462// ---------------------------------------------------------------------------
463// Max-Min Ant System (MMAS)
464// ---------------------------------------------------------------------------
465
466/// Options for Max-Min Ant System
467#[derive(Debug, Clone)]
468pub struct MaxMinAntSystemOptions {
469    /// Base AS options
470    pub base: AntSystemOptions,
471    /// Minimum pheromone level (tau_min)
472    pub tau_min: f64,
473    /// Maximum pheromone level (tau_max)
474    pub tau_max: f64,
475    /// Use iteration-best (true) or global-best (false) for pheromone update
476    pub use_iteration_best: bool,
477    /// Smooth pheromone initialization
478    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
493/// Max-Min Ant System (MMAS) optimizer
494///
495/// Extends Ant System with bounded pheromone levels to prevent premature convergence.
496pub struct MaxMinAntSystem {
497    options: MaxMinAntSystemOptions,
498    rng: StdRng,
499}
500
501impl MaxMinAntSystem {
502    /// Create a new MMAS optimizer
503    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    /// Solve a combinatorial problem using MMAS
512    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        // Extract values from self.options to avoid borrowing self immutably
532        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            // Each ant constructs a solution
549            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            // Evaporate pheromone
569            for row in &mut pheromone {
570                for val in row.iter_mut() {
571                    *val *= 1.0 - evaporation_rate;
572                }
573            }
574
575            // Deposit pheromone from best ant only
576            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            // Clamp pheromone to [tau_min, tau_max]
593            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    /// Construct a solution for MMAS (same logic as AS but with clamped pheromone)
625    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    /// Select next node for MMAS
649    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// ---------------------------------------------------------------------------
702// Tests
703// ---------------------------------------------------------------------------
704
705#[cfg(test)]
706mod tests {
707    use super::*;
708
709    fn make_simple_tsp() -> TspProblem {
710        // 4-city TSP with known optimal tour
711        // Cities at: (0,0), (1,0), (1,1), (0,1)  => square, optimal tour = 4.0
712        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        // 5 cities in a line: 0,1,2,3,4
718        // Optimal tour: 0-1-2-3-4-0 = 1+1+1+1+4 = 8
719        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    // --- TSP problem tests ---
724
725    #[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        // Tour 0->1->2->3->0 = perimeter of unit square = 4
738        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        // heuristic = 1/distance
746        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    // --- Ant System tests ---
763
764    #[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        // Optimal tour is 4.0 for unit square
783        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        // Optimal for 5 points in a line: tour = 8
812        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, // don't stop early
827            ..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        // Cost history should be non-increasing (best cost tracked)
835        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            // Create a minimal valid TSP to pass creation, then test with 0 nodes via trait
847            TspProblem::new(vec![vec![0.0]]).expect("valid")
848        });
849        // Actually test with an empty problem
850        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    // --- MMAS tests ---
873
874    #[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, // global-best update
918            ..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    // --- Permutation problem tests ---
947
948    #[test]
949    fn test_permutation_problem() {
950        // Simple assignment problem: minimize sum of position * value
951        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        // Check that it's a valid permutation
976        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, // very short patience
1034                ..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        // Should stop well before 1000 iterations
1043        assert!(result.iterations < 1000);
1044        assert!(result.converged);
1045    }
1046}