aprender_tsp/solver/
aco.rs

1//! Ant Colony Optimization for TSP - Adapter for core aprender::AntColony.
2//!
3//! This module wraps the core aprender metaheuristics library
4//! to provide a TSP-specific interface.
5
6use crate::error::TspResult;
7use crate::instance::TspInstance;
8use crate::solver::{Budget, TspSolution, TspSolver};
9use aprender::metaheuristics::{
10    AntColony, Budget as CoreBudget, ConstructiveMetaheuristic, SearchSpace,
11};
12
13/// Ant Colony Optimization solver for TSP.
14///
15/// This is a thin adapter around `aprender::metaheuristics::AntColony`.
16#[derive(Debug, Clone)]
17pub struct AcoSolver {
18    /// Number of artificial ants
19    pub num_ants: usize,
20    /// Pheromone importance (α)
21    pub alpha: f64,
22    /// Heuristic importance (β)
23    pub beta: f64,
24    /// Evaporation rate (ρ)
25    pub rho: f64,
26    /// Exploitation probability (q₀) for ACS rule
27    /// Note: Not used by core ACO (uses simpler Ant System)
28    pub q0: f64,
29    /// Random seed
30    seed: Option<u64>,
31    /// Core ACO instance (lazy initialized)
32    inner: Option<AntColony>,
33}
34
35impl Default for AcoSolver {
36    fn default() -> Self {
37        Self {
38            num_ants: 20,
39            alpha: 1.0,
40            beta: 2.5,
41            rho: 0.1,
42            q0: 0.9,
43            seed: None,
44            inner: None,
45        }
46    }
47}
48
49impl AcoSolver {
50    /// Create a new ACO solver with default parameters
51    #[must_use]
52    pub fn new() -> Self {
53        Self::default()
54    }
55
56    /// Set number of ants
57    #[must_use]
58    pub fn with_num_ants(mut self, num_ants: usize) -> Self {
59        self.num_ants = num_ants;
60        self
61    }
62
63    /// Set pheromone importance (α)
64    #[must_use]
65    pub fn with_alpha(mut self, alpha: f64) -> Self {
66        self.alpha = alpha;
67        self
68    }
69
70    /// Set heuristic importance (β)
71    #[must_use]
72    pub fn with_beta(mut self, beta: f64) -> Self {
73        self.beta = beta;
74        self
75    }
76
77    /// Set evaporation rate (ρ)
78    #[must_use]
79    pub fn with_rho(mut self, rho: f64) -> Self {
80        self.rho = rho;
81        self
82    }
83
84    /// Set exploitation probability (q₀)
85    /// Note: This parameter is stored for API compatibility but not used
86    /// by the underlying core ACO (which uses simpler Ant System).
87    #[must_use]
88    pub fn with_q0(mut self, q0: f64) -> Self {
89        self.q0 = q0;
90        self
91    }
92
93    /// Set random seed
94    #[must_use]
95    pub fn with_seed(mut self, seed: u64) -> Self {
96        self.seed = Some(seed);
97        self
98    }
99
100    /// Build the core AntColony instance
101    fn build_inner(&self) -> AntColony {
102        let mut aco = AntColony::new(self.num_ants)
103            .with_alpha(self.alpha)
104            .with_beta(self.beta)
105            .with_rho(self.rho);
106
107        if let Some(seed) = self.seed {
108            aco = aco.with_seed(seed);
109        }
110
111        aco
112    }
113
114    /// Convert TSP budget to core budget
115    fn to_core_budget(budget: Budget) -> CoreBudget {
116        match budget {
117            Budget::Iterations(n) => CoreBudget::Iterations(n),
118            Budget::Evaluations(n) => CoreBudget::Evaluations(n),
119        }
120    }
121}
122
123impl TspSolver for AcoSolver {
124    fn solve(&mut self, instance: &TspInstance, budget: Budget) -> TspResult<TspSolution> {
125        // Build search space from TSP distance matrix
126        let space = SearchSpace::tsp(&instance.distances);
127
128        // Build core ACO solver
129        let mut aco = self.build_inner();
130
131        // Define objective function (tour length)
132        let objective = |tour: &Vec<usize>| -> f64 { instance.tour_length(tour) };
133
134        // Run optimization
135        let result = aco.optimize(&objective, &space, Self::to_core_budget(budget));
136
137        // Store inner for potential reuse
138        self.inner = Some(aco);
139
140        Ok(TspSolution {
141            tour: result.solution,
142            length: result.objective_value,
143            evaluations: result.evaluations,
144            history: result.history,
145        })
146    }
147
148    fn name(&self) -> &'static str {
149        "Ant Colony Optimization"
150    }
151
152    fn reset(&mut self) {
153        self.inner = None;
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    fn square_instance() -> TspInstance {
162        // 4 cities forming a unit square
163        let coords = vec![(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)];
164        TspInstance::from_coords("square", coords).expect("should create")
165    }
166
167    fn triangle_instance() -> TspInstance {
168        // 3-4-5 right triangle
169        let coords = vec![(0.0, 0.0), (3.0, 0.0), (3.0, 4.0)];
170        TspInstance::from_coords("triangle", coords).expect("should create")
171    }
172
173    #[test]
174    fn test_aco_default_params() {
175        let aco = AcoSolver::default();
176        assert_eq!(aco.num_ants, 20);
177        assert!((aco.alpha - 1.0).abs() < 1e-10);
178        assert!((aco.beta - 2.5).abs() < 1e-10);
179        assert!((aco.rho - 0.1).abs() < 1e-10);
180        assert!((aco.q0 - 0.9).abs() < 1e-10);
181    }
182
183    #[test]
184    fn test_aco_builder() {
185        let aco = AcoSolver::new()
186            .with_num_ants(50)
187            .with_alpha(2.0)
188            .with_beta(3.0)
189            .with_rho(0.2)
190            .with_seed(42);
191
192        assert_eq!(aco.num_ants, 50);
193        assert!((aco.alpha - 2.0).abs() < 1e-10);
194        assert!((aco.beta - 3.0).abs() < 1e-10);
195        assert!((aco.rho - 0.2).abs() < 1e-10);
196        assert_eq!(aco.seed, Some(42));
197    }
198
199    #[test]
200    fn test_aco_solves_square() {
201        let instance = square_instance();
202        let mut solver = AcoSolver::new().with_seed(42).with_num_ants(10);
203
204        let solution = solver
205            .solve(&instance, Budget::Iterations(50))
206            .expect("should solve");
207
208        // Optimal tour around square is 4.0
209        assert!(solution.length <= 5.0, "Length {} > 5.0", solution.length);
210        assert_eq!(solution.tour.len(), 4);
211        assert!(instance.validate_tour(&solution.tour).is_ok());
212    }
213
214    #[test]
215    fn test_aco_solves_triangle() {
216        let instance = triangle_instance();
217        let mut solver = AcoSolver::new().with_seed(42).with_num_ants(10);
218
219        let solution = solver
220            .solve(&instance, Budget::Iterations(50))
221            .expect("should solve");
222
223        // Triangle tour: 3 + 4 + 5 = 12
224        assert!(solution.length <= 13.0, "Length {} > 13.0", solution.length);
225        assert_eq!(solution.tour.len(), 3);
226    }
227
228    #[test]
229    fn test_aco_deterministic_with_seed() {
230        let instance = square_instance();
231
232        let mut solver1 = AcoSolver::new().with_seed(42).with_num_ants(5);
233        let mut solver2 = AcoSolver::new().with_seed(42).with_num_ants(5);
234
235        let solution1 = solver1
236            .solve(&instance, Budget::Iterations(10))
237            .expect("should solve");
238        let solution2 = solver2
239            .solve(&instance, Budget::Iterations(10))
240            .expect("should solve");
241
242        assert!((solution1.length - solution2.length).abs() < 1e-10);
243        assert_eq!(solution1.tour, solution2.tour);
244    }
245
246    #[test]
247    fn test_aco_tracks_history() {
248        let instance = square_instance();
249        let mut solver = AcoSolver::new().with_seed(42).with_num_ants(5);
250
251        let solution = solver
252            .solve(&instance, Budget::Iterations(20))
253            .expect("should solve");
254
255        assert_eq!(solution.history.len(), 20);
256        // History should be non-increasing (best-so-far)
257        for window in solution.history.windows(2) {
258            assert!(window[1] <= window[0] + 1e-10);
259        }
260    }
261
262    #[test]
263    fn test_aco_counts_evaluations() {
264        let instance = square_instance();
265        let mut solver = AcoSolver::new().with_seed(42).with_num_ants(10);
266
267        let solution = solver
268            .solve(&instance, Budget::Iterations(5))
269            .expect("should solve");
270
271        // 5 iterations * 10 ants = 50 evaluations
272        assert_eq!(solution.evaluations, 50);
273    }
274
275    #[test]
276    fn test_aco_reset() {
277        let instance = square_instance();
278        let mut solver = AcoSolver::new().with_seed(42);
279
280        solver
281            .solve(&instance, Budget::Iterations(5))
282            .expect("should solve");
283        assert!(solver.inner.is_some());
284
285        solver.reset();
286        assert!(solver.inner.is_none());
287    }
288
289    #[test]
290    fn test_aco_name() {
291        let solver = AcoSolver::new();
292        assert_eq!(solver.name(), "Ant Colony Optimization");
293    }
294
295    #[test]
296    fn test_aco_improves_over_iterations() {
297        let instance = square_instance();
298        let mut solver = AcoSolver::new().with_seed(42).with_num_ants(10);
299
300        let solution = solver
301            .solve(&instance, Budget::Iterations(100))
302            .expect("should solve");
303
304        // Final should be better than or equal to first
305        let first = solution.history[0];
306        let last = solution.history[solution.history.len() - 1];
307        assert!(last <= first);
308    }
309}