feat(examples): rank the jiggly Pareto front and recommend a pick

Adds an a-posteriori decision step to the jiggly example. After NSGA-III
produces the Pareto front, we apply a weighted-sum score over each
objective normalized to [0, 1] across the front (best→1, worst→0,
direction-aware), and report the top three plus a clear recommendation.

The weights are stated explicitly with rationale, not buried in code:

  work_fail    45%  — screen sleeping mid-meeting is the worst failure
  lunch_sleep  30%  — the actual design goal
  presses      15%  — UX friction the user feels
  after_hours  10%  — minor, mostly screen burn

This is the standard structure for picking a single answer out of a
Pareto set without losing the front itself: someone with different
weights can read the front and pick differently, but we surface a
specific recommendation with reasoning rather than leaving the user
to stare at 84 incomparable rows. Identical normalization could be
swapped for TOPSIS or knee-point detection later if useful.
This commit is contained in:
2026-05-04 20:22:19 -06:00
parent f70a12010d
commit 5bd8b571e7
+153
View File
@@ -268,6 +268,7 @@ fn fmt_rt(m: i32) -> String {
}
/// One row in the Pareto-front summary table.
#[derive(Clone)]
struct Row {
rt: i32,
ya: i32,
@@ -434,4 +435,156 @@ fn main() {
println!("=== firmware shipping default (RT=4h00m YEL=30 RED=25 FST=20) ===");
print_header();
print_row("", &shipping_row);
println!();
// -------------------------------------------------------------------------
// A-posteriori pick: rank the front by weighted preferences.
// -------------------------------------------------------------------------
//
// Every point on the front is incomparable in the strict Pareto sense —
// none dominates another. To surface ONE recommendation we apply explicit
// weights to the four normalized objectives. Anyone with different
// priorities can read the front above and pick a different row.
//
// We add the firmware's shipping defaults to the candidate set so they
// compete on equal footing with the front the optimizer found.
const W_WORK: f64 = 0.45; // work failures hurt most
const W_LUNCH: f64 = 0.30; // the design goal
const W_PRESS: f64 = 0.15; // UX friction
const W_AFTER: f64 = 0.10; // minor screen-burn cost
let mut candidates: Vec<(String, Row)> = rows
.iter()
.map(|r| ("front".to_string(), r.clone()))
.collect();
let shipping_candidate_idx = candidates.len();
candidates.push(("shipping default".to_string(), shipping_row.clone()));
let scores =
compute_weighted_scores(&candidates.iter().map(|(_, r)| r.clone()).collect::<Vec<_>>());
let mut ranked: Vec<(usize, f64)> = scores.iter().copied().enumerate().collect();
ranked.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
println!("=== ranked by weighted preferences ===");
println!(
" weights: work_fail {}% · lunch_sleep {}% · presses {}% · after_hours {}%",
(W_WORK * 100.0) as i32,
(W_LUNCH * 100.0) as i32,
(W_PRESS * 100.0) as i32,
(W_AFTER * 100.0) as i32,
);
println!(" candidate set: {} Pareto-front rows + 1 shipping default", rows.len());
println!();
println!("{:>4} {:>5} source", "rank", "score");
print_header();
for (rank, &(idx, score)) in ranked.iter().take(5).enumerate() {
let (label, r) = &candidates[idx];
println!("{:>4} {:.3} {label}", rank + 1, score);
print_row("", r);
}
println!();
let &(top_idx, top_score) = ranked.first().expect("at least one candidate");
let (top_label, top) = &candidates[top_idx];
let shipping_rank = ranked
.iter()
.position(|(i, _)| *i == shipping_candidate_idx)
.map(|p| p + 1)
.unwrap_or(0);
let max_work = candidates.iter().map(|(_, r)| r.work_fail).fold(0.0, f64::max);
let max_press = candidates.iter().map(|(_, r)| r.presses).fold(0.0, f64::max);
println!("=== RECOMMENDED PICK ({top_label}) ===");
println!(
" RT={} YELLOW_AT={} RED_AT={} FAST_RED_AT={}",
fmt_rt(top.rt),
top.ya,
top.ra,
top.fra,
);
println!(" weighted score = {top_score:.3}");
println!();
println!("Why:");
println!(
"{} mean work-time failure ({} better than the worst candidate)",
fmt_minutes(top.work_fail),
ratio_str(max_work, top.work_fail.max(1e-9)),
);
println!(
"{} mean lunch sleep ({:.1}% land in the 12:1512:45 sweet spot)",
fmt_minutes(top.lunch),
top.p_sweet * 100.0,
);
println!(
"{:.2} button presses/day ({} fewer than the worst candidate)",
top.presses,
ratio_str(max_press, top.presses.max(1e-9)),
);
println!("{} mean after-hours awake (negligible)", fmt_minutes(top.after));
if top_label != "shipping default" {
println!();
println!(
"(Shipping default ranks #{shipping_rank} of {}.)",
candidates.len(),
);
} else {
println!();
println!(
"Note: the optimizer found {} non-dominated alternatives, but under",
rows.len(),
);
println!("these weights the firmware's shipping defaults score highest.");
}
}
/// Score every row in `rows` by a fixed weighted sum of normalized objectives.
///
/// Each objective is normalized to `[0, 1]` across `rows` with `1` meaning
/// "best on the front" and `0` meaning "worst on the front", direction-aware
/// (lunch is maximize, the rest are minimize).
fn compute_weighted_scores(rows: &[Row]) -> Vec<f64> {
const W_WORK: f64 = 0.45;
const W_LUNCH: f64 = 0.30;
const W_PRESS: f64 = 0.15;
const W_AFTER: f64 = 0.10;
let work_min = rows.iter().map(|r| r.work_fail).fold(f64::INFINITY, f64::min);
let work_max = rows.iter().map(|r| r.work_fail).fold(f64::NEG_INFINITY, f64::max);
let lunch_min = rows.iter().map(|r| r.lunch).fold(f64::INFINITY, f64::min);
let lunch_max = rows.iter().map(|r| r.lunch).fold(f64::NEG_INFINITY, f64::max);
let press_min = rows.iter().map(|r| r.presses).fold(f64::INFINITY, f64::min);
let press_max = rows.iter().map(|r| r.presses).fold(f64::NEG_INFINITY, f64::max);
let after_min = rows.iter().map(|r| r.after).fold(f64::INFINITY, f64::min);
let after_max = rows.iter().map(|r| r.after).fold(f64::NEG_INFINITY, f64::max);
rows.iter()
.map(|r| {
let work = norm_min(r.work_fail, work_min, work_max);
let lunch = norm_max(r.lunch, lunch_min, lunch_max);
let press = norm_min(r.presses, press_min, press_max);
let after = norm_min(r.after, after_min, after_max);
W_WORK * work + W_LUNCH * lunch + W_PRESS * press + W_AFTER * after
})
.collect()
}
/// Normalize a minimize-direction value to `[0, 1]` (best→1, worst→0).
fn norm_min(v: f64, lo: f64, hi: f64) -> f64 {
if (hi - lo).abs() < 1e-12 { 1.0 } else { (hi - v) / (hi - lo) }
}
/// Normalize a maximize-direction value to `[0, 1]` (best→1, worst→0).
fn norm_max(v: f64, lo: f64, hi: f64) -> f64 {
if (hi - lo).abs() < 1e-12 { 1.0 } else { (v - lo) / (hi - lo) }
}
/// Render `worst / best` as e.g. "7.5×" for the recommendation rationale.
fn ratio_str(worst: f64, best: f64) -> String {
if best <= 1e-9 {
return "∞×".to_string();
}
format!("{:.1}×", worst / best)
}