mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Slice S0380.178: oil 6 circulation pump x1.3 for absent room thermostat
Closes the residual S0380.177 exposed on oil 6. The cascade's central
heating pump used the bare Table 4f age default (41 kWh for "2013 or
later") but the worksheet (230c) = 53.3 kWh.
SAP 10.2 Table 4f (PDF p.175) footnote a) on the "Circulation pump"
rows reads verbatim: "Multiply by a factor of 1.3 if room thermostat
is absent." oil 6 lodges control code 2101 ("No time or thermostatic
control of room temperature") = no room thermostat, so 41 x 1.3 = 53.3
= ws (230c) EXACTLY; pumps/fans (231) = 53.3 + 100 (liquid-fuel boiler
flue fan/pump) = 153.3 EXACT. Same root cause (absent room thermostat)
as the S0380.177 Table 4c(2) interlock fix — both keyed on the new
`_BOILER_NO_ROOM_THERMOSTAT_CONTROL_CODES = {2101, 2102}`.
`_table_4f_circulation_pump_kwh` now multiplies the resolved pump kWh
by `_TABLE_4F_NO_ROOM_THERMOSTAT_PUMP_MULTIPLIER = 1.3` when the main's
control code is in that set.
oil 6 now FULLY EXACT on all four metrics (ΔSAP/cost/CO2/PE < 1e-4).
The sibling oil 5 (same "2013 or later" pump age but control 2106 WITH
a room thermostat) keeps the bare 41 kWh and is unaffected — as do the
other 39 corpus variants (2101/2102 appear only on oil 6). 935 pass;
pyright net-zero 32 -> 32.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
546bca3277
commit
698c61950e
2 changed files with 58 additions and 13 deletions
|
|
@ -462,15 +462,17 @@ _EXPECTATIONS: tuple[_CorpusExpectation, ...] = (
|
|||
# fuel (219) = 4099.5872 EXACT. ΔSAP +3.0518 → +0.0782; Δcost
|
||||
# -£69.79 → -£1.68; ΔCO2 -240.66 → -1.71; ΔPE -1112.66 → -18.61.
|
||||
#
|
||||
# The residual that remains is a SINGLE distinct cause the interlock
|
||||
# fix exposed: the central heating pump (230c). Cascade reads
|
||||
# `central_heating_pump_age=2` → Table 4f 41 kWh, but ws (230c) =
|
||||
# 53.3 kWh (non-standard — not a Table 4f age value of 41/115/165;
|
||||
# likely a lodged pump power). The 12.3 kWh gap fully explains the
|
||||
# residual: cost 12.3 x 0.1367 = £1.68, CO2 12.3 x 0.1387 = 1.71 kg,
|
||||
# PE 12.3 x 1.5128 = 18.61 kWh. Pinned as the next-slice forcing
|
||||
# function (S0380.178 central-heating-pump 53.3 kWh).
|
||||
_CorpusExpectation(variant='oil 6', block='11a', expected_sap_resid=+0.0782, expected_cost_resid_gbp=-1.6814, expected_co2_resid_kg=-1.7061, expected_pe_resid_kwh=-18.6074),
|
||||
# Slice S0380.178 then closed the residual S0380.177 exposed — the
|
||||
# central heating pump (230c). SAP 10.2 Table 4f (PDF p.175) footnote
|
||||
# a) on the "Circulation pump" rows: "Multiply by a factor of 1.3 if
|
||||
# room thermostat is absent." Control 2101 has no room thermostat, so
|
||||
# the cert's "2013 or later" pump (Table 4f 41 kWh) scales to 41 x
|
||||
# 1.3 = 53.3 kWh = ws (230c); pumps/fans (231) = 53.3 + 100 (oil aux)
|
||||
# = 153.3 EXACT. Same root cause (no room thermostat) as the .177
|
||||
# interlock fix. oil 6 now FULLY EXACT on all four metrics. The
|
||||
# sibling oil 5 (same pump age but control 2106 WITH a room
|
||||
# thermostat) keeps the bare 41 kWh and is unaffected.
|
||||
_CorpusExpectation(variant='oil 6', block='11a', expected_sap_resid=-0.0000, expected_cost_resid_gbp=-0.0000, expected_co2_resid_kg=+0.0000, expected_pe_resid_kwh=+0.0000),
|
||||
_CorpusExpectation(variant='oil pcdb 1', block='11a', expected_sap_resid=+0.0000, expected_cost_resid_gbp=+0.0000, expected_co2_resid_kg=-0.0000, expected_pe_resid_kwh=+0.0000),
|
||||
_CorpusExpectation(variant='oil pcdb 2', block='11a', expected_sap_resid=+0.0000, expected_cost_resid_gbp=+0.0000, expected_co2_resid_kg=-0.0000, expected_pe_resid_kwh=+0.0000),
|
||||
_CorpusExpectation(variant='oil pcdb 3', block='11a', expected_sap_resid=+0.0000, expected_cost_resid_gbp=+0.0000, expected_co2_resid_kg=+0.0000, expected_pe_resid_kwh=-0.0000),
|
||||
|
|
@ -890,6 +892,34 @@ def test_oil_6_no_room_thermostat_applies_table_4c2_minus_5pp_space_efficiency()
|
|||
)
|
||||
|
||||
|
||||
def test_oil_6_absent_room_thermostat_applies_table_4f_pump_1_3_multiplier() -> None:
|
||||
# Arrange — oil 6 lodges Main Heating Controls Sap code 2101 ("No
|
||||
# time or thermostatic control of room temperature") = no room
|
||||
# thermostat. SAP 10.2 Table 4f (PDF p.175) footnote a) on the
|
||||
# "Circulation pump" rows reads verbatim: "Multiply by a factor of
|
||||
# 1.3 if room thermostat is absent." The cert's central heating
|
||||
# pump is "2013 or later" -> Table 4f 41 kWh; with the absent-room-
|
||||
# thermostat x1.3 it becomes 41 x 1.3 = 53.3 kWh, matching worksheet
|
||||
# (230c) = 53.3000. With the liquid-fuel boiler flue-fan/pump 100
|
||||
# kWh (230d), total pumps/fans (231) = 153.3000. The sibling oil 5
|
||||
# (same "2013 or later" pump age but control 2106 WITH a room
|
||||
# thermostat) keeps the bare 41 kWh — worksheet (230c) = 41.0000.
|
||||
summary_pdf, _ = _variant_paths('oil 6')
|
||||
pages = _summary_pdf_to_textract_style_pages(summary_pdf)
|
||||
site_notes = ElmhurstSiteNotesExtractor(pages).extract()
|
||||
epc = EpcPropertyDataMapper.from_elmhurst_site_notes(site_notes)
|
||||
|
||||
# Act — run the rating cascade and read the resolved pumps/fans kWh.
|
||||
inputs = cert_to_inputs(epc, prices=SAP_10_2_SPEC_PRICES)
|
||||
|
||||
# Assert — 41 x 1.3 (circulation pump) + 100 (oil flue fan/pump) =
|
||||
# 153.3 kWh (matches worksheet (231)).
|
||||
assert abs(inputs.pumps_fans_kwh_per_yr - 153.3) <= 1e-9, (
|
||||
f"oil 6 pumps/fans {inputs.pumps_fans_kwh_per_yr:.4f} kWh "
|
||||
f"!= 153.3 (41 x 1.3 absent-room-thermostat pump + 100 oil aux)"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not _BLOCKED_BY_MISSING_MAIN_FUEL_TYPE,
|
||||
reason="all blocked variants have been unblocked (latest: S0380.170)",
|
||||
|
|
|
|||
|
|
@ -229,6 +229,14 @@ _TABLE_4F_CIRCULATION_PUMP_KWH_BY_AGE: Final[dict[int, float]] = {
|
|||
# to use the unknown-date value.
|
||||
_TABLE_4F_CIRCULATION_PUMP_KWH_DEFAULT: Final[float] = 115.0
|
||||
|
||||
# SAP 10.2 Table 4f (PDF p.175) footnote a) on the "Circulation pump"
|
||||
# rows reads verbatim: "Multiply by a factor of 1.3 if room thermostat
|
||||
# is absent." A gas/liquid-fuel boiler under control code 2101 / 2102
|
||||
# (`_BOILER_NO_ROOM_THERMOSTAT_CONTROL_CODES`) has no room thermostat,
|
||||
# so its circulation pump electricity is scaled by 1.3 — e.g. oil 6
|
||||
# (pump_age "2013 or later" → 41 kWh) lands ws (230c) = 41 × 1.3 = 53.3.
|
||||
_TABLE_4F_NO_ROOM_THERMOSTAT_PUMP_MULTIPLIER: Final[float] = 1.3
|
||||
|
||||
# Heat pumps from PCDB include circulation pump electricity in COP per
|
||||
# Table 4f note: "Not applicable for electric heat pumps from
|
||||
# database." Cat 4 (heat pump) → 0 kWh circulation pump.
|
||||
|
|
@ -352,16 +360,23 @@ def _table_4f_circulation_pump_kwh(main: Optional[MainHeatingDetail]) -> float:
|
|||
0 / None → 115 kWh (Unknown date)
|
||||
1 → 165 kWh (Pre 2013 / 2012 or earlier)
|
||||
2 → 41 kWh (2013 or later)
|
||||
|
||||
Table 4f footnote a) then multiplies the row by 1.3 when the room
|
||||
thermostat is absent (control code 2101 / 2102).
|
||||
"""
|
||||
if not _is_wet_boiler_main(main):
|
||||
return 0.0
|
||||
assert main is not None # _is_wet_boiler_main guards None
|
||||
age = main.central_heating_pump_age
|
||||
if age is None:
|
||||
return _TABLE_4F_CIRCULATION_PUMP_KWH_DEFAULT
|
||||
return _TABLE_4F_CIRCULATION_PUMP_KWH_BY_AGE.get(
|
||||
age, _TABLE_4F_CIRCULATION_PUMP_KWH_DEFAULT
|
||||
)
|
||||
kwh = _TABLE_4F_CIRCULATION_PUMP_KWH_DEFAULT
|
||||
else:
|
||||
kwh = _TABLE_4F_CIRCULATION_PUMP_KWH_BY_AGE.get(
|
||||
age, _TABLE_4F_CIRCULATION_PUMP_KWH_DEFAULT
|
||||
)
|
||||
if main.main_heating_control in _BOILER_NO_ROOM_THERMOSTAT_CONTROL_CODES:
|
||||
kwh *= _TABLE_4F_NO_ROOM_THERMOSTAT_PUMP_MULTIPLIER
|
||||
return kwh
|
||||
|
||||
|
||||
def _table_4f_main_1_gas_boiler_flue_fan_kwh(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue