Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add adaptive nsga3 #27

Merged
merged 14 commits into from
Sep 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading