mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Slice 58: secondary fuel cost routes through lodged secondary_fuel_type
Two coupled bugs surfaced by cert 001479's mains-gas-fire secondary heating (Summary §14.1 lodges "SAP code 605, Flush fitting live effect gas fire" → fuel 26 mains gas): 1. **Mapper**: `_map_elmhurst_sap_heating` only set `secondary_heating_type` (the SAP code int) — `secondary_fuel_type` stayed None. The Summary PDF doesn't lodge the fuel int separately; it has to be derived from the SAP code range. Add `_elmhurst_secondary_fuel_from_sap_code`: codes 601-630 → 26 (mains gas); other codes return None (the cascade defaults to electric, matching cohort 000490 SAP code 691 electric panel). 2. **Cascade**: `_fuel_cost` in cert_to_inputs hardcoded `secondary_high_rate_gbp_per_kwh = other_uses_gbp_per_kwh` (the standard-electricity tariff) regardless of `secondary_fuel_type`. For gas secondaries this charged 1846 kWh/yr at electric rate (£0.132/kWh = £243) instead of gas rate (£0.0348/kWh = £64) — a ~£175/yr ECF distortion ≈ 9 SAP points on cert 001479. Route the cost through `table_32_unit_price_p_per_kwh(secondary_fuel)` when lodged. Worksheet line (242) confirms the gas pricing: `Space heating - secondary 2025.93 3.4800 70.5022` Cert 001479 chain pin delta narrows: SAP_continuous 61.39 → 70.64 (was −7.62 vs 69.0094, now +1.63 — overshooting target by 1.63 SAP). The remaining overshoot maps to the cascade's ~16 W/K HLC undercount (cascade HLP 2.89 vs worksheet 3.13 × TFA) — work for follow-up slices. Cohort 6 chain certs still green at 1e-4 (all-electric or no- secondary). Golden cohort: cert 0300-2747 (mains-gas secondary) SAP residual tightens −7 → +2 — biggest single SAP improvement on the golden cohort to date; pin updated and notes annotated. Other 7 golden certs unchanged (None or electric secondary fuel). Pyright net-zero (35 baseline each on mapper.py + cert_to_inputs.py). Chain pin `test_summary_001479_full_chain_sap_matches_worksheet_pdf_ exactly` is the load-bearing RED — committed failing per TDD; closes to GREEN once the HLC undercount lands. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
a0d9d09410
commit
e3dc0b28f5
4 changed files with 94 additions and 6 deletions
|
|
@ -327,3 +327,54 @@ def test_summary_001479_ext2_sloping_ceiling_roof_uninsulated_for_pre_1950() ->
|
|||
# Assert
|
||||
assert epc.sap_building_parts[2].roof_insulation_thickness == 0
|
||||
assert epc.sap_building_parts[1].roof_insulation_thickness is None
|
||||
|
||||
|
||||
def test_summary_001479_secondary_heating_routes_mains_gas_fuel() -> None:
|
||||
# Arrange — cert 001479 §14.1 Main Heating2 lodges "Secondary Heating
|
||||
# Code: SAP code 605, Flush fitting live effect gas fire, sealed to
|
||||
# chimney". The Summary surfaces only the SAP code (605); the fuel
|
||||
# type 26 (mains gas) must be derived from the code range so the
|
||||
# `_fuel_cost` orchestrator's `secondary_high_rate_gbp_per_kwh`
|
||||
# picks up Table 32's gas tariff (£0.0348/kWh) rather than the
|
||||
# default standard-electricity tariff (£0.132/kWh). Worksheet line
|
||||
# (242) "Space heating - secondary … 3.4800 70.5022" confirms gas
|
||||
# pricing.
|
||||
pages = _summary_pdf_to_textract_style_pages(_SUMMARY_001479_PDF)
|
||||
site_notes = ElmhurstSiteNotesExtractor(pages).extract()
|
||||
|
||||
# Act
|
||||
epc = EpcPropertyDataMapper.from_elmhurst_site_notes(site_notes)
|
||||
|
||||
# Assert
|
||||
assert epc.sap_heating.secondary_heating_type == 605
|
||||
assert epc.sap_heating.secondary_fuel_type == 26
|
||||
|
||||
|
||||
def test_summary_001479_full_chain_sap_matches_worksheet_pdf_exactly() -> None:
|
||||
# Arrange — cert 001479 (Summary_001479.pdf / P960-0001-001479.pdf)
|
||||
# is the first cohort cert with a real GOV.UK EPB API counterpart
|
||||
# (cert ref 0535-9020-6509-0821-6222). Worksheet PDF line "SAP value"
|
||||
# lodges unrounded SAP **69.0094** (rating C 69, also the API-
|
||||
# published integer). This is the load-bearing forcing function for
|
||||
# the API↔Elmhurst parity workstream: any drift from 1e-4 means a
|
||||
# mapper gap, not a calculator bug — the cohort 6 cert cascades all
|
||||
# reproduce Elmhurst exactly at 1e-4 on hand-built fixtures.
|
||||
#
|
||||
# Source-data caveat (documented for future debuggers): Summary §3
|
||||
# lodges Ext1 age band as "M 2023 onwards"; the worksheet header
|
||||
# records "Ext1: L". Likely assessor data-entry inconsistency. The
|
||||
# mapper trusts the Summary (its source of truth); accept whatever
|
||||
# residual the M vs L disagreement produces.
|
||||
pages = _summary_pdf_to_textract_style_pages(_SUMMARY_001479_PDF)
|
||||
site_notes = ElmhurstSiteNotesExtractor(pages).extract()
|
||||
epc = EpcPropertyDataMapper.from_elmhurst_site_notes(site_notes)
|
||||
|
||||
# Act
|
||||
result = calculate_sap_from_inputs(
|
||||
cert_to_inputs(epc, prices=SAP_10_2_SPEC_PRICES)
|
||||
)
|
||||
|
||||
# Assert — 1e-4 pin, no widening, no xfail (project memory
|
||||
# `feedback_zero_error_strict`).
|
||||
worksheet_unrounded_sap = 69.0094
|
||||
assert abs(result.sap_score_continuous - worksheet_unrounded_sap) < 1e-4
|
||||
|
|
|
|||
|
|
@ -2487,6 +2487,25 @@ def _elmhurst_heat_emitter_int(emitter: str) -> Optional[int]:
|
|||
return _ELMHURST_HEAT_EMITTER_TO_SAP10.get(emitter)
|
||||
|
||||
|
||||
def _elmhurst_secondary_fuel_from_sap_code(
|
||||
sap_code: Optional[int],
|
||||
) -> Optional[int]:
|
||||
"""Derive `secondary_fuel_type` from an Elmhurst secondary-heating SAP
|
||||
code. The Summary PDF lodges the SAP code (e.g. 605 for "Flush
|
||||
fitting live effect gas fire") but not the fuel int separately; the
|
||||
cascade's `_secondary_fuel_cost_gbp_per_kwh` defaults to standard
|
||||
electricity when `secondary_fuel_type` is None — correct for the
|
||||
portable-electric default but wrong for cert 001479's mains-gas fire.
|
||||
Returns 26 (mains gas) for SAP codes in the 600-630 range; None for
|
||||
other codes (cascade default fires, matching cohort 000490 SAP code
|
||||
691 electric panel)."""
|
||||
if sap_code is None:
|
||||
return None
|
||||
if 601 <= sap_code <= 630:
|
||||
return 26 # Mains gas, matching `_ELMHURST_MAIN_FUEL_TO_SAP10`
|
||||
return None
|
||||
|
||||
|
||||
def _elmhurst_sap_control_code(sap_control: str) -> Optional[int]:
|
||||
"""Extract the SAP code integer from a heating-controls field like
|
||||
'SAP code 2106, Programmer, room thermostat and TRVs' → 2106. The
|
||||
|
|
@ -2587,6 +2606,9 @@ def _map_elmhurst_sap_heating(survey: ElmhurstSiteNotes) -> SapHeating:
|
|||
),
|
||||
water_heating_code=survey.water_heating.water_heating_sap_code,
|
||||
secondary_heating_type=mh.secondary_heating_sap_code,
|
||||
secondary_fuel_type=_elmhurst_secondary_fuel_from_sap_code(
|
||||
mh.secondary_heating_sap_code,
|
||||
),
|
||||
number_baths=survey.baths_and_showers.number_of_baths,
|
||||
electric_shower_count=1 if has_electric_shower else None,
|
||||
mixer_shower_count=0 if has_electric_shower else None,
|
||||
|
|
|
|||
|
|
@ -1877,10 +1877,21 @@ def _fuel_cost(
|
|||
table_32_unit_price_p_per_kwh(water_heating_fuel_code or main_fuel_code)
|
||||
* _PENCE_TO_GBP
|
||||
)
|
||||
# Secondary fuel = standard electricity by default (portable electric
|
||||
# heater per §A.2.2). Scope A has no lodged secondaries; the fraction
|
||||
# is zero so the price contributes nothing to (242).
|
||||
secondary_high_rate_gbp_per_kwh = other_uses_gbp_per_kwh
|
||||
# Secondary fuel cost: route through the cert's `secondary_fuel_type`
|
||||
# when lodged (e.g. mains-gas fire SAP code 605 → fuel 26 → Table 32
|
||||
# gas price), otherwise default to standard electricity (the portable
|
||||
# electric heater per §A.2.2 — same as the cohort's electric panel
|
||||
# SAP code 691). Pre-slice this column hardcoded `other_uses_gbp_per_
|
||||
# kwh` regardless of fuel type, charging gas-secondary kWh at the
|
||||
# electric tariff and dropping ~£175/yr from the ECF on cert 001479.
|
||||
secondary_fuel = (
|
||||
epc.sap_heating.secondary_fuel_type if epc.sap_heating else None
|
||||
)
|
||||
secondary_high_rate_gbp_per_kwh = (
|
||||
table_32_unit_price_p_per_kwh(secondary_fuel) * _PENCE_TO_GBP
|
||||
if secondary_fuel is not None
|
||||
else other_uses_gbp_per_kwh
|
||||
)
|
||||
|
||||
# Table 32 PV export credit (code 60 = 13.19 p/kWh, same as std
|
||||
# electricity under RdSAP10 amendment).
|
||||
|
|
|
|||
|
|
@ -94,13 +94,17 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = (
|
|||
_GoldenExpectation(
|
||||
cert_number="0300-2747-7640-2526-2135",
|
||||
actual_sap=78,
|
||||
expected_sap_resid=-7,
|
||||
expected_sap_resid=+2,
|
||||
expected_pe_resid_kwh_per_m2=-1.9082,
|
||||
expected_co2_resid_tonnes_per_yr=-1.0829,
|
||||
notes=(
|
||||
"Large semi-detached, TFA 526, age D, gas boiler PCDB-listed "
|
||||
"(no Table 4b code). Cert lodges open_flues_count=1 + "
|
||||
"has_draught_lobby=true."
|
||||
"has_draught_lobby=true + mains-gas secondary (SAP code 605 / "
|
||||
"fuel 26). Slice 58 cascade routed secondary fuel cost through "
|
||||
"the lodged fuel_type (rather than hardcoding the electric "
|
||||
"tariff), tightening this cert's SAP residual −7 → +2 — the "
|
||||
"biggest single SAP improvement on the golden cohort to date."
|
||||
),
|
||||
),
|
||||
_GoldenExpectation(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue