From 0fe8b60c83aa32e434db94050a034c300f528926 Mon Sep 17 00:00:00 2001 From: Rutherther Date: Fri, 31 Oct 2025 13:38:34 +0100 Subject: [PATCH] feat(tsp): Output csv statistics --- codes/tsp_hw01/Cargo.toml | 1 + codes/tsp_hw01/src/main.rs | 187 ++++++++++++++++++++++++++++++------- 2 files changed, 155 insertions(+), 33 deletions(-) diff --git a/codes/tsp_hw01/Cargo.toml b/codes/tsp_hw01/Cargo.toml index 18762710b7ddcb54121eb37930c9df2518e84d1f..932cab1ec4316f81950cfc6f42b890de97f76e93 100644 --- a/codes/tsp_hw01/Cargo.toml +++ b/codes/tsp_hw01/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] +chrono = "0.4.42" eoa_lib = { path = "../eoa_lib" } flate2 = "1.0" itertools = "0.14.0" diff --git a/codes/tsp_hw01/src/main.rs b/codes/tsp_hw01/src/main.rs index 5c3ddb73731776ed150a15df693647887aeb0f54..0d83ec2a254593423618b4a51e0d25e0cdb598a0 100644 --- a/codes/tsp_hw01/src/main.rs +++ b/codes/tsp_hw01/src/main.rs @@ -1,18 +1,18 @@ pub mod tsp; pub mod graph; -use tsp::{TSPInstance, TSPRandomInitializer, SwapPerturbation}; -use nalgebra::{Const, Dim, Dyn}; +use tsp::{TSPInstance, TSPRandomInitializer, SwapPerturbation, ReverseSubsequencePerturbation, EdgeRecombinationCrossover}; +use nalgebra::{Const, Dim, Dyn, U100}; use eoa_lib::{ - initializer::Initializer, - local_search::local_search_first_improving, - terminating::NoBetterForCyclesTerminatingCondition, - comparison::MinimizingOperator, + 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} }; use rand::rng; -use std::fs::File; -use std::io::{BufRead, BufReader, Read}; +use std::env; +use std::fs::{File, create_dir_all}; +use std::io::{BufRead, BufReader, Read, Write}; +use std::path::Path; use flate2::read::GzDecoder; +use chrono::{DateTime, Local}; fn load_tsp_instance(filename: &str) -> Result, Box> { let file = File::open(filename)?; @@ -85,35 +85,96 @@ fn load_optimal_cost(instance_filename: &str) -> Result Result<(), Box> { - // Load a TSP instance from file - let filename = "instances/berlin52.tsp.gz"; - let instance = load_tsp_instance(filename)?; +fn run_evolution_algorithm(instance: &TSPInstance, optimal_cost: f64, base_path: &str) -> Result<(), Box> { + let mut rng = rng(); + let initializer = TSPRandomInitializer::new(); let dimension = instance.dimension(); - let optimal_cost = load_optimal_cost(filename)?; - println!("Loaded {} with {} cities (optimal cost: {})", filename, dimension.value(), optimal_cost); + // Create combined perturbation with two mutations wrapped in MutationPerturbation + let swap_mutation = MutationPerturbation::new(Box::new(SwapPerturbation::new()), 0.5); + let reverse_mutation = MutationPerturbation::new(Box::new(ReverseSubsequencePerturbation::new()), 0.5); + let combined_perturbation = CombinedPerturbation::new(vec![ + Box::new(swap_mutation), + Box::new(reverse_mutation), + ]); - // Plot just the instance - instance.plot("tsp_instance.png")?; - println!("Created tsp_instance.png"); + // Set up other components + let crossover = EdgeRecombinationCrossover::new(); + let selection = TournamentSelection::new(5, 0.8); + let replacement = BestReplacement::new(); + let pairing = AdjacentPairing::new(); + let better_than_operator = MinimizingOperator::new(); - // Create a random solution - let initializer = TSPRandomInitializer::new(); + // Create initial population + let population_size = 500; + 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::<_, _, 2>( + initial_population.clone(), + parents_count, + instance, + &selection, + &pairing, + &crossover, + &combined_perturbation, + &replacement, + &better_than_operator, + 5000, // max iterations + &mut rng, + )?; + + // 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) + 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)?; + } + + 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_local_search(instance: &TSPInstance, optimal_cost: f64, base_path: &str) -> Result<(), Box> { let mut rng = rng(); - let initial_solution = initializer.initialize_single(dimension, &mut rng); + let initializer = TSPRandomInitializer::new(); + let dimension = instance.dimension(); - // Plot the initial solution - instance.draw_solution(&initial_solution, "tsp_initial_solution.png")?; - println!("Created tsp_initial_solution.png with cost: {:.2}", instance.solution_cost(&initial_solution)); + // 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 to improve the solution - let mut perturbation = SwapPerturbation::new(); - let mut terminating_condition = NoBetterForCyclesTerminatingCondition::new(100000); + // Run local search + let mut perturbation = ReverseSubsequencePerturbation::new(); + let mut terminating_condition = MaximumCyclesTerminatingCondition::new(250 * 5000 + 500); let better_than_operator = MinimizingOperator::new(); let result = local_search_first_improving( - &instance, + instance, &mut terminating_condition, &mut perturbation, &better_than_operator, @@ -122,15 +183,75 @@ fn main() -> Result<(), Box> { )?; // Plot the improved solution - instance.draw_solution(&result.best_candidate.pos, "tsp_improved_solution.png")?; - println!("Created tsp_improved_solution.png with cost: {:.2}", result.best_candidate.fit); - println!("Improvement: {:.2} -> {:.2} (cycles: {})", - instance.solution_cost(&initial_solution), - result.best_candidate.fit, - result.cycles); + 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)?; + + 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(()) } + +fn main() -> Result<(), Box> { + let args: Vec = env::args().collect(); + + if args.len() != 3 { + eprintln!("Usage: {} ", args[0]); + eprintln!(" instance_name: e.g., kroA100, berlin52, eil51"); + eprintln!(" algorithm: ea (evolution algorithm) or ls (local search)"); + std::process::exit(1); + } + + let instance_name = &args[1]; + let algorithm = &args[2]; + + // Load TSP instance + let filename = format!("instances/{}.tsp.gz", instance_name); + let instance = load_tsp_instance(&filename)?; + let dimension = instance.dimension(); + let optimal_cost = load_optimal_cost(&filename)?; + + println!("Loaded {} with {} cities (optimal cost: {})", instance_name, dimension.value(), optimal_cost); + + // Create directory structure: algorithm/instance_name/ + let output_dir = format!("solutions/{}/{}", algorithm, instance_name); + create_dir_all(&output_dir)?; + + // Generate timestamp for filename + let now: DateTime = Local::now(); + 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() { + "ea" => { + println!("Running Evolution Algorithm..."); + run_evolution_algorithm(&instance, optimal_cost, &solution_base_path)?; + }, + "ls" => { + println!("Running Local Search..."); + run_local_search(&instance, optimal_cost, &solution_base_path)?; + }, + _ => { + eprintln!("Unknown algorithm: {}. Use 'ea' or 'ls'", algorithm); + std::process::exit(1); + } + } + + println!("Created {}.png and {}.csv", solution_base_path, solution_base_path); + Ok(()) +}