From c604f7c5c47c53d20995f32a9139d647566f674d Mon Sep 17 00:00:00 2001 From: Rutherther Date: Sat, 1 Nov 2025 21:39:37 +0100 Subject: [PATCH] tests(tsp): move tests to proper modules, fix uses --- .../src/binary_string_representation.rs | 122 ++++++ codes/tsp_hw01/src/crossovers.rs | 81 ++++ codes/tsp_hw01/src/main.rs | 33 +- codes/tsp_hw01/src/perturbations.rs | 182 +++++++++ codes/tsp_hw01/src/tsp.rs | 368 +----------------- 5 files changed, 411 insertions(+), 375 deletions(-) diff --git a/codes/tsp_hw01/src/binary_string_representation.rs b/codes/tsp_hw01/src/binary_string_representation.rs index d53c6a19a5355f657b5aeb15a61c2800cfba8500..a50cd94beaeb1378e142e9c574c098937b23f476 100644 --- a/codes/tsp_hw01/src/binary_string_representation.rs +++ b/codes/tsp_hw01/src/binary_string_representation.rs @@ -105,3 +105,125 @@ pub enum DimensionMismatch { #[error("The input dimension should be equal to half matrix NxN where the output is N")] Mismatch } + +#[cfg(test)] +mod tests { + use super::*; + use nalgebra::{Const, SVector, U15, U6}; + use eoa_lib::binary_string::BinaryString; + + #[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() + ); + } +} diff --git a/codes/tsp_hw01/src/crossovers.rs b/codes/tsp_hw01/src/crossovers.rs index 581dddd9effc6c3d4a116071d5be358abb1a5391..d6eb4e917f4e6f518102b6366237ab095ffa8bbd 100644 --- a/codes/tsp_hw01/src/crossovers.rs +++ b/codes/tsp_hw01/src/crossovers.rs @@ -336,3 +336,84 @@ where Population::from_vec(offsprings) } } + +#[cfg(test)] +mod tests { + use super::*; + use std::convert::Infallible; + use nalgebra::{SVector, U6}; + use rand::{rngs::StdRng, RngCore, SeedableRng}; + use eoa_lib::{fitness::FitnessFunction, initializer::Initializer, pairing::{AdjacentPairing, Pairing}, replacement::Population}; + use crate::initializers::TSPRandomInitializer; + use crate::tsp::{NodePermutation, TSPInstance}; + + 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_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::>()) + } +} diff --git a/codes/tsp_hw01/src/main.rs b/codes/tsp_hw01/src/main.rs index 1cf999fc41fe99913039874224bafd667d270a7c..e8cb896be2b11cc5bea13a8d19c10b6b3e429ec7 100644 --- a/codes/tsp_hw01/src/main.rs +++ b/codes/tsp_hw01/src/main.rs @@ -1,4 +1,5 @@ pub mod tsp; +pub mod union_find; pub mod initializers; pub mod crossovers; pub mod binary_string_representation; @@ -6,13 +7,13 @@ pub mod perturbations; pub mod graph; use tsp::{NodePermutation, TSPInstance}; -use initializers::TSPRandomInitializer; -use crossovers::EdgeRecombinationCrossover; -use perturbations::{MovePerturbation, ReverseSubsequencePerturbation, SwapPerturbation}; +use initializers::{MinimumSpanningTreeInitializer, NearestNeighborInitializer, TSPRandomInitializer}; +use crossovers::{CycleCrossover, EdgeRecombinationCrossover, NoCrossover, PartiallyMappedCrossover}; +use perturbations::{MovePerturbation, Random2OptPerturbation, ReverseSubsequencePerturbation, SwapPerturbation}; use binary_string_representation::TSPBinaryStringWrapper; use nalgebra::{Dim, Dyn}; use eoa_lib::{ - binary_string::BinaryString, comparison::MinimizingOperator, crossover::BinaryNPointCrossover, evolution::{evolution_algorithm, EvolutionStats}, initializer::{Initializer, RandomInitializer}, local_search::{local_search_first_improving, LocalSearchStats}, pairing::AdjacentPairing, perturbation::{apply_to_perturbations, BinaryStringBitPerturbation, BinaryStringFlipNPerturbation, BinaryStringSingleBitPerturbation, CombinedPerturbation, MutationPerturbation}, replacement::{BestReplacement, TournamentReplacement}, selection::{BestSelection, RouletteWheelSelection}, terminating::MaximumCyclesTerminatingCondition + binary_string::BinaryString, comparison::MinimizingOperator, crossover::BinaryNPointCrossover, evolution::{evolution_algorithm, EvolutionStats}, initializer::{Initializer, RandomInitializer}, local_search::{local_search_first_improving, LocalSearchStats}, pairing::AdjacentPairing, perturbation::{apply_to_perturbations, BinaryStringBitPerturbation, BinaryStringFlipNPerturbation, BinaryStringSingleBitPerturbation, CombinedPerturbation, MutationPerturbation, PerturbationOperator}, replacement::{BestReplacement, TournamentReplacement}, selection::{BestSelection, RouletteWheelSelection}, terminating::MaximumCyclesTerminatingCondition }; use rand::rng; use std::env; @@ -217,13 +218,13 @@ fn load_optimal_cost(instance_filename: &str) -> Result) -> Result> { let mut rng = rng(); - let initializer = TSPRandomInitializer::new(); + let initializer = MinimumSpanningTreeInitializer::new(instance); let dimension = instance.dimension(); // Create combined perturbation with two mutations wrapped in MutationPerturbation - let move_mutation = MutationPerturbation::new(Box::new(MovePerturbation::new()), 0.5); - let swap_mutation = MutationPerturbation::new(Box::new(SwapPerturbation::new()), 0.5); - let reverse_mutation = MutationPerturbation::new(Box::new(ReverseSubsequencePerturbation::new()), 0.5); + let move_mutation = MutationPerturbation::new(Box::new(MovePerturbation::new()), 0.05); + let swap_mutation = MutationPerturbation::new(Box::new(SwapPerturbation::new()), 0.05); + let reverse_mutation = MutationPerturbation::new(Box::new(ReverseSubsequencePerturbation::new()), 0.0); let mut combined_perturbation = CombinedPerturbation::new(vec![ Box::new(move_mutation), Box::new(swap_mutation), @@ -239,7 +240,13 @@ fn run_evolution_algorithm(instance: &TSPInstance) -> Result) -> Result Result<(), Box> { // Print results println!("{} completed in {} iterations", plot_data.algorithm_name, plot_data.total_iterations); + let initial_cost = plot_data.evaluations[0]; + println!( + "Initial cost: {:.2} ({:.1}%)", + initial_cost, + ((initial_cost - optimal_cost) / optimal_cost) * 100.0 + ); println!("Final cost: {:.2}", plot_data.final_cost); println!("Gap to optimal: {:.2} ({:.1}%)", plot_data.final_cost - optimal_cost, diff --git a/codes/tsp_hw01/src/perturbations.rs b/codes/tsp_hw01/src/perturbations.rs index a5c65d94623729d0655b27eb578fea81b4f9976b..4ff3e444c3fc117d62cd1061924be9a21118ee7c 100644 --- a/codes/tsp_hw01/src/perturbations.rs +++ b/codes/tsp_hw01/src/perturbations.rs @@ -173,3 +173,185 @@ where } } } + +#[cfg(test)] +mod tests { + use super::*; + use nalgebra::{Const, SVector}; + use rand::{rngs::StdRng, SeedableRng}; + use crate::tsp::{NodePermutation, TSPInstance}; + use crate::initializers::TSPRandomInitializer; + use eoa_lib::initializer::Initializer; + + #[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); + } +} diff --git a/codes/tsp_hw01/src/tsp.rs b/codes/tsp_hw01/src/tsp.rs index 2ecbcf27fd0009bb03d57b3c2104ff8471ae27a8..2b8ab4de90e210a773ded59fa05a92bee02e0cd9 100644 --- a/codes/tsp_hw01/src/tsp.rs +++ b/codes/tsp_hw01/src/tsp.rs @@ -251,42 +251,10 @@ where #[cfg(test)] mod tests { - use std::convert::Infallible; + use nalgebra::{Const, SVector}; + use rand::seq::SliceRandom; - 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) - } - } + use super::{NodePermutation, TSPInstance}; #[test] fn test_verify_solution() { @@ -320,335 +288,5 @@ mod tests { 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); - } }