DAS-CMOP9
The problem
Fan et al. (2020) built a toolkit of constrained test problems whose difficulty is set by a triplet (η, ζ, γ) in [0, 1]³: η for diversity (type I constraints, which cut the front into pieces), ζ for feasibility (a type II constraint, a band of distances from the unconstrained front) and γ for convergence (type III constraints, infeasible regions in the objective space). DAS-CMOP7 to DAS-CMOP9 have three objectives; see DAS-CMOP1 for the two-objective problems.
DAS-CMOP9 (table 2) has three objectives over 30 variables in [0, 1]:
f₁ = cos(0.5πx₁) cos(0.5πx₂) + g
f₂ = cos(0.5πx₁) sin(0.5πx₂) + g
f₃ = sin(0.5πx₁) + g
g = Σⱼ₌₃³⁰ (xⱼ − cos(0.25jπ(x₁ + x₂)/30))²
subject to
sin(20πx₁) − b ≥ 0, cos(20πx₂) − b ≥ 0, b = 2η − 1 (type I)
(e − g)(g − 0.5) ≥ 0, e = 0.5 − ln ζ (type II)
Σⱼ fⱼ² − fₖ² + (fₖ − 1)² − r² ≥ 0, k = 1, 2, 3 (type III)
Σⱼ (fⱼ − 1/√3)² − r² ≥ 0, r = γ/2 (type III)
All three are minimized. At g = 0 the unconstrained front is the unit sphere's octant. The distance function g is 0 where each xⱼ is its own cosine of x₁ + x₂, different for each j: the variables are linked to the position variables. The example uses the triplet with which the paper's figure 6 plots DAS-CMOP9, (0.5, 0.5, 0.5): b = 0, which keeps x₁ in [0, 0.05], [0.1, 0.15], … and x₂ in [0, 0.025], [0.075, 0.125], …; g between 0.5 and 0.5 + ln 2 ≈ 1.193; and spheres of radius 0.25.
The front depends on the triplet, and the paper samples it; genoxide samples it in the same way, as the first feasible point of each ray α(x₁, x₂) + g (1, 1, 1) from the 41,905 points of Das and Dennis's method with 288 divisions, non-dominated. With this triplet it is DAS-CMOP8's: patches of the sphere of radius 1 centered at (0.5, 0.5, 0.5) where x₁ and x₂ are both in the type I intervals. Its ideal point is (0.5, 0.5, 0.5) and its nadir point (1.5, 1.5, 1.4969) (1.4969 for f₃, where x₁ is at most 0.95).
What makes it hard
The linked variables. A solution on the front at (x₁, x₂) has every other variable at its own cosine of x₁ + x₂: to move along the front, all 28 must move together, each by its own amount. Simulated binary crossover and polynomial mutation change each variable on its own, so a population that has settled on some values of x₁ + x₂ keeps them, and covers only part of the front's patches. Differential evolution moves them together, as its steps are differences between solutions of the population: the paper's MOEA/D-CDP uses it, and so does MOEA/D-DE here. Then, as for DAS-CMOP8, the band of feasible g and the grid of patches.
Representation
A Real genome of 30 genes in [0, 1]. The problem is genoxide's DasCmop9, whose fitness is the three objectives and the total constraint violation, 0 when it is feasible; in Python, gx.problems.DasCmop9(), with difficulty=(η, ζ, γ) or the number of one of the paper's sixteen triplets. Solutions compare by constrained dominance: a feasible solution beats an infeasible one, of two infeasible ones the smaller violation wins, and of two feasible ones Pareto dominance decides.
Algorithm
Two runs, each with the paper's population of 300 for 1,000 generations, 300,000 evaluations (section 7.1), and polynomial mutation with η = 20 at a rate of 1/30 per gene:
- MOEA/D with differential evolution, MOEA/D-DE (Li and Zhang, 2009, IEEE Transactions on Evolutionary Computation 13(2): 284-302): a subproblem for each of the 300 weight vectors of Das and Dennis's method with 23 divisions, each the weighted Tchebycheff distance to the ideal point, with the paper's 30 neighbours (0.1 N) and at most 2 replacements per child; a child is its subproblem's solution moved by F = 0.5 times the difference of two parents, every gene (CR = 1), the parents from the neighbourhood with probability δ = 0.2, where the paper and Li and Zhang use 0.9 (DAS-CMOP1's page says why);
- NSGA-II (Deb, Pratap, Agarwal and Meyarivan, 2002, IEEE Transactions on Evolutionary Computation 6(2): 182-197) with the paper's settings, simulated binary crossover with η = 20 at a rate of 0.9, as the contrast.
Both compare solutions by constrained dominance.
Output
A line per run: the size of its final front, how many of its solutions the problem finds feasible, their IGD+ and hypervolume, the hypervolume as a share of that of a sample of the optimal front with at least as many points as the population, and the median and largest distance from its solutions to the nearest point of a dense sample of the front (44,643 points). Then a line for what MOEA/D's decomposition can reach: for each of its 300 weight vectors, the point of the dense sample with the least Tchebycheff value, the best that subproblem can have, and the same indicators for these points; and the same with 666 weight vectors (35 divisions). Then the hypervolumes of the whole front and of the sample.
The indicators use the objectives normalized by the front's ideal and nadir points, so that the front spans [0, 1] in each. IGD+ (Ishibuchi et al., 2015, EMO 2015, LNCS 9019: 110-125) averages, over 2,000 points of the optimal front from genoxide's optimal_front, the distance to the nearest feasible solution of the found front, counting only the objectives in which the solution is worse. Smaller is better; a front of as many points as the population can't cover the 2,000 exactly, and the sample shows what it can. The hypervolume (Zitzler and Thiele, 1999, IEEE Transactions on Evolutionary Computation 3(4): 257-271) is the volume the feasible solutions dominate up to the reference point (1.1, 1.1, 1.1). Larger is better; the whole front's is computed from 3,000 of its points, and the sample's is what a front of that many points reaches. In Python, run evaluates the problem in Rust, so both versions print the same.
The page plays the runs back side by side, each population as the problem scores it: its feasible non-dominated solutions, its infeasible ones (hollow, at most 100 a frame), and the optimal front, sampled.
The project page plays these runs back.
Good results
The target: every solution feasible, and 99% of the hypervolume of the sample of 346 points of the front, about what a front of 300 solutions spread like it has.
MOEA/D-DE's run with seed 1 ends with 196 solutions, 196 feasible, IGD+ 0.0158, hypervolume 0.7484, 97.9% of the sample's; half of its solutions are within 0.0018 of the front, and the farthest 0.0200 away. NSGA-II's ends with 300 solutions, 300 feasible, IGD+ 0.2006, hypervolume 0.4880, 63.8% of the sample's: as close to the front (a median of 0.0017), but on only part of it. Over seeds 1 to 20, MOEA/D-DE ends with IGD+ from 0.0151 to 0.0167 and 97.3% to 98.0% of the sample's hypervolume; half of each run's solutions within 0.0018 to 0.0023 of the front, 89% to 97% of them within 0.01, and the farthest 0.014 to 0.047 away. NSGA-II ends with IGD+ from 0.0158 to 0.3408 and 45% to 97% of the sample's hypervolume. With δ = 0.9, MOEA/D-DE ends with IGD+ from 0.0152 to 0.5144 and 20% to 98%.
MOEA/D-DE reaches the front, and spreads on it as well as its decomposition allows, but no run reaches the 99% target, because the target doesn't fit 300 Tchebycheff subproblems. Each subproblem can at best hold the front's point with its least Tchebycheff value. Those best points, one per weight vector, are only 223 distinct points, as several weight vectors share one on the patched front. Together they have IGD+ 0.0184 and 96.7% of the sample's hypervolume: every run of MOEA/D-DE does better on both. With 666 weight vectors, the best points reach 99.4%: the target asks for a front spread evenly by length, as the sample is, which the 300 subproblems can't give. The few solutions farther from the front than 0.01 haven't converged yet: with seed 1, the farthest is dominated by 81 points of the dense sample.
The paper's NSGA-II-CDP ends with a mean IGD of 0.227 on DAS-CMOP9 with this triplet (its table 5, number 8), and MOEA/D-CDP, with differential evolution, with 0.0767.
Known optimum: for the difficulty triplet (0.5, 0.5, 0.5), DAS-CMOP8's, patches of the unit sphere's octant moved to (0.5, 0.5, 0.5); ideal point (0.5, 0.5, 0.5), nadir point (1.5, 1.5, 1.4969); hypervolume 0.7853 (normalized objectives, reference point (1.1, 1.1, 1.1))
Source: examples/das_cmop9
Interactive run: tachsin.gr/projects/genoxide/examples/das-cmop9
cargo run --release --example das_cmop9
//! DAS-CMOP9: minimize three objectives over 30 variables subject to 7 constraints, whose front is
//! patches of a sphere, with linked variables, with MOEA/D-DE and NSGA-II.
//!
//! From genoxide's `multi::problems::DasCmop9`. Runs MOEA/D with differential evolution
//! (MOEA/D-DE) and, as a contrast, NSGA-II with the paper's settings, a population of 300 for
//! 1,000 generations each, with the difficulty triplet of the paper's figure 6, (0.5, 0.5, 0.5).
//! Prints each final front's size, how many of its solutions are feasible, their IGD+ to 2,000
//! points of the optimal front and their hypervolume, with the objectives normalized by the
//! front's ideal and nadir points, as a share of that of a sample of the front with at least as
//! many points as the population, and the median and largest distance from them to a dense sample
//! of the front. Then the same indicators for the best point of the dense sample for each of
//! MOEA/D's 300 weight vectors, and for 666, and the hypervolumes of the whole front and of the
//! sample.
//!
//! With `GENOXIDE_TRACE=<file>`, it also writes a trace of its runs for the plot on the example's
//! page, with `trace.rs`.
//!
//! ```text
//! cargo run --release --example das_cmop9
//! ```
mod trace;
use genoxide::Objective::Minimize;
use genoxide::multi::indicator::{hypervolume, igd_plus};
use genoxide::multi::problems::{DasCmop9, MultiProblem};
use genoxide::multi::{DifferentialEvolutionCrossover, MultiFitnessFunction};
use genoxide::prelude::*;
// the reference point of the hypervolume, with the objectives normalized by the front's ideal and
// nadir points
pub const REFERENCE: [f64; 3] = [1.1, 1.1, 1.1];
// the paper's population and evaluations: 300 for 1,000 generations; MOEA/D's 300 weight vectors
// are Das and Dennis's with 23 divisions
const POPULATION: usize = 300;
const GENERATIONS: u64 = 1_000;
const DIVISIONS: usize = 23;
fn main() -> Result<()> {
let problem = DasCmop9::default();
// with GENOXIDE_TRACE=<file>, a trace of the runs for the plot on the example's page
let mut trace = trace::Trace::from_env();
// the sample: at least as many points of the optimal front as the population has, about what
// a front of that many solutions can be
let sample = normalized(&problem, &problem.optimal_front(POPULATION).expect("known"));
// a dense sample of the front, at least 40,000 points
let dense = problem.optimal_front(40_000).expect("known");
let rate = 1.0 / problem.variables() as f64;
// MOEA/D-DE: a subproblem per weight vector, 30 neighbours (the paper's 0.1 N), parents from
// the neighbourhood with probability 0.2, differential evolution with F = 0.5 and CR = 1
let moead = Moead::builder(
problem.representation(),
[Minimize; 3],
multi::das_dennis::<3>(DIVISIONS),
)
.neighbors(30)
.neighbor_mating(0.2)
.crossover(DifferentialEvolutionCrossover::new(0.5, 1.0)?)
.mutate(PolynomialMutation::per_gene(rate, 20.0)?)
.seed(1)
.build()?;
run(&problem, "MOEA/D-DE", moead, &sample, &dense, &mut trace)?;
// the contrast: NSGA-II with the paper's settings
let nsga2 = Nsga2::builder(problem.representation(), [Minimize; 3])
.population_size(POPULATION)
.crossover(SimulatedBinaryCrossover::new(20.0)?)
.mutate(PolynomialMutation::per_gene(rate, 20.0)?)
.seed(1)
.build()?;
run(&problem, "NSGA-II", nsga2, &sample, &dense, &mut trace)?;
// what a decomposition into Tchebycheff subproblems can reach: each weight vector's best point
// of the dense sample, with MOEA/D's 300 vectors and with 666 (35 divisions)
for divisions in [DIVISIONS, 35] {
let weights = multi::das_dennis::<3>(divisions);
let best = per_weight(&problem, &dense, &weights);
let (distance, volume, percent) = indicators(&problem, &best, &sample);
println!(
"the best point of the front for each of {} weight vectors: {} points, IGD+ \
{distance:.4}, hypervolume {volume:.4}, {percent:.1}% of the sample's",
weights.len(),
best.len()
);
}
let whole = normalized(&problem, &problem.optimal_front(3_000).expect("known"));
println!(
"the whole front: hypervolume {:.4}; a sample of {} of its points: {:.4}",
hypervolume(&whole, &REFERENCE, &[Minimize; 3]),
sample.len(),
hypervolume(&sample, &REFERENCE, &[Minimize; 3])
);
trace.write(&problem);
Ok(())
}
// runs `algorithm` for the paper's 1,000 generations, and prints its final front's size, how many
// of its solutions the problem finds feasible, their IGD+ to 2,000 points of the optimal front and
// their hypervolume, with normalized objectives, as a share of the sample's, and the median and
// largest distance from them to the dense sample of the front
fn run<A>(
problem: &DasCmop9,
name: &'static str,
algorithm: A,
sample: &[[f64; 3]],
dense: &[[f64; 3]],
trace: &mut trace::Trace,
) -> Result<()>
where
A: MultiObjectiveAlgorithm<3, Genome = Reals>,
{
let mut record = trace.front(name, *problem);
let outcome = MultiEngine::new(algorithm, *problem)
.stop_when(Stop::generations(GENERATIONS))
.on_generation(|snapshot| record(snapshot))
.run()?;
let scores: Vec<([f64; 3], f64)> = outcome
.front()
.iter()
.map(|x| problem.evaluate(x.genome()))
.collect();
let feasible: Vec<[f64; 3]> = scores.iter().filter(|s| s.1 == 0.0).map(|s| s.0).collect();
let noun = if scores.len() == 1 {
"solution"
} else {
"solutions"
};
print!(
"{name}, {GENERATIONS} generations: {} {noun}, ",
scores.len()
);
if feasible.is_empty() {
let least = scores.iter().map(|s| s.1).fold(f64::INFINITY, f64::min);
println!("none feasible, the least violation {least:.4}");
return Ok(());
}
let (distance, volume, percent) = indicators(problem, &feasible, sample);
let (median, farthest) = distances(problem, &feasible, dense);
println!(
"{} feasible, IGD+ {distance:.4}, hypervolume {volume:.4}, {percent:.1}% of the \
sample's; from the front: {median:.4} (median), {farthest:.4} (farthest)",
feasible.len()
);
Ok(())
}
// the IGD+ of `points` to 2,000 points of the optimal front, their hypervolume, and that as a
// percentage of the sample's, with normalized objectives
fn indicators(problem: &DasCmop9, points: &[[f64; 3]], sample: &[[f64; 3]]) -> (f64, f64, f64) {
let found = normalized(problem, points);
let optimal = normalized(problem, &problem.optimal_front(2_000).expect("known"));
let distance = igd_plus(&found, &optimal, &[Minimize; 3]);
let volume = hypervolume(&found, &REFERENCE, &[Minimize; 3]);
let percent = 100.0 * volume / hypervolume(sample, &REFERENCE, &[Minimize; 3]);
(distance, volume, percent)
}
// the median and the largest distance from a point of `points` to the nearest point of `dense`,
// with normalized objectives
fn distances(problem: &DasCmop9, points: &[[f64; 3]], dense: &[[f64; 3]]) -> (f64, f64) {
let dense = normalized(problem, dense);
let squared =
|p: &[f64; 3], q: &[f64; 3]| -> f64 { (0..3).map(|j| (p[j] - q[j]) * (p[j] - q[j])).sum() };
let mut distances: Vec<f64> = normalized(problem, points)
.iter()
.map(|p| {
dense
.iter()
.map(|q| squared(p, q))
.fold(f64::INFINITY, f64::min)
.sqrt()
})
.collect();
distances.sort_by(f64::total_cmp);
let n = distances.len();
let median = if n % 2 == 1 {
distances[n / 2]
} else {
(distances[n / 2 - 1] + distances[n / 2]) / 2.0
};
(median, distances[n - 1])
}
// for each weight vector, the point of `dense` that minimizes MOEA/D's Tchebycheff value
// maxⱼ wⱼ |fⱼ − zⱼ| around the ideal point z (of two equal, the one with the least Σ (fⱼ − zⱼ),
// then the first), each point once
fn per_weight(problem: &DasCmop9, dense: &[[f64; 3]], weights: &[[f64; 3]]) -> Vec<[f64; 3]> {
let ideal = problem.ideal_point().expect("known");
let mut best: Vec<[f64; 3]> = weights
.iter()
.map(|w| {
let mut chosen = (f64::INFINITY, f64::INFINITY, 0);
for (i, f) in dense.iter().enumerate() {
let value = (0..3)
.map(|j| w[j] * (f[j] - ideal[j]).abs())
.fold(f64::NEG_INFINITY, f64::max);
let sum = (f[0] - ideal[0]) + (f[1] - ideal[1]) + (f[2] - ideal[2]);
if value < chosen.0 || (value == chosen.0 && sum < chosen.1) {
chosen = (value, sum, i);
}
}
dense[chosen.2]
})
.collect();
best.sort_by(|a, b| {
a[0].total_cmp(&b[0])
.then(a[1].total_cmp(&b[1]))
.then(a[2].total_cmp(&b[2]))
});
best.dedup();
best
}
// the objectives normalized by the front's ideal and nadir points: the front spans [0, 1] in each
pub fn normalized(problem: &DasCmop9, points: &[[f64; 3]]) -> Vec<[f64; 3]> {
let ideal = problem.ideal_point().expect("known");
let nadir = problem.nadir_point().expect("known");
let scale = |p: &[f64; 3]| std::array::from_fn(|j| (p[j] - ideal[j]) / (nadir[j] - ideal[j]));
points.iter().map(scale).collect()
}
python examples/das_cmop9/main.py
"""DAS-CMOP9: minimize three objectives over 30 variables subject to 7 constraints, whose front is
patches of a sphere, with linked variables, with MOEA/D-DE and NSGA-II.
From genoxide's problems.DasCmop9; run evaluates it in Rust. Runs MOEA/D with differential
evolution (MOEA/D-DE) and, as a contrast, NSGA-II with the paper's settings, a population of 300
for 1,000 generations each, with the difficulty triplet of the paper's figure 6, (0.5, 0.5, 0.5).
Prints each final front's size, how many of its solutions are feasible, their IGD+ to 2,000 points
of the optimal front and their hypervolume, with the objectives normalized by the front's ideal and
nadir points, as a share of that of a sample of the front with at least as many points as the
population, and the median and largest distance from them to a dense sample of the front. Then the
same indicators for the best point of the dense sample for each of MOEA/D's 300 weight vectors, and
for 666, and the hypervolumes of the whole front and of the sample.
With ``GENOXIDE_TRACE=<file>``, it also writes a trace of its runs for the plot on the example's
page, with trace.py.
python examples/das_cmop9/main.py
"""
import numpy as np
import genoxide as gx
from trace import Trace
# the reference point of the hypervolume, with the objectives normalized by the front's ideal and
# nadir points
REFERENCE = [1.1, 1.1, 1.1]
# the paper's population and evaluations: 300 for 1,000 generations; MOEA/D's 300 weight vectors
# are Das and Dennis's with 23 divisions
POPULATION = 300
GENERATIONS = 1_000
DIVISIONS = 23
problem = gx.problems.DasCmop9()
ideal, nadir = problem.ideal_point, problem.nadir_point
rate = 1 / problem.dimensions
def normalized(points):
"""The objectives normalized by the front's ideal and nadir points: the front spans [0, 1] in
each."""
return (points - ideal) / (nadir - ideal)
# with GENOXIDE_TRACE=<file>, a trace of the runs for the plot on the example's page
trace = Trace(problem, normalized, REFERENCE)
# the sample: at least as many points of the optimal front as the population has, about what a
# front of that many solutions can be
sample = normalized(problem.optimal_front(POPULATION))
# a dense sample of the front, at least 40,000 points
dense = problem.optimal_front(40_000)
def indicators(points):
"""The IGD+ of ``points`` to 2,000 points of the optimal front, their hypervolume, and that as a
percentage of the sample's, with normalized objectives."""
found = normalized(points)
distance = gx.indicators.igd_plus(found, normalized(problem.optimal_front(2_000)))
volume = gx.indicators.hypervolume(found, REFERENCE)
return distance, volume, 100 * volume / gx.indicators.hypervolume(sample, REFERENCE)
def distances(points):
"""The median and the largest distance from a point of ``points`` to the nearest point of the
dense sample, with normalized objectives."""
near = normalized(dense)
found = [np.sqrt(((near - p) * (near - p)).sum(axis=1).min()) for p in normalized(points)]
return np.median(found), max(found)
def per_weight(weights):
"""For each weight vector, the point of the dense sample that minimizes MOEA/D's Tchebycheff
value maxⱼ wⱼ |fⱼ − zⱼ| around the ideal point z (of two equal, the one with the least
Σ (fⱼ − zⱼ), then the first), each point once, sorted."""
shifted = dense - ideal
sums = (shifted[:, 0] + shifted[:, 1]) + shifted[:, 2]
best = []
for w in weights:
values = (w * np.abs(shifted)).max(axis=1)
tied = np.flatnonzero(values == values.min())
best.append(dense[tied[np.argmin(sums[tied])]])
return np.unique(np.array(best), axis=0)
def run(name, algorithm):
"""Runs ``algorithm`` for the paper's 1,000 generations, and prints its final front's size, how
many of its solutions the problem finds feasible, their IGD+ to 2,000 points of the optimal
front and their hypervolume, with normalized objectives, as a share of the sample's, and the
median and largest distance from them to the dense sample of the front."""
result = algorithm.run(problem, generations=GENERATIONS, on_generation=trace.front(name))
objectives, violations = problem.evaluate(result.front_genomes)
feasible = objectives[violations == 0]
noun = "solution" if len(objectives) == 1 else "solutions"
start = f"{name}, {GENERATIONS} generations: {len(objectives)} {noun}, "
if len(feasible) == 0:
print(start + f"none feasible, the least violation {violations.min():.4f}")
return
distance, volume, percent = indicators(feasible)
median, largest = distances(feasible)
print(
start + f"{len(feasible)} feasible, IGD+ {distance:.4f}, hypervolume {volume:.4f}, "
f"{percent:.1f}% of the sample's; from the front: {median:.4f} (median), "
f"{largest:.4f} (farthest)"
)
# MOEA/D-DE: a subproblem per weight vector, 30 neighbours (the paper's 0.1 N), parents from the
# neighbourhood with probability 0.2, differential evolution with F = 0.5 and CR = 1
moead = gx.Moead(
problem.genome,
objectives=problem.objectives,
weights=gx.das_dennis(3, DIVISIONS),
neighbors=30,
neighbor_mating=0.2,
crossover=gx.DifferentialEvolutionCrossover(f=0.5, cr=1.0),
mutation=gx.PolynomialMutation(20, rate=rate),
seed=1,
)
run("MOEA/D-DE", moead)
# the contrast: NSGA-II with the paper's settings
nsga2 = gx.Nsga2(
problem.genome,
objectives=problem.objectives,
population_size=POPULATION,
crossover=gx.SimulatedBinaryCrossover(20),
mutation=gx.PolynomialMutation(20, rate=rate),
seed=1,
)
run("NSGA-II", nsga2)
# what a decomposition into Tchebycheff subproblems can reach: each weight vector's best point of
# the dense sample, with MOEA/D's 300 vectors and with 666 (35 divisions)
for divisions in (DIVISIONS, 35):
weights = gx.das_dennis(3, divisions)
best = per_weight(weights)
distance, volume, percent = indicators(best)
print(
f"the best point of the front for each of {len(weights)} weight vectors: {len(best)} "
f"points, IGD+ {distance:.4f}, hypervolume {volume:.4f}, {percent:.1f}% of the sample's"
)
whole = normalized(problem.optimal_front(3_000))
print(
f"the whole front: hypervolume {gx.indicators.hypervolume(whole, REFERENCE):.4f}; "
f"a sample of {len(sample)} of its points: "
f"{gx.indicators.hypervolume(sample, REFERENCE):.4f}"
)
trace.write()
What it prints, from a seeded run:
MOEA/D-DE, 1000 generations: 196 solutions, 196 feasible, IGD+ 0.0158, hypervolume 0.7484, 97.9% of the sample's; from the front: 0.0018 (median), 0.0200 (farthest)
NSGA-II, 1000 generations: 300 solutions, 300 feasible, IGD+ 0.2006, hypervolume 0.4880, 63.8% of the sample's; from the front: 0.0017 (median), 0.0471 (farthest)
the best point of the front for each of 300 weight vectors: 223 points, IGD+ 0.0184, hypervolume 0.7397, 96.7% of the sample's
the best point of the front for each of 666 weight vectors: 492 points, IGD+ 0.0121, hypervolume 0.7597, 99.4% of the sample's
the whole front: hypervolume 0.7853; a sample of 346 of its points: 0.7647