Pick one answer off a Pareto front

A multi-objective optimizer hands you a front — a Pareto-optimal trade-off curve — not a single answer. Eventually you have to pick one point off it. There are several principled ways to do that; this recipe covers the most common: the a-posteriori weighted decision rule.

The pattern: optimize without baking your preferences into the search, then apply your preferences as a scoring function over the front.

This is exactly the pattern from examples/jiggly_tuning.rs (the USB-jiggler firmware tuning example).

The shape

#![allow(unused)]
fn main() {
use heuropt::prelude::*;

struct Cost;
impl Problem for Cost {
    type Decision = Vec<f64>;
    fn objectives(&self) -> ObjectiveSpace {
        ObjectiveSpace::new(vec![Objective::minimize("a"), Objective::minimize("b"), Objective::minimize("c")])
    }
    fn evaluate(&self, _x: &Vec<f64>) -> Evaluation { Evaluation::new(vec![0.0,0.0,0.0]) }
}

let problem = Cost;
let mut opt = Nsga2::new(
    Nsga2Config { population_size: 100, generations: 200, seed: 42 },
    RealBounds::new(vec![(-1.0, 1.0); 4]),
    CompositeVariation {
        crossover: SimulatedBinaryCrossover::new(vec![(-1.0, 1.0); 4], 15.0, 0.5),
        mutation:  PolynomialMutation::new(vec![(-1.0, 1.0); 4], 20.0, 1.0),
    },
);
let result = opt.run(&problem);

// 1. Get the Pareto front.
let front = &result.pareto_front;

// 2. Define your preferences as a scoring function over (oriented)
//    objective values. Lower score = preferred.
let space = problem.objectives();
let weights = [1.0, 2.0, 0.5];

let scored: Vec<(f64, &Candidate<Vec<f64>>)> = front.iter()
    .map(|c| {
        let oriented = space.as_minimization(&c.evaluation.objectives);
        let score: f64 = oriented.iter().zip(&weights)
            .map(|(v, w)| v * w)
            .sum();
        (score, c)
    })
    .collect();

// 3. Pick the lowest-scoring point.
let best = scored.iter()
    .min_by(|a, b| a.0.partial_cmp(&b.0).unwrap())
    .unwrap();

println!("picked: {:?} with weighted score {:.3}",
    best.1.evaluation.objectives, best.0);
}

as_minimization returns the objective vector with maximized axes flipped to negative — so a single set of positive weights does the right thing whether each axis is min or max.

Why a-posteriori vs a-priori weighting

If you know your weights up front, you could just optimize the weighted sum directly with a single-objective algorithm. Why bother with the multi-objective dance?

Two reasons:

  1. Weighted sum can't reach concave parts of the Pareto front. Any single-objective optimization with a linear scalarization converges to a point at the boundary of the convex hull. Concave front segments are unreachable. The multi-objective optimizer finds them.
  2. Weights are usually wrong on the first try. Optimizing the front first lets you see what's actually possible before deciding how much each axis is worth. Run once, look at the trade-offs, adjust weights.

Penalty terms beyond linear weights

The jiggly example also adds a hinge penalty — a term that's zero inside an acceptable region and grows quadratically once you exceed some hard cap. Useful when one axis is "soft up to X, hard cap at Y":

#![allow(unused)]
fn main() {
fn hinge(x: f64, soft_cap: f64, hard_cap: f64) -> f64 {
    if x <= soft_cap { 0.0 }
    else if x >= hard_cap { f64::INFINITY }
    else {
        let t = (x - soft_cap) / (hard_cap - soft_cap);
        100.0 * t * t
    }
}
}

Compose linear weights + hinge penalties and you have a flexible scoring function over the front without re-running the optimizer.

Other strategies

  • Knee point. Pick the point where small gains in one axis cost large losses in another — the "elbow" of the trade-off curve. Knea explicitly biases the search toward knees during the run.
  • Reference-direction. Pick the point closest to a desired trade-off direction (a unit vector in objective space). Moead / Nsga3 use this internally during search; you can apply it post-hoc the same way.
  • Random / interactive selection. Show the front to a user (perhaps via a plotting library), let them pick.

The right pick depends on the problem; the front itself doesn't prescribe one.