use std::convert::Infallible; use eoa_lib::fitness::FitnessFunction; use itertools::Itertools; use nalgebra::{allocator::Allocator, distance, Const, DefaultAllocator, Dim, Dyn, OMatrix, OVector, Point}; use plotters::prelude::*; #[derive(PartialEq, Clone, Debug)] pub struct TSPCity { point: Point } #[derive(PartialEq, Clone, Debug)] pub struct NodePermutation where DefaultAllocator: Allocator { pub permutation: OVector } /// An instance of TSP, a fully connected graph /// with cities that connect to each other. /// The D parameter represents the number of cities. #[derive(PartialEq, Clone, Debug)] pub struct TSPInstance where D: Dim, DefaultAllocator: Allocator { pub cities: Vec, pub distances: OMatrix } impl TSPInstance where { pub fn new_dyn(cities: Vec<(f64, f64)>) -> Self { let dim = Dyn(cities.len()); let cities = OMatrix::>::from_fn_generic(dim, Const::<2>, |i, j| if j == 0 { cities[i].0 } else { cities[i].1 }); TSPInstance::new(cities) } } impl TSPInstance> where { pub fn new_const(cities: Vec<(f64, f64)>) -> Self { let cities = OMatrix::, Const<2>>::from_fn(|i, j| if j == 0 { cities[i].0 } else { cities[i].1 }); TSPInstance::new(cities) } } impl TSPInstance where D: Dim, DefaultAllocator: Allocator, DefaultAllocator: Allocator, DefaultAllocator: Allocator>, { pub fn new(cities: OMatrix>) -> Self { let dim = cities.shape_generic().0; let cities = cities.row_iter() .map(|position| TSPCity { point: Point::::new(position[0], position[1]) } ) .collect::>(); let distances = OMatrix::from_fn_generic( dim, dim, |i, j| distance(&cities[i].point, &cities[j].point) ); Self { cities, distances } } } impl TSPInstance where D: Dim, DefaultAllocator: Allocator, DefaultAllocator: Allocator, { pub fn dimension(&self) -> D { self.distances.shape_generic().0 } pub fn verify_solution(solution: &NodePermutation) -> bool { let mut seen_vertices = OVector::from_element_generic( solution.permutation.shape_generic().0, solution.permutation.shape_generic().1, false ); for &vertex in solution.permutation.iter() { // This vertex index is out of bounds if vertex >= solution.permutation.len() { return false; } // A node is repeating if seen_vertices[vertex] { return false; } seen_vertices[vertex] = true; } true } pub fn solution_cost(&self, solution: &NodePermutation) -> f64 { solution.permutation .iter() .circular_tuple_windows() .map(|(&node1, &node2): (&usize, &usize)| self.distances(node1, node2)) .sum() } pub fn distances(&self, city_a: usize, city_b: usize) -> f64 { self.distances[(city_a, city_b)] } fn plot_internal(&self, solution: Option<&NodePermutation>, filename: &str) -> Result<(), Box> { let root = BitMapBackend::new(filename, (800, 600)).into_drawing_area(); root.fill(&WHITE)?; let x_coords: Vec = self.cities.iter().map(|city| city.point.x).collect(); let y_coords: Vec = self.cities.iter().map(|city| city.point.y).collect(); let x_min = x_coords.iter().fold(f64::INFINITY, |a, &b| a.min(b)); let x_max = x_coords.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b)); let y_min = y_coords.iter().fold(f64::INFINITY, |a, &b| a.min(b)); let y_max = y_coords.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b)); let x_padding = (x_max - x_min) * 0.1; let y_padding = (y_max - y_min) * 0.1; let x_range = (x_min - x_padding)..(x_max + x_padding); let y_range = (y_min - y_padding)..(y_max + y_padding); let title = if let Some(sol) = solution { format!("TSP Solution (Cost: {:.2})", self.solution_cost(sol)) } else { "TSP Instance".to_string() }; let mut chart = ChartBuilder::on(&root) .caption(&title, ("sans-serif", 40)) .margin(10) .x_label_area_size(40) .y_label_area_size(40) .build_cartesian_2d(x_range, y_range)?; chart.configure_mesh().draw()?; if let Some(sol) = solution { chart.draw_series( sol.permutation.iter().circular_tuple_windows().map(|(&city1_idx, &city2_idx)| { let city1 = &self.cities[city1_idx]; let city2 = &self.cities[city2_idx]; PathElement::new(vec![(city1.point.x, city1.point.y), (city2.point.x, city2.point.y)], BLUE) }) )?; } chart.draw_series( self.cities.iter().map(|city| { Circle::new((city.point.x, city.point.y), 5, RED.filled()) }) )?; root.present()?; Ok(()) } pub fn plot(&self, filename: &str) -> Result<(), Box> { self.plot_internal(None, filename) } pub fn draw_solution(&self, solution: &NodePermutation, filename: &str) -> Result<(), Box> { self.plot_internal(Some(solution), filename) } } impl FitnessFunction for TSPInstance where D: Dim, DefaultAllocator: Allocator, DefaultAllocator: Allocator, { type In = NodePermutation; type Out = f64; type Err = Infallible; fn fit(self: &Self, inp: &Self::In) -> Result { assert_eq!(inp.permutation.len(), self.cities.len()); assert!(TSPInstance::verify_solution(inp)); Ok(self.solution_cost(inp)) } } #[cfg(test)] mod tests { use std::convert::Infallible; use eoa_lib::{binary_string::BinaryString, crossover::Crossover, fitness::FitnessFunction, initializer::Initializer, pairing::{AdjacentPairing, Pairing}, replacement::Population}; use nalgebra::{Const, SVector, U15, U6}; use rand::{rngs::StdRng, seq::SliceRandom, RngCore, SeedableRng}; use crate::tsp::TSPInstance; use super::{TSPBinaryStringWrapper, EdgeRecombinationCrossover, NodePermutation, ReverseSubsequencePerturbation, TSPRandomInitializer}; use eoa_lib::perturbation::PerturbationOperator; struct MockRng; impl RngCore for MockRng { fn next_u32(&mut self) -> u32 { 0 } fn next_u64(&mut self) -> u64 { 0 } fn fill_bytes(&mut self, _: &mut [u8]) { panic!() } } struct ZeroFitness; impl FitnessFunction for ZeroFitness { type In = NodePermutation>; type Out = f64; type Err = Infallible; fn fit(self: &Self, _: &Self::In) -> Result { Ok(0.0) } } #[test] fn test_verify_solution() { let mut rng = rand::rng(); let rng = &mut rng; let mut chromosome = NodePermutation::> { permutation: SVector::from_vec(vec![0, 1, 2, 3, 4, 5]) }; for _ in 0..100 { chromosome.permutation.as_mut_slice().shuffle(rng); assert!(TSPInstance::verify_solution(&chromosome)); } // Out of bounds chromosome.permutation[0] = 6; assert!(!TSPInstance::verify_solution(&chromosome)); chromosome.permutation[0] = 7; assert!(!TSPInstance::verify_solution(&chromosome)); chromosome.permutation[0] = 8; assert!(!TSPInstance::verify_solution(&chromosome)); // Repeating chromosome.permutation[0] = 5; chromosome.permutation[1] = 5; assert!(!TSPInstance::verify_solution(&chromosome)); let chromosome = NodePermutation::> { permutation: SVector::from_vec(vec![0, 1, 2, 3, 1, 5]) }; assert!(!TSPInstance::verify_solution(&chromosome)); } #[test] fn test_binary_string_representation() { // x 0 1 2 3 4 5 // 0 0 0 0 0 0 0 // 1 1 0 0 0 0 0 // 2 1 1 0 0 0 0 // 3 1 1 1 0 0 0 // 4 1 1 1 1 0 0 // 5 1 1 1 1 1 0 // x 0 1 2 3 4 5 // 0 0 0 0 0 0 // 1 0 0 0 0 // 2 0 0 0 // 3 0 0 // 4 0 // 5 // 6 nodes // length of binary string: 5 + 4 + 3 + 2 + 1 = 15 let tsp = TSPInstance::new_const( vec![ (0.0, 0.0), (0.0, 0.0), (0.0, 0.0), (0.0, 0.0), (0.0, 0.0), (0.0, 0.0), ] ); let converter = TSPBinaryStringWrapper::new( &tsp, U15, U6 ).unwrap(); let binary_string_ordering = BinaryString::::new(vec![1; 15]); let mut expected_permutation = vec![0, 1, 2, 3, 4, 5]; let mut permutation = converter.to_permutation(&binary_string_ordering) .unwrap(); assert_eq!( expected_permutation, permutation.permutation.as_mut_slice().to_vec() ); let binary_string_ordering = BinaryString::::new(vec![0; 15]); expected_permutation.reverse(); let mut permutation = converter.to_permutation(&binary_string_ordering) .unwrap(); assert_eq!( expected_permutation, permutation.permutation.as_mut_slice().to_vec() ) } #[test] fn test_nontrivial_binary_string_representation() { // x 0 1 2 3 4 5 // 0 0 1 0 0 0 0 // 1 0 0 0 0 0 0 // 2 1 1 0 0 0 1 // 3 1 1 1 0 0 0 // 4 1 1 1 1 0 0 // 5 1 1 0 1 1 0 // x 0 1 2 3 4 5 // 0 0 0 0 0 0 // 1 0 0 0 0 // 2 1 1 1 // 3 0 0 // 4 1 // 5 // 6 nodes // length of binary string: 5 + 4 + 3 + 2 + 1 = 15 let tsp = TSPInstance::new_const( vec![ (0.0, 0.0), (0.0, 0.0), (0.0, 0.0), (0.0, 0.0), (0.0, 0.0), (0.0, 0.0), ] ); let converter = TSPBinaryStringWrapper::new( &tsp, U15, U6 ).unwrap(); let mut binary_string_ordering = BinaryString::::new(vec![0; 15]); binary_string_ordering.vec[9] = 1; binary_string_ordering.vec[10] = 1; binary_string_ordering.vec[11] = 1; binary_string_ordering.vec[14] = 1; let expected_permutation = vec![2, 4, 5, 3, 1, 0]; let mut permutation = converter.to_permutation(&binary_string_ordering) .unwrap(); assert_eq!( expected_permutation, permutation.permutation.as_mut_slice().to_vec() ); } #[test] fn test_edge_recombination_properties() { let crossover = EdgeRecombinationCrossover::>::new(); let initializer = TSPRandomInitializer::>::new(); let adjacency_pairing = AdjacentPairing::new(); let mut rng = StdRng::seed_from_u64(0); for _ in 0..100 { let parents = Population::from_vec(initializer.initialize(Const::<10>, 10, &mut rng)); let parents = parents.evaluate(&ZeroFitness).unwrap(); let pairs = adjacency_pairing.pair(&parents, 0..10); let result = crossover.crossover(&parents, pairs, &mut rng); // Test invariants that should always hold: for chromosome in result.into_iter() { assert!(TSPInstance::verify_solution(&chromosome)); } } } #[test] fn test_edge_recombination_specific_case() { let parent1: Vec = vec![0, 1, 2, 4, 5, 3]; let parent2: Vec = vec![2, 0, 1, 3, 4, 5]; let parent1 = NodePermutation:: { permutation: SVector::::from_vec(parent1) }; let parent2 = NodePermutation:: { permutation: SVector::::from_vec(parent2) }; let pairing = SVector::::new(0, 1); let pairings = vec![pairing].into_iter(); let parents = Population::from_vec(vec![parent1, parent2]).evaluate(&ZeroFitness).unwrap(); let crossover = EdgeRecombinationCrossover::::new(); let offsprings = crossover.crossover(&parents, pairings, &mut MockRng); let offspring = offsprings.into_iter().next().unwrap(); // NOTE: this sort of relies on the implementation of the algorithm (when there are multiple possibilities // currently the algorithm always chooses last). It's possible this test will break due to valid changes to the algorithm. assert_eq!(vec![0usize, 1, 3, 4, 5, 2], offspring.permutation.into_iter().copied().collect::>()) } #[test] fn test_reverse_subsequence_perturbation_behavior() { let perturbation = ReverseSubsequencePerturbation::>::new(); // Test multiple specific seeds to get predictable behavior // We'll try different seeds until we find ones that give us the patterns we want to test // Test case 1: Try to find a seed that reverses a middle subsequence let mut found_middle_reverse = false; for seed in 0..1000 { let mut rng = StdRng::seed_from_u64(seed); let mut chromosome = NodePermutation::> { permutation: SVector::::from_vec(vec![0, 1, 2, 3, 4, 5]) }; let original = chromosome.clone(); perturbation.perturb(&mut chromosome, &mut rng); // Check if it's a valid reverse pattern and not the whole array or single element let result: Vec = chromosome.permutation.into_iter().copied().collect(); if result != vec![0, 1, 2, 3, 4, 5] && // Changed result != vec![5, 4, 3, 2, 1, 0] && // Not whole array reverse TSPInstance::verify_solution(&chromosome) { found_middle_reverse = true; break; } } assert!(found_middle_reverse, "Should find at least one case of partial subsequence reversal"); } #[test] fn test_reverse_subsequence_perturbation_deterministic_seed() { let perturbation = ReverseSubsequencePerturbation::>::new(); // Use a specific seed that we know produces a certain result let mut rng1 = StdRng::seed_from_u64(42); let mut chromosome1 = NodePermutation::> { permutation: SVector::::from_vec(vec![0, 1, 2, 3, 4, 5]) }; perturbation.perturb(&mut chromosome1, &mut rng1); // Same seed should produce same result let mut rng2 = StdRng::seed_from_u64(42); let mut chromosome2 = NodePermutation::> { permutation: SVector::::from_vec(vec![0, 1, 2, 3, 4, 5]) }; perturbation.perturb(&mut chromosome2, &mut rng2); assert_eq!(chromosome1.permutation, chromosome2.permutation); assert!(TSPInstance::verify_solution(&chromosome1)); assert!(TSPInstance::verify_solution(&chromosome2)); } #[test] fn test_reverse_subsequence_perturbation_different_initial_permutations() { let perturbation = ReverseSubsequencePerturbation::>::new(); // Test with a non-sequential initial permutation let mut rng = StdRng::seed_from_u64(123); let mut chromosome = NodePermutation::> { permutation: SVector::::from_vec(vec![2, 0, 4, 1, 3]) }; let original_elements: std::collections::HashSet = chromosome.permutation.iter().copied().collect(); perturbation.perturb(&mut chromosome, &mut rng); // Verify all original elements are still present let new_elements: std::collections::HashSet = chromosome.permutation.iter().copied().collect(); assert_eq!(original_elements, new_elements); // Verify it's still a valid permutation assert!(TSPInstance::verify_solution(&chromosome)); } #[test] fn test_reverse_subsequence_perturbation_edge_cases() { let perturbation = ReverseSubsequencePerturbation::>::new(); // Test with minimum size permutation (2 elements) let mut rng = StdRng::seed_from_u64(456); let mut chromosome = NodePermutation::> { permutation: SVector::::from_vec(vec![0, 1]) }; perturbation.perturb(&mut chromosome, &mut rng); let result: Vec = chromosome.permutation.into_iter().copied().collect(); // With 2 elements, it should either stay [0,1] or become [1,0] assert!(result == vec![0, 1] || result == vec![1, 0]); assert!(TSPInstance::verify_solution(&chromosome)); } #[test] fn test_reverse_subsequence_perturbation_is_reversible() { let perturbation = ReverseSubsequencePerturbation::>::new(); // Any sequence of reversals should be reversible let mut rng = StdRng::seed_from_u64(789); let original = NodePermutation::> { permutation: SVector::::from_vec(vec![0, 1, 2, 3, 4, 5]) }; let mut chromosome = original.clone(); // Apply perturbation twice with same seed (reset RNG) perturbation.perturb(&mut chromosome, &mut rng); let after_first = chromosome.clone(); // Since we can't easily reverse the exact operation, at least verify // that multiple applications maintain the permutation property for _ in 0..10 { perturbation.perturb(&mut chromosome, &mut rng); assert!(TSPInstance::verify_solution(&chromosome)); } } #[test] fn test_reverse_subsequence_perturbation_preserves_elements() { let perturbation = ReverseSubsequencePerturbation::>::new(); let initializer = TSPRandomInitializer::>::new(); let mut rng = StdRng::seed_from_u64(42); // Test with multiple random permutations for _ in 0..50 { let mut chromosome = initializer.initialize_single(Const::<10>, &mut rng); let original_elements: std::collections::HashSet = chromosome.permutation.iter().copied().collect(); perturbation.perturb(&mut chromosome, &mut rng); // Verify all elements are still present let new_elements: std::collections::HashSet = chromosome.permutation.iter().copied().collect(); assert_eq!(original_elements, new_elements); // Verify it's still a valid permutation assert!(TSPInstance::verify_solution(&chromosome)); } } #[test] fn test_reverse_subsequence_perturbation_actually_changes_permutation() { let perturbation = ReverseSubsequencePerturbation::>::new(); let mut rng = StdRng::seed_from_u64(12345); // Test that the perturbation actually changes the permutation (with high probability) let mut changes_detected = 0; let total_tests = 100; for _ in 0..total_tests { let mut chromosome = NodePermutation::> { permutation: SVector::::from_vec(vec![0, 1, 2, 3, 4, 5, 6, 7]) }; let original = chromosome.clone(); perturbation.perturb(&mut chromosome, &mut rng); if chromosome.permutation != original.permutation { changes_detected += 1; } // Always verify it's still a valid permutation assert!(TSPInstance::verify_solution(&chromosome)); } // We expect at least 85% of random perturbations to actually change the permutation // (only fails if start == end randomly, which should be rare) assert!(changes_detected >= 85, "Expected at least 85 changes out of {} tests, but got {}", total_tests, changes_detected); } }