~ruther/ctu-fee-eoa

e326216f5d90907cef897216b573751e08967817 — Rutherther a month ago 25d8378
feat(tsp): add ea binary algorithm
1 files changed, 118 insertions(+), 4 deletions(-)

M codes/tsp_hw01/src/main.rs
M codes/tsp_hw01/src/main.rs => codes/tsp_hw01/src/main.rs +118 -4
@@ 1,10 1,10 @@
pub mod tsp;
pub mod graph;

use tsp::{TSPInstance, TSPRandomInitializer, SwapPerturbation, ReverseSubsequencePerturbation, EdgeRecombinationCrossover};
use tsp::{EdgeRecombinationCrossover, MovePerturbation, ReverseSubsequencePerturbation, SwapPerturbation, TSPBinaryStringWrapper, TSPInstance, TSPRandomInitializer};
use nalgebra::{Const, Dim, Dyn, U100};
use eoa_lib::{
    comparison::MinimizingOperator, evolution::evolution_algorithm, initializer::Initializer, local_search::local_search_first_improving, pairing::AdjacentPairing, perturbation::{CombinedPerturbation, MutationPerturbation}, replacement::BestReplacement, selection::TournamentSelection, terminating::{MaximumCyclesTerminatingCondition, NoBetterForCyclesTerminatingCondition}
    comparison::MinimizingOperator, crossover::BinaryNPointCrossover, evolution::evolution_algorithm, initializer::{Initializer, RandomInitializer}, local_search::local_search_first_improving, pairing::AdjacentPairing, perturbation::{apply_to_perturbations, BinaryStringBitPerturbation, BinaryStringFlipNPerturbation, BinaryStringFlipPerturbation, BinaryStringSingleBitPerturbation, CombinedPerturbation, MutationPerturbation}, replacement::{BestReplacement, TournamentReplacement}, selection::{BestSelection, TournamentSelection}, terminating::{MaximumCyclesTerminatingCondition, NoBetterForCyclesTerminatingCondition}
};
use rand::rng;
use std::env;


@@ 91,9 91,11 @@ fn run_evolution_algorithm(instance: &TSPInstance<Dyn>, optimal_cost: f64, base_
    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 mut combined_perturbation = CombinedPerturbation::new(vec![
        Box::new(move_mutation),
        Box::new(swap_mutation),
        Box::new(reverse_mutation),
    ]);


@@ 160,6 162,115 @@ fn run_evolution_algorithm(instance: &TSPInstance<Dyn>, optimal_cost: f64, base_
        let fitness_evaluations = initial_population_size + candidate.iteration * offspring_count;
        writeln!(stats_file, "{},{}", fitness_evaluations, candidate.evaluated_chromosome.evaluation)?;
    }
    writeln!(stats_file, "{},{}", result.iterations, result.stats.best_candidates.iter().last().unwrap().evaluated_chromosome.evaluation)?;

    println!("Evolution completed in {} generations", result.iterations);
    println!("Final cost: {:.2}", result.best_candidate.evaluation);
    println!("Gap to optimal: {:.2} ({:.1}%)",
        result.best_candidate.evaluation - optimal_cost,
        ((result.best_candidate.evaluation - optimal_cost) / optimal_cost) * 100.0);

    Ok(())
}

fn run_evolution_algorithm_binary(instance: &TSPInstance<Dyn>, optimal_cost: f64, base_path: &str) -> Result<(), Box<dyn std::error::Error>> {
    let mut rng = rng();
    let initializer = RandomInitializer::new_binary();
    let output_dimension = instance.dimension();
    let input_dimension = Dyn(output_dimension.value() * (output_dimension.value() - 1) / 2);

    // Create combined perturbation with two mutations wrapped in MutationPerturbation
    let bit_mutation = MutationPerturbation::new(Box::new(BinaryStringBitPerturbation::new(0.1)), 0.2);
    let single_bit_mutation = MutationPerturbation::new(Box::new(BinaryStringSingleBitPerturbation::new()), 0.4);
    let flip1_mutation = MutationPerturbation::new(Box::new(BinaryStringFlipNPerturbation::new(30)), 0.4);
    let flip2_mutation = MutationPerturbation::new(Box::new(BinaryStringFlipNPerturbation::new(20)), 0.4);
    let mut combined_perturbation = CombinedPerturbation::new(vec![
        Box::new(bit_mutation),
        Box::new(single_bit_mutation),
        Box::new(flip1_mutation),
        Box::new(flip2_mutation),
    ]);

    // Set up other components
    let mut crossover = BinaryNPointCrossover::<10, _, _>::new();
    let mut selection = BestSelection::new();
    let mut replacement = TournamentReplacement::new(5, 1.0);
    let mut pairing = AdjacentPairing::new();
    let better_than_operator = MinimizingOperator::new();

    // Create initial population
    let population_size = 500;
    let initial_population = initializer.initialize(input_dimension, population_size, &mut rng);
    let initial_population = eoa_lib::replacement::Population::from_vec(initial_population);

    let fitness = TSPBinaryStringWrapper::new(instance, input_dimension, output_dimension).unwrap();
    let evaluated_initial = initial_population.clone().evaluate(&fitness)?;
    let initial_best = evaluated_initial.best_candidate(&better_than_operator);
    println!("Initial best cost: {:.2}", initial_best.evaluation);

    // Run evolution algorithm
    let parents_count = 250;
    let result = evolution_algorithm(
        initial_population.clone(),
        parents_count,
        &fitness,
        &mut selection,
        &mut pairing,
        &mut crossover,
        &mut combined_perturbation,
        &mut replacement,
        &better_than_operator,
        5000, // max iterations
        &mut rng,
        |iteration, stats, _, _, _, _, perturbation, _| {
            let iters_till_end = 5000 - iteration + 1;
            let iters_since_better =
                iteration - stats.best_candidates.last().map(|c| c.iteration).unwrap_or(0);
            let mut found = false;
            apply_to_perturbations::<_, BinaryStringBitPerturbation<Dyn>>(
                perturbation,
                &mut |p| {
                    found = true;
                    p.p = (0.025 * (1.0 + (iters_since_better as f64 / iters_till_end as f64))).min(0.2);
                }
            );
            assert!(found);

            let mut found = 0;
            MutationPerturbation::apply_to_mutations(
                perturbation,
                &mut |p| {
                    // Do not touch multi bit mutation
                    if found > 0 {
                        p.probability = (0.5 * (1.0 + (iters_since_better as f64 / iters_till_end as f64))).min(1.0);
                    }
                    found += 1;
                }
            );
            assert_eq!(found, 4);
        }
    )?;

    // Plot the best solution
    let best_solution = &result.best_candidate.chromosome;
    let plot_path = format!("{}.png", base_path);
    instance.draw_solution(&fitness.to_permutation(best_solution).unwrap(), &plot_path)?;

    // Save statistics to CSV
    let stats_path = format!("{}.csv", base_path);
    let mut stats_file = File::create(&stats_path)?;
    writeln!(stats_file, "fitness_evaluations,evaluation")?;

    // Calculate fitness evaluations: initial_population + iteration * offspring_count
    // offspring_count = parents_count / 2 (due to adjacent pairing)
    let offspring_count = parents_count / 2;
    let initial_population_size = initial_population.iter().count();

    for candidate in &result.stats.best_candidates {
        let fitness_evaluations = initial_population_size + candidate.iteration * offspring_count;
        writeln!(stats_file, "{},{}", fitness_evaluations, candidate.evaluated_chromosome.evaluation)?;
    }
    writeln!(stats_file, "{},{}", result.iterations, result.stats.best_candidates.iter().last().unwrap().evaluated_chromosome.evaluation)?;

    println!("Evolution completed in {} generations", result.iterations);
    println!("Final cost: {:.2}", result.best_candidate.evaluation);


@@ 180,7 291,7 @@ fn run_local_search(instance: &TSPInstance<Dyn>, optimal_cost: f64, base_path: &
    println!("Initial cost: {:.2}", instance.solution_cost(&initial_solution));

    // Run local search
    let mut perturbation = ReverseSubsequencePerturbation::new();
    let mut perturbation = MovePerturbation::new();
    let mut terminating_condition = MaximumCyclesTerminatingCondition::new(250 * 5000 + 500);
    let better_than_operator = MinimizingOperator::new();



@@ 205,7 316,6 @@ fn run_local_search(instance: &TSPInstance<Dyn>, optimal_cost: f64, base_path: &
    for candidate in result.stats.candidates() {
        writeln!(stats_file, "{},{}", candidate.cycle, candidate.fit)?;
    }

    writeln!(stats_file, "{},{}", result.cycles, result.stats.candidates().iter().last().unwrap().fit)?;

    println!("Local search completed in {} cycles", result.cycles);


@@ 253,6 363,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
            println!("Running Evolution Algorithm...");
            run_evolution_algorithm(&instance, optimal_cost, &solution_base_path)?;
        },
        "ea_binary" => {
            println!("Running Evolution Algorithm...");
            run_evolution_algorithm_binary(&instance, optimal_cost, &solution_base_path)?;
        },
        "ls" => {
            println!("Running Local Search...");
            run_local_search(&instance, optimal_cost, &solution_base_path)?;