aprender_tsp/solver/
ga.rs

1//! Genetic Algorithm for TSP.
2//!
3//! Reference: Goldberg (1989) "Genetic Algorithms in Search, Optimization, and Machine Learning"
4
5use crate::error::TspResult;
6use crate::instance::TspInstance;
7use crate::solver::{Budget, TspSolution, TspSolver};
8use rand::prelude::*;
9
10/// Genetic Algorithm solver for TSP
11#[derive(Debug, Clone)]
12pub struct GaSolver {
13    /// Population size
14    pub population_size: usize,
15    /// Crossover probability
16    pub crossover_rate: f64,
17    /// Mutation probability
18    pub mutation_rate: f64,
19    /// Tournament selection size
20    pub tournament_size: usize,
21    /// Elitism: number of best individuals to preserve
22    pub elitism: usize,
23    /// Random seed
24    seed: Option<u64>,
25}
26
27impl Default for GaSolver {
28    fn default() -> Self {
29        Self {
30            population_size: 50,
31            crossover_rate: 0.9,
32            mutation_rate: 0.1,
33            tournament_size: 5,
34            elitism: 2,
35            seed: None,
36        }
37    }
38}
39
40impl GaSolver {
41    /// Create a new GA solver
42    pub fn new() -> Self {
43        Self::default()
44    }
45
46    /// Set population size
47    pub fn with_population_size(mut self, size: usize) -> Self {
48        self.population_size = size;
49        self
50    }
51
52    /// Set crossover rate
53    pub fn with_crossover_rate(mut self, rate: f64) -> Self {
54        self.crossover_rate = rate;
55        self
56    }
57
58    /// Set mutation rate
59    pub fn with_mutation_rate(mut self, rate: f64) -> Self {
60        self.mutation_rate = rate;
61        self
62    }
63
64    /// Set tournament size
65    pub fn with_tournament_size(mut self, size: usize) -> Self {
66        self.tournament_size = size;
67        self
68    }
69
70    /// Set elitism count
71    pub fn with_elitism(mut self, count: usize) -> Self {
72        self.elitism = count;
73        self
74    }
75
76    /// Set random seed
77    pub fn with_seed(mut self, seed: u64) -> Self {
78        self.seed = Some(seed);
79        self
80    }
81
82    /// Generate random tour
83    fn random_tour(n: usize, rng: &mut StdRng) -> Vec<usize> {
84        let mut tour: Vec<usize> = (0..n).collect();
85        tour.shuffle(rng);
86        tour
87    }
88
89    /// Tournament selection
90    fn tournament_select<'a>(
91        &self,
92        population: &'a [(Vec<usize>, f64)],
93        rng: &mut StdRng,
94    ) -> &'a Vec<usize> {
95        let mut best_idx = rng.gen_range(0..population.len());
96        let mut best_fitness = population[best_idx].1;
97
98        for _ in 1..self.tournament_size {
99            let idx = rng.gen_range(0..population.len());
100            if population[idx].1 < best_fitness {
101                best_fitness = population[idx].1;
102                best_idx = idx;
103            }
104        }
105
106        &population[best_idx].0
107    }
108
109    /// Order Crossover (OX)
110    fn order_crossover(parent1: &[usize], parent2: &[usize], rng: &mut StdRng) -> Vec<usize> {
111        let n = parent1.len();
112        if n < 2 {
113            return parent1.to_vec();
114        }
115
116        let mut child = vec![usize::MAX; n];
117
118        // Select crossover segment
119        let start = rng.gen_range(0..n);
120        let end = rng.gen_range(start..n);
121
122        // Copy segment from parent1
123        child[start..=end].copy_from_slice(&parent1[start..=end]);
124
125        // Fill remaining from parent2 in order
126        let mut pos = (end + 1) % n;
127        let mut p2_pos = (end + 1) % n;
128
129        while child.contains(&usize::MAX) {
130            let city = parent2[p2_pos];
131            if !child.contains(&city) {
132                child[pos] = city;
133                pos = (pos + 1) % n;
134            }
135            p2_pos = (p2_pos + 1) % n;
136        }
137
138        child
139    }
140
141    /// 2-opt mutation
142    fn mutate(&self, tour: &mut [usize], rng: &mut StdRng) {
143        if rng.gen::<f64>() < self.mutation_rate {
144            let n = tour.len();
145            if n < 2 {
146                return;
147            }
148
149            let i = rng.gen_range(0..n);
150            let j = rng.gen_range(0..n);
151
152            if i != j {
153                let (i, j) = if i < j { (i, j) } else { (j, i) };
154                tour[i..=j].reverse();
155            }
156        }
157    }
158
159    /// Evolve population for one generation
160    fn evolve_generation(
161        &self,
162        population: &mut Vec<(Vec<usize>, f64)>,
163        instance: &TspInstance,
164        rng: &mut StdRng,
165    ) -> usize {
166        let mut evaluations = 0;
167
168        // Sort by fitness (lower is better for TSP)
169        population.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
170
171        let mut new_population = Vec::with_capacity(self.population_size);
172
173        // Elitism: keep best individuals
174        for individual in population.iter().take(self.elitism) {
175            new_population.push(individual.clone());
176        }
177
178        // Generate rest of population
179        while new_population.len() < self.population_size {
180            let parent1 = self.tournament_select(population, rng);
181            let parent2 = self.tournament_select(population, rng);
182
183            let mut child = if rng.gen::<f64>() < self.crossover_rate {
184                Self::order_crossover(parent1, parent2, rng)
185            } else {
186                parent1.clone()
187            };
188
189            self.mutate(&mut child, rng);
190
191            let fitness = instance.tour_length(&child);
192            evaluations += 1;
193
194            new_population.push((child, fitness));
195        }
196
197        *population = new_population;
198        evaluations
199    }
200
201    /// Get best individual from population
202    pub fn evolve(
203        &mut self,
204        instance: &TspInstance,
205        generations: usize,
206    ) -> TspResult<Vec<(Vec<usize>, f64)>> {
207        let n = instance.num_cities();
208
209        let mut rng = match self.seed {
210            Some(s) => StdRng::seed_from_u64(s),
211            None => StdRng::from_entropy(),
212        };
213
214        // Initialize population
215        let mut population: Vec<(Vec<usize>, f64)> = (0..self.population_size)
216            .map(|_| {
217                let tour = Self::random_tour(n, &mut rng);
218                let fitness = instance.tour_length(&tour);
219                (tour, fitness)
220            })
221            .collect();
222
223        // Evolve
224        for _ in 0..generations {
225            self.evolve_generation(&mut population, instance, &mut rng);
226        }
227
228        // Sort by fitness
229        population.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
230
231        Ok(population)
232    }
233}
234
235impl TspSolver for GaSolver {
236    fn solve(&mut self, instance: &TspInstance, budget: Budget) -> TspResult<TspSolution> {
237        let n = instance.num_cities();
238        let max_generations = budget.limit();
239
240        let mut rng = match self.seed {
241            Some(s) => StdRng::seed_from_u64(s),
242            None => StdRng::from_entropy(),
243        };
244
245        // Initialize population
246        let mut population: Vec<(Vec<usize>, f64)> = (0..self.population_size)
247            .map(|_| {
248                let tour = Self::random_tour(n, &mut rng);
249                let fitness = instance.tour_length(&tour);
250                (tour, fitness)
251            })
252            .collect();
253
254        let mut best_tour = population[0].0.clone();
255        let mut best_length = population[0].1;
256        let mut history = Vec::with_capacity(max_generations);
257        let mut evaluations = self.population_size;
258
259        // Find initial best
260        for (tour, fitness) in &population {
261            if *fitness < best_length {
262                best_length = *fitness;
263                best_tour.clone_from(tour);
264            }
265        }
266        history.push(best_length);
267
268        // Evolve
269        for _ in 1..max_generations {
270            evaluations += self.evolve_generation(&mut population, instance, &mut rng);
271
272            // Update best
273            for (tour, fitness) in &population {
274                if *fitness < best_length {
275                    best_length = *fitness;
276                    best_tour.clone_from(tour);
277                }
278            }
279
280            history.push(best_length);
281        }
282
283        Ok(TspSolution {
284            tour: best_tour,
285            length: best_length,
286            evaluations,
287            history,
288        })
289    }
290
291    fn name(&self) -> &'static str {
292        "Genetic Algorithm"
293    }
294
295    fn reset(&mut self) {
296        // GA is stateless between runs
297    }
298}
299
300#[cfg(test)]
301mod tests {
302    use super::*;
303
304    fn square_instance() -> TspInstance {
305        let coords = vec![(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)];
306        TspInstance::from_coords("square", coords).expect("should create")
307    }
308
309    fn triangle_instance() -> TspInstance {
310        let coords = vec![(0.0, 0.0), (3.0, 0.0), (3.0, 4.0)];
311        TspInstance::from_coords("triangle", coords).expect("should create")
312    }
313
314    fn pentagon_instance() -> TspInstance {
315        // Regular pentagon
316        let angle_step = 2.0 * std::f64::consts::PI / 5.0;
317        let coords: Vec<(f64, f64)> = (0..5)
318            .map(|i| {
319                let angle = i as f64 * angle_step;
320                (angle.cos(), angle.sin())
321            })
322            .collect();
323        TspInstance::from_coords("pentagon", coords).expect("should create")
324    }
325
326    #[test]
327    fn test_ga_default_params() {
328        let ga = GaSolver::default();
329        assert_eq!(ga.population_size, 50);
330        assert!((ga.crossover_rate - 0.9).abs() < 1e-10);
331        assert!((ga.mutation_rate - 0.1).abs() < 1e-10);
332        assert_eq!(ga.tournament_size, 5);
333        assert_eq!(ga.elitism, 2);
334    }
335
336    #[test]
337    fn test_ga_builder() {
338        let ga = GaSolver::new()
339            .with_population_size(100)
340            .with_crossover_rate(0.8)
341            .with_mutation_rate(0.2)
342            .with_tournament_size(3)
343            .with_elitism(5)
344            .with_seed(42);
345
346        assert_eq!(ga.population_size, 100);
347        assert!((ga.crossover_rate - 0.8).abs() < 1e-10);
348        assert!((ga.mutation_rate - 0.2).abs() < 1e-10);
349        assert_eq!(ga.tournament_size, 3);
350        assert_eq!(ga.elitism, 5);
351        assert_eq!(ga.seed, Some(42));
352    }
353
354    #[test]
355    fn test_ga_solves_square() {
356        let instance = square_instance();
357        let mut solver = GaSolver::new().with_seed(42).with_population_size(20);
358
359        let solution = solver
360            .solve(&instance, Budget::Iterations(50))
361            .expect("should solve");
362
363        // Optimal tour around square is 4.0
364        assert!(solution.length <= 5.0, "Length {} > 5.0", solution.length);
365        assert_eq!(solution.tour.len(), 4);
366        assert!(instance.validate_tour(&solution.tour).is_ok());
367    }
368
369    #[test]
370    fn test_ga_solves_triangle() {
371        let instance = triangle_instance();
372        let mut solver = GaSolver::new().with_seed(42).with_population_size(20);
373
374        let solution = solver
375            .solve(&instance, Budget::Iterations(50))
376            .expect("should solve");
377
378        // Triangle tour: 3 + 4 + 5 = 12
379        assert!(solution.length <= 13.0, "Length {} > 13.0", solution.length);
380    }
381
382    #[test]
383    fn test_ga_solves_pentagon() {
384        let instance = pentagon_instance();
385        let mut solver = GaSolver::new().with_seed(42).with_population_size(30);
386
387        let solution = solver
388            .solve(&instance, Budget::Iterations(100))
389            .expect("should solve");
390
391        assert_eq!(solution.tour.len(), 5);
392        assert!(instance.validate_tour(&solution.tour).is_ok());
393    }
394
395    #[test]
396    fn test_ga_deterministic_with_seed() {
397        let instance = square_instance();
398
399        let mut solver1 = GaSolver::new().with_seed(42).with_population_size(20);
400        let mut solver2 = GaSolver::new().with_seed(42).with_population_size(20);
401
402        let solution1 = solver1
403            .solve(&instance, Budget::Iterations(20))
404            .expect("should solve");
405        let solution2 = solver2
406            .solve(&instance, Budget::Iterations(20))
407            .expect("should solve");
408
409        assert!((solution1.length - solution2.length).abs() < 1e-10);
410    }
411
412    #[test]
413    fn test_ga_tracks_history() {
414        let instance = square_instance();
415        let mut solver = GaSolver::new().with_seed(42).with_population_size(20);
416
417        let solution = solver
418            .solve(&instance, Budget::Iterations(30))
419            .expect("should solve");
420
421        assert_eq!(solution.history.len(), 30);
422        // History should be non-increasing
423        for window in solution.history.windows(2) {
424            assert!(window[1] <= window[0] + 1e-10);
425        }
426    }
427
428    #[test]
429    fn test_order_crossover() {
430        let mut rng = StdRng::seed_from_u64(42);
431
432        let parent1 = vec![0, 1, 2, 3, 4];
433        let parent2 = vec![4, 3, 2, 1, 0];
434
435        let child = GaSolver::order_crossover(&parent1, &parent2, &mut rng);
436
437        // Child should be a valid permutation
438        assert_eq!(child.len(), 5);
439        let mut sorted = child.clone();
440        sorted.sort();
441        assert_eq!(sorted, vec![0, 1, 2, 3, 4]);
442    }
443
444    #[test]
445    fn test_mutation() {
446        let solver = GaSolver::new().with_mutation_rate(1.0); // Always mutate
447        let mut rng = StdRng::seed_from_u64(42);
448
449        let mut tour = vec![0, 1, 2, 3, 4];
450        let original = tour.clone();
451
452        solver.mutate(&mut tour, &mut rng);
453
454        // Tour should still be valid permutation
455        let mut sorted = tour.clone();
456        sorted.sort();
457        assert_eq!(sorted, vec![0, 1, 2, 3, 4]);
458
459        // Should likely be different (with mutation rate 1.0)
460        // Note: could still be same if i==j selected
461        assert!(tour != original || true); // Just check it doesn't crash
462    }
463
464    #[test]
465    fn test_ga_evolve_population() {
466        let instance = square_instance();
467        let mut solver = GaSolver::new().with_seed(42).with_population_size(20);
468
469        let population = solver.evolve(&instance, 50).expect("should evolve");
470
471        assert_eq!(population.len(), 20);
472        // Population should be sorted by fitness
473        for window in population.windows(2) {
474            assert!(window[0].1 <= window[1].1);
475        }
476    }
477
478    #[test]
479    fn test_ga_name() {
480        let solver = GaSolver::new();
481        assert_eq!(solver.name(), "Genetic Algorithm");
482    }
483
484    #[test]
485    fn test_ga_elitism_preserves_best() {
486        let instance = square_instance();
487        let mut solver = GaSolver::new()
488            .with_seed(42)
489            .with_population_size(20)
490            .with_elitism(2);
491
492        let solution = solver
493            .solve(&instance, Budget::Iterations(50))
494            .expect("should solve");
495
496        // With elitism, best should never get worse
497        for window in solution.history.windows(2) {
498            assert!(window[1] <= window[0] + 1e-10);
499        }
500    }
501}