Skip to content

Commit

Permalink
Merge pull request #27 from s-simoncelli/add-adaptive-nsga3
Browse files Browse the repository at this point in the history
Add adaptive nsga3
  • Loading branch information
s-simoncelli committed Sep 1, 2024
2 parents ff40d84 + 57a4270 commit 59d98f5
Show file tree
Hide file tree
Showing 32 changed files with 5,332 additions and 61 deletions.
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# Changelog

## 1.0.0

- Added new Python API to generate reference points with `DasDarren1998`. The new class
allows getting the weights for the `NSGA3` algorithm and plotting them. See the Python
type hints for name and description of the new class methods.
- Added `AdaptiveNSGA3` to use the adaptive approach to handle the reference points. This
implements the new algorithm from Jain and Deb (2014) (doi.org/10.1109/TEVC.2013.2281534)
to handle problems where not all reference points intersect the optimal Pareto front. This
helps to reduce crowding and enhance the solution quality. See the
new [example file](./examples/nsga3_inverted_dtlz1.rs)
and [results](./examples/results/DTLZ1_3obj_Adaptive_NSGA3_gen400_obj_vs_ref_points.png).
- The algorithm additional data are now exported in `AlgorithExport` in the `Export additional_data in AlgorithmExport`
field. This contains, for example, the reference points for `NSGA3`.

## 0.6.0

- Removed crate `hv-wfg-sys`. The hyper-volume from `HyperVolumeWhile2012` is now calculated
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "optirustic"
version = "0.6.0"
version = "1.0.0"
authors = ["Stefano Simoncelli <16114781+s-simoncelli@users.noreply.github.com>"]
edition = "2021"
rust-version = "1.80"
Expand Down
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ evolutionary algorithms (MOEAs). It allows you to:
- export the population history as JSON and resume its evolution from file
- generate charts with the dedicated [Python package](https://pypi.org/project/optirustic/)

At the moment, it comes with the `NSGA2` and `NSGA3` algorithms.
The library comes with the following
algorithms: [`NSGA2`](https://docs.rs/optirustic/latest/optirustic/algorithms/struct.NSGA2.html),
[`NSGA3`](https://docs.rs/optirustic/latest/optirustic/algorithms/struct.NSGA3.html) and
[`AdaptiveNSGA3`](https://docs.rs/optirustic/latest/optirustic/algorithms/struct.AdaptiveNSGA3.html).

The API documentation is available on [docs.rs](https://docs.rs/optirustic/).
Examples showcasing this library's features are available in
Expand Down Expand Up @@ -160,6 +163,18 @@ and these are the plotted solutions:

<p align="right">(<a href="#optirustic">back to top</a>)</p>

### Plotting and inspecting data

With the library, you can set
the [`export_history`](https://docs.rs/optirustic/latest/optirustic/algorithms/struct.NSGA2Arg.html#structfield.export_history)
option, to export serialised results as JSON files as the algorithm evolves, or
call [`save_to_json`](https://docs.rs/optirustic/latest/optirustic/algorithms/trait.Algorithm.html#method.save_to_json)
to export the results at the last population evolution.

This crate comes with a companion [Python package](./optirustic-py) to inspect the results
and easily plot the Pareto front or the algorithm convergence. This is how all the charts within
this README file were generated. Have a look at the `py` file in the [example folder](./examples).

# License

This project is licensed under the terms of the MIT license.
2 changes: 1 addition & 1 deletion examples/convergence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use optirustic::metrics::HyperVolume;
///
/// Make sure to compile this in release mode to speed up the calculation:
///
/// `cargo run --example convergence --release`
/// `cargo run --example convergence -p optirustic --release`
fn main() -> Result<(), OError> {
let problem = SCHProblem::create()?;
let out_path = PathBuf::from(&env::current_dir().expect("Cannot fetch current directory"))
Expand Down
2 changes: 1 addition & 1 deletion examples/nsga2_sch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use optirustic::core::builtin_problems::SCHProblem;
///
/// Make sure to compile this in release mode to speed up the calculation:
///
/// `cargo run --example nsga2 --release`
/// `cargo run --example nsga2 -p optirustic --release`
fn main() -> Result<(), Box<dyn Error>> {
// Add log
env_logger::builder().filter_level(LevelFilter::Info).init();
Expand Down
2 changes: 1 addition & 1 deletion examples/nsga2_zdt1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use optirustic::core::builtin_problems::ZTD1Problem;
///
/// Make sure to compile this in release mode to speed up the calculation:
///
/// `cargo run --example nsga2_zdt1 --release`
/// `cargo run --example nsga2_zdt1 -p optirustic --release`
fn main() -> Result<(), Box<dyn Error>> {
// Add log
env_logger::builder().filter_level(LevelFilter::Info).init();
Expand Down
6 changes: 3 additions & 3 deletions examples/nsga3_dtlz1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use optirustic::utils::{DasDarren1998, NumberOfPartitions};
///
/// Make sure to compile this in release mode to speed up the calculation:
///
/// `cargo run --example nsga3_dtlz1 --release`
/// `cargo run --example nsga3_dtlz1 -p optirustic --release`
fn main() -> Result<(), Box<dyn Error>> {
// Add log
env_logger::builder().filter_level(LevelFilter::Info).init();
Expand All @@ -28,7 +28,7 @@ fn main() -> Result<(), Box<dyn Error>> {
let k: usize = 5;
let number_variables: usize = number_objectives + k - 1;
// Get the built-in problem
let problem = DTLZ1Problem::create(number_variables, number_objectives)?;
let problem = DTLZ1Problem::create(number_variables, number_objectives, false)?;

// Set the number of partitions to create the reference points for the NSGA3 algorithm. This
// uses one layer of 12 uniform gaps
Expand Down Expand Up @@ -64,7 +64,7 @@ fn main() -> Result<(), Box<dyn Error>> {
};

// Initialise the algorithm
let mut algo = NSGA3::new(problem, args).unwrap();
let mut algo = NSGA3::new(problem, args, false).unwrap();

// Run the algorithm
algo.run()?;
Expand Down
6 changes: 3 additions & 3 deletions examples/nsga3_dtlz1_8obj.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use optirustic::utils::{NumberOfPartitions, TwoLayerPartitions};
///
/// Make sure to compile this in release mode to speed up the calculation:
///
/// `cargo run --example nsga3_dtlz1_8obj --release`
/// `cargo run --example nsga3_dtlz1_8obj -p optirustic --release`
fn main() -> Result<(), Box<dyn Error>> {
// Add log
env_logger::builder().filter_level(LevelFilter::Info).init();
Expand All @@ -24,7 +24,7 @@ fn main() -> Result<(), Box<dyn Error>> {
let number_objectives = 8;
let k: usize = 5;
let number_variables: usize = number_objectives + k - 1; // M + k - 1 with k = 5 (Section Va)
let problem = DTLZ1Problem::create(number_variables, number_objectives)?;
let problem = DTLZ1Problem::create(number_variables, number_objectives, false)?;
// The number of partitions used in the paper when from section 5
let number_of_partitions = NumberOfPartitions::TwoLayers(TwoLayerPartitions {
boundary_layer: 3,
Expand Down Expand Up @@ -54,7 +54,7 @@ fn main() -> Result<(), Box<dyn Error>> {
};

// Initialise the algorithm
let mut algo = NSGA3::new(problem, args).unwrap();
let mut algo = NSGA3::new(problem, args, false).unwrap();

// Run the algorithm
algo.run()?;
Expand Down
File renamed without changes.
File renamed without changes.
4 changes: 2 additions & 2 deletions examples/nsga3_dtlz2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use optirustic::utils::{DasDarren1998, NumberOfPartitions};
///
/// Make sure to compile this in release mode to speed up the calculation:
///
/// `cargo run --example nsga3_dtlz2 --release`
/// `cargo run --example nsga3_dtlz2 -p optirustic --release`
fn main() -> Result<(), Box<dyn Error>> {
// Add log
env_logger::builder().filter_level(LevelFilter::Info).init();
Expand Down Expand Up @@ -62,7 +62,7 @@ fn main() -> Result<(), Box<dyn Error>> {
};

// Initialise the algorithm
let mut algo = NSGA3::new(problem, args).unwrap();
let mut algo = NSGA3::new(problem, args, false).unwrap();

// Run the algorithm
algo.run()?;
Expand Down
74 changes: 74 additions & 0 deletions examples/nsga3_inverted_dtlz1.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
use std::env;
use std::error::Error;
use std::path::PathBuf;

use log::LevelFilter;

use optirustic::algorithms::{
AdaptiveNSGA3, Algorithm, MaxGeneration, NSGA3Arg, Nsga3NumberOfIndividuals,
StoppingConditionType,
};
use optirustic::core::builtin_problems::DTLZ1Problem;
use optirustic::operators::SimulatedBinaryCrossoverArgs;
use optirustic::utils::NumberOfPartitions;

/// Solve the inverted DTLZ1 problem from Deb et al. (2013) with 3 objectives. This is a problem where the
/// optimal solutions or objectives lie on the hyper-plane passing through the intercept point
/// at 0.5 on each objective axis. This code replicates the first testing problem in Deb et al.
/// (2013).
///
/// Make sure to compile this in release mode to speed up the calculation:
///
/// `cargo run --example nsga3_inverted_dtlz1 -p optirustic --release`
fn main() -> Result<(), Box<dyn Error>> {
// Add log
env_logger::builder().filter_level(LevelFilter::Info).init();

let number_objectives: usize = 3;
let k: usize = 5;
// Set the number of variables to use in the DTLZ1 problem
let number_variables: usize = number_objectives + k - 1;
// Get the built-in problem
let problem = DTLZ1Problem::create(number_variables, number_objectives, true)?;

// Set the number of partitions to create the reference points for the NSGA3 algorithm. This
// uses one layer of 12 uniform gaps
let number_of_partitions = NumberOfPartitions::OneLayer(12);

// Customise the SBX and PM operators like in the paper
let crossover_operator_options = SimulatedBinaryCrossoverArgs {
distribution_index: 30.0,
crossover_probability: 1.0,
..SimulatedBinaryCrossoverArgs::default()
};

// Set up the adaptive NSGA3 algorithm
let args = NSGA3Arg {
// number of individuals from the paper (possibly equal to number of reference points)
number_of_individuals: Nsga3NumberOfIndividuals::Custom(92),
number_of_partitions,
crossover_operator_options: Some(crossover_operator_options),
mutation_operator_options: None,
// stop at generation 400
stopping_condition: StoppingConditionType::MaxGeneration(MaxGeneration(400)),
parallel: None,
export_history: None,
// to reproduce results
seed: Some(1),
};

// Initialise the algorithm
let mut algo = AdaptiveNSGA3::new(problem, args).unwrap();

// Run the algorithm
algo.run()?;

// Export the last results to a JSON file
let destination = PathBuf::from(&env::current_dir().unwrap())
.join("examples")
.join("results");

algo.save_to_json(&destination, Some("DTLZ1_3obj_Adaptive"))?;

Ok(())
}
68 changes: 68 additions & 0 deletions examples/nsga3_inverted_dtlz1_plot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
from pathlib import Path

import numpy as np
from matplotlib import pyplot as plt
from optirustic import NSGA3

# Generate a 3D Pareto front charts and objective vs. reference point charts
file = Path(__file__).parent / "results" / "DTLZ1_3obj_Adaptive_NSGA3_gen400.json"
data = NSGA3(file.as_posix())

# Generate Pareto front chart
data.plot()
plt.gca().view_init(
azim=60,
elev=20,
)
plt.savefig(file.parent / f"{file.stem}_Pareto_front.png")

# Generate a chart with the normalised objectives against the reference points
# The plane is limited in the [0, 1] range.
normalised_objectives = [ind.data["normalised_objectives"] for ind in data.individuals]
normalised_objectives = np.array(normalised_objectives)
obj_names = data.problem.objective_names

ref_points = data.additional_data["reference_points"]
ref_points = np.array(ref_points)

fig = plt.figure()
ax = plt.axes(projection="3d")

ax.scatter(
ref_points[:91, 0],
ref_points[:91, 1],
ref_points[:91, 2],
color="r",
marker="x",
s=20,
label="Original reference points",
)
ax.scatter(
ref_points[91:, 0],
ref_points[91:, 1],
ref_points[91:, 2],
color="b",
marker="x",
s=20,
label="Adaptive reference points",
)
ax.scatter(
normalised_objectives[:, 0],
normalised_objectives[:, 1],
normalised_objectives[:, 2],
color="k",
marker=".",
label="Normalised objectives",
)

ax.set_xlabel(obj_names[0])
ax.set_ylabel(obj_names[1])
ax.set_zlabel(obj_names[2])
ax.view_init(azim=10)

plt.legend()
plt.title(
f"Normalised objectives vs. reference points \n"
f"for {data.algorithm} @ generation={data.generation}"
)
plt.savefig(file.parent / f"{file.stem}_obj_vs_ref_points.png")
Loading

0 comments on commit 59d98f5

Please sign in to comment.