Skip to content

Rocket injector

The problem

Vaidyanathan, Tucker, Papila and Shyy (2003) designed a single-element injector of hydrogen and oxygen for a rocket engine: an element that injects the hydrogen at an angle towards the oxygen. Its four design variables are the hydrogen's flow angle α, the changes in the hydrogen's and the oxygen's flow areas ΔHA and ΔOA, and the oxidizer post tip's thickness OPTT, each scaled to [0, 1] over its range. They ran CFD simulations of a set of designs and fitted response surfaces to four results: the face's highest temperature TF_max, the wall temperature three inches from the face TW₄, the post tip's highest temperature TT_max, and the combustion length X_cc, where combustion is 99% complete. Lower temperatures mean a longer life, a shorter combustion a better performance.

minimize   TF_max = 0.692 + 0.477α − 0.687ΔHA − 0.080ΔOA − 0.0650OPTT − 0.167α² − 0.0129ΔHAα
                    + 0.0796ΔHA² − 0.0634ΔOAα − 0.0257ΔOAΔHA + 0.0877ΔOA² − 0.0521OPTTα
                    + 0.00156OPTTΔHA + 0.00198OPTTΔOA + 0.0184OPTT²
           TT_max = 0.370 − 0.205α + 0.0307ΔHA + 0.108ΔOA + 1.019OPTT − 0.135α² + 0.0141ΔHAα
                    + 0.0998ΔHA² + 0.208ΔOAα − 0.0301ΔOAΔHA − 0.226ΔOA² + 0.353OPTTα
                    − 0.0497OPTTΔOA − 0.423OPTT² + 0.202ΔHAα² − 0.281ΔOAα² − 0.342ΔHA²α
                    − 0.245ΔHA²ΔOA + 0.281ΔOA²ΔHA − 0.184OPTT²α − 0.281ΔHAαΔOA
           X_cc   = 0.153 − 0.322α + 0.396ΔHA + 0.424ΔOA + 0.0226OPTT + 0.175α² + 0.0185ΔHAα
                    − 0.0701ΔHA² − 0.251ΔOAα + 0.179ΔOAΔHA + 0.0150ΔOA² + 0.0134OPTTα
                    + 0.0296OPTTΔHA + 0.0752OPTTΔOA + 0.0192OPTT²
α, ΔHA, ΔOA, OPTT in [0, 1]

The surfaces are the paper's eqs. A1, A3 and A4, checked in the authors' copy (NASA NTRS 20030060421). The paper's second objective, TW₄ (eq. A2), is left out, as in the three-objective form of Goel et al. (2007, Computer Methods in Applied Mechanics and Engineering 196: 879-893), which Tanabe and Ishibuchi (2020, problem RE3-4-7) restate; genoxide couldn't read Goel et al.'s paper, and its RocketInjector keeps the original's order of the other three. The objectives are the surfaces' values, scaled as in the paper; TT_max's surface falls below 0 at a corner of the box, at (1, 1, 1, 0).

The front isn't known. Its ideal point, from genoxide's SHADE, is (0.0088934, −0.4315, 0.00488). genoxide's reference front, the non-dominated designs of about 15,000 runs of SHADE, each minimizing an achievement scalarizing function along one of Das and Dennis's directions, and of twenty runs of SMS-EMOA and NSGA-III of 2,000 generations, has worst values (1.002, 1.0965, 1.0539), the estimated nadir point, and a hypervolume of 0.9019 in the scaled objectives below: a lower bound on the whole front's.

What makes it hard

The surfaces are quadratic and, for TT_max, cubic, and the objectives conflict strongly: the design that minimizes TF_max, (0, 1, 0.591, 1), is near the worst in both others. The front is a curved, irregular surface.

Representation

A Real genome of 4 genes, the scaled design variables. The problem is genoxide's multi::problems::engineering::RocketInjector (gx.problems.multi_engineering.RocketInjector 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/4 per gene). NSGA-III with 91 directions ends with less hypervolume, 93.4% to 94.4% of the reference front's over seeds 1 to 10.

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 spreads over the whole surface, reaching each objective's minimum. 92 points of the reference front, chosen one by one for the most hypervolume, give 97.21% of its hypervolume.

The run's front has 92 solutions, reaching the three minima (0.0089, −0.4315, 0.0049), with 95.58% of the reference front's hypervolume, 98.3% of what 92 chosen points reach. Its X_cc goes up to 1.0604, beyond the reference front's worst: a solution there is dominated by the reference front, though not by the run's own. Over seeds 1 to 20, every run ends between 95.27% and 95.70%.

Reference: Vaidyanathan, R., Tucker, P. K., Papila, N. and Shyy, W. (2003). CFD-based design optimization for single element rocket injector. 41st AIAA Aerospace Sciences Meeting, AIAA paper 2003-296.

Known optimum: not known in closed form; ideal point (0.0088934, −0.4315, 0.00488); genoxide's reference front has a hypervolume of 0.9019 in objectives scaled by the ideal point and its estimated nadir point (1.002, 1.0965, 1.0539) (reference point (1.1, 1.1, 1.1))

Source: examples/rocket_injector

Interactive run: tachsin.gr/projects/genoxide/examples/rocket-injector

cargo run --release --example rocket_injector
//! Rocket injector: minimize two temperatures of a rocket injector and the length of its
//! combustion, 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 rocket_injector
//! ```

mod trace;

use genoxide::Objective::Minimize;
use genoxide::multi::indicator::hypervolume;
use genoxide::multi::problems::MultiProblem;
use genoxide::multi::problems::engineering::RocketInjector;
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] = [1.002, 1.0965, 1.0539];

// 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 = 0.9019;

fn main() -> Result<()> {
    let problem = RocketInjector;
    let sms_emoa = SmsEmoa::builder(problem.representation(), [Minimize; 3])
        .population_size(92)
        .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(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 = RocketInjector.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!(
        "  TF_max from {:.4} to {:.4}, TT_max from {:.4} to {:.4}, \
         X_cc 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/rocket_injector/main.py
"""Rocket injector: minimize two temperatures of a rocket injector and the length of its
combustion, 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/rocket_injector/main.py
"""

import genoxide as gx

from trace import REFERENCE, Trace, scaled

# the run's length
GENERATIONS = 500

problem = gx.problems.multi_engineering.RocketInjector()
sms_emoa = gx.SmsEmoa(
    problem.genome,
    objectives=problem.objectives,
    population_size=92,
    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 = 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"  TF_max from {low[0]:.4f} to {high[0]:.4f}, "
    f"TT_max from {low[1]:.4f} to {high[1]:.4f}, "
    f"X_cc 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
  TF_max from 0.0089 to 1.0020, TT_max from -0.4315 to 1.0961, X_cc from 0.0049 to 1.0604
  hypervolume 0.8621, 95.58% of the reference front's 0.9019