From 48d0912f632e44005cedd27f13de6f4925e3312a Mon Sep 17 00:00:00 2001 From: Rutherther Date: Sat, 6 Dec 2025 22:16:02 +0100 Subject: [PATCH] feat: add a simple python plotter --- codes/constr_hw02/src/main.rs | 67 +++-- codes/constr_hw02/src/problems.rs | 8 +- codes/eoa_lib/src/constraints.rs | 44 +-- codes/eoa_lib/src/population.rs | 2 +- codes/py_plotter/README.md | 86 ++++++ codes/py_plotter/config_best_g09.json | 57 ++++ .../config_constraint_violation.json | 35 +++ codes/py_plotter/config_example.json | 50 ++++ .../py_plotter/config_feasible_fraction.json | 55 ++++ codes/py_plotter/config_feasible_g05.json | 57 ++++ codes/py_plotter/config_feasible_g06.json | 57 ++++ codes/py_plotter/config_feasible_g11.json | 57 ++++ codes/py_plotter/config_violation_g05.json | 57 ++++ codes/py_plotter/config_violation_g06.json | 57 ++++ codes/py_plotter/config_violation_g11.json | 57 ++++ codes/py_plotter/objectives.json | 10 + codes/py_plotter/plotter.py | 273 ++++++++++++++++++ 17 files changed, 976 insertions(+), 53 deletions(-) create mode 100644 codes/py_plotter/README.md create mode 100644 codes/py_plotter/config_best_g09.json create mode 100644 codes/py_plotter/config_constraint_violation.json create mode 100644 codes/py_plotter/config_example.json create mode 100644 codes/py_plotter/config_feasible_fraction.json create mode 100644 codes/py_plotter/config_feasible_g05.json create mode 100644 codes/py_plotter/config_feasible_g06.json create mode 100644 codes/py_plotter/config_feasible_g11.json create mode 100644 codes/py_plotter/config_violation_g05.json create mode 100644 codes/py_plotter/config_violation_g06.json create mode 100644 codes/py_plotter/config_violation_g11.json create mode 100644 codes/py_plotter/objectives.json create mode 100644 codes/py_plotter/plotter.py diff --git a/codes/constr_hw02/src/main.rs b/codes/constr_hw02/src/main.rs index e632a7055999c2ce79eb45c0d0b5293796ae8157..75547cb9a824bd151c4011d5cb611fc3b0a65cc8 100644 --- a/codes/constr_hw02/src/main.rs +++ b/codes/constr_hw02/src/main.rs @@ -42,23 +42,22 @@ pub fn solve_with_stochastic_ranking let mut selection = TournamentSelection::new(5, 0.95); let mut replacement = GenerationalReplacement; let mut pairing = AdjacentPairing::new(); - let mut crossover = ArithmeticCrossover::new(); - // let mut crossover = BoundedCrossover::, 2, _>::new( - // ArithmeticCrossover::new(), - // problem.bounds.0, - // problem.bounds.1, - // BoundedCrossoverStrategy::Retry(5) - // ); + let crossover = ArithmeticCrossover::new(); + let mut crossover = BoundedCrossover::, 2, _>::new( + crossover, + problem.bounds.0, + problem.bounds.1, + BoundedCrossoverStrategy::Retry(5) + ); // Setup bounded random distribution perturbation with Normal distribution let normal_perturbation = RandomDistributionPerturbation::>::normal(mutation_std_dev)?; - let mut perturbation = normal_perturbation; - // let perturbation = BoundedPerturbation::new( - // normal_perturbation, - // problem.bounds.0, - // problem.bounds.1, - // BoundedPerturbationStrategy::Retry(5) - // ); + let perturbation = BoundedPerturbation::new( + normal_perturbation, + problem.bounds.0, + problem.bounds.1, + BoundedPerturbationStrategy::Retry(5) + ); let mut mutation = MutationPerturbation::new(Box::new(perturbation), 0.1); // The weight is so large mainly because of the g11 that has very small values. @@ -72,6 +71,8 @@ pub fn solve_with_stochastic_ranking let constraint_refs = problem.constraints.iter().collect::>().try_into() .map_err(|_| "Failed to convert constraint references")?; + let mut avg_constraint_violations = vec![0.0; initial_population.population.len()]; + let result = stochastic_ranking_evolution_algorithm( initial_population, parents_count, @@ -87,15 +88,22 @@ pub fn solve_with_stochastic_ranking &mut replacement, &better_than, iterations, - rng)?; + rng, + |_, _, population| { + + let avg_constraint_violation = population.population + .iter() + .map(|individual| { + individual.evaluation.weighted_sum.max(0.0) // Only positive values are violations + }) + .sum::() / population.population.len() as f64; + + avg_constraint_violations.push(avg_constraint_violation); + })?; // Extract feasible fractions from the result let (evolution_result, feasible_fractions) = result; - // For now, create placeholder constraint violations data - // TODO: This needs library-level changes to properly track constraint violations - let avg_constraint_violations = vec![0.0; feasible_fractions.len()]; - Ok((evolution_result, feasible_fractions, avg_constraint_violations)) } @@ -571,16 +579,16 @@ pub fn solve_with_nsga_improved( Ok((objective_result, feasible_fractions, avg_constraint_violations)) } -const ITERATIONS: usize = 1000; -const POPULATION: usize = 500; +const ITERATIONS: usize = 5000; +const POPULATION: usize = 250; +const PARENTS_COUNT: usize = 125; +const G11_EPS: f64 = 0.00015; // FeasibleCrossoverWrapper global probability parameters const P_SINGLE_REPLACED: f64 = 0.4; const P_DOUBLE_FIRST_REPLACED: f64 = 0.6; const P_DOUBLE_SECOND_REPLACED: f64 = 0.3; const ARCHIVE_SIZE: usize = 100; -const PARENTS_COUNT: usize = 500; -const G11_EPS: f64 = 0.00015; fn handle_g06_srank() -> Result<(), Box> { let problem = problem_g06(); @@ -617,7 +625,7 @@ fn handle_g11_srank() -> Result<(), Box> { n_param: POPULATION * 2, p_param: 0.45, - mutation_std_dev: 0.01 / 50.0, + mutation_std_dev: 0.01, }; run_stochastic_ranking(problem, config) } @@ -629,7 +637,7 @@ fn handle_g04_srank() -> Result<(), Box> { parents_count: PARENTS_COUNT, iterations: ITERATIONS, n_param: 2 * POPULATION, - p_param: 0.45, + p_param: 0.65, mutation_std_dev: 1.0, }; run_stochastic_ranking(problem, config) @@ -642,7 +650,7 @@ fn handle_g05_srank() -> Result<(), Box> { parents_count: PARENTS_COUNT, iterations: ITERATIONS, n_param: 2 * POPULATION, - p_param: 0.45, + p_param: 0.20, mutation_std_dev: 10.0, }; run_stochastic_ranking(problem, config) @@ -695,6 +703,13 @@ fn save_evolution_results Result<(), Box> { + + // Nothing to save... + + if evolution_result.best_candidate.is_none() { + return Ok(()); + } + // Get current date and time for unique file naming let now = Local::now(); let timestamp = now.format("%Y%m%d_%H%M%S").to_string(); diff --git a/codes/constr_hw02/src/problems.rs b/codes/constr_hw02/src/problems.rs index 634801d0950a065506adb187e55a14986b1133bb..d82ef24c9815e65ad5439d081c6cce629a163b24 100644 --- a/codes/constr_hw02/src/problems.rs +++ b/codes/constr_hw02/src/problems.rs @@ -165,8 +165,8 @@ pub fn problem_g06() -> ConstrainedProblem<2, 2> { ), ], bounds: ( - SVector::::new(0.0, 0.0), // min bounds - SVector::::new(50.0, 50.0), // max bounds + SVector::::new(13.0, 0.0), // min bounds + SVector::::new(100.0, 100.0), // max bounds ), optimal_value: -6961.8137558015, instantiate_fn: None, @@ -269,8 +269,8 @@ pub fn problem_g11(eps: f64) -> ConstrainedProblem<2, 1> { })), ], bounds: ( - SVector::::new(-50.0, -50.0), // min bounds - SVector::::new(50.0, 50.0), // max bounds + SVector::::new(-1.0, -1.0), // min bounds + SVector::::new(1.0, 1.0), // max bounds ), optimal_value: 0.7499, // Best known optimum instantiate_fn: None, diff --git a/codes/eoa_lib/src/constraints.rs b/codes/eoa_lib/src/constraints.rs index 32da66035a3dc6ad3e74d83f256e96ead4e498a2..17ee334f61999aa20d138d67d72eee226c11cd8a 100644 --- a/codes/eoa_lib/src/constraints.rs +++ b/codes/eoa_lib/src/constraints.rs @@ -240,11 +240,11 @@ pub fn evolve_constraint_penalty_weight_tau_target #[derive(PartialEq, Clone, Debug)] pub struct ConstrainedEvaluation { - fitness: TOut, - constraints: [TOut; CONSTRAINTS], - weighted_sum: TOut, - constr_weighted_sum: TOut, - is_feasible: bool, + pub fitness: TOut, + pub constraints: [TOut; CONSTRAINTS], + pub weighted_sum: TOut, + pub constr_weighted_sum: TOut, + pub is_feasible: bool, } impl PartialOrd for ConstrainedEvaluation { @@ -430,17 +430,18 @@ pub fn stochastic_ranking_evolution_algorithm // TODO: termination condition iterations: usize, rng: &mut dyn RngCore, - // mut evolutionary_strategy: impl FnMut( - // usize, - // &EvolutionStats, - // &EvaluatedPopulation, - - // &mut TPairing, - // &mut TCrossover, - // &mut TPerturbation, - // &mut TReplacement, - // &mut ConstrainedEvalFitness, - // ), + mut evolutionary_strategy: impl FnMut( + usize, + &EvolutionStats>, + &EvaluatedPopulation>, + + // &mut TFitness, + // &mut TPairing, + // &mut TCrossover, + // &mut TPerturbation, + // &mut TReplacement, + // &mut ConstrainedEvalFitness, + ), ) -> Result<(EvolutionResult, Vec), Box> { let mut constrained_fitness = ConstrainedEvalFitness { @@ -480,12 +481,11 @@ pub fn stochastic_ranking_evolution_algorithm feasible_fractions.push(feasible_fraction); - // evolutionary_strategy( - // iteration, - // stats, - // population, - // fitness - // ) + evolutionary_strategy( + iteration, + stats, + population, + ) }, |_, evaluation, best_candidate| { // Do not save enfeasible solutions! diff --git a/codes/eoa_lib/src/population.rs b/codes/eoa_lib/src/population.rs index 96cecdcda98d9acf30a463d9ddf9964b32489d2a..4367ee66f4c31784f7653cd428d9610cab8a3013 100644 --- a/codes/eoa_lib/src/population.rs +++ b/codes/eoa_lib/src/population.rs @@ -2,7 +2,7 @@ use crate::{comparison::BetterThanOperator, fitness::FitnessFunction}; #[derive(Clone, Debug)] pub struct Population { - population: Vec + pub population: Vec } #[derive(Clone, Debug)] diff --git a/codes/py_plotter/README.md b/codes/py_plotter/README.md new file mode 100644 index 0000000000000000000000000000000000000000..c93f7bc56603d2863596ed07395c6b79d2df642e --- /dev/null +++ b/codes/py_plotter/README.md @@ -0,0 +1,86 @@ +# Python Plotter for Constraint Optimization Results + +This directory contains tools for plotting results from constraint optimization experiments. + +## Files + +- `plotter.py` - Main plotting script +- `config_example.json` - Example configuration file +- `config_feasible_fraction.json` - Example config for feasible fraction plots +- `config_constraint_violation.json` - Example config for constraint violation plots +- `objectives.json` - Global configuration with optimal objective values + +## Configuration + +The JSON configuration specifies: + +- `plot_type`: Type of data to plot (`best_candidates`, `constraint_violation`, `feasible_fraction`) +- `algorithms`: List of algorithms with visualization settings + - `name`: Algorithm directory name + - `label`: Display name in legend + - `color`: Algorithm color (hex code) + - `linestyle`: Line style (`-`, `--`, `-.`, `:`) +- `instances`: List of problem instances with colors + - `name`: Instance directory name + - `label`: Display name in legend + - `color`: Instance color (hex code) +- `data_path`: Path to solutions directory +- `output_dir`: Directory for saving plots +- `plot_settings`: Figure settings (size, labels, grid, log scales, etc.) + - `log_x`: Enable logarithmic x-axis + - `log_y`: Enable logarithmic y-axis + - `show_std`: Show standard deviation bands (default: true) + - `color_combination`: How to combine algorithm and instance colors + - `"blend"`: Blend algorithm and instance colors (default) + - `"algorithm"`: Use only algorithm colors + - `"instance"`: Use only instance colors + +## Usage + +Basic usage: +```bash +python3 plotter.py config_example.json +``` + +With Guix (recommended): +```bash +guix shell python python-matplotlib python-pandas python-numpy -- python3 plotter.py config_example.json +``` + +## Data Structure + +The plotter expects data in this structure: +``` +data_path/ +├── algorithm1/ +│ ├── instance1/ +│ │ ├── best_candidates_*.csv +│ │ ├── constraint_violation/ +│ │ │ └── constraint_violations_*.csv +│ │ └── feasible_fraction/ +│ │ └── feasible_fractions_*.csv +│ └── instance2/... +└── algorithm2/... +``` + +## Features + +- Averages multiple runs automatically +- Shows standard deviation as transparent bands around mean +- Single plot with multiple instances (colored by instance) +- Different line styles for algorithms +- Logarithmic axes support for both x and y +- **Percentage deviation from optimal** for `best_candidates` plots +- Absolute values for other plot types (`constraint_violation`, `feasible_fraction`) +- Automatic legend with run counts +- High-quality SVG output + +## Percentage Deviation Calculation + +For `best_candidates` plots, the plotter automatically: +1. Loads optimal objective values from `objectives.json` +2. Validates that all values are >= optimal (raises error if better values found) +3. Calculates percentage deviation: `(current - optimal) / |optimal| * 100` +4. Plots deviation percentages so you can see convergence to 0% + +Other plot types use absolute values as normal. \ No newline at end of file diff --git a/codes/py_plotter/config_best_g09.json b/codes/py_plotter/config_best_g09.json new file mode 100644 index 0000000000000000000000000000000000000000..fdfba9f9e14e07f716f8ac47c0a695ab8ec5c35f --- /dev/null +++ b/codes/py_plotter/config_best_g09.json @@ -0,0 +1,57 @@ +{ + "plot_type": "best_candidates", + "algorithms": [ + { + "name": "srank", + "label": "S-Rank", + "color": "#1f77b4", + "linestyle": "-" + }, + { + "name": "nsga", + "label": "NSGA-II", + "color": "#ff7f0e", + "linestyle": "-" + }, + { + "name": "nsga_multi", + "label": "NSGA-II Multi", + "color": "#2ca02c", + "linestyle": "-" + }, + { + "name": "nsga_improved", + "label": "NSGA-II Improved", + "color": "#ff7f0e", + "linestyle": "--" + }, + { + "name": "nsga_constr", + "label": "NSGA-II Constr", + "color": "#ff7f0e", + "linestyle": ":" + } + ], + "instances": [ + { + "name": "g09", + "label": "G09", + "color": "#8c564b" + } + ], + "data_path": "../constr_hw02/solutions", + "output_dir": "plots", + "plot_settings": { + "figsize": [12, 8], + "xlabel": "Function evaluation", + "ylabel": "Percentage deviation from optimum", + "title": "Best Candidates Percentage Deviation - G09", + "grid": true, + "legend": true, + "alpha_fill": 0.2, + "log_x": true, + "log_y": true, + "color_combination": "algorithm", + "show_std": true + } +} \ No newline at end of file diff --git a/codes/py_plotter/config_constraint_violation.json b/codes/py_plotter/config_constraint_violation.json new file mode 100644 index 0000000000000000000000000000000000000000..9d6b6321f6dc071e352ce233e2154fecf42bf741 --- /dev/null +++ b/codes/py_plotter/config_constraint_violation.json @@ -0,0 +1,35 @@ +{ + "plot_type": "constraint_violation", + "algorithms": [ + { + "name": "nsga", + "label": "NSGA-II", + "color": "#1f77b4", + "linestyle": "-" + }, + { + "name": "nsga_multi", + "label": "NSGA-II Multi", + "color": "#ff7f0e", + "linestyle": "--" + }, + { + "name": "srank", + "label": "S-Rank", + "color": "#d62728", + "linestyle": "-." + } + ], + "instances": ["g04", "g05", "g06"], + "data_path": "../constr_hw02/solutions", + "output_dir": "plots", + "plot_settings": { + "figsize": [15, 5], + "xlabel": "Iteration", + "ylabel": "Average Constraint Violation", + "title": "Constraint Violation Over Time", + "grid": true, + "legend": true, + "alpha_fill": 0.3 + } +} \ No newline at end of file diff --git a/codes/py_plotter/config_example.json b/codes/py_plotter/config_example.json new file mode 100644 index 0000000000000000000000000000000000000000..98cde025cf518ad84e5551846e78db4fe0170570 --- /dev/null +++ b/codes/py_plotter/config_example.json @@ -0,0 +1,50 @@ +{ + "plot_type": "best_candidates", + "algorithms": [ + { + "name": "srank", + "label": "Stochastic ranking", + "color": "#1f77b4", + "linestyle": "-" + }, + { + "name": "nsga", + "label": "NSGA-II", + "color": "#1f77b4", + "linestyle": "-" + }, + { + "name": "nsga_multi", + "label": "NSGA-II Multi", + "color": "#ff7f0e", + "linestyle": "--" + }, + { + "name": "nsga_improved", + "label": "NSGA-II Improved", + "color": "#2ca02c", + "linestyle": "-." + } + ], + "instances": [ + { + "name": "g24", + "label": "G24", + "color": "#d62728" + } + ], + "data_path": "../constr_hw02/solutions", + "output_dir": "plots", + "plot_settings": { + "figsize": [12, 8], + "xlabel": "Function evaluation", + "ylabel": "Percentage Deviation from Optimal (%)", + "title": "Best Candidate Evolution - Deviation from Optimal", + "grid": true, + "legend": true, + "alpha_fill": 0.3, + "log_x": true, + "log_y": true, + "color_combination": "blend" + } +} diff --git a/codes/py_plotter/config_feasible_fraction.json b/codes/py_plotter/config_feasible_fraction.json new file mode 100644 index 0000000000000000000000000000000000000000..ebdb8a83eb43f47ef07d0892784233077cc4c6b5 --- /dev/null +++ b/codes/py_plotter/config_feasible_fraction.json @@ -0,0 +1,55 @@ +{ + "plot_type": "feasible_fraction", + "algorithms": [ + { + "name": "nsga", + "label": "NSGA-II", + "color": "#1f77b4", + "linestyle": "-" + }, + { + "name": "nsga_constr", + "label": "NSGA-II Constr", + "color": "#ff7f0e", + "linestyle": "--" + }, + { + "name": "nsga_improved", + "label": "NSGA-II Improved", + "color": "#2ca02c", + "linestyle": "-." + }, + { + "name": "srank", + "label": "S-Rank", + "color": "#d62728", + "linestyle": ":" + } + ], + "instances": [ + { + "name": "g04", + "label": "G04", + "color": "#1f77b4" + }, + { + "name": "g05", + "label": "G05", + "color": "#ff7f0e" + } + ], + "data_path": "../constr_hw02/solutions", + "output_dir": "plots", + "plot_settings": { + "figsize": [12, 6], + "xlabel": "Iteration", + "ylabel": "Feasible Fraction", + "title": "Feasible Solution Fraction Over Time", + "grid": true, + "legend": true, + "alpha_fill": 0.2, + "log_x": true, + "log_y": false, + "color_combination": "blend" + } +} \ No newline at end of file diff --git a/codes/py_plotter/config_feasible_g05.json b/codes/py_plotter/config_feasible_g05.json new file mode 100644 index 0000000000000000000000000000000000000000..532b9c4fca171e790f675760f7ccf90cf5b00a6d --- /dev/null +++ b/codes/py_plotter/config_feasible_g05.json @@ -0,0 +1,57 @@ +{ + "plot_type": "feasible_fraction", + "algorithms": [ + { + "name": "srank", + "label": "S-Rank", + "color": "#1f77b4", + "linestyle": "-" + }, + { + "name": "nsga", + "label": "NSGA-II", + "color": "#ff7f0e", + "linestyle": "-" + }, + { + "name": "nsga_multi", + "label": "NSGA-II Multi", + "color": "#2ca02c", + "linestyle": "-" + }, + { + "name": "nsga_improved", + "label": "NSGA-II Improved", + "color": "#ff7f0e", + "linestyle": "--" + }, + { + "name": "nsga_constr", + "label": "NSGA-II Constr", + "color": "#ff7f0e", + "linestyle": ":" + } + ], + "instances": [ + { + "name": "g05", + "label": "G05", + "color": "#8c564b" + } + ], + "data_path": "../constr_hw02/solutions", + "output_dir": "plots", + "plot_settings": { + "figsize": [12, 8], + "xlabel": "Function evaluation", + "ylabel": "Feasible Fraction", + "title": "Feasible Solution Fraction - G05", + "grid": true, + "legend": true, + "alpha_fill": 0.2, + "log_x": true, + "log_y": false, + "color_combination": "algorithm", + "show_std": false + } +} \ No newline at end of file diff --git a/codes/py_plotter/config_feasible_g06.json b/codes/py_plotter/config_feasible_g06.json new file mode 100644 index 0000000000000000000000000000000000000000..bf1637518d441622414b483e676d40ed74b2cb08 --- /dev/null +++ b/codes/py_plotter/config_feasible_g06.json @@ -0,0 +1,57 @@ +{ + "plot_type": "feasible_fraction", + "algorithms": [ + { + "name": "srank", + "label": "S-Rank", + "color": "#1f77b4", + "linestyle": "-" + }, + { + "name": "nsga", + "label": "NSGA-II", + "color": "#ff7f0e", + "linestyle": "-" + }, + { + "name": "nsga_multi", + "label": "NSGA-II Multi", + "color": "#2ca02c", + "linestyle": "-" + }, + { + "name": "nsga_improved", + "label": "NSGA-II Improved", + "color": "#ff7f0e", + "linestyle": "--" + }, + { + "name": "nsga_constr", + "label": "NSGA-II Constr", + "color": "#ff7f0e", + "linestyle": ":" + } + ], + "instances": [ + { + "name": "g06", + "label": "G06", + "color": "#8c564b" + } + ], + "data_path": "../constr_hw02/solutions", + "output_dir": "plots", + "plot_settings": { + "figsize": [12, 8], + "xlabel": "Function evaluation", + "ylabel": "Feasible Fraction", + "title": "Feasible Solution Fraction - G06", + "grid": true, + "legend": true, + "alpha_fill": 0.2, + "log_x": true, + "log_y": false, + "color_combination": "algorithm", + "show_std": false + } +} \ No newline at end of file diff --git a/codes/py_plotter/config_feasible_g11.json b/codes/py_plotter/config_feasible_g11.json new file mode 100644 index 0000000000000000000000000000000000000000..e5994fa1ac03e8297cbd72b6d0dcab1abeea776e --- /dev/null +++ b/codes/py_plotter/config_feasible_g11.json @@ -0,0 +1,57 @@ +{ + "plot_type": "feasible_fraction", + "algorithms": [ + { + "name": "srank", + "label": "S-Rank", + "color": "#1f77b4", + "linestyle": "-" + }, + { + "name": "nsga", + "label": "NSGA-II", + "color": "#ff7f0e", + "linestyle": "-" + }, + { + "name": "nsga_multi", + "label": "NSGA-II Multi", + "color": "#2ca02c", + "linestyle": "-" + }, + { + "name": "nsga_improved", + "label": "NSGA-II Improved", + "color": "#ff7f0e", + "linestyle": "--" + }, + { + "name": "nsga_constr", + "label": "NSGA-II Constr", + "color": "#ff7f0e", + "linestyle": ":" + } + ], + "instances": [ + { + "name": "g11", + "label": "G11", + "color": "#8c564b" + } + ], + "data_path": "../constr_hw02/solutions", + "output_dir": "plots", + "plot_settings": { + "figsize": [12, 8], + "xlabel": "Function evaluation", + "ylabel": "Feasible Fraction", + "title": "Feasible Solution Fraction - G11", + "grid": true, + "legend": true, + "alpha_fill": 0.2, + "log_x": true, + "log_y": false, + "color_combination": "algorithm", + "show_std": false + } +} \ No newline at end of file diff --git a/codes/py_plotter/config_violation_g05.json b/codes/py_plotter/config_violation_g05.json new file mode 100644 index 0000000000000000000000000000000000000000..c7a1e4e49d1ab346f60deb16e4a90427fe8f9a46 --- /dev/null +++ b/codes/py_plotter/config_violation_g05.json @@ -0,0 +1,57 @@ +{ + "plot_type": "constraint_violation", + "algorithms": [ + { + "name": "srank", + "label": "S-Rank", + "color": "#1f77b4", + "linestyle": "-" + }, + { + "name": "nsga", + "label": "NSGA-II", + "color": "#ff7f0e", + "linestyle": "-" + }, + { + "name": "nsga_multi", + "label": "NSGA-II Multi", + "color": "#2ca02c", + "linestyle": "-" + }, + { + "name": "nsga_improved", + "label": "NSGA-II Improved", + "color": "#ff7f0e", + "linestyle": "--" + }, + { + "name": "nsga_constr", + "label": "NSGA-II Constr", + "color": "#ff7f0e", + "linestyle": ":" + } + ], + "instances": [ + { + "name": "g05", + "label": "G05", + "color": "#8c564b" + } + ], + "data_path": "../constr_hw02/solutions", + "output_dir": "plots", + "plot_settings": { + "figsize": [12, 8], + "xlabel": "Function evaluation", + "ylabel": "Average Constraint Violation", + "title": "Average Constraint Violation - G05", + "grid": true, + "legend": true, + "alpha_fill": 0.2, + "log_x": true, + "log_y": true, + "color_combination": "algorithm", + "show_std": false + } +} \ No newline at end of file diff --git a/codes/py_plotter/config_violation_g06.json b/codes/py_plotter/config_violation_g06.json new file mode 100644 index 0000000000000000000000000000000000000000..fe18d9039536d90f4ae10be1df7b317501c05bbf --- /dev/null +++ b/codes/py_plotter/config_violation_g06.json @@ -0,0 +1,57 @@ +{ + "plot_type": "constraint_violation", + "algorithms": [ + { + "name": "srank", + "label": "S-Rank", + "color": "#1f77b4", + "linestyle": "-" + }, + { + "name": "nsga", + "label": "NSGA-II", + "color": "#ff7f0e", + "linestyle": "-" + }, + { + "name": "nsga_multi", + "label": "NSGA-II Multi", + "color": "#2ca02c", + "linestyle": "-" + }, + { + "name": "nsga_improved", + "label": "NSGA-II Improved", + "color": "#ff7f0e", + "linestyle": "--" + }, + { + "name": "nsga_constr", + "label": "NSGA-II Constr", + "color": "#ff7f0e", + "linestyle": ":" + } + ], + "instances": [ + { + "name": "g06", + "label": "G06", + "color": "#8c564b" + } + ], + "data_path": "../constr_hw02/solutions", + "output_dir": "plots", + "plot_settings": { + "figsize": [12, 8], + "xlabel": "Function evaluation", + "ylabel": "Average Constraint Violation", + "title": "Average Constraint Violation - G06", + "grid": true, + "legend": true, + "alpha_fill": 0.2, + "log_x": true, + "log_y": true, + "color_combination": "algorithm", + "show_std": false + } +} \ No newline at end of file diff --git a/codes/py_plotter/config_violation_g11.json b/codes/py_plotter/config_violation_g11.json new file mode 100644 index 0000000000000000000000000000000000000000..4e91dd64a04f7d291fabc3b267d512cac77ee779 --- /dev/null +++ b/codes/py_plotter/config_violation_g11.json @@ -0,0 +1,57 @@ +{ + "plot_type": "constraint_violation", + "algorithms": [ + { + "name": "srank", + "label": "S-Rank", + "color": "#1f77b4", + "linestyle": "-" + }, + { + "name": "nsga", + "label": "NSGA-II", + "color": "#ff7f0e", + "linestyle": "-" + }, + { + "name": "nsga_multi", + "label": "NSGA-II Multi", + "color": "#2ca02c", + "linestyle": "-" + }, + { + "name": "nsga_improved", + "label": "NSGA-II Improved", + "color": "#ff7f0e", + "linestyle": "--" + }, + { + "name": "nsga_constr", + "label": "NSGA-II Constr", + "color": "#ff7f0e", + "linestyle": ":" + } + ], + "instances": [ + { + "name": "g11", + "label": "G11", + "color": "#8c564b" + } + ], + "data_path": "../constr_hw02/solutions", + "output_dir": "plots", + "plot_settings": { + "figsize": [12, 8], + "xlabel": "Function evaluation", + "ylabel": "Average Constraint Violation", + "title": "Average Constraint Violation - G11", + "grid": true, + "legend": true, + "alpha_fill": 0.2, + "log_x": true, + "log_y": true, + "color_combination": "algorithm", + "show_std": false + } +} \ No newline at end of file diff --git a/codes/py_plotter/objectives.json b/codes/py_plotter/objectives.json new file mode 100644 index 0000000000000000000000000000000000000000..3434a407260665c278e8a0a8ecf4faae8b08c7ea --- /dev/null +++ b/codes/py_plotter/objectives.json @@ -0,0 +1,10 @@ +{ + "g04": -30665.53867178333, + "g05": 5126.4967140071, + "g06": -6961.8137558015, + "g08": -0.0958250414180359, + "g09": 680.6300573745, + "g11": 0.7499, + "g21": 193.724510070035, + "g24": -5.50801327159536 +} \ No newline at end of file diff --git a/codes/py_plotter/plotter.py b/codes/py_plotter/plotter.py new file mode 100644 index 0000000000000000000000000000000000000000..a713bac77490c5c7bf1bebb4cc3e59d1d27bc4fa --- /dev/null +++ b/codes/py_plotter/plotter.py @@ -0,0 +1,273 @@ +#!/usr/bin/env python3 + +import json +import pandas as pd +import matplotlib +matplotlib.use('Agg') # Use non-interactive backend +import matplotlib.pyplot as plt +import matplotlib.colors as mcolors +import numpy as np +from pathlib import Path +import glob +import argparse + +class ConstraintOptimizationPlotter: + def __init__(self, config_path): + with open(config_path, 'r') as f: + self.config = json.load(f) + + self.data_path = Path(self.config['data_path']) + self.output_dir = Path(self.config['output_dir']) + self.output_dir.mkdir(exist_ok=True) + + # Load objectives for percentage deviation calculation + objectives_path = Path(__file__).parent / 'objectives.json' + with open(objectives_path, 'r') as f: + self.objectives = json.load(f) + + def get_csv_pattern(self, plot_type): + patterns = { + 'best_candidates': 'best_candidates_*.csv', + 'constraint_violation': 'constraint_violation/constraint_violations_*.csv', + 'feasible_fraction': 'feasible_fraction/feasible_fractions_*.csv' + } + return patterns.get(plot_type, 'best_candidates_*.csv') + + def get_value_column(self, plot_type): + columns = { + 'best_candidates': 'evaluation', # Changed from 'fitness' to 'evaluation' + 'constraint_violation': 'avg_constraint_violation', + 'feasible_fraction': 'feasible_fraction' + } + return columns.get(plot_type, 'evaluation') + + def blend_colors(self, color1, color2, alpha=0.5): + """Blend two colors together""" + c1 = mcolors.to_rgb(color1) + c2 = mcolors.to_rgb(color2) + blended = tuple(alpha * c1[i] + (1 - alpha) * c2[i] for i in range(3)) + return mcolors.to_hex(blended) + + def get_combined_color(self, algorithm_color, instance_color, combination_method="blend"): + """Combine algorithm and instance colors based on the specified method""" + if combination_method == "blend": + return self.blend_colors(algorithm_color, instance_color, alpha=0.6) + elif combination_method == "algorithm": + return algorithm_color + elif combination_method == "instance": + return instance_color + else: + return self.blend_colors(algorithm_color, instance_color, alpha=0.5) + + def calculate_percentage_deviation(self, values, instance_name): + """Calculate percentage deviation from optimal value for best_candidates plots""" + if self.config['plot_type'] != 'best_candidates' or instance_name not in self.objectives: + return values + + optimal_value = self.objectives[instance_name] + + # Check if any values are significantly better than the known optimum + # Allow small tolerance for numerical precision + tolerance = 1e-4 * np.abs(optimal_value) + significantly_better = values < (optimal_value - tolerance) + # print(values - optimal_value) + + if np.any(significantly_better): + better_indices = np.where(significantly_better)[0] + best_found = np.min(values[better_indices]) + improvement = optimal_value - best_found + improvement_pct = improvement / np.abs(optimal_value) * 100 + + print(f"WARNING: Found {np.sum(significantly_better)} values better than known optimum for {instance_name}!") + print(f"Known optimum: {optimal_value}") + print(f"Best found: {best_found}") + print(f"Improvement: {improvement} ({improvement_pct:.3f}%)") + print(f"Using best found value as new reference point.") + + # Update the optimal value to the best found for this calculation + optimal_value = best_found + + + new_optimal_value = optimal_value + if optimal_value < 0: + new_optimal_value = - optimal_value + values = values + 2*new_optimal_value + + # Calculate percentage deviation: (current - optimal) / |optimal| * 100 + # This will always be >= 0 since current >= optimal + percentage_deviations = (values - new_optimal_value) / new_optimal_value * 100 + return percentage_deviations + + def load_data_for_algorithm_instance(self, algorithm, instance): + csv_pattern = self.get_csv_pattern(self.config['plot_type']) + + algorithm_path = self.data_path / algorithm / instance + csv_files = list(algorithm_path.glob(csv_pattern)) + + if not csv_files: + print(f"Warning: No CSV files found for {algorithm}/{instance}") + return None + + print(f"Found {len(csv_files)} files for {algorithm}/{instance}") + + value_col = self.get_value_column(self.config['plot_type']) + all_data = [] + + for csv_file in csv_files: + try: + df = pd.read_csv(csv_file) + if 'iteration' in df.columns and value_col in df.columns: + all_data.append(df[['iteration', value_col]].copy()) + except Exception as e: + print(f"Error reading {csv_file}: {e}") + + if not all_data: + return None + + # Find the maximum function evaluation (iteration value) across all runs + max_evaluation = max(df['iteration'].max() for df in all_data) + + # Create a common evaluation grid - collect all unique evaluation points + all_evaluations = set() + for df in all_data: + all_evaluations.update(df['iteration'].tolist()) + all_evaluations.add(max_evaluation) # Ensure max is included + common_grid = sorted(list(all_evaluations)) + + aligned_data = [] + + for df in all_data: + df_copy = df.copy().sort_values('iteration').reset_index(drop=True) + + # Interpolate/extend this run to the common grid + aligned_values = [] + current_value = df_copy[value_col].iloc[0] if len(df_copy) > 0 else 0 + df_idx = 0 + + for eval_point in common_grid: + # Update current_value if we have data at this evaluation point or before + while df_idx < len(df_copy) and df_copy['iteration'].iloc[df_idx] <= eval_point: + current_value = df_copy[value_col].iloc[df_idx] + df_idx += 1 + + aligned_values.append(current_value) + + aligned_data.append(aligned_values) + + # All runs now have the same length and evaluation points + values_matrix = np.column_stack(aligned_data) + values_matrix = values_matrix.astype(np.float64) + iterations = np.array(common_grid) + + # Apply percentage deviation calculation for best_candidates + if self.config['plot_type'] == 'best_candidates': + # Apply percentage deviation to each column (run) separately + for i in range(values_matrix.shape[1]): + values_matrix[:, i] = self.calculate_percentage_deviation(values_matrix[:, i], instance) + + mean_values = np.mean(values_matrix, axis=1) + std_values = np.std(values_matrix, axis=1) + + return { + 'iterations': iterations, + 'mean': mean_values, + 'std': std_values, + 'runs': len(all_data) + } + + def create_plot(self): + fig, ax = plt.subplots(1, 1, figsize=self.config['plot_settings']['figsize']) + + combination_method = self.config['plot_settings'].get('color_combination', 'blend') + + for instance in self.config['instances']: + instance_name = instance['name'] if isinstance(instance, dict) else instance + instance_label = instance.get('label', instance_name) if isinstance(instance, dict) else instance_name + instance_color = instance.get('color', '#000000') if isinstance(instance, dict) else '#000000' + + for algorithm in self.config['algorithms']: + alg_name = algorithm['name'] + alg_label = algorithm['label'] + alg_color = algorithm.get('color', '#000000') + linestyle = algorithm['linestyle'] + + # Get combined color + combined_color = self.get_combined_color(alg_color, instance_color, combination_method) + + data = self.load_data_for_algorithm_instance(alg_name, instance_name) + + if data is None: + print(f"No data found for {alg_name}/{instance_name}") + continue + + # Handle data values (percentage deviation for best_candidates, absolute for others) + mean_values = data['mean'] + std_values = data['std'] + + # For log scale with negative values (non-best_candidates plots), take absolute value + if self.config['plot_settings'].get('log_y', False) and self.config['plot_type'] != 'best_candidates': + mean_values = np.abs(mean_values) + + ax.plot(data['iterations'], mean_values, + color=combined_color, + linestyle=linestyle, + label=f"{alg_label} - {instance_label}", + linewidth=2, + drawstyle='steps-post') + + # Add fill_between for standard deviation bands (if enabled) + if self.config['plot_settings'].get('show_std', True): + lower_bound = mean_values - std_values + upper_bound = mean_values + std_values + + # For log scale, ensure positive values + if self.config['plot_settings'].get('log_y', False): + lower_bound = np.maximum(lower_bound, 0.01) + + ax.fill_between(data['iterations'], + lower_bound, + upper_bound, + color=combined_color, + alpha=self.config['plot_settings']['alpha_fill']) + + ax.set_xlabel(self.config['plot_settings']['xlabel'], fontsize=14) + ax.set_ylabel(self.config['plot_settings']['ylabel'], fontsize=14) + ax.set_title(self.config['plot_settings']['title'], fontsize=16) + + # Set log scale if requested + if self.config['plot_settings'].get('log_x', False): + ax.set_xscale('log') + if self.config['plot_settings'].get('log_y', False): + ax.set_yscale('log') + + # Format y-axis with percentages for best_candidates plots + if self.config['plot_type'] == 'best_candidates': + ax.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, p: f'{x:.1f}%')) + + # Set tick label font sizes + ax.tick_params(axis='both', which='major', labelsize=12) + ax.tick_params(axis='both', which='minor', labelsize=10) + + if self.config['plot_settings']['grid']: + ax.grid(True, alpha=0.3) + + if self.config['plot_settings']['legend']: + ax.legend() + + plt.tight_layout() + + instance_names = [inst['name'] if isinstance(inst, dict) else inst for inst in self.config['instances']] + output_file = self.output_dir / f"{self.config['plot_type']}_{'_'.join(instance_names)}.svg" + plt.savefig(output_file, format='svg', bbox_inches='tight') + print(f"Plot saved to: {output_file}") + +def main(): + parser = argparse.ArgumentParser(description='Plot constraint optimization results') + parser.add_argument('config', help='Path to JSON configuration file') + args = parser.parse_args() + + plotter = ConstraintOptimizationPlotter(args.config) + plotter.create_plot() + +if __name__ == '__main__': + main()