Vehicle crashworthiness
The problem
Liao, Li, Yang, Zhang and Li (2008) designed the front of a car for crash safety. Its five genes are the thicknesses, in mm, of five reinforcing members around the front, and response surfaces fitted by stepwise regression to crash simulations give three objectives:
minimize f₁ = 1640.2823 + 2.3573285x₁ + 2.3220035x₂ + 4.5688768x₃ + 7.7213633x₄ + 4.4559504x₅
the mass, in kg
f₂ = 6.5856 + 1.15x₁ − 1.0427x₂ + 0.9738x₃ + 0.8364x₄ − 0.3695x₁x₄ + 0.0861x₁x₅
+ 0.3628x₂x₄ − 0.1106x₁² − 0.3437x₃² + 0.1764x₄²
the integral of the deceleration in the full frontal crash
f₃ = −0.0551 + 0.0181x₁ + 0.1024x₂ + 0.0421x₃ − 0.0073x₁x₂ + 0.024x₂x₃ − 0.0118x₂x₄
− 0.0204x₃x₄ − 0.008x₃x₅ − 0.0241x₂² + 0.0109x₄²
the toe board's intrusion in the 40% offset frontal crash
x₁ … x₅ in [1, 3]
Liao et al.'s paper couldn't be read. The surfaces and bounds are those of Tanabe and Ishibuchi
(2020, problem RE3-5-4) and, the same, of de Carvalho and Sichman (2018, OptMAS 2018, eqs. 1-3);
they are genoxide's VehicleCrashworthiness, still to be checked against the original. Deb and Jain
(2014, part I, figure 30) draw NSGA-III's front over masses from about 1660 to 1700, where only 40
of 91 reference directions found a solution.
The front isn't known. Its ideal point is at bounds: the least mass with every member at 1 mm, the least deceleration at (1, 3, 3, 1, 1) and the least intrusion at (1, 1, 3, 3, 3). genoxide's reference front, the non-dominated designs of about 10,000 runs of SHADE, each minimizing an achievement scalarizing function along one of Das and Dennis's directions, and of sixteen runs of SMS-EMOA and NSGA-III of 2,000 generations, has worst values (1695.161, 10.736, 0.264), the estimated nadir point, and a hypervolume of 1.0525 in the scaled objectives below: a lower bound on the whole front's.
What makes it hard
The front covers only part of the plane of the reference directions, as Deb and Jain found, and some of its parts are small: a run can miss one and keep its population elsewhere, since nothing dominates the solutions it has.
Representation
A Real genome of 5 genes, the thicknesses. The problem is genoxide's
multi::problems::engineering::VehicleCrashworthiness
(gx.problems.multi_engineering.VehicleCrashworthiness in Python). It has no constraints besides
the bounds.
Algorithm
SMS-EMOA, which keeps the solutions that add the most hypervolume, with a population of 92 for 500 generations, simulated binary crossover (η = 20, at a rate of 0.9) and polynomial mutation (η = 20, at a rate of 1/5 per gene).
Output
The first line gives the size of the final front and how many of its solutions are feasible (all
are), the second the range of each objective on it. The third gives its hypervolume up to the
reference point (1.1, 1.1, 1.1), in objectives scaled to [0, 1] by the ideal point and the estimated
nadir point, as a share of the reference front's. In Python, run evaluates the problem in Rust, so
both versions print the same.
The project page plays this run back.
Good results
A good front reaches the three minima and covers every part of the front. 92 points of the reference front, chosen one by one for the most hypervolume, give 99.29% of its hypervolume.
The run's front has 92 solutions, reaching the least mass and deceleration, but intrusions only down to 0.0523 of the least 0.0394, with masses up to 1684.3 and 98.78% of the reference front's hypervolume, 99.5% of what 92 chosen points reach. Over seeds 1 to 20, the runs end in two groups, at 98.78% and at 99.2%, but for seed 5, which misses a part of the front and ends at 95.15%. Longer runs, 2,000 generations, and other mutation rates end the same.
Known optimum: not known in closed form; ideal point (1661.7078, 6.1428, 0.0394), at bounds; genoxide's reference front has a hypervolume of 1.0525 in objectives scaled by the ideal point and its estimated nadir point (1695.161, 10.736, 0.264) (reference point (1.1, 1.1, 1.1))
Source: examples/vehicle_crashworthiness
Interactive run: tachsin.gr/projects/genoxide/examples/vehicle-crashworthiness
cargo run --release --example vehicle_crashworthiness
//! Vehicle crashworthiness: minimize a car's mass, the deceleration in a full frontal crash and the
//! toe board's intrusion in an offset one, three response surfaces.
//!
//! SMS-EMOA with a population of 92, for 500 generations. Prints how many solutions of the final
//! front are feasible, the range of each objective on it, and its hypervolume, as a share of that
//! of genoxide's reference front.
//!
//! With `GENOXIDE_TRACE=<file>`, it also writes a trace of its run for the plot on the example's
//! page, with `trace.rs`.
//!
//! ```text
//! cargo run --release --example vehicle_crashworthiness
//! ```
mod trace;
use genoxide::Objective::Minimize;
use genoxide::multi::indicator::hypervolume;
use genoxide::multi::problems::MultiProblem;
use genoxide::multi::problems::engineering::VehicleCrashworthiness;
use genoxide::prelude::*;
// the run's length
const GENERATIONS: u64 = 500;
// the front's nadir point, estimated from genoxide's reference front (see the README): with the
// ideal point, what scales the objectives to [0, 1]
pub const NADIR: [f64; 3] = [1695.161, 10.736, 0.264];
// the hypervolume of genoxide's reference front, in scaled objectives with the reference point
// (1.1, 1.1, 1.1): a lower bound on the whole front's
pub const REFERENCE: f64 = 1.0525;
fn main() -> Result<()> {
let problem = VehicleCrashworthiness;
let sms_emoa = SmsEmoa::builder(problem.representation(), [Minimize; 3])
.population_size(92)
.crossover(SimulatedBinaryCrossover::new(20.0)?)
.mutate(PolynomialMutation::per_gene(0.2, 20.0)?)
.seed(1)
.build()?;
// with GENOXIDE_TRACE=<file>, a trace of the run for the plot on the example's page
let mut trace = trace::Trace::from_env();
let outcome = MultiEngine::new(sms_emoa, problem)
.stop_when(Stop::generations(GENERATIONS))
.on_generation(|snapshot| trace.record(snapshot))
.run()?;
let (front, size) = feasible(outcome.front());
report(&front, size);
trace.write();
Ok(())
}
// the objectives scaled to [0, 1] by the front's ideal point and its estimated nadir point
pub fn scaled(points: &[[f64; 3]]) -> Vec<[f64; 3]> {
let ideal = VehicleCrashworthiness.ideal_point().expect("known");
let scale = |p: &[f64; 3]| [0, 1, 2].map(|j| (p[j] - ideal[j]) / (NADIR[j] - ideal[j]));
points.iter().map(scale).collect()
}
// the feasible solutions of the run's front: how many of them, the range of each objective, and
// their hypervolume, as a share of the reference front's
fn report(front: &[[f64; 3]], size: usize) {
let feasible = if front.len() == size {
"all feasible".to_string()
} else {
format!("{} feasible", front.len())
};
println!("SMS-EMOA, {GENERATIONS} generations: {size} solutions on the front, {feasible}");
let low = |j: usize| front.iter().map(|p| p[j]).fold(f64::INFINITY, f64::min);
let high = |j: usize| front.iter().map(|p| p[j]).fold(f64::NEG_INFINITY, f64::max);
println!(
" mass from {:.3} to {:.3}, deceleration from {:.4} to {:.4}, \
intrusion from {:.4} to {:.4}",
low(0),
high(0),
low(1),
high(1),
low(2),
high(2)
);
let volume = hypervolume(&scaled(front), &[1.1; 3], &[Minimize; 3]);
println!(
" hypervolume {volume:.4}, {:.2}% of the reference front's {REFERENCE}",
100.0 * volume / REFERENCE
);
}
// the objective values of the feasible solutions of a front, and the front's size
fn feasible<G: Genome>(front: &[Individual<G, multi::Scores<3>>]) -> (Vec<[f64; 3]>, usize) {
let values = front
.iter()
.filter_map(|x| {
x.fitness()
.filter(|s| s.is_feasible())
.and_then(|s| s.values())
})
.collect();
(values, front.len())
}
python examples/vehicle_crashworthiness/main.py
"""Vehicle crashworthiness: minimize a car's mass, the deceleration in a full frontal crash and the
toe board's intrusion in an offset one, three response surfaces.
SMS-EMOA with a population of 92, for 500 generations. Prints how many solutions of the final front
are feasible, the range of each objective on it, and its hypervolume, as a share of that of
genoxide's reference front.
With ``GENOXIDE_TRACE=<file>``, it also writes a trace of its run for the plot on the example's
page, with trace.py.
python examples/vehicle_crashworthiness/main.py
"""
import genoxide as gx
from trace import REFERENCE, Trace, scaled
# the run's length
GENERATIONS = 500
problem = gx.problems.multi_engineering.VehicleCrashworthiness()
sms_emoa = gx.SmsEmoa(
problem.genome,
objectives=problem.objectives,
population_size=92,
crossover=gx.SimulatedBinaryCrossover(20),
mutation=gx.PolynomialMutation(20, rate=0.2),
seed=1,
)
# with GENOXIDE_TRACE=<file>, a trace of the run for the plot on the example's page
trace = Trace(problem)
result = sms_emoa.run(problem, generations=GENERATIONS, on_generation=trace.on_generation)
feasible = result.front_violations == 0
front = result.front_objectives[feasible]
size = len(result.front_objectives)
count = "all feasible" if feasible.all() else f"{int(feasible.sum())} feasible"
print(f"SMS-EMOA, {GENERATIONS} generations: {size} solutions on the front, {count}")
low, high = front.min(axis=0), front.max(axis=0)
print(
f" mass from {low[0]:.3f} to {high[0]:.3f}, "
f"deceleration from {low[1]:.4f} to {high[1]:.4f}, "
f"intrusion from {low[2]:.4f} to {high[2]:.4f}"
)
volume = gx.indicators.hypervolume(scaled(problem, front), [1.1, 1.1, 1.1])
print(
f" hypervolume {volume:.4f}, {100 * volume / REFERENCE:.2f}% of the reference front's "
f"{REFERENCE}"
)
trace.write()
What it prints, from a seeded run:
SMS-EMOA, 500 generations: 92 solutions on the front, all feasible
mass from 1661.708 to 1684.299, deceleration from 6.1428 to 9.0835, intrusion from 0.0523 to 0.2640
hypervolume 1.0397, 98.78% of the reference front's 1.0525