Skip to content

Disc brake

The problem

A multiple-disc brake should be light and stop fast. Its design variables are the discs' inner and outer radii r and R, in mm, the engaging force F, in N, and the number of friction surfaces s, an integer:

minimize   f₁ = 4.9·10⁻⁵ (R² − r²)(s − 1)                     the mass, in kg
           f₂ = 9.82·10⁶ (R² − r²) / (F s (R³ − r³))           the stopping time, in s
subject to R − r ≥ 20                                          the discs' width
           2.5 (s + 1) ≤ 30                                    the brake's length
           F / (3.14 (R² − r²)) ≤ 0.4                          the pressure
           2.22·10⁻³ F (R³ − r³) / (R² − r²)² ≤ 1              the temperature
           0.0266 F s (R³ − r³) / (R² − r²) ≥ 900              the torque
r in [55, 80], R in [75, 110], F in [1000, 3000], s in [2, 20]

Osyczka and Kundu's paper (1995) and Ray and Liew's (2002), the problem's sources, couldn't be read: the definition and bounds are those of Yang, Karamanoglu and He (2013, Procedia Computer Science 18: 861-868, eqs. 10-12), and the same in Saad, Emam and Houssein (2025, Scientific Reports 15: 5126, eqs. 24-25). Tanabe and Ishibuchi's (2020, problem RE3-4-3) drops the length constraint and bounds s by [11, 20], though their note says the constraint gives s ≤ 11: a different problem.

The front isn't known in closed form, but its ends follow from the definition. The lightest brake has the narrowest discs at the smallest radii, r = 55 and R = 75, and two surfaces: 0.1274 kg, stopping in 16.654925 s at the largest force, 3000. The fastest has the widest discs, r = 80 and R = 110, the most surfaces the length allows, 11, and the largest force: 2.0710401 s at 2.793 kg. Both meet every constraint. They give genoxide's DiscBrake its ideal and nadir points.

genoxide's reference front, the non-dominated designs of about 6,000 ε-constraint problems (the least mass at a stopping time of at most ε, and the least time at a mass of at most ε, for ε evenly spread between the ends), each solved by SHADE with 30,000 or 60,000 evaluations, and of eight NSGA-II runs of 1,000 generations, has a hypervolume of 1.0853 in the scaled objectives below: a lower bound on the whole front's.

What makes it hard

The number of surfaces is an integer: the genome is real, and the problem rounds its fourth gene to the nearest integer, so the front is ten pieces, one for each number of surfaces from 2 to 11, and a solution moves between pieces in jumps. The mass grows with s − 1 and the stopping time falls with s, so every piece is needed. The force stays near its upper bound of 3000 along the whole front, and the radii trade mass against stopping time within each piece.

Representation

A Real genome of 4 genes: r, R, F and s, which the problem rounds; design gives the rounded design. The problem is genoxide's multi::problems::engineering::DiscBrake (gx.problems.multi_engineering.DiscBrake in Python), whose fitness is the two objectives and the total constraint violation. Solutions compare by constrained dominance.

Algorithm

NSGA-II with a population of 100 for 250 generations, simulated binary crossover (η = 20, at a rate of 0.9) and polynomial mutation (η = 20, at a rate of 1/4 per gene).

Output

The first line gives the size of the final front and how many of its solutions are feasible, the second the range of each objective on it. The third gives its hypervolume up to the reference point (1.1, 1.1), in objectives scaled to [0, 1] by the ideal point (0.1274, 2.0710401) and the nadir point (2.793, 16.654925), 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 is feasible, with solutions on every piece from 2 to 11 surfaces. 100 points of the reference front, chosen one by one for the most hypervolume, give 99.79% of its hypervolume.

The run's front has 100 feasible solutions on all ten pieces (23 with two surfaces, 26 with eleven, 2 to 14 with each number between), from 0.1303 kg to the fastest brake, 2.0710 s, with 99.60% of the reference front's hypervolume. Over seeds 1 to 20, every run ends between 99.41% and 99.61%.

Reference: Osyczka, A. and Kundu, S. (1995). A new method to solve generalized multicriteria optimization problems using the simple genetic algorithm. Structural Optimization 10(2): 94-99.

Known optimum: not known in closed form; from 0.1274 kg stopping in 16.654925 s to 2.0710401 s at 2.793 kg, both ends derived; genoxide's reference front has a hypervolume of 1.0853 in scaled objectives (reference point (1.1, 1.1))

Source: examples/disc_brake

Interactive run: tachsin.gr/projects/genoxide/examples/disc-brake

cargo run --release --example disc_brake
//! Disc brake: minimize the mass of a multiple-disc brake and its stopping time, subject to five
//! constraints.
//!
//! NSGA-II with a population of 100, simulated binary crossover and polynomial mutation at a rate
//! of 1/4 per gene, for 250 generations. Prints how many solutions of the final front are feasible,
//! the range of each objective on it, and their 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 disc_brake
//! ```

mod trace;

use genoxide::Objective::Minimize;
use genoxide::multi::indicator::hypervolume;
use genoxide::multi::problems::MultiProblem;
use genoxide::multi::problems::engineering::DiscBrake;
use genoxide::prelude::*;

// the run's length
const GENERATIONS: u64 = 250;

// the hypervolume of genoxide's reference front, in scaled objectives with the reference point
// (1.1, 1.1): the non-dominated solutions of ε-constraint runs of SHADE (see the README), a lower
// bound on the whole front's
pub const REFERENCE: f64 = 1.0853;

fn main() -> Result<()> {
    let problem = DiscBrake;
    let nsga2 = Nsga2::builder(problem.representation(), [Minimize; 2])
        .population_size(100)
        .crossover(SimulatedBinaryCrossover::new(20.0)?)
        .mutate(PolynomialMutation::per_gene(0.25, 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(nsga2, 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] on the front, by its ideal and nadir points
pub fn scaled(points: &[[f64; 2]]) -> Vec<[f64; 2]> {
    let (ideal, nadir) = (
        DiscBrake.ideal_point().expect("known"),
        DiscBrake.nadir_point().expect("known"),
    );
    let scale = |p: &[f64; 2]| [0, 1].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; 2]], size: usize) {
    let feasible = if front.len() == size {
        "all feasible".to_string()
    } else {
        format!("{} feasible", front.len())
    };
    println!("NSGA-II, {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 {:.4} to {:.4}, stopping time from {:.4} to {:.4}",
        low(0),
        high(0),
        low(1),
        high(1)
    );
    let found = scaled(front);
    let volume = hypervolume(&found, &[1.1, 1.1], &[Minimize; 2]);
    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<2>>]) -> (Vec<[f64; 2]>, 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/disc_brake/main.py
"""Disc brake: minimize the mass of a multiple-disc brake and its stopping time, subject to five
constraints.

NSGA-II with a population of 100, simulated binary crossover and polynomial mutation at a rate of
1/4 per gene, for 250 generations. Prints how many solutions of the final front are feasible, the
range of each objective on it, and their 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/disc_brake/main.py
"""

import genoxide as gx

from trace import REFERENCE, Trace, scaled

# the run's length
GENERATIONS = 250

problem = gx.problems.multi_engineering.DiscBrake()
nsga2 = gx.Nsga2(
    problem.genome,
    objectives=problem.objectives,
    population_size=100,
    crossover=gx.SimulatedBinaryCrossover(20),
    mutation=gx.PolynomialMutation(20, rate=0.25),
    seed=1,
)
# with GENOXIDE_TRACE=<file>, a trace of the run for the plot on the example's page
trace = Trace(problem)
result = nsga2.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"NSGA-II, {GENERATIONS} generations: {size} solutions on the front, {count}")
low, high = front.min(axis=0), front.max(axis=0)
print(
    f"  mass from {low[0]:.4f} to {high[0]:.4f}, "
    f"stopping time from {low[1]:.4f} to {high[1]:.4f}"
)
found = scaled(problem, front)
volume = gx.indicators.hypervolume(found, [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:

NSGA-II, 250 generations: 100 solutions on the front, all feasible
  mass from 0.1303 to 2.7930, stopping time from 2.0710 to 16.2957
  hypervolume 1.0810, 99.60% of the reference front's 1.0853