~ruther/ctu-fee-eoa

48d0912f632e44005cedd27f13de6f4925e3312a — Rutherther 5 days ago a5d7436
feat: add a simple python plotter
M codes/constr_hw02/src/main.rs => codes/constr_hw02/src/main.rs +41 -26
@@ 42,23 42,22 @@ pub fn solve_with_stochastic_ranking<const DIM: usize, const CONSTRAINTS: usize>
    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::<nalgebra::Const<2>, 2, _>::new(
    //     ArithmeticCrossover::new(),
    //     problem.bounds.0,
    //     problem.bounds.1,
    //     BoundedCrossoverStrategy::Retry(5)
    // );
    let crossover = ArithmeticCrossover::new();
    let mut crossover = BoundedCrossover::<nalgebra::Const<DIM>, 2, _>::new(
        crossover,
        problem.bounds.0,
        problem.bounds.1,
        BoundedCrossoverStrategy::Retry(5)
    );

    // Setup bounded random distribution perturbation with Normal distribution
    let normal_perturbation = RandomDistributionPerturbation::<DIM, Normal<f64>>::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<const DIM: usize, const CONSTRAINTS: usize>
    let constraint_refs = problem.constraints.iter().collect::<Vec<_>>().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<const DIM: usize, const CONSTRAINTS: usize>
        &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::<f64>() / 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<const DIM: usize, const CONSTRAINTS: usize>(
    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<dyn std::error::Error>> {
    let problem = problem_g06();


@@ 617,7 625,7 @@ fn handle_g11_srank() -> Result<(), Box<dyn std::error::Error>> {
        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<dyn std::error::Error>> {
        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<dyn std::error::Error>> {
        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<const DIM: usize, const CONSTRAINTS: usize, TEval: std
    feasible_fractions: &[f64],
    avg_constraint_violations: &[f64],
) -> Result<(), Box<dyn std::error::Error>> {

    // 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();

M codes/constr_hw02/src/problems.rs => codes/constr_hw02/src/problems.rs +4 -4
@@ 165,8 165,8 @@ pub fn problem_g06() -> ConstrainedProblem<2, 2> {
                ),
            ],
            bounds: (
                SVector::<f64, 2>::new(0.0, 0.0),   // min bounds
                SVector::<f64, 2>::new(50.0, 50.0), // max bounds
                SVector::<f64, 2>::new(13.0, 0.0),   // min bounds
                SVector::<f64, 2>::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::<f64, 2>::new(-50.0, -50.0), // min bounds
                SVector::<f64, 2>::new(50.0, 50.0),   // max bounds
                SVector::<f64, 2>::new(-1.0, -1.0), // min bounds
                SVector::<f64, 2>::new(1.0, 1.0),   // max bounds
            ),
            optimal_value: 0.7499, // Best known optimum
            instantiate_fn: None,

M codes/eoa_lib/src/constraints.rs => codes/eoa_lib/src/constraints.rs +22 -22
@@ 240,11 240,11 @@ pub fn evolve_constraint_penalty_weight_tau_target

#[derive(PartialEq, Clone, Debug)]
pub struct ConstrainedEvaluation<const CONSTRAINTS: usize, TOut> {
    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<const CONSTRAINTS: usize, TOut: PartialOrd> PartialOrd for ConstrainedEvaluation<CONSTRAINTS, TOut> {


@@ 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<TChromosome, TResult>,
        //     &EvaluatedPopulation<TChromosome, TResult>,

        //     &mut TPairing,
        //     &mut TCrossover,
        //     &mut TPerturbation,
        //     &mut TReplacement,
        //     &mut ConstrainedEvalFitness<CONSTRAINTS, TChromosome, TResult, TFitness, TConstraint>,
        // ),
        mut evolutionary_strategy: impl FnMut(
            usize,
            &EvolutionStats<TChromosome, ConstrainedEvaluation<CONSTRAINTS, TResult>>,
            &EvaluatedPopulation<TChromosome, ConstrainedEvaluation<CONSTRAINTS, TResult>>,

            // &mut TFitness,
            // &mut TPairing,
            // &mut TCrossover,
            // &mut TPerturbation,
            // &mut TReplacement,
            // &mut ConstrainedEvalFitness<CONSTRAINTS, TChromosome, TResult, TFitness, TConstraint>,
        ),
    ) -> Result<(EvolutionResult<TChromosome, TResult>, Vec<f64>), Box<dyn Error>>
{
    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!

M codes/eoa_lib/src/population.rs => codes/eoa_lib/src/population.rs +1 -1
@@ 2,7 2,7 @@ use crate::{comparison::BetterThanOperator, fitness::FitnessFunction};

#[derive(Clone, Debug)]
pub struct Population<TChromosome> {
    population: Vec<TChromosome>
    pub population: Vec<TChromosome>
}

#[derive(Clone, Debug)]

A codes/py_plotter/README.md => codes/py_plotter/README.md +86 -0
@@ 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

A codes/py_plotter/config_best_g09.json => codes/py_plotter/config_best_g09.json +57 -0
@@ 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

A codes/py_plotter/config_constraint_violation.json => codes/py_plotter/config_constraint_violation.json +35 -0
@@ 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

A codes/py_plotter/config_example.json => codes/py_plotter/config_example.json +50 -0
@@ 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"
  }
}

A codes/py_plotter/config_feasible_fraction.json => codes/py_plotter/config_feasible_fraction.json +55 -0
@@ 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

A codes/py_plotter/config_feasible_g05.json => codes/py_plotter/config_feasible_g05.json +57 -0
@@ 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

A codes/py_plotter/config_feasible_g06.json => codes/py_plotter/config_feasible_g06.json +57 -0
@@ 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

A codes/py_plotter/config_feasible_g11.json => codes/py_plotter/config_feasible_g11.json +57 -0
@@ 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

A codes/py_plotter/config_violation_g05.json => codes/py_plotter/config_violation_g05.json +57 -0
@@ 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

A codes/py_plotter/config_violation_g06.json => codes/py_plotter/config_violation_g06.json +57 -0
@@ 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

A codes/py_plotter/config_violation_g11.json => codes/py_plotter/config_violation_g11.json +57 -0
@@ 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

A codes/py_plotter/objectives.json => codes/py_plotter/objectives.json +10 -0
@@ 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

A codes/py_plotter/plotter.py => codes/py_plotter/plotter.py +273 -0
@@ 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()