Commit graph

916 commits

Author SHA1 Message Date
Jun-te Kim
ecefd439fd Cylinder with unknown insulation defaults per RdSAP Table 29 age band 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 16:11:10 +00:00
Jun-te Kim
233f085fb3 Elmhurst 'No Insulation' cylinder (0 mm lodged) takes the uninsulated storage loss 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 16:08:46 +00:00
Jun-te Kim
e813cc0ef2 Uninsulated cylinder (type 0) takes the SAP Table 2 jacket t=0 storage loss 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 16:06:33 +00:00
Jun-te Kim
e08048244a
Merge pull request #1429 from Hestia-Homes/fix/property-overlay-storage-category
Set electric-storage category (7) on storage property overlays
2026-07-02 12:51:58 +01:00
Jun-te Kim
4ca3491eb2
Merge pull request #1426 from Hestia-Homes/fix/hhrsh-storage-offpeak-pricing
Fix HHRSH storage heaters priced at peak rate (engine 48 vs accredited Elmhurst 71)
2026-07-02 12:50:32 +01:00
KhalimCK
61321d0d47
Merge pull request #1422 from Hestia-Homes/fix/roof-vaulted-nd-sentinel-eligibility
Roof eligibility resolves sentinel thickness by age band (vaulted joins sloping)
2026-07-02 12:45:51 +01:00
Jun-te Kim
e27ecf4a26 Set electric-storage category (7) on storage property overlays
`_category_for` mapped room heaters / heat pumps / heat networks but
returned None for electric-storage-heater codes (401-409), so a storage
system override emitted `sap_main_heating_code=409` +
`main_heating_category=None` while `_control_for` right beside it correctly
set the storage charge control — an asymmetry. Map storage codes to
category 7 so the overlaid cert is internally consistent (code 401-409 <->
category 7), matching the control/fuel/meter companions the overlay drags.

Consistency follow-up to the modelling HHRSH off-peak-pricing fix (#1426):
that PR's Table 12a resolver already prices any storage-coded system
off-peak regardless of category, so this is NOT a live mispricing — it
removes a latent code<->category inconsistency that other category-keyed
logic (responsiveness, pumps/fans) could trip on.

Tests: storage/HHRSH overlay tests now also assert `main_heating_category==7`.
tests/domain/epc (347) + prediction/override/reclassify (118) green; source
pyright-clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 11:45:49 +00:00
Jun-te Kim
f5fbeb8e7e Fix HHRSH storage heaters priced at peak rate (engine 48 vs Elmhurst 71)
The high-heat-retention storage-heater (HHRSH) recommendation was computed
as LOWERING SAP and wrongly rejected by the Optimiser, leaving electric-
room-heater flats/maisonettes stuck at band E with no sensible plan.

Root cause: `_HHR_STORAGE_OVERLAY` sets `sap_main_heating_code=409` (HHR
storage) + `meter_type="Dual"` but left `main_heating_category` at the
pre-existing value (10 = electric room heaters). The SAP 10.2 Table 12a
space-heating high-rate-fraction resolver classifies by category, so it
misread the off-peak storage heating as direct-acting electric (1.00
high-rate) and priced 100% at the 7-hour peak rate (15.29p) instead of the
storage off-peak split (0.00, ~5.5p).

Verified against accredited Elmhurst RdSAP-10 on UPRN 100020942571
(8 Booth Close, SE28 8BW): engine HHRSH SAP 48 (peak-priced, cost £1087)
vs Elmhurst 71 (cost ~£611); with the fix the engine gives 73 / £576.

Two-part fix (defence in depth, so no future overlay/mapper can
reintroduce it):
- `_HHR_STORAGE_OVERLAY` now sets `main_heating_category=7`, keeping the
  overlaid EPC internally consistent.
- `_table_12a_system_for_main` now classifies electric storage heaters by
  their Table 4a SAP code (401-409) as well as `category==7`, checked
  before the room-heater branch, so a specific storage code is
  authoritative over a stale/inconsistent category.

Tests: new regression `test_storage_heater_classified_by_sap_code_over_stale_category`;
updated the HHRSH overlay pin. Full accuracy corpus still passes (no regressions).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 11:24:03 +00:00
Khalim Conn-Kowlessar
6d812d99c5 An unlodged roof type with ND thickness gets no roof recommendation 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 11:20:55 +00:00
Jun-te Kim
248e230d37 Dominant secondary-glazing split resolves deterministically to SECONDARY 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 11:10:40 +00:00
Khalim Conn-Kowlessar
63c889bd02 Roof dispatcher docstring and constants reflect the sentinel-aware pitched trigger 🟪
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 11:09:07 +00:00
Jun-te Kim
f873d5cad3 Landlord 'Secondary glazing' override overlays SAP10 glazing code 5 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 11:08:16 +00:00
Khalim Conn-Kowlessar
1d13dee38e ND loft roof on a pre-1950 dwelling gets loft insulation 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 11:05:47 +00:00
Khalim Conn-Kowlessar
f3bdf0e884 Vaulted ND roof on a pre-1950 dwelling gets sloping-ceiling insulation 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 11:01:43 +00:00
Jun-te Kim
c1169b2d39
Merge pull request #1402 from Hestia-Homes/fix/glazing-dominant-single-guard
Resolve dominant-single glazing deterministically in the mix guard
2026-07-02 10:19:16 +01:00
Jun-te Kim
0fa47b1047
Merge pull request #1409 from Hestia-Homes/fix/property-type-leading-token-guard
Guard property_type to its leading dwelling-type token
2026-07-02 10:00:03 +01:00
Jun-te Kim
761dcf2a82
Merge pull request #1406 from Hestia-Homes/fix/wood-logs-main-fuel-archetype
Add individual wood-logs main-fuel archetype (API 6 / RdSAP Table 32 code 20)
2026-07-02 09:57:25 +01:00
Jun-te Kim
deec22302a Resolve dominant double/triple glazing when the era is stated
Addresses PR #1402 review: the guard only claimed dominant-single, leaving
the symmetric bug open — the LLM can flatten a dominant-double/triple split
onto its minority single (e.g. "96% Double glazing 2002 or later, 4% Single").

A dominant double/triple whose era is explicit ("pre-2002" / "2002 or later")
is just as fully determined as era-free single, so the guard now claims it via
_DOMINANT_MEMBER. Only a genuinely ambiguous era ("unknown age", unstated) still
defers to the LLM — the "96% double -> None" contract now holds solely for the
era-unknown case, not the era-stated one.

Backfill script reuses the same guard, so it now corrects any dominant split;
renamed reclassify_dominant_single_glazing.py -> reclassify_dominant_glazing.py
to match. Tests cover double/triple x pre-2002/2002-or-later and the still-
deferred unknown-age case; 14 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 08:07:13 +00:00
Jun-te Kim
88bae2166f Guard property_type to its leading dwelling-type token
The landlord property-type description is a "<dwelling type>: <built form>: <floor>"
split whose leading token IS the dwelling type; the built-form tail is not. The
LLM occasionally over-read the tail and flipped the type — a handful of
"Bungalow: EndTerrace" / "Bungalow: MidTerrace" dwellings were stored as House.

Adds property_type_guard (claims the recognised leading token: House / Bungalow /
Flat / Maisonette / Park home; defers unrecognised phrasings to the LLM) and wires
property_type through a GuardedColumnClassifier, so the built-form tail can never
flip the type and the live path is deterministic.

Applied the scoped backfill to portfolio 796 (Hyde): 3 rows corrected from House
back to Bungalow. No enum migration needed — the targets are original members.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 17:24:37 +00:00
Jun-te Kim
5330bc20d9 Add individual wood-logs main-fuel archetype (API 6 / RdSAP Table 32 code 20)
MainFuelType had no individual wood-logs member — only "biomass (community)" —
so the LLM classifier funnelled "Solid Fuel: Wood Logs" into the community fuel,
inventing a community heat network the dwelling isn't on (and mislabelling the
connection). main_fuel had no deterministic guard at all, so nothing caught it.

Verified against domain/sap10_calculator/docs/specs: RdSAP 10 Specification
Table 32 lists "wood logs" as a solid fuel (code 20, 0.028 kgCO2e/kWh); the
calculator's input scheme (the gov EPC API fuel enum) codes it 6 -> Table 32 20
(sap_efficiencies._API_TO_TABLE32), and water_heating_overlay already pins the
same fuel to 6. So _FUEL_CODES["wood logs"] = 6 is confirmed, not guessed.

Adds MainFuelType.WOOD_LOGS + the _FUEL_CODES entry, a main_fuel_guard mirroring
water_heating_guard (claims the "wood log" token; dual fuel keeps its own member
since it has no "wood log" substring), and wires main_fuel through a
GuardedColumnClassifier so the live path is deterministic.

Applied the scoped backfill to portfolio 796 (Hyde): 21 rows off
"biomass (community)" -> "wood logs". property_overrides (TEXT) only; the
classifier-cache pgEnum member is deferred to the FE Drizzle migration.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 17:00:24 +00:00
Khalim Conn-Kowlessar
9d106d9d6d Deterministically guard the electric-underfloor descriptions 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 16:48:45 +00:00
Khalim Conn-Kowlessar
6883864c32 Defer the screed-underfloor meter to the cert while off-peak underfloor asserts Dual 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 16:46:33 +00:00
Khalim Conn-Kowlessar
749ac3a5ff Resolve electric-underfloor overrides to their own SAP codes dragging electricity 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 16:36:21 +00:00
Khalim Conn-Kowlessar
fedae2ea00 Resolve an electric-CPSU override to SAP 192 electric on an off-peak meter 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 16:09:56 +00:00
Jun-te Kim
646f66ff89 Resolve dominant-single glazing deterministically in the mix guard
A landlord glazing split like "4% Double glazing 2002 or later, 96% Single
Glazing" is a near-uniform *single*-glazing dwelling, but the LLM classifier
latched onto the 4% minority and wrote a "Double glazing…" override — over-
crediting the dwelling.

glazing_mix_guard rescued genuine mixes (no type >= 90%) to MIXED but returned
None for near-uniform splits, trusting the LLM to have applied the dominant type.
It hadn't. Single glazing is era-free, so a dominant-single split is fully
determined: the guard now returns SINGLE for it (dominant double/triple still
carry era ambiguity, so they stay the LLM's job — the existing "96% double" case
still defers).

Adds a portfolio-scoped backfill (reuses the same guard, so it can't drift from
the live path) and applied it to portfolio 796 (Hyde): 115 glazing overrides
corrected from a spurious Double back to Single.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 15:59:18 +00:00
Khalim Conn-Kowlessar
f6f4c3717d Deterministically guard the Gas CPSU solid-fuel and electric-boiler descriptions 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 15:58:29 +00:00
Khalim Conn-Kowlessar
098edf6592 Resolve an electric-boiler override to SAP 191 dragging electricity 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 15:58:27 +00:00
Khalim Conn-Kowlessar
de4e44aacc Resolve solid-fuel room-heater overrides off Gas CPSU to their SAP codes 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 15:58:25 +00:00
Jun-te Kim
5b4a1b4966 Classify PCDB-index gas boilers with no SAP code for heating recs
Gov-API certs that lodge a gas boiler as main_heating_category=2 + a PCDB
main_heating_index_number but omit sap_main_heating_code were carried with
a null code, so the heating generators (which gated on sap_main_heating_code
alone) never evaluated them for a boiler upgrade or a system tune-up — they
silently got nothing (£0 works, plateau at D). The SAP calculator already
handles this shape (cert_to_inputs._is_wet_boiler_main), so the calculator
modelled these as wet boilers while the recommenders ignored them.

Mirror the calculator's classification in the generator:
- _is_wet_boiler / _is_electric_boiler fall back, when no SAP code is
  lodged, to a PCDB Table 105 boiler index or main_heating_category in
  {1,2} (heat pumps excluded); electric boilers stay excluded by the code
  range when lodged, else by an electricity main fuel.
- _already_condensing reads the boiler's declared PCDB winter efficiency
  when there is no Table 4b code, so a modern condensing boiler lodged this
  way still isn't offered a pointless like-for-like swap (gets a tune-up).

Verified read-only on portfolio 814 / scenario 1271: pids 742304, 742306,
742322, 742325, 742336 now get a gas_boiler_upgrade (two cross D->C on the
boiler alone) where they had £0 works; predicted properties whose heating
donor is a PCDB-shape boiler (e.g. 742095/742102/742105) also recover a
tune-up. Fixes the A#2 slice of #1388 (and unblocks 742302/742367).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 14:52:47 +00:00
Daniel Roth
0488696df8
Merge pull request #1397 from Hestia-Homes/fix/1376-hhrsh-baseline-archetype
Add the HHRSH baseline archetype so it stops scoring as old storage (#1376)
2026-07-01 15:41:52 +01:00
Daniel Roth
ae4a8569e9
Merge pull request #1396 from Hestia-Homes/community-heating-hhrsh
Don't recommend HHRSH or ASHP to properties with community heating
2026-07-01 15:33:57 +01:00
Khalim Conn-Kowlessar
b7848848a5 Deterministically guard the high-heat-retention storage description 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 14:30:40 +00:00
Khalim Conn-Kowlessar
9711b2e5f5 Deterministically guard the high-heat-retention storage description 🟥
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 14:30:13 +00:00
Khalim Conn-Kowlessar
01e7e1d831 Drag HHRSH's intrinsic control, electricity fuel and off-peak meter 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 14:29:41 +00:00
Khalim Conn-Kowlessar
a2083b9c7d Resolve a high-heat-retention storage override to SAP code 409 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 14:27:30 +00:00
Daniel Roth
baaadcdf98
Merge pull request #1392 from Hestia-Homes/fix/1376-water-heating-house-coal
Reclassify water-heating overrides off the house-coal dumping ground (#1376)
2026-07-01 15:19:39 +01:00
Daniel Roth
6eb2a02a6e Gate community-heated dwellings from ASHP recommendation 🟩
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-01 14:18:52 +00:00
Daniel Roth
3d562be398 Gate community-heated dwellings from HHRSH recommendation 🟩
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-01 14:14:52 +00:00
Daniel Roth
afe6638b36 Gate community-heated dwellings from HHRSH recommendation 🟥
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-01 14:13:57 +00:00
Daniel Roth
221e310fd8
Merge pull request #1382 from Hestia-Homes/incorrect-overrides
Fix HHRSH eligibility for on-gas-street electric dwellings; rename mains_gas → gas_connection_available
2026-07-01 14:53:14 +01:00
Khalim Conn-Kowlessar
d0aa596ae7 Deterministically guard structured biomass and immersion water-heating descriptions 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 13:06:52 +00:00
Khalim Conn-Kowlessar
d7f069b7c5 Deterministically guard structured biomass and immersion water-heating descriptions 🟥
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 13:05:47 +00:00
Khalim Conn-Kowlessar
84fcf9da49 Resolve biomass, dual-fuel and biodiesel water-heating overrides off house coal 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 13:03:28 +00:00
Khalim Conn-Kowlessar
ea3696e8d5 Resolve a from-main wood-logs water-heating override to the wood fuel 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 13:02:13 +00:00
KhalimCK
9db8b60a54
Merge pull request #1384 from Hestia-Homes/fix/1376-glazing-proportion-reconcile
Defer aggregate-mix glazing overrides to the cert's per-window glazing (#1376)
2026-07-01 13:37:28 +01:00
Daniel Roth
90abcd9f54 Merge branch 'main' into incorrect-overrides 2026-07-01 12:30:15 +00:00
Khalim Conn-Kowlessar
38448e47cc Classify an aggregate-mix glazing description as MIXED 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 11:04:24 +00:00
Khalim Conn-Kowlessar
f959cc14c9 Classify an aggregate-mix glazing description as MIXED 🟥
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 11:03:28 +00:00
Daniel Roth
9b58bebfb8 Rename SapEnergySource.mains_gas to gas_connection_available 🟩
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-01 10:59:21 +00:00
KhalimCK
9968e2b9df
Merge pull request #1379 from Hestia-Homes/fix/1376-flat-roof-insulation-thickness
Score flat roofs by their known insulation depth, not the age-band default (#1376)
2026-07-01 11:58:46 +01:00