feat(examples): personalize jiggly weights with hinge press term and balance bonus
Tweak the a-posteriori scoring to match the user's stated preferences:
- Reweight: lunch_sleep 30%, after_hours 25%, work_fail 20%,
presses 15% (with hinge below), balance 10%.
- Press term is now a hinge instead of a normalized minimize:
* <= 2 presses/day → score 1.0 (no penalty)
* 2 → 3 presses/day → linear ramp from 1.0 to 0.0
* > 3 presses/day → -inf (excluded; comfort cap)
- New balance term: bonus for longer warning phases. Computed as
min(YA - RA, RA - FRA), saturated at 10 minutes. So a 5/5/X split
scores 0.5, an 8/8/X split scores 0.8, and 10/10/X or wider saturates
at 1.0.
Constants moved to module scope so the printout in main and the
scoring function stay in sync.
This commit is contained in:
+106
-34
@@ -62,6 +62,24 @@ const SWEET_HI: u32 = 45;
|
||||
|
||||
const N_DAYS: usize = 1000;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// A-posteriori decision weights (must sum to 1.0).
|
||||
// -----------------------------------------------------------------------------
|
||||
const W_LUNCH: f64 = 0.30; // top — design goal
|
||||
const W_AFTER: f64 = 0.25; // top — minimize after-hours waste
|
||||
const W_WORK: f64 = 0.20; // medium — failures bad but recoverable
|
||||
const W_PRESS: f64 = 0.15; // matters with a hinge below
|
||||
const W_BALANCE: f64 = 0.10; // bonus for longer yellow + red phases
|
||||
|
||||
// Press hinge: full reward at or below LOW, linearly drops to 0 at COMFORT_CAP,
|
||||
// and any candidate with mean_presses > COMFORT_CAP is rejected outright.
|
||||
const PRESS_HINGE_LOW: f64 = 2.0;
|
||||
const PRESS_COMFORT_CAP: f64 = 3.0;
|
||||
|
||||
// Balance bonus saturates: a min(yellow_width, red_width) of >= this many
|
||||
// minutes scores the full balance term.
|
||||
const BALANCE_SATURATION_MIN: f64 = 10.0;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Day model + Monte Carlo (same model as scripts/tune_runtime.py)
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -443,16 +461,18 @@ fn main() {
|
||||
//
|
||||
// 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.
|
||||
// weights to four normalized outcome axes plus two structural terms:
|
||||
//
|
||||
// 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
|
||||
// * `lunch_sleep` (max), `after_hours` (min), `work_fail` (min) —
|
||||
// normalized to [0, 1] across the candidate set.
|
||||
// * `presses` — hinge: full reward when <= PRESS_HINGE_LOW, ramps to
|
||||
// zero at PRESS_COMFORT_CAP, candidates above the cap are rejected.
|
||||
// * `balance` — bonus for longer warning phases:
|
||||
// `min(YA - RA, RA - FRA)` saturated at BALANCE_SATURATION_MIN.
|
||||
//
|
||||
// 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.
|
||||
|
||||
let mut candidates: Vec<(String, Row)> = rows
|
||||
.iter()
|
||||
@@ -468,11 +488,20 @@ fn main() {
|
||||
|
||||
println!("=== ranked by weighted preferences ===");
|
||||
println!(
|
||||
" weights: work_fail {}% · lunch_sleep {}% · presses {}% · after_hours {}%",
|
||||
(W_WORK * 100.0) as i32,
|
||||
" weights: lunch_sleep {}% · after_hours {}% · work_fail {}% · presses {}% · balance {}%",
|
||||
(W_LUNCH * 100.0) as i32,
|
||||
(W_PRESS * 100.0) as i32,
|
||||
(W_AFTER * 100.0) as i32,
|
||||
(W_WORK * 100.0) as i32,
|
||||
(W_PRESS * 100.0) as i32,
|
||||
(W_BALANCE * 100.0) as i32,
|
||||
);
|
||||
println!(
|
||||
" press hinge: full reward ≤ {:.0}/d, ramps to 0 at {:.0}/d, REJECTED above",
|
||||
PRESS_HINGE_LOW, PRESS_COMFORT_CAP,
|
||||
);
|
||||
println!(
|
||||
" balance bonus: min(yellow_width, red_width), saturates at {:.0} min",
|
||||
BALANCE_SATURATION_MIN,
|
||||
);
|
||||
println!(" candidate set: {} Pareto-front rows + 1 shipping default", rows.len());
|
||||
println!();
|
||||
@@ -494,7 +523,6 @@ fn main() {
|
||||
.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!(
|
||||
@@ -506,23 +534,42 @@ fn main() {
|
||||
);
|
||||
println!(" weighted score = {top_score:.3}");
|
||||
println!();
|
||||
let yellow_w = top.ya - top.ra;
|
||||
let red_w = top.ra - top.fra;
|
||||
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:15–12:45 sweet spot)",
|
||||
fmt_minutes(top.lunch),
|
||||
top.p_sweet * 100.0,
|
||||
);
|
||||
println!(
|
||||
" • {:.2} button presses/day ({} fewer than the worst candidate)",
|
||||
" • {} mean after-hours awake (kept tight, your second priority)",
|
||||
fmt_minutes(top.after),
|
||||
);
|
||||
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)),
|
||||
);
|
||||
let press_note = if top.presses <= PRESS_HINGE_LOW {
|
||||
format!("inside your no-penalty zone ≤{:.0}/d", PRESS_HINGE_LOW)
|
||||
} else {
|
||||
format!("{:.2}/d above the {:.0}-press hinge", top.presses - PRESS_HINGE_LOW, PRESS_HINGE_LOW)
|
||||
};
|
||||
println!(
|
||||
" • {:.2} button presses/day ({}, {} below your {:.0}/d comfort cap)",
|
||||
top.presses,
|
||||
ratio_str(max_press, top.presses.max(1e-9)),
|
||||
press_note,
|
||||
ratio_str(PRESS_COMFORT_CAP, top.presses.max(1e-9)),
|
||||
PRESS_COMFORT_CAP,
|
||||
);
|
||||
println!(
|
||||
" • warning phases: yellow {} min, red {} min, fast-red {} min (balance score {:.2})",
|
||||
yellow_w,
|
||||
red_w,
|
||||
top.fra,
|
||||
balance_score_for(top),
|
||||
);
|
||||
println!(" • {} mean after-hours awake (negligible)", fmt_minutes(top.after));
|
||||
|
||||
if top_label != "shipping default" {
|
||||
println!();
|
||||
@@ -540,37 +587,62 @@ fn main() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Score every row in `rows` by a fixed weighted sum of normalized objectives.
|
||||
/// Score every row in `rows` by a weighted sum that combines normalized
|
||||
/// outcome axes with a press hinge and a phase-balance bonus.
|
||||
///
|
||||
/// 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).
|
||||
/// `work_fail`, `lunch`, and `after` are normalized to `[0, 1]` across `rows`
|
||||
/// (best→1, worst→0; direction-aware). `presses` uses a hinge that rewards
|
||||
/// values at or below `PRESS_HINGE_LOW`, ramps linearly to zero at
|
||||
/// `PRESS_COMFORT_CAP`, and rejects candidates above the cap by returning
|
||||
/// `f64::NEG_INFINITY`. `balance` is a bonus for longer yellow + red
|
||||
/// phases, computed as `min(YA - RA, RA - FRA)` saturated at
|
||||
/// `BALANCE_SATURATION_MIN`.
|
||||
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| {
|
||||
// Hard comfort cap on presses.
|
||||
if r.presses > PRESS_COMFORT_CAP {
|
||||
return f64::NEG_INFINITY;
|
||||
}
|
||||
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
|
||||
// Hinge: 1.0 at or below LOW, linear ramp to 0.0 at the cap.
|
||||
let press_score = if r.presses <= PRESS_HINGE_LOW {
|
||||
1.0
|
||||
} else {
|
||||
((PRESS_COMFORT_CAP - r.presses) / (PRESS_COMFORT_CAP - PRESS_HINGE_LOW))
|
||||
.clamp(0.0, 1.0)
|
||||
};
|
||||
// Balance bonus: longer yellow + red is better, saturated.
|
||||
let balance_score = balance_score_for(r);
|
||||
|
||||
W_LUNCH * lunch
|
||||
+ W_AFTER * after
|
||||
+ W_WORK * work
|
||||
+ W_PRESS * press_score
|
||||
+ W_BALANCE * balance_score
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Balance bonus for a row: `min(YA - RA, RA - FRA)` clamped to
|
||||
/// `[0, BALANCE_SATURATION_MIN]` and divided by saturation so the result is
|
||||
/// in `[0, 1]`.
|
||||
fn balance_score_for(r: &Row) -> f64 {
|
||||
let yellow_w = (r.ya - r.ra) as f64;
|
||||
let red_w = (r.ra - r.fra) as f64;
|
||||
let raw = yellow_w.min(red_w).max(0.0);
|
||||
(raw / BALANCE_SATURATION_MIN).clamp(0.0, 1.0)
|
||||
}
|
||||
|
||||
/// 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) }
|
||||
|
||||
Reference in New Issue
Block a user