From fa9a5aff839b19a79677e6fdc8f605456c389f08 Mon Sep 17 00:00:00 2001 From: Rutherther Date: Tue, 28 Oct 2025 16:27:49 +0100 Subject: [PATCH] feat: add plotting of TSP --- codes/tsp_hw01/Cargo.toml | 1 + codes/tsp_hw01/src/tsp.rs | 68 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/codes/tsp_hw01/Cargo.toml b/codes/tsp_hw01/Cargo.toml index ed229b7aeccaf2240281432be58aaee2941b8622..ba5531cc3db55a2c6e330056c16db52fe8f5b3ab 100644 --- a/codes/tsp_hw01/Cargo.toml +++ b/codes/tsp_hw01/Cargo.toml @@ -7,4 +7,5 @@ edition = "2024" eoa_lib = { path = "../eoa_lib" } itertools = "0.14.0" nalgebra = "0.33.2" +plotters = "0.3" rand = "0.9.2" diff --git a/codes/tsp_hw01/src/tsp.rs b/codes/tsp_hw01/src/tsp.rs index b59a164db8bbc9a8f6f636e7d97f30365711f52a..4bbc06afa34ae1513099a6eeece727848c94bcd5 100644 --- a/codes/tsp_hw01/src/tsp.rs +++ b/codes/tsp_hw01/src/tsp.rs @@ -3,6 +3,7 @@ use std::{convert::Infallible, marker::PhantomData}; use eoa_lib::{fitness::FitnessFunction, initializer::Initializer, perturbation::PerturbationOperator}; use itertools::Itertools; use nalgebra::{allocator::Allocator, distance, Const, DefaultAllocator, Dim, Dyn, OMatrix, OVector, Point, U1}; +use plotters::prelude::*; use rand::{seq::SliceRandom, Rng, RngCore}; #[derive(PartialEq, Clone, Debug)] @@ -117,6 +118,73 @@ where .map(|(&node1, &node2): (&usize, &usize)| self.distances[(node1, node2)]) .sum() } + + fn plot_internal(&self, solution: Option<&NodePermutation>, filename: &str) -> Result<(), Box> { + let root = BitMapBackend::new(filename, (800, 600)).into_drawing_area(); + root.fill(&WHITE)?; + + let x_coords: Vec = self.cities.iter().map(|city| city.point.x).collect(); + let y_coords: Vec = self.cities.iter().map(|city| city.point.y).collect(); + + let x_min = x_coords.iter().fold(f64::INFINITY, |a, &b| a.min(b)); + let x_max = x_coords.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b)); + let y_min = y_coords.iter().fold(f64::INFINITY, |a, &b| a.min(b)); + let y_max = y_coords.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b)); + + let x_padding = (x_max - x_min) * 0.1; + let y_padding = (y_max - y_min) * 0.1; + + let x_range = (x_min - x_padding)..(x_max + x_padding); + let y_range = (y_min - y_padding)..(y_max + y_padding); + + let title = if let Some(sol) = solution { + format!("TSP Solution (Cost: {:.2})", self.solution_cost(sol)) + } else { + "TSP Instance".to_string() + }; + + let mut chart = ChartBuilder::on(&root) + .caption(&title, ("sans-serif", 40)) + .margin(10) + .x_label_area_size(40) + .y_label_area_size(40) + .build_cartesian_2d(x_range, y_range)?; + + chart.configure_mesh().draw()?; + + if let Some(sol) = solution { + chart.draw_series( + sol.permutation.iter().circular_tuple_windows().map(|(&city1_idx, &city2_idx)| { + let city1 = &self.cities[city1_idx]; + let city2 = &self.cities[city2_idx]; + PathElement::new(vec![(city1.point.x, city1.point.y), (city2.point.x, city2.point.y)], BLUE) + }) + )?; + } + + chart.draw_series( + self.cities.iter().map(|city| { + Circle::new((city.point.x, city.point.y), 5, RED.filled()) + }) + )?; + + chart.draw_series( + self.cities.iter().enumerate().map(|(i, city)| { + Text::new(format!("{}", i), (city.point.x + 0.1, city.point.y + 0.1), ("sans-serif", 15)) + }) + )?; + + root.present()?; + Ok(()) + } + + pub fn plot(&self, filename: &str) -> Result<(), Box> { + self.plot_internal(None, filename) + } + + pub fn draw_solution(&self, solution: &NodePermutation, filename: &str) -> Result<(), Box> { + self.plot_internal(Some(solution), filename) + } } impl FitnessFunction for TSPInstance