From 45b0d91f04ac4787ea8aecbe1a7c321b6d44d227 Mon Sep 17 00:00:00 2001 From: Rutherther Date: Sat, 1 Nov 2025 11:55:08 +0100 Subject: [PATCH] refactor(tsp): put plotting to main to not repeat code --- codes/tsp_hw01/src/main.rs | 275 ++++++++++++++++++++++++------------- 1 file changed, 179 insertions(+), 96 deletions(-) diff --git a/codes/tsp_hw01/src/main.rs b/codes/tsp_hw01/src/main.rs index 3c2085484db681e5474e66b8b3f2acef40c45236..8892481cbc90dfec4a145c345b204817adb91cf4 100644 --- a/codes/tsp_hw01/src/main.rs +++ b/codes/tsp_hw01/src/main.rs @@ -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, Box, + iterations: Vec, + evaluations: Vec, + final_cost: f64, + total_iterations: usize, + algorithm_name: String, +} + +fn extract_evolution_data( + stats: &EvolutionStats, f64>, + final_solution: &NodePermutation, + 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, f64>, + final_solution: &NodePermutation, + 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, f64>, + final_solution: &NodePermutation, + 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, + plot_data: &PlotData, + base_path: &str, +) -> Result<(), Box> { + // 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> { let instance_name = std::path::Path::new(instance_filename) .file_stem() @@ -85,7 +207,7 @@ fn load_optimal_cost(instance_filename: &str) -> Result, optimal_cost: f64, base_path: &str) -> Result<(), Box> { +fn run_evolution_algorithm(instance: &TSPInstance) -> Result> { let mut rng = rng(); let initializer = TSPRandomInitializer::new(); let dimension = instance.dimension(); @@ -112,10 +234,6 @@ fn run_evolution_algorithm(instance: &TSPInstance, 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, 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, optimal_cost: f64, base_path: &str) -> Result<(), Box> { +fn run_evolution_algorithm_binary(instance: &TSPInstance) -> Result> { 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, 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, 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, optimal_cost: f64, base_path: &str) -> Result<(), Box> { +fn run_local_search(instance: &TSPInstance) -> Result> { 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, 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> { @@ -357,25 +430,35 @@ fn main() -> Result<(), Box> { 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(())