M codes/tsp_hw01/src/binary_string_representation.rs => codes/tsp_hw01/src/binary_string_representation.rs +122 -0
@@ 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::<U15>::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::<U15>::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::<U15>::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()
+ );
+ }
+}
M codes/tsp_hw01/src/crossovers.rs => codes/tsp_hw01/src/crossovers.rs +81 -0
@@ 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<const LEN: usize>;
+ impl<const LEN: usize> FitnessFunction for ZeroFitness<LEN> {
+ type In = NodePermutation<Const<LEN>>;
+ type Out = f64;
+ type Err = Infallible;
+
+ fn fit(self: &Self, _: &Self::In) -> Result<Self::Out, Self::Err> {
+ Ok(0.0)
+ }
+ }
+
+ #[test]
+ fn test_edge_recombination_properties() {
+ let crossover = EdgeRecombinationCrossover::<Const<10>>::new();
+ let initializer = TSPRandomInitializer::<Const<10>>::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<usize> = vec![0, 1, 2, 4, 5, 3];
+ let parent2: Vec<usize> = vec![2, 0, 1, 3, 4, 5];
+
+ let parent1 = NodePermutation::<U6> { permutation: SVector::<usize, 6>::from_vec(parent1) };
+ let parent2 = NodePermutation::<U6> { permutation: SVector::<usize, 6>::from_vec(parent2) };
+
+ let pairing = SVector::<usize, 2>::new(0, 1);
+ let pairings = vec![pairing].into_iter();
+
+ let parents = Population::from_vec(vec![parent1, parent2]).evaluate(&ZeroFitness).unwrap();
+
+ let crossover = EdgeRecombinationCrossover::<U6>::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::<Vec<_>>())
+ }
+}
M codes/tsp_hw01/src/main.rs => codes/tsp_hw01/src/main.rs +23 -10
@@ 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<f64, Box<dyn std::error:
fn run_evolution_algorithm(instance: &TSPInstance<Dyn>) -> Result<PlotData, Box<dyn std::error::Error>> {
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<Dyn>) -> Result<PlotData, Box<
// Create initial population
let population_size = 500;
- let initial_population = initializer.initialize(dimension, population_size, &mut rng);
+ let mut initial_population = initializer.initialize(dimension, population_size, &mut rng);
+ let two_opt = Random2OptPerturbation::new(instance, 10);
+
+ for individual in initial_population.iter_mut() {
+ two_opt.perturb(individual, &mut rng);
+ }
+
let initial_population = eoa_lib::replacement::Population::from_vec(initial_population);
// Run evolution algorithm
@@ 263,7 270,7 @@ fn run_evolution_algorithm(instance: &TSPInstance<Dyn>) -> Result<PlotData, Box<
MutationPerturbation::apply_to_mutations(
perturbation,
&mut |p| {
- p.probability = (0.5 * (1.0 + (iters_since_better as f64 / iters_till_end as f64))).min(1.0);
+ p.probability = (0.05 * (1.0 + (iters_since_better as f64 / iters_till_end as f64))).min(1.0);
}
);
}
@@ 460,6 467,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// 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,
M codes/tsp_hw01/src/perturbations.rs => codes/tsp_hw01/src/perturbations.rs +182 -0
@@ 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::<Const<6>>::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::<Const<6>> {
+ permutation: SVector::<usize, 6>::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<usize> = 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::<Const<6>>::new();
+
+ // Use a specific seed that we know produces a certain result
+ let mut rng1 = StdRng::seed_from_u64(42);
+ let mut chromosome1 = NodePermutation::<Const<6>> {
+ permutation: SVector::<usize, 6>::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::<Const<6>> {
+ permutation: SVector::<usize, 6>::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::<Const<5>>::new();
+
+ // Test with a non-sequential initial permutation
+ let mut rng = StdRng::seed_from_u64(123);
+ let mut chromosome = NodePermutation::<Const<5>> {
+ permutation: SVector::<usize, 5>::from_vec(vec![2, 0, 4, 1, 3])
+ };
+ let original_elements: std::collections::HashSet<usize> =
+ chromosome.permutation.iter().copied().collect();
+
+ perturbation.perturb(&mut chromosome, &mut rng);
+
+ // Verify all original elements are still present
+ let new_elements: std::collections::HashSet<usize> =
+ 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::<Const<2>>::new();
+
+ // Test with minimum size permutation (2 elements)
+ let mut rng = StdRng::seed_from_u64(456);
+ let mut chromosome = NodePermutation::<Const<2>> {
+ permutation: SVector::<usize, 2>::from_vec(vec![0, 1])
+ };
+
+ perturbation.perturb(&mut chromosome, &mut rng);
+
+ let result: Vec<usize> = 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::<Const<6>>::new();
+
+ // Any sequence of reversals should be reversible
+ let mut rng = StdRng::seed_from_u64(789);
+ let original = NodePermutation::<Const<6>> {
+ permutation: SVector::<usize, 6>::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::<Const<10>>::new();
+ let initializer = TSPRandomInitializer::<Const<10>>::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<usize> = chromosome.permutation.iter().copied().collect();
+
+ perturbation.perturb(&mut chromosome, &mut rng);
+
+ // Verify all elements are still present
+ let new_elements: std::collections::HashSet<usize> = 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::<Const<8>>::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::<Const<8>> {
+ permutation: SVector::<usize, 8>::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);
+ }
+}
M codes/tsp_hw01/src/tsp.rs => codes/tsp_hw01/src/tsp.rs +3 -365
@@ 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<const LEN: usize>;
- impl<const LEN: usize> FitnessFunction for ZeroFitness<LEN> {
- type In = NodePermutation<Const<LEN>>;
- type Out = f64;
- type Err = Infallible;
-
- fn fit(self: &Self, _: &Self::In) -> Result<Self::Out, Self::Err> {
- 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::<U15>::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::<U15>::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::<U15>::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::<Const<10>>::new();
- let initializer = TSPRandomInitializer::<Const<10>>::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<usize> = vec![0, 1, 2, 4, 5, 3];
- let parent2: Vec<usize> = vec![2, 0, 1, 3, 4, 5];
-
- let parent1 = NodePermutation::<U6> { permutation: SVector::<usize, 6>::from_vec(parent1) };
- let parent2 = NodePermutation::<U6> { permutation: SVector::<usize, 6>::from_vec(parent2) };
-
- let pairing = SVector::<usize, 2>::new(0, 1);
- let pairings = vec![pairing].into_iter();
-
- let parents = Population::from_vec(vec![parent1, parent2]).evaluate(&ZeroFitness).unwrap();
-
- let crossover = EdgeRecombinationCrossover::<U6>::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::<Vec<_>>())
- }
-
- #[test]
- fn test_reverse_subsequence_perturbation_behavior() {
- let perturbation = ReverseSubsequencePerturbation::<Const<6>>::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::<Const<6>> {
- permutation: SVector::<usize, 6>::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<usize> = 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::<Const<6>>::new();
-
- // Use a specific seed that we know produces a certain result
- let mut rng1 = StdRng::seed_from_u64(42);
- let mut chromosome1 = NodePermutation::<Const<6>> {
- permutation: SVector::<usize, 6>::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::<Const<6>> {
- permutation: SVector::<usize, 6>::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::<Const<5>>::new();
-
- // Test with a non-sequential initial permutation
- let mut rng = StdRng::seed_from_u64(123);
- let mut chromosome = NodePermutation::<Const<5>> {
- permutation: SVector::<usize, 5>::from_vec(vec![2, 0, 4, 1, 3])
- };
- let original_elements: std::collections::HashSet<usize> =
- chromosome.permutation.iter().copied().collect();
-
- perturbation.perturb(&mut chromosome, &mut rng);
-
- // Verify all original elements are still present
- let new_elements: std::collections::HashSet<usize> =
- 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::<Const<2>>::new();
-
- // Test with minimum size permutation (2 elements)
- let mut rng = StdRng::seed_from_u64(456);
- let mut chromosome = NodePermutation::<Const<2>> {
- permutation: SVector::<usize, 2>::from_vec(vec![0, 1])
- };
-
- perturbation.perturb(&mut chromosome, &mut rng);
-
- let result: Vec<usize> = 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::<Const<6>>::new();
-
- // Any sequence of reversals should be reversible
- let mut rng = StdRng::seed_from_u64(789);
- let original = NodePermutation::<Const<6>> {
- permutation: SVector::<usize, 6>::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::<Const<10>>::new();
- let initializer = TSPRandomInitializer::<Const<10>>::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<usize> = chromosome.permutation.iter().copied().collect();
-
- perturbation.perturb(&mut chromosome, &mut rng);
-
- // Verify all elements are still present
- let new_elements: std::collections::HashSet<usize> = 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::<Const<8>>::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::<Const<8>> {
- permutation: SVector::<usize, 8>::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);
- }
}