aprender_tsp/solver/
hybrid.rs

1//! Hybrid solver combining GA, Tabu Search, and ACO.
2//!
3//! Toyota Way Principle: *Kaizen* - Combine strengths of multiple algorithms.
4//!
5//! References:
6//! - Burke et al. (2013) "Hyper-heuristics: A Survey"
7//! - Talbi (2002) "A Taxonomy of Hybrid Metaheuristics"
8
9use crate::error::TspResult;
10use crate::instance::TspInstance;
11use crate::solver::{AcoSolver, Budget, GaSolver, TabuSolver, TspSolution, TspSolver};
12
13/// Hybrid solver that combines GA, Tabu Search, and ACO
14#[derive(Debug, Clone)]
15pub struct HybridSolver {
16    /// Fraction of budget for GA exploration (0.0-1.0)
17    pub ga_fraction: f64,
18    /// Fraction of budget for Tabu refinement (0.0-1.0)
19    pub tabu_fraction: f64,
20    /// Fraction of budget for ACO intensification (0.0-1.0)
21    pub aco_fraction: f64,
22    /// GA population size
23    pub ga_population: usize,
24    /// Number of top GA solutions to refine
25    pub refine_top_k: usize,
26    /// Random seed
27    seed: Option<u64>,
28}
29
30impl Default for HybridSolver {
31    fn default() -> Self {
32        Self {
33            ga_fraction: 0.4,
34            tabu_fraction: 0.3,
35            aco_fraction: 0.3,
36            ga_population: 30,
37            refine_top_k: 3,
38            seed: None,
39        }
40    }
41}
42
43impl HybridSolver {
44    /// Create a new hybrid solver
45    pub fn new() -> Self {
46        Self::default()
47    }
48
49    /// Set GA budget fraction
50    pub fn with_ga_fraction(mut self, fraction: f64) -> Self {
51        self.ga_fraction = fraction.clamp(0.0, 1.0);
52        self
53    }
54
55    /// Set Tabu budget fraction
56    pub fn with_tabu_fraction(mut self, fraction: f64) -> Self {
57        self.tabu_fraction = fraction.clamp(0.0, 1.0);
58        self
59    }
60
61    /// Set ACO budget fraction
62    pub fn with_aco_fraction(mut self, fraction: f64) -> Self {
63        self.aco_fraction = fraction.clamp(0.0, 1.0);
64        self
65    }
66
67    /// Set GA population size
68    pub fn with_ga_population(mut self, size: usize) -> Self {
69        self.ga_population = size;
70        self
71    }
72
73    /// Set number of top solutions to refine
74    pub fn with_refine_top_k(mut self, k: usize) -> Self {
75        self.refine_top_k = k;
76        self
77    }
78
79    /// Set random seed
80    pub fn with_seed(mut self, seed: u64) -> Self {
81        self.seed = Some(seed);
82        self
83    }
84
85    /// Normalize fractions to sum to 1.0
86    fn normalize_fractions(&self) -> (f64, f64, f64) {
87        let total = self.ga_fraction + self.tabu_fraction + self.aco_fraction;
88        if total <= 0.0 {
89            return (0.34, 0.33, 0.33);
90        }
91        (
92            self.ga_fraction / total,
93            self.tabu_fraction / total,
94            self.aco_fraction / total,
95        )
96    }
97}
98
99impl TspSolver for HybridSolver {
100    fn solve(&mut self, instance: &TspInstance, budget: Budget) -> TspResult<TspSolution> {
101        let total_iterations = budget.limit();
102        let (ga_frac, tabu_frac, aco_frac) = self.normalize_fractions();
103
104        let ga_iters = ((total_iterations as f64 * ga_frac) as usize).max(1);
105        let tabu_iters = ((total_iterations as f64 * tabu_frac) as usize).max(1);
106        let aco_iters = ((total_iterations as f64 * aco_frac) as usize).max(1);
107
108        let mut total_evaluations = 0;
109        let mut history = Vec::new();
110
111        // Phase 1: Global exploration with GA
112        let mut ga = GaSolver::new().with_population_size(self.ga_population);
113        if let Some(seed) = self.seed {
114            ga = ga.with_seed(seed);
115        }
116
117        let population = ga.evolve(instance, ga_iters)?;
118        total_evaluations += self.ga_population * ga_iters;
119
120        // Get top-k solutions for refinement
121        let top_k: Vec<Vec<usize>> = population
122            .iter()
123            .take(self.refine_top_k)
124            .map(|(tour, _)| tour.clone())
125            .collect();
126
127        let mut best_tour = top_k[0].clone();
128        let mut best_length = instance.tour_length(&best_tour);
129        history.push(best_length);
130
131        // Phase 2: Local refinement with Tabu Search
132        let iters_per_solution = tabu_iters / self.refine_top_k.max(1);
133
134        for tour in &top_k {
135            let mut tabu = TabuSolver::new().with_tenure(15);
136            if let Some(seed) = self.seed {
137                tabu = tabu.with_seed(seed);
138            }
139
140            let refined = tabu.refine(tour.clone(), instance, iters_per_solution)?;
141            total_evaluations += refined.evaluations;
142
143            if refined.length < best_length {
144                best_length = refined.length;
145                best_tour = refined.tour;
146            }
147
148            history.push(best_length);
149        }
150
151        // Phase 3: Intensification with ACO
152        // Note: ACO starts fresh - pheromone seeding would require core library extension
153        let mut aco = AcoSolver::new().with_num_ants(10);
154        if let Some(seed) = self.seed {
155            aco = aco.with_seed(seed);
156        }
157
158        let aco_result = aco.solve(instance, Budget::Iterations(aco_iters))?;
159        total_evaluations += aco_result.evaluations;
160
161        if aco_result.length < best_length {
162            best_length = aco_result.length;
163            best_tour = aco_result.tour;
164        }
165
166        for &h in &aco_result.history {
167            history.push(h.min(best_length));
168        }
169
170        Ok(TspSolution {
171            tour: best_tour,
172            length: best_length,
173            evaluations: total_evaluations,
174            history,
175        })
176    }
177
178    fn name(&self) -> &'static str {
179        "Hybrid (GA+Tabu+ACO)"
180    }
181
182    fn reset(&mut self) {
183        // Hybrid is stateless
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190
191    fn square_instance() -> TspInstance {
192        let coords = vec![(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)];
193        TspInstance::from_coords("square", coords).expect("should create")
194    }
195
196    fn pentagon_instance() -> TspInstance {
197        let angle_step = 2.0 * std::f64::consts::PI / 5.0;
198        let coords: Vec<(f64, f64)> = (0..5)
199            .map(|i| {
200                let angle = i as f64 * angle_step;
201                (angle.cos(), angle.sin())
202            })
203            .collect();
204        TspInstance::from_coords("pentagon", coords).expect("should create")
205    }
206
207    fn random_instance(n: usize, seed: u64) -> TspInstance {
208        use rand::prelude::*;
209        let mut rng = StdRng::seed_from_u64(seed);
210        let coords: Vec<(f64, f64)> = (0..n).map(|_| (rng.gen(), rng.gen())).collect();
211        TspInstance::from_coords("random", coords).expect("should create")
212    }
213
214    #[test]
215    fn test_hybrid_default_params() {
216        let hybrid = HybridSolver::default();
217        assert!((hybrid.ga_fraction - 0.4).abs() < 1e-10);
218        assert!((hybrid.tabu_fraction - 0.3).abs() < 1e-10);
219        assert!((hybrid.aco_fraction - 0.3).abs() < 1e-10);
220        assert_eq!(hybrid.ga_population, 30);
221        assert_eq!(hybrid.refine_top_k, 3);
222    }
223
224    #[test]
225    fn test_hybrid_builder() {
226        let hybrid = HybridSolver::new()
227            .with_ga_fraction(0.5)
228            .with_tabu_fraction(0.25)
229            .with_aco_fraction(0.25)
230            .with_ga_population(50)
231            .with_refine_top_k(5)
232            .with_seed(42);
233
234        assert!((hybrid.ga_fraction - 0.5).abs() < 1e-10);
235        assert!((hybrid.tabu_fraction - 0.25).abs() < 1e-10);
236        assert!((hybrid.aco_fraction - 0.25).abs() < 1e-10);
237        assert_eq!(hybrid.ga_population, 50);
238        assert_eq!(hybrid.refine_top_k, 5);
239        assert_eq!(hybrid.seed, Some(42));
240    }
241
242    #[test]
243    fn test_hybrid_solves_square() {
244        let instance = square_instance();
245        let mut solver = HybridSolver::new().with_seed(42).with_ga_population(15);
246
247        let solution = solver
248            .solve(&instance, Budget::Iterations(100))
249            .expect("should solve");
250
251        // Optimal tour around square is 4.0
252        assert!(solution.length <= 5.0, "Length {} > 5.0", solution.length);
253        assert_eq!(solution.tour.len(), 4);
254        assert!(instance.validate_tour(&solution.tour).is_ok());
255    }
256
257    #[test]
258    fn test_hybrid_solves_pentagon() {
259        let instance = pentagon_instance();
260        let mut solver = HybridSolver::new().with_seed(42).with_ga_population(20);
261
262        let solution = solver
263            .solve(&instance, Budget::Iterations(100))
264            .expect("should solve");
265
266        assert_eq!(solution.tour.len(), 5);
267        assert!(instance.validate_tour(&solution.tour).is_ok());
268    }
269
270    #[test]
271    fn test_hybrid_solves_larger_instance() {
272        let instance = random_instance(20, 123);
273        let mut solver = HybridSolver::new().with_seed(42).with_ga_population(25);
274
275        let solution = solver
276            .solve(&instance, Budget::Iterations(200))
277            .expect("should solve");
278
279        assert_eq!(solution.tour.len(), 20);
280        assert!(instance.validate_tour(&solution.tour).is_ok());
281        assert!(solution.length > 0.0);
282    }
283
284    #[test]
285    fn test_hybrid_deterministic_with_seed() {
286        let instance = pentagon_instance();
287
288        let mut solver1 = HybridSolver::new().with_seed(42).with_ga_population(15);
289        let mut solver2 = HybridSolver::new().with_seed(42).with_ga_population(15);
290
291        let solution1 = solver1
292            .solve(&instance, Budget::Iterations(50))
293            .expect("should solve");
294        let solution2 = solver2
295            .solve(&instance, Budget::Iterations(50))
296            .expect("should solve");
297
298        assert!((solution1.length - solution2.length).abs() < 1e-10);
299    }
300
301    #[test]
302    fn test_hybrid_tracks_history() {
303        let instance = square_instance();
304        let mut solver = HybridSolver::new().with_seed(42).with_ga_population(15);
305
306        let solution = solver
307            .solve(&instance, Budget::Iterations(50))
308            .expect("should solve");
309
310        assert!(!solution.history.is_empty());
311    }
312
313    #[test]
314    fn test_hybrid_normalize_fractions() {
315        let solver = HybridSolver::new()
316            .with_ga_fraction(0.6)
317            .with_tabu_fraction(0.2)
318            .with_aco_fraction(0.2);
319
320        let (ga, tabu, aco) = solver.normalize_fractions();
321        assert!((ga + tabu + aco - 1.0).abs() < 1e-10);
322        assert!((ga - 0.6).abs() < 1e-10);
323    }
324
325    #[test]
326    fn test_hybrid_normalize_zero_fractions() {
327        let solver = HybridSolver::new()
328            .with_ga_fraction(0.0)
329            .with_tabu_fraction(0.0)
330            .with_aco_fraction(0.0);
331
332        let (ga, tabu, aco) = solver.normalize_fractions();
333        // Should default to roughly equal
334        assert!((ga + tabu + aco - 1.0).abs() < 1e-10);
335    }
336
337    #[test]
338    fn test_hybrid_name() {
339        let solver = HybridSolver::new();
340        assert_eq!(solver.name(), "Hybrid (GA+Tabu+ACO)");
341    }
342
343    #[test]
344    fn test_hybrid_counts_evaluations() {
345        let instance = square_instance();
346        let mut solver = HybridSolver::new().with_seed(42).with_ga_population(10);
347
348        let solution = solver
349            .solve(&instance, Budget::Iterations(30))
350            .expect("should solve");
351
352        // Should have some evaluations
353        assert!(solution.evaluations > 0);
354    }
355
356    #[test]
357    fn test_hybrid_improves_solution() {
358        // Use a slightly larger instance where improvement is more visible
359        let instance = random_instance(15, 999);
360
361        let mut solver = HybridSolver::new().with_seed(42).with_ga_population(20);
362
363        let solution = solver
364            .solve(&instance, Budget::Iterations(150))
365            .expect("should solve");
366
367        // First and last in history - should improve or stay same
368        let first = solution.history[0];
369        let last = solution.history[solution.history.len() - 1];
370        assert!(last <= first);
371    }
372}