From e326216f5d90907cef897216b573751e08967817 Mon Sep 17 00:00:00 2001 From: Rutherther Date: Sat, 1 Nov 2025 10:25:25 +0100 Subject: [PATCH] feat(tsp): add ea binary algorithm --- codes/tsp_hw01/src/main.rs | 122 +++++++++++++++++++++++++++++++++++-- 1 file changed, 118 insertions(+), 4 deletions(-) diff --git a/codes/tsp_hw01/src/main.rs b/codes/tsp_hw01/src/main.rs index 7efeebc76e1ddfb7c7d139057d9529308774ed1d..3c2085484db681e5474e66b8b3f2acef40c45236 100644 --- a/codes/tsp_hw01/src/main.rs +++ b/codes/tsp_hw01/src/main.rs @@ -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, 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, 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, optimal_cost: f64, base_path: &str) -> Result<(), Box> { + 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>( + 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, 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, 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> { 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)?;