~ruther/ctu-fee-eoa

45b0d91f04ac4787ea8aecbe1a7c321b6d44d227 — Rutherther a month ago 9063396
refactor(tsp): put plotting to main to not repeat code
1 files changed, 179 insertions(+), 96 deletions(-)

M codes/tsp_hw01/src/main.rs
M codes/tsp_hw01/src/main.rs => codes/tsp_hw01/src/main.rs +179 -96
@@ 1,16 1,15 @@
pub mod tsp;
pub mod graph;

use tsp::{EdgeRecombinationCrossover, MovePerturbation, ReverseSubsequencePerturbation, SwapPerturbation, TSPBinaryStringWrapper, TSPInstance, TSPRandomInitializer};
use nalgebra::{Const, Dim, Dyn, U100};
use tsp::{EdgeRecombinationCrossover, MovePerturbation, NodePermutation, ReverseSubsequencePerturbation, SwapPerturbation, TSPBinaryStringWrapper, TSPInstance, TSPRandomInitializer};
use nalgebra::{Dim, Dyn};
use eoa_lib::{
    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}
    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, TournamentSelection}, terminating::MaximumCyclesTerminatingCondition
};
use rand::rng;
use std::env;
use std::fs::{File, create_dir_all};
use std::io::{BufRead, BufReader, Read, Write};
use std::path::Path;
use std::io::{BufRead, BufReader, Write};
use flate2::read::GzDecoder;
use chrono::{DateTime, Local};



@@ 56,6 55,129 @@ fn load_tsp_instance(filename: &str) -> Result<TSPInstance<Dyn>, Box<dyn std::er
    Ok(TSPInstance::new_dyn(cities))
}

#[derive(Debug, Clone)]
struct PlotData {
    best_solution: NodePermutation<Dyn>,
    iterations: Vec<usize>,
    evaluations: Vec<f64>,
    final_cost: f64,
    total_iterations: usize,
    algorithm_name: String,
}

fn extract_evolution_data(
    stats: &EvolutionStats<NodePermutation<Dyn>, f64>,
    final_solution: &NodePermutation<Dyn>,
    final_evaluation: f64,
    final_iteration: usize,
    initial_population_size: usize,
    offspring_count: usize,
) -> PlotData {
    let mut iterations = Vec::new();
    let mut evaluations = Vec::new();

    for candidate in &stats.best_candidates {
        let fitness_evaluations = initial_population_size + candidate.iteration * offspring_count;
        iterations.push(fitness_evaluations);
        evaluations.push(candidate.evaluated_chromosome.evaluation);
    }

    // Add final result
    let final_fitness_evaluations = initial_population_size + final_iteration * offspring_count;
    iterations.push(final_fitness_evaluations);
    evaluations.push(final_evaluation);

    PlotData {
        best_solution: final_solution.clone(),
        iterations,
        evaluations,
        final_cost: final_evaluation,
        total_iterations: final_iteration,
        algorithm_name: "Evolution Algorithm".to_string(),
    }
}

fn extract_binary_evolution_data(
    stats: &EvolutionStats<BinaryString<Dyn>, f64>,
    final_solution: &NodePermutation<Dyn>,
    final_evaluation: f64,
    final_iteration: usize,
    initial_population_size: usize,
    offspring_count: usize,
) -> PlotData {
    let mut iterations = Vec::new();
    let mut evaluations = Vec::new();

    for candidate in &stats.best_candidates {
        let fitness_evaluations = initial_population_size + candidate.iteration * offspring_count;
        iterations.push(fitness_evaluations);
        evaluations.push(candidate.evaluated_chromosome.evaluation);
    }

    // Add final result
    let final_fitness_evaluations = initial_population_size + final_iteration * offspring_count;
    iterations.push(final_fitness_evaluations);
    evaluations.push(final_evaluation);

    PlotData {
        best_solution: final_solution.clone(),
        iterations,
        evaluations,
        final_cost: final_evaluation,
        total_iterations: final_iteration,
        algorithm_name: "Evolution Algorithm (Binary)".to_string(),
    }
}

fn extract_local_search_data(
    stats: &LocalSearchStats<NodePermutation<Dyn>, f64>,
    final_solution: &NodePermutation<Dyn>,
    final_evaluation: f64,
    final_cycle: usize,
) -> PlotData {
    let mut iterations = Vec::new();
    let mut evaluations = Vec::new();

    for candidate in stats.candidates() {
        iterations.push(candidate.cycle);
        evaluations.push(candidate.fit);
    }

    // Add final result
    iterations.push(final_cycle);
    evaluations.push(final_evaluation);

    PlotData {
        best_solution: final_solution.clone(),
        iterations,
        evaluations,
        final_cost: final_evaluation,
        total_iterations: final_cycle,
        algorithm_name: "Local Search".to_string(),
    }
}

fn save_results(
    instance: &TSPInstance<Dyn>,
    plot_data: &PlotData,
    base_path: &str,
) -> Result<(), Box<dyn std::error::Error>> {
    // Plot the best solution
    let plot_path = format!("{}.png", base_path);
    instance.draw_solution(&plot_data.best_solution, &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")?;

    for (iteration, evaluation) in plot_data.iterations.iter().zip(plot_data.evaluations.iter()) {
        writeln!(stats_file, "{},{}", iteration, evaluation)?;
    }

    Ok(())
}

fn load_optimal_cost(instance_filename: &str) -> Result<f64, Box<dyn std::error::Error>> {
    let instance_name = std::path::Path::new(instance_filename)
        .file_stem()


@@ 85,7 207,7 @@ fn load_optimal_cost(instance_filename: &str) -> Result<f64, Box<dyn std::error:
    Err(format!("Optimal cost not found for instance '{}'", instance_name).into())
}

fn run_evolution_algorithm(instance: &TSPInstance<Dyn>, optimal_cost: f64, base_path: &str) -> Result<(), Box<dyn std::error::Error>> {
fn run_evolution_algorithm(instance: &TSPInstance<Dyn>) -> Result<PlotData, Box<dyn std::error::Error>> {
    let mut rng = rng();
    let initializer = TSPRandomInitializer::new();
    let dimension = instance.dimension();


@@ 112,10 234,6 @@ fn run_evolution_algorithm(instance: &TSPInstance<Dyn>, optimal_cost: f64, base_
    let initial_population = initializer.initialize(dimension, population_size, &mut rng);
    let initial_population = eoa_lib::replacement::Population::from_vec(initial_population);

    let evaluated_initial = initial_population.clone().evaluate(instance)?;
    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(


@@ 143,37 261,22 @@ fn run_evolution_algorithm(instance: &TSPInstance<Dyn>, optimal_cost: f64, base_
        }
    )?;

    // Plot the best solution
    let best_solution = &result.best_candidate.chromosome;
    let plot_path = format!("{}.png", base_path);
    instance.draw_solution(best_solution, &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)
    // Extract plotting data
    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);
    println!("Gap to optimal: {:.2} ({:.1}%)",
        result.best_candidate.evaluation - optimal_cost,
        ((result.best_candidate.evaluation - optimal_cost) / optimal_cost) * 100.0);

    Ok(())
    let plot_data = extract_evolution_data(
        &result.stats,
        &result.best_candidate.chromosome,
        result.best_candidate.evaluation,
        result.iterations,
        initial_population_size,
        offspring_count,
    );

    Ok(plot_data)
}

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


@@ 204,9 307,6 @@ fn run_evolution_algorithm_binary(instance: &TSPInstance<Dyn>, optimal_cost: f64
    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;


@@ 251,44 351,29 @@ fn run_evolution_algorithm_binary(instance: &TSPInstance<Dyn>, optimal_cost: f64
        }
    )?;

    // 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)
    // Extract plotting data
    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);
    println!("Gap to optimal: {:.2} ({:.1}%)",
        result.best_candidate.evaluation - optimal_cost,
        ((result.best_candidate.evaluation - optimal_cost) / optimal_cost) * 100.0);

    Ok(())
    let best_permutation = fitness.to_permutation(&result.best_candidate.chromosome).unwrap();
    let plot_data = extract_binary_evolution_data(
        &result.stats,
        &best_permutation,
        result.best_candidate.evaluation,
        result.iterations,
        initial_population_size,
        offspring_count,
    );

    Ok(plot_data)
}

fn run_local_search(instance: &TSPInstance<Dyn>, optimal_cost: f64, base_path: &str) -> Result<(), Box<dyn std::error::Error>> {
fn run_local_search(instance: &TSPInstance<Dyn>) -> Result<PlotData, Box<dyn std::error::Error>> {
    let mut rng = rng();
    let initializer = TSPRandomInitializer::new();
    let dimension = instance.dimension();

    // Create a random initial solution
    let initial_solution = initializer.initialize_single(dimension, &mut rng);
    println!("Initial cost: {:.2}", instance.solution_cost(&initial_solution));

    // Run local search
    let mut perturbation = MovePerturbation::new();


@@ 304,27 389,15 @@ fn run_local_search(instance: &TSPInstance<Dyn>, optimal_cost: f64, base_path: &
        &mut rng,
    )?;

    // Plot the improved solution
    let plot_path = format!("{}.png", base_path);
    instance.draw_solution(&result.best_candidate.pos, &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")?;
    // For local search, each cycle = 1 fitness evaluation
    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)?;
    // Extract plotting data
    let plot_data = extract_local_search_data(
        &result.stats,
        &result.best_candidate.pos,
        result.best_candidate.fit,
        result.cycles,
    );

    println!("Local search completed in {} cycles", result.cycles);
    println!("Final cost: {:.2}", result.best_candidate.fit);
    println!("Gap to optimal: {:.2} ({:.1}%)",
        result.best_candidate.fit - optimal_cost,
        ((result.best_candidate.fit - optimal_cost) / optimal_cost) * 100.0);

    Ok(())
    Ok(plot_data)
}

fn main() -> Result<(), Box<dyn std::error::Error>> {


@@ 357,25 430,35 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
    let timestamp = now.format("%Y-%m-%d_%H-%M-%S");
    let solution_base_path = format!("{}/{}", output_dir, timestamp);

    // Run the specified algorithm
    match algorithm.as_str() {
    // Run the specified algorithm and get plotting data
    let plot_data = match algorithm.as_str() {
        "ea" => {
            println!("Running Evolution Algorithm...");
            run_evolution_algorithm(&instance, optimal_cost, &solution_base_path)?;
            run_evolution_algorithm(&instance)?
        },
        "ea_binary" => {
            println!("Running Evolution Algorithm...");
            run_evolution_algorithm_binary(&instance, optimal_cost, &solution_base_path)?;
            println!("Running Evolution Algorithm (Binary)...");
            run_evolution_algorithm_binary(&instance)?
        },
        "ls" => {
            println!("Running Local Search...");
            run_local_search(&instance, optimal_cost, &solution_base_path)?;
            run_local_search(&instance)?
        },
        _ => {
            eprintln!("Unknown algorithm: {}. Use 'ea' or 'ls'", algorithm);
            eprintln!("Unknown algorithm: {}. Use 'ea', 'ea_binary', or 'ls'", algorithm);
            std::process::exit(1);
        }
    }
    };

    // Print results
    println!("{} completed in {} iterations", plot_data.algorithm_name, plot_data.total_iterations);
    println!("Final cost: {:.2}", plot_data.final_cost);
    println!("Gap to optimal: {:.2} ({:.1}%)",
        plot_data.final_cost - optimal_cost,
        ((plot_data.final_cost - optimal_cost) / optimal_cost) * 100.0);

    // Save results (plot and CSV)
    save_results(&instance, &plot_data, &solution_base_path)?;

    println!("Created {}.png and {}.csv", solution_base_path, solution_base_path);
    Ok(())