mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Slice S0380.175: Community heating main_heating_control extraction
SAP 10.2 Table 4e Group 3 (PDF p.173) — heat-network control codes
2301-2314 dispatch to control_type 1, 2, or 3. Code 2306 = "Charging
system linked to use of heating, programmer and TRVs" →
control_type=3, temperature_adjustment=0. Per Table 9 the elsewhere-
zone off-hours depend on control_type: type 1/2 → (7, 8); type 3 →
(9, 8). The two extra off-hours change the §7 (90) T_rest mean by
~0.6 K → (92) MIT by ~0.4 K → (98) SH demand by ~390 kWh/yr.
Pre-slice diagnosis: cascade defaulted `main_heating_control=2`
(modal RdSAP) when the §14.0 "Main Heating Controls Sap" field was
empty. The 5 community heating corpus variants ALL lodge the SAP
code in §14.1 Community Heating "Heating Controls SAP" instead
(format: bare 4-digit integer, e.g. "2306"). The extractor was
storing this in `CommunityHeating.heating_controls_sap` but the
mapper only read `mh.heating_controls_sap` (§14.0).
Two changes:
1. `_elmhurst_sap_control_code` extended to accept bare 4-digit form
("2306") in addition to the §14.0 narrative form ("SAP code 2106,
Programmer, room thermostat and TRVs"). Empty-string returns None
instead of swallowing through the original `re.match` regex.
2. `_map_elmhurst_sap_heating` falls through to
`mh.community_heating.heating_controls_sap` when the §14.0 main
block leaves `heating_controls_sap` empty.
Closures (heating-systems corpus 001431):
CH1 ΔSAP_c -1.0572 → +0.0000 EXACT
Δcost +£24.36 → -£0.00 EXACT
CH3 ΔSAP_c -1.0572 → +0.0000 EXACT
Δcost +£24.36 → -£0.00 EXACT
CH2/CH4 SAP-side flip ±0.42 → ±0.53 (CHP-split blend reacts to
the now-lower SH demand × CHP rate)
CH6 ΔSAP_c -8.4406 → -7.4942 (DLF=1.0 P960 quirk untouched)
Remaining CH1/CH3 ΔCO2 -23.60 / ΔPE -208.23 is the §13a (372)
"Electrical energy for heat distribution" line (118.38 kWh × electric
factors 0.1993 CO2 / 1.760 PE). Cascade doesn't currently meter this
electricity overhead separately from heat-network heat — next slice.
932 pass + 0 fail (+5 new mapper tests). No regressions on the other
36 corpus variants — the mapper change is gated on `mh.community_
heating is not None` and only fires when §14.0 leaves the control
field empty. Pyright net-zero on mapper.py + corpus test.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
45036e821b
commit
a002c7895f
2 changed files with 97 additions and 9 deletions
|
|
@ -608,11 +608,28 @@ _EXPECTATIONS: tuple[_CorpusExpectation, ...] = (
|
|||
# offset the §7 MIT over-count, masking the bug. Per
|
||||
# [[feedback-software-no-special-handling]] apply spec-correct fix
|
||||
# uniformly; the exposed §7 MIT residual is the next closure front.
|
||||
_CorpusExpectation(variant='community heating 1', block='11b', expected_sap_resid=-1.0572, expected_cost_resid_gbp=+24.3605, expected_co2_resid_kg=+127.2164, expected_pe_resid_kwh=+408.6704),
|
||||
_CorpusExpectation(variant='community heating 2', block='11b', expected_sap_resid=-0.4187, expected_cost_resid_gbp=+9.6470, expected_co2_resid_kg=-1356.9498, expected_pe_resid_kwh=+1778.5550),
|
||||
_CorpusExpectation(variant='community heating 3', block='11b', expected_sap_resid=-1.0572, expected_cost_resid_gbp=+24.3605, expected_co2_resid_kg=-72.8776, expected_pe_resid_kwh=-239.0266),
|
||||
_CorpusExpectation(variant='community heating 4', block='11b', expected_sap_resid=-0.4187, expected_cost_resid_gbp=+9.6470, expected_co2_resid_kg=-4323.7080, expected_pe_resid_kwh=+767.1285),
|
||||
_CorpusExpectation(variant='community heating 6', block='11b', expected_sap_resid=-8.4406, expected_cost_resid_gbp=+194.4846, expected_co2_resid_kg=-2861.5307, expected_pe_resid_kwh=+8137.1145),
|
||||
#
|
||||
# Slice S0380.175 wired the §14.1 Community Heating "Heating Controls
|
||||
# SAP" lodging (bare 4-digit form like "2306") into the
|
||||
# `main_heating_control` field on the mapper-produced Main 1. Pre-
|
||||
# slice the mapper only read §14.0 Main Heating "Main Heating
|
||||
# Controls Sap" which is empty for community heating certs; the
|
||||
# cascade defaulted to control_type=2, mis-routing the §7 elsewhere-
|
||||
# zone off-hours to (7, 8) when SAP code 2306 ("Charging system
|
||||
# linked to use of heating, programmer and TRVs") dispatches via
|
||||
# Table 4e Group 3 to control_type=3 / off-hours (9, 8). The fix
|
||||
# closes CH1 and CH3 SAP / cost EXACTLY; CH2/CH4 cost flip from
|
||||
# +£9.65 to -£12.16 (CHP-split blend now sees lower SH kWh × CHP
|
||||
# rate); CH6 SAP narrows -8.44 → -7.49. Remaining CH1/CH3 CO2/PE
|
||||
# residuals are the §13a (372) "Electrical energy for heat
|
||||
# distribution" line — 118.38 kWh billed at electricity factors
|
||||
# (CO2 0.1993, PE 1.760), not heat-network factors — the cascade
|
||||
# doesn't currently meter this. Next follow-up slice.
|
||||
_CorpusExpectation(variant='community heating 1', block='11b', expected_sap_resid=+0.0000, expected_cost_resid_gbp=-0.0000, expected_co2_resid_kg=-23.6007, expected_pe_resid_kwh=-208.2267),
|
||||
_CorpusExpectation(variant='community heating 2', block='11b', expected_sap_resid=+0.5277, expected_cost_resid_gbp=-12.1598, expected_co2_resid_kg=-1435.0874, expected_pe_resid_kwh=+1123.0063),
|
||||
_CorpusExpectation(variant='community heating 3', block='11b', expected_sap_resid=+0.0000, expected_cost_resid_gbp=-0.0000, expected_co2_resid_kg=-98.9235, expected_pe_resid_kwh=-457.5428),
|
||||
_CorpusExpectation(variant='community heating 4', block='11b', expected_sap_resid=+0.5277, expected_cost_resid_gbp=-12.1598, expected_co2_resid_kg=-4401.8456, expected_pe_resid_kwh=+111.5798),
|
||||
_CorpusExpectation(variant='community heating 6', block='11b', expected_sap_resid=-7.4942, expected_cost_resid_gbp=+172.6778, expected_co2_resid_kg=-2939.6683, expected_pe_resid_kwh=+7481.5658),
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -945,6 +962,53 @@ def test_community_heating_mapper_populates_chp_split_fields(
|
|||
assert main_1.community_heating_boiler_fuel_type == expected_boiler_fuel_code
|
||||
|
||||
|
||||
# S0380.175 — Community heating main_heating_control extraction.
|
||||
#
|
||||
# Per SAP 10.2 Table 4e Group 3 (PDF p.173): heat-network control codes
|
||||
# 2301-2314 dispatch to control_type 1, 2, or 3. The cert lodges the
|
||||
# code in §14.1 Community Heating "Heating Controls SAP" rather than
|
||||
# §14.0 Main Heating's "Main Heating Controls Sap". Pre-slice the mapper
|
||||
# only read the §14.0 field, leaving `main_heating_control=''` and the
|
||||
# cascade defaulting to type 2 (modal RdSAP default). The §14.1 lodging
|
||||
# carries the actual control code, which feeds Table 9 elsewhere-zone
|
||||
# off-hours selection (type 1/2 → (7,8); type 3 → (9,8)) and the §7
|
||||
# T_h2 MIT cascade.
|
||||
@pytest.mark.parametrize(
|
||||
("variant", "expected_main_heating_control"),
|
||||
(
|
||||
# All 5 CH variants lodge "Heating Controls SAP: 2306" in §14.1
|
||||
# Community Heating. SAP 10.2 Table 4e Group 3 row 2306 =
|
||||
# "Charging system linked to use of heating, programmer and TRVs"
|
||||
# → control_type 3, temperature_adjustment 0 °C.
|
||||
('community heating 1', 2306),
|
||||
('community heating 2', 2306),
|
||||
('community heating 3', 2306),
|
||||
('community heating 4', 2306),
|
||||
('community heating 6', 2306),
|
||||
),
|
||||
ids=lambda v: v if isinstance(v, str) else str(v),
|
||||
)
|
||||
def test_community_heating_mapper_picks_up_section_14_1_heating_controls_sap(
|
||||
variant: str, expected_main_heating_control: int,
|
||||
) -> None:
|
||||
# Arrange — community heating Summary lodges the SAP control code in
|
||||
# §14.1 Community Heating "Heating Controls SAP", NOT in §14.0 Main
|
||||
# Heating "Main Heating Controls Sap" (which is empty for community
|
||||
# heating certs). Mapper must read from the community block when the
|
||||
# main block is empty.
|
||||
summary_pdf, _ = _variant_paths(variant)
|
||||
pages = _summary_pdf_to_textract_style_pages(summary_pdf)
|
||||
site_notes = ElmhurstSiteNotesExtractor(pages).extract()
|
||||
|
||||
# Act
|
||||
epc = EpcPropertyDataMapper.from_elmhurst_site_notes(site_notes)
|
||||
|
||||
# Assert — Main 1 picks up the §14.1 community heating control code.
|
||||
main_heating_details = epc.sap_heating.main_heating_details
|
||||
assert main_heating_details is not None and len(main_heating_details) >= 1
|
||||
assert main_heating_details[0].main_heating_control == expected_main_heating_control
|
||||
|
||||
|
||||
# S0380.172 — Heat-network heat-source-eff scaling residual coverage.
|
||||
#
|
||||
# Per SAP 10.2 Table 4a (PDF p.164): "Boilers (RdSAP)" eff=80%, "Heat
|
||||
|
|
|
|||
|
|
@ -4042,11 +4042,27 @@ def _elmhurst_secondary_fuel_from_sap_code(
|
|||
|
||||
|
||||
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
|
||||
cascade reads `main_heating_control` as int when present."""
|
||||
"""Extract the SAP code integer from a heating-controls field.
|
||||
|
||||
Two lodgement forms across the Elmhurst Summary corpus:
|
||||
1. '§14.0 Main Heating Controls Sap: SAP code 2106, Programmer,
|
||||
room thermostat and TRVs' (individually-heated dwellings).
|
||||
2. '§14.1 Community Heating Heating Controls SAP: 2306' — bare
|
||||
4-digit integer string (community heating dwellings, per
|
||||
SAP 10.2 Table 4e Group 3 codes 2301-2314).
|
||||
|
||||
Either form yields the cascade-readable int. Returns None when the
|
||||
lodgement is empty or doesn't carry a recognisable code.
|
||||
"""
|
||||
if not sap_control:
|
||||
return None
|
||||
m = re.match(r"SAP code\s+(\d+)", sap_control)
|
||||
return int(m.group(1)) if m else None
|
||||
if m:
|
||||
return int(m.group(1))
|
||||
bare = sap_control.strip()
|
||||
if bare.isdigit():
|
||||
return int(bare)
|
||||
return None
|
||||
|
||||
|
||||
# SAP10.2 Table 4a main-heating-category codes. The cascade reads
|
||||
|
|
@ -4697,7 +4713,15 @@ def _map_elmhurst_main_heating_2(
|
|||
|
||||
def _map_elmhurst_sap_heating(survey: ElmhurstSiteNotes) -> SapHeating:
|
||||
mh = survey.main_heating
|
||||
# Community heating dwellings lodge the SAP control code in §14.1
|
||||
# Community Heating "Heating Controls SAP" (bare 4-digit form, e.g.
|
||||
# "2306"), not in §14.0 Main Heating "Main Heating Controls Sap".
|
||||
# Fall through to the §14.1 lodging when §14.0 is empty so the
|
||||
# cascade reads `main_heating_control` as the lodged Table 4e Group 3
|
||||
# code instead of defaulting to type 2.
|
||||
sap_control = mh.heating_controls_sap
|
||||
if not sap_control and mh.community_heating is not None:
|
||||
sap_control = mh.community_heating.heating_controls_sap
|
||||
control = (
|
||||
sap_control.split(", ", 1)[1]
|
||||
if sap_control.startswith("SAP code") and ", " in sap_control
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue