~ruther/ctu-fee-eoa

c604f7c5c47c53d20995f32a9139d647566f674d — Rutherther a month ago be95568
tests(tsp): move tests to proper modules, fix uses
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);
    }

}