mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +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
|
||||||
assert epc.sap_building_parts[2].roof_insulation_thickness == 0
|
assert epc.sap_building_parts[2].roof_insulation_thickness == 0
|
||||||
assert epc.sap_building_parts[1].roof_insulation_thickness is None
|
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)
|
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]:
|
def _elmhurst_sap_control_code(sap_control: str) -> Optional[int]:
|
||||||
"""Extract the SAP code integer from a heating-controls field like
|
"""Extract the SAP code integer from a heating-controls field like
|
||||||
'SAP code 2106, Programmer, room thermostat and TRVs' → 2106. The
|
'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,
|
water_heating_code=survey.water_heating.water_heating_sap_code,
|
||||||
secondary_heating_type=mh.secondary_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,
|
number_baths=survey.baths_and_showers.number_of_baths,
|
||||||
electric_shower_count=1 if has_electric_shower else None,
|
electric_shower_count=1 if has_electric_shower else None,
|
||||||
mixer_shower_count=0 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)
|
table_32_unit_price_p_per_kwh(water_heating_fuel_code or main_fuel_code)
|
||||||
* _PENCE_TO_GBP
|
* _PENCE_TO_GBP
|
||||||
)
|
)
|
||||||
# Secondary fuel = standard electricity by default (portable electric
|
# Secondary fuel cost: route through the cert's `secondary_fuel_type`
|
||||||
# heater per §A.2.2). Scope A has no lodged secondaries; the fraction
|
# when lodged (e.g. mains-gas fire SAP code 605 → fuel 26 → Table 32
|
||||||
# is zero so the price contributes nothing to (242).
|
# gas price), otherwise default to standard electricity (the portable
|
||||||
secondary_high_rate_gbp_per_kwh = other_uses_gbp_per_kwh
|
# 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
|
# Table 32 PV export credit (code 60 = 13.19 p/kWh, same as std
|
||||||
# electricity under RdSAP10 amendment).
|
# electricity under RdSAP10 amendment).
|
||||||
|
|
|
||||||
|
|
@ -94,13 +94,17 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = (
|
||||||
_GoldenExpectation(
|
_GoldenExpectation(
|
||||||
cert_number="0300-2747-7640-2526-2135",
|
cert_number="0300-2747-7640-2526-2135",
|
||||||
actual_sap=78,
|
actual_sap=78,
|
||||||
expected_sap_resid=-7,
|
expected_sap_resid=+2,
|
||||||
expected_pe_resid_kwh_per_m2=-1.9082,
|
expected_pe_resid_kwh_per_m2=-1.9082,
|
||||||
expected_co2_resid_tonnes_per_yr=-1.0829,
|
expected_co2_resid_tonnes_per_yr=-1.0829,
|
||||||
notes=(
|
notes=(
|
||||||
"Large semi-detached, TFA 526, age D, gas boiler PCDB-listed "
|
"Large semi-detached, TFA 526, age D, gas boiler PCDB-listed "
|
||||||
"(no Table 4b code). Cert lodges open_flues_count=1 + "
|
"(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(
|
_GoldenExpectation(
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue