fix(fire-target): Family/home targets monotonic (kills Family==Household)
Some checks failed
Build and Push / lint-and-test (push) Has been cancelled
Build and Push / build (push) Has been cancelled
Build and Push / deploy (push) Has been cancelled
Build and Push / notify-failure (push) Has been cancelled

The recompute solved each Case's FIRE number with an independent binary search,
so Monte-Carlo path noise + the coarse £15k tolerance made Family (Household + 2
kids) tie or even UNDERCUT Household (6 hard inversions + 5 exact ties across the
22 countries) — the ~£20k kids cost quantised to ~£0.

Now solve the Cases in increasing-cost order and lower-bound each by the previous
Case's target on the SAME return paths: a heavier Case (more spend / +kids /
+home) can never need less net worth than a lighter one, so
Solo <= Household <= Family <= Family+home holds by construction. tol tightened
15k -> 1k so the genuine but small kids/home increment resolves instead of
snapping to the previous grid step. Kids/home were already modelled correctly
(verified) — this is purely a solver-resolution + monotonicity fix.

Found + verified via the fire-countdown flaw-hunt workflow. 346 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Viktor Barzin 2026-07-01 22:42:34 +00:00
parent 6efe3b0c31
commit 1b8809a01b
2 changed files with 33 additions and 3 deletions

View file

@ -112,3 +112,20 @@ def test_unreachable_bar_returns_not_reached() -> None:
# Spend far above what any NW in range can sustain.
res = solve_target_nw(_paths(), _inp(annual_spend_gbp=2_000_000.0), hi=1_000_000.0, tol=2_000.0)
assert not res.reached_bar
def test_nested_lo_bound_guarantees_case_monotonicity() -> None:
"""The recompute solves Cases in increasing-cost order, lower-bounding each
by the previous target on the same paths. That must yield
Solo <= Household <= Family <= Family+home with NO inversions the fix for
the 'Family == / < Household' Monte-Carlo-noise flaw."""
paths = _paths()
solo = solve_target_nw(paths, _inp(annual_spend_gbp=30_000.0), tol=1_000.0)
hh = solve_target_nw(paths, _inp(annual_spend_gbp=50_000.0),
lo=solo.target_nw_gbp, tol=1_000.0)
fam = solve_target_nw(paths, _inp(annual_spend_gbp=50_000.0, kids_annual_gbp=12_000.0),
lo=hh.target_nw_gbp, tol=1_000.0)
famh = solve_target_nw(paths, _inp(annual_spend_gbp=50_000.0, kids_annual_gbp=12_000.0,
with_home=True, home_amount_gbp=100_000.0),
lo=fam.target_nw_gbp, tol=1_000.0)
assert solo.target_nw_gbp <= hh.target_nw_gbp <= fam.target_nw_gbp <= famh.target_nw_gbp