mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
Add plan-stuck-in-low-band audit check + refresh weird-recs catalogue
`plan-stops-short-of-goal` fires on anything below the goal band — 322/338 on portfolio 814, mostly flats a single band short of B, which drowns the real hits. Add a sharper, band-keyed check that isolates the genuinely- stuck tail: a plan that leaves the dwelling still rated E/F/G with the band UNCHANGED from baseline on an unlimited-budget scenario — the "E->E, no big movement" worklist. On portfolio 814 / scenario 1271 it flags just 4 (pids 742121, 742210, 742265, 742347) vs 322. No SAP-points threshold; the strictly-worse case is left to plan-below-baseline-band so the two partition cleanly. Also refresh the find-weird-recommendations skill's Phase 4 catalogue: register the new check in Phase 1, correct the stale 742121 note (it is an electric maisonette whose HHRSH IS offered but scores -6.3 SAP, so the Optimiser correctly drops it — not the old mains-gas story), and add the 742265 community-flat findings (HHRSH/ASHP gated out by heat-network topology; vaulted-ceiling roof with ND insulation). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2892ce8560
commit
66d70b6833
2 changed files with 84 additions and 1 deletions
|
|
@ -61,6 +61,17 @@ Writes `modelling_audit.md` / `.csv`. The check that carries this skill:
|
|||
no budget cap, the only reasons to fall short are (a) a measure was wrongly
|
||||
withheld — a bug — or (b) the dwelling genuinely can't reach the band. Phase 4
|
||||
decides which. Budget-capped scenarios are excluded (falling short is expected).
|
||||
**Caveat: this check is noisy on flat-heavy portfolios** — it fired 322/338 on
|
||||
portfolio 814 (every flat a band short of B). Use `plan-stuck-in-low-band` to
|
||||
cut straight to the real hits.
|
||||
- **`plan-stuck-in-low-band`** (HIGH) — the plan leaves the dwelling **still
|
||||
rated E/F/G with the band unchanged** from baseline (unlimited budget). This is
|
||||
the sharp "E→E, no big movement" worklist — the genuinely-trapped tail, not
|
||||
every dwelling a band short of goal. On portfolio 814 it flagged just **4**
|
||||
(pids 742121, 742210, 742265, 742347) vs `plan-stops-short-of-goal`'s 322. A
|
||||
wrongly-withheld measure or a physically-trapped dwelling both surface here;
|
||||
Phase 4 tells them apart. Band-keyed (no SAP-points threshold), and the
|
||||
strictly-worse case is left to `plan-below-baseline-band` so the two partition.
|
||||
|
||||
Also read `plan-below-baseline-band`, `already-meets-goal-with-works`, and — if
|
||||
you pass `--with-recommendations` (EXPLAIN-gated; confirm with the user first) —
|
||||
|
|
@ -136,11 +147,39 @@ Match each deep-dive against these before hypothesising a novel cause:
|
|||
main fuel / heat-network flag, then fix by restoring the gate in
|
||||
`_hhr_storage_eligible` (community heating ⇒ not HHR-eligible). A community
|
||||
network is a shared asset a single dwelling can't rip out for storage heaters.
|
||||
- **Measure withheld → plan stops short of goal** (pid 742121, "why no HHRSH").
|
||||
- **Measure withheld → plan stops short of goal** (the "why no HHRSH" class).
|
||||
The dwelling can't reach the goal band, and the expected bundle is *absent from
|
||||
candidates*. Read the generator's eligibility predicate against the live EPC —
|
||||
an over-tight gate (a fuel/description/category condition that shouldn't
|
||||
exclude this dwelling) is the usual cause.
|
||||
- **Measure OFFERED but not selected because it *lowers* SAP** (pid 742121, "why
|
||||
no HHRSH" — re-modelled 2026-07, NOT the stale mains-gas story). This dwelling
|
||||
is an **electric** maisonette (`main_fuel_type=29`), so HHRSH **is** in the
|
||||
candidate set — eligibility passes. But scoring the HHRSH overlay in isolation
|
||||
gives **−6.3 SAP** (54.3 → 48.0): swapping its efficient existing electric
|
||||
system for off-peak storage heaters *worsens* the cost-based SAP, so the
|
||||
Optimiser correctly drops it. Its fabric is already done (double-glazed, walls
|
||||
insulated, no cylinder, no own roof), so the plan is **empty** and it sits at E,
|
||||
unreachable to B — a legitimate £0 result, not a bug. **Lesson: a measure
|
||||
*absent from candidates* is a generator-gate bug; a measure *present but not
|
||||
chosen* is the Optimiser judging it not worth it — score the overlay
|
||||
(`PackageScorer.score(epc, [EpcSimulation(...)])`) before blaming a gate.** The
|
||||
old note here claimed 742121 was a mains-gas maisonette that "correctly gets
|
||||
only lighting" — that was stale; verify fuel against the live EPC every time.
|
||||
- **Community-heated flat stuck low, correctly** (pid 742265, "might need loft
|
||||
insulation?"). A **community-heated** flat (SAP main-heating code 301, category
|
||||
6) on portfolio 814. HHRSH and ASHP are both gated out by `is_heat_network_main`
|
||||
(the topology guard) — right, per the community bug above. Its roof is
|
||||
`"Pitched (vaulted ceiling)"` with insulation lodged as **`ND` (no data)**, so
|
||||
the loft generator withholds insulation: it only fires on a definite `0 mm`
|
||||
(`roof_insulation_thickness != _ROOF_UNINSULATED_MM` → None), and a vaulted
|
||||
ceiling isn't the loft-fallback's target anyway. Its only real lever is IWI
|
||||
(+6.7), not enough to clear E — so it stays E→E, legitimately. **Watch item:**
|
||||
a vaulted-ceiling roof with `ND` insulation gets *no* roof measure at all
|
||||
(neither the sloping-ceiling branch, which matches the literal `"sloping
|
||||
ceiling"`, nor the loft branch, which needs `0 mm`). If several such roofs turn
|
||||
out to be genuinely uninsulated upstream, that's a mapper/gate gap — but `ND`
|
||||
is unknown, not zero, so withholding is defensible for now.
|
||||
- **Missing `secondary_heating_removal`** (pid 742264, "gov site shows secondary
|
||||
heating"). Check whether the live EPC carries a secondary-heating system and
|
||||
whether
|
||||
|
|
|
|||
|
|
@ -175,6 +175,50 @@ def _plan_stops_short_of_goal(a: PropertyAudit) -> Optional[str]:
|
|||
)
|
||||
|
||||
|
||||
@check("plan-stuck-in-low-band", Severity.HIGH)
|
||||
def _plan_stuck_in_low_band(a: PropertyAudit) -> Optional[str]:
|
||||
"""The plan leaves the dwelling STUCK in a low band (E or worse) with the band
|
||||
UNCHANGED from baseline — the engine spent (or spent nothing) yet moved the
|
||||
dwelling nowhere off the floor.
|
||||
|
||||
Complements ``plan-stops-short-of-goal``. That check fires on anything below
|
||||
the goal band — 322/338 on portfolio 814, mostly flats a single band short of
|
||||
B, which is expected and drowns the real hits. This one isolates the
|
||||
genuinely-stuck tail: the handful still rated E/F/G *after* works, where a
|
||||
wrongly-withheld measure or a physically-trapped dwelling actually shows up.
|
||||
It is the "E→E, no big movement" worklist the tech team asked for.
|
||||
|
||||
Motivated by portfolio 814 (scenario 1271, goal B, unlimited budget): only 4
|
||||
properties land E-or-worse and all 4 stay at E — pid 742121 (electric
|
||||
maisonette, £0 works: HHRSH *is* offered as a candidate but scores −6.3 SAP,
|
||||
so the Optimiser correctly drops it, and the fabric is already done — no lever
|
||||
left); pid 742265 (community-heated flat, SAP code 301: HHRSH and ASHP are
|
||||
gated out by the heat-network topology, leaving IWI its only real lever); pids
|
||||
742210 / 742347 (flats that move ~5-8 SAP but never clear E). Legitimate-but-
|
||||
trapped and real-bug cases both surface here — the deep-dive
|
||||
(``run_modelling_e2e`` + the candidates CSV) tells them apart. Budget-capped
|
||||
scenarios are excluded: staying low is expected once the money runs out.
|
||||
|
||||
Band-keyed, not a SAP-points threshold, so it needs no tuned magic number —
|
||||
"post band E/F/G and no band improvement" is the whole rule. The strictly-
|
||||
worse case (post below baseline) is left to ``plan-below-baseline-band`` so
|
||||
the two partition cleanly."""
|
||||
post, base = _band_rank(a.post_band), _band_rank(a.effective_band)
|
||||
if post is None or base is None:
|
||||
return None
|
||||
if post < _BANDS.index("E"): # A–D: out of the low tail, not "stuck"
|
||||
return None
|
||||
if post != base: # band moved (a lift, or the worse case above) — not stuck
|
||||
return None
|
||||
if a.scenario_budget is not None: # budget-capped: staying low is expected
|
||||
return None
|
||||
return (
|
||||
f"still {a.post_band} ({a.post_sap}) after works — no band lift from "
|
||||
f"baseline {a.effective_band} ({a.effective_sap}) on an unlimited-budget "
|
||||
f"scenario (cost_of_works £{a.cost_of_works or 0:.0f})"
|
||||
)
|
||||
|
||||
|
||||
@check("post-band-score-mismatch", Severity.MEDIUM)
|
||||
def _post_band_score_mismatch(a: PropertyAudit) -> Optional[str]:
|
||||
"""The persisted post band disagrees with the band the post SAP implies — a
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue