mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Slice S0380.145: Table 4e temperature adjustment — apply (92)m → (93)m offset per Table 9c step 8
SAP 10.2 Table 4e (PDF p.170-173) "Heating system controls":
3. The 'Temperature adjustment' modifies the mean internal
temperature and is added to worksheet (92)m.
SAP 10.2 Table 9c step 8 (PDF p.184): "Apply adjustment to the mean
internal temperature from Table 4e, where appropriate".
Pre-slice the cascade hardcoded `control_temperature_adjustment_c
=0.0` at all three call sites of `mean_internal_temperature_monthly`
and `space_heating_section_with_results`. The §8 heat loss calc
therefore drove off (92)m unchanged → §8 SH demand under-counted on
every cert whose `main_heating_control` lodges a non-zero adjustment.
Table 4e adjustments by code (full p.170-173 coverage):
Group 0 — No heating system:
2699: +0.3
Group 1 — Boilers with radiators/UFH (+ micro-CHP):
2101, 2102: +0.6 (no thermo / programmer-only)
2103..2113: 0
Group 2 — Heat pumps:
2201, 2202: +0.3
2203..2210: 0
Group 3 — Heat networks:
2301, 2302: +0.3
2303..2314: 0
Group 4 — Electric storage:
2401 (Manual charge): +0.7
2402 (Automatic charge): +0.4
2403 (Celect): +0.4
2404 (HHR controls): 0
Group 5 — Warm air:
2501, 2502: +0.3
2503..2506: 0
Group 6 — Room heaters:
2601: +0.3
2602..2605: 0
Group 7 — Other systems:
2701, 2702: +0.3
2703..2706: 0
New `_control_temperature_adjustment_c(main)` helper consults
`_CONTROL_TEMPERATURE_ADJUSTMENT_BY_CODE` (52 entries, full Table 4e
coverage). Strict-raises `UnmappedSapCode` on present-but-unmapped
codes per [[reference-unmapped-sap-code]] so spec-coverage gaps
surface at test time. The helper is wired to all three call sites
of the MIT/SH orchestrators in cert_to_inputs.
Corpus impact — closes the +2.5 SAP cluster substantially:
Variant | control | pre → post | delta
------- | ------- | -------------- | -----
e3 (401)| 2401 | +2.55 → -0.09 | -2.46 (massive close)
e6 (404)| 2402 | +1.33 → -0.17 | -1.50
e7 (408)| 2402 | +1.29 → -0.20 | -1.49
e2 (524)| 2502 | +0.47 → -0.18 | -0.65
e5 (402)| 2402 | +0.07 → -1.43 | -1.50 (regressed —
previously net-zero
from offsetting bugs)
Cumulative |ΔSAP| across these 5: 5.71 → 2.07 (-3.64 pts closed).
electric 3 / 6 / 7 / 8 / 9 now all within 0.20 SAP of worksheet.
Golden fixtures unchanged (API certs in those tests don't lodge
non-zero-adjustment control codes; suite stays 888 pass).
Extended handover suite: 888 pass, 0 fail (was 887 + 1 new AAA test).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
ec6661cbb6
commit
b1478cff63
3 changed files with 202 additions and 8 deletions
|
|
@ -221,11 +221,11 @@ class _CorpusExpectation:
|
|||
_EXPECTATIONS: tuple[_CorpusExpectation, ...] = (
|
||||
_CorpusExpectation(variant='ashp', block='11a', expected_sap_resid=+0.2418, expected_cost_resid_gbp=-5.5706, expected_co2_resid_kg=-1.4283, expected_pe_resid_kwh=-11.8017),
|
||||
_CorpusExpectation(variant='electric 1', block='11a', expected_sap_resid=-0.0573, expected_cost_resid_gbp=+1.3188, expected_co2_resid_kg=+8.0120, expected_pe_resid_kwh=+94.4789),
|
||||
_CorpusExpectation(variant='electric 2', block='11a', expected_sap_resid=+0.4737, expected_cost_resid_gbp=-10.9153, expected_co2_resid_kg=+10.9544, expected_pe_resid_kwh=+100.9401),
|
||||
_CorpusExpectation(variant='electric 3', block='11a', expected_sap_resid=+2.5452, expected_cost_resid_gbp=-58.6455, expected_co2_resid_kg=-108.8821, expected_pe_resid_kwh=-1093.1815),
|
||||
_CorpusExpectation(variant='electric 5', block='11a', expected_sap_resid=+0.0747, expected_cost_resid_gbp=-1.7232, expected_co2_resid_kg=-2.4846, expected_pe_resid_kwh=-133.3636),
|
||||
_CorpusExpectation(variant='electric 6', block='11a', expected_sap_resid=+1.3278, expected_cost_resid_gbp=-30.5954, expected_co2_resid_kg=-56.1047, expected_pe_resid_kwh=-562.5298),
|
||||
_CorpusExpectation(variant='electric 7', block='11a', expected_sap_resid=+1.2903, expected_cost_resid_gbp=-29.7300, expected_co2_resid_kg=-53.5730, expected_pe_resid_kwh=-549.3654),
|
||||
_CorpusExpectation(variant='electric 2', block='11a', expected_sap_resid=-0.1842, expected_cost_resid_gbp=+4.2439, expected_co2_resid_kg=+38.7768, expected_pe_resid_kwh=+392.8379),
|
||||
_CorpusExpectation(variant='electric 3', block='11a', expected_sap_resid=-0.0874, expected_cost_resid_gbp=+2.0136, expected_co2_resid_kg=+4.2088, expected_pe_resid_kwh=+81.9107),
|
||||
_CorpusExpectation(variant='electric 5', block='11a', expected_sap_resid=-1.4255, expected_cost_resid_gbp=+32.8452, expected_co2_resid_kg=+61.9651, expected_pe_resid_kwh=+535.1955),
|
||||
_CorpusExpectation(variant='electric 6', block='11a', expected_sap_resid=-0.1698, expected_cost_resid_gbp=+3.9118, expected_co2_resid_kg=+7.8972, expected_pe_resid_kwh=+103.4643),
|
||||
_CorpusExpectation(variant='electric 7', block='11a', expected_sap_resid=-0.2044, expected_cost_resid_gbp=+4.7098, expected_co2_resid_kg=+9.6362, expected_pe_resid_kwh=+112.7064),
|
||||
_CorpusExpectation(variant='electric 8', block='11a', expected_sap_resid=-0.2568, expected_cost_resid_gbp=+5.9163, expected_co2_resid_kg=+11.6231, expected_pe_resid_kwh=+126.0896),
|
||||
_CorpusExpectation(variant='electric 9', block='11a', expected_sap_resid=-0.1181, expected_cost_resid_gbp=+2.7217, expected_co2_resid_kg=+5.6819, expected_pe_resid_kwh=+91.4145),
|
||||
_CorpusExpectation(variant='gshp', block='11a', expected_sap_resid=+1.1491, expected_cost_resid_gbp=-26.4775, expected_co2_resid_kg=-41.4461, expected_pe_resid_kwh=-454.5023),
|
||||
|
|
|
|||
|
|
@ -740,6 +740,75 @@ _CONTROL_TYPE_BY_CODE: Final[dict[int, int]] = {
|
|||
}
|
||||
|
||||
|
||||
# SAP 10.2 Table 4e (PDF p.171-173) — "Temperature adjustment, °C"
|
||||
# column. Spec verbatim (p.170): "3. The 'Temperature adjustment'
|
||||
# modifies the mean internal temperature and is added to worksheet
|
||||
# (92)m." Table 9c step 8: "Apply adjustment to the mean internal
|
||||
# temperature from Table 4e, where appropriate".
|
||||
#
|
||||
# Pre-S0380.145 the cascade hardcoded `control_temperature_adjustment
|
||||
# _c=0.0` at all three call sites of `mean_internal_temperature_
|
||||
# monthly`. The non-zero adjustments are concentrated on systems
|
||||
# without thermostatic control (which run permanently at setpoint
|
||||
# during their heating periods, raising MIT) and on Group 4 electric
|
||||
# storage where the storage charging strategy raises the maintained
|
||||
# mean (Manual charge +0.7, Automatic charge +0.4, Celect +0.4,
|
||||
# HHR-specific controls 0).
|
||||
_CONTROL_TEMPERATURE_ADJUSTMENT_BY_CODE: Final[dict[int, float]] = {
|
||||
# Group 0 — NO HEATING SYSTEM PRESENT
|
||||
2699: +0.3,
|
||||
# Group 1 — BOILER SYSTEMS WITH RADIATORS / UFH (and micro-CHP)
|
||||
# 2100 = "Not applicable (boiler DHW only)" — no MIT contribution
|
||||
# from this main; treat as 0.
|
||||
2100: 0.0,
|
||||
2101: +0.6, 2102: +0.6,
|
||||
2103: 0.0, 2104: 0.0, 2105: 0.0, 2106: 0.0, 2107: 0.0, 2108: 0.0,
|
||||
2109: 0.0, 2110: 0.0, 2111: 0.0, 2112: 0.0, 2113: 0.0,
|
||||
# Group 2 — HEAT PUMPS WITH RADIATORS / UFH
|
||||
2201: +0.3, 2202: +0.3,
|
||||
2203: 0.0, 2204: 0.0, 2205: 0.0, 2206: 0.0,
|
||||
2207: 0.0, 2208: 0.0, 2209: 0.0, 2210: 0.0,
|
||||
# Group 3 — HEAT NETWORKS
|
||||
2301: +0.3, 2302: +0.3,
|
||||
2303: 0.0, 2304: 0.0, 2305: 0.0, 2306: 0.0, 2307: 0.0, 2308: 0.0,
|
||||
2309: 0.0, 2310: 0.0, 2311: 0.0, 2312: 0.0, 2313: 0.0, 2314: 0.0,
|
||||
# Group 4 — ELECTRIC STORAGE SYSTEMS
|
||||
2401: +0.7, 2402: +0.4, 2403: +0.4, 2404: 0.0,
|
||||
# Group 5 — WARM AIR SYSTEMS (incl. HP with warm air distribution)
|
||||
2501: +0.3, 2502: +0.3,
|
||||
2503: 0.0, 2504: 0.0, 2505: 0.0, 2506: 0.0,
|
||||
# Group 6 — ROOM HEATER SYSTEMS
|
||||
2601: +0.3,
|
||||
2602: 0.0, 2603: 0.0, 2604: 0.0, 2605: 0.0,
|
||||
# Group 7 — OTHER SYSTEMS
|
||||
2701: +0.3, 2702: +0.3,
|
||||
2703: 0.0, 2704: 0.0, 2705: 0.0, 2706: 0.0,
|
||||
}
|
||||
|
||||
|
||||
def _control_temperature_adjustment_c(
|
||||
main: Optional[MainHeatingDetail],
|
||||
) -> float:
|
||||
"""SAP 10.2 Table 4e (PDF p.171-173) "Temperature adjustment, °C"
|
||||
per Table 9c step 8 (PDF p.184). The adjustment is added to (92)m
|
||||
to produce (93)m, which feeds the §8 heat loss rate calc and the
|
||||
Table 9c step 9 re-calculated utilisation factor.
|
||||
|
||||
Returns 0.0 when no main is lodged or the cert's
|
||||
`main_heating_control` is not an int. Raises `UnmappedSapCode`
|
||||
for present-but-unmapped codes per [[reference-unmapped-sap-code]]
|
||||
so spec-coverage gaps surface at test time.
|
||||
"""
|
||||
if main is None:
|
||||
return 0.0
|
||||
code = main.main_heating_control
|
||||
if not isinstance(code, int):
|
||||
return 0.0
|
||||
if code in _CONTROL_TEMPERATURE_ADJUSTMENT_BY_CODE:
|
||||
return _CONTROL_TEMPERATURE_ADJUSTMENT_BY_CODE[code]
|
||||
raise UnmappedSapCode("main_heating_control_temperature_adjustment", code)
|
||||
|
||||
|
||||
from domain.sap10_calculator.exceptions import (
|
||||
MissingMainFuelType,
|
||||
UnmappedSapCode,
|
||||
|
|
@ -2607,7 +2676,7 @@ def mean_internal_temperature_section_from_cert(
|
|||
living_area_fraction=_living_area_fraction(
|
||||
epc.habitable_rooms_count, dim.total_floor_area_m2
|
||||
),
|
||||
control_temperature_adjustment_c=0.0,
|
||||
control_temperature_adjustment_c=_control_temperature_adjustment_c(main),
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -4615,7 +4684,7 @@ def cert_to_inputs(
|
|||
control_type=control_type_value,
|
||||
responsiveness=responsiveness_value,
|
||||
living_area_fraction=living_area_fraction_value,
|
||||
control_temperature_adjustment_c=0.0,
|
||||
control_temperature_adjustment_c=_control_temperature_adjustment_c(main),
|
||||
extended_heating_days_per_month=extended_heating_days,
|
||||
)
|
||||
|
||||
|
|
@ -4809,7 +4878,7 @@ def cert_to_inputs(
|
|||
control_type=control_type_value,
|
||||
responsiveness=responsiveness_value,
|
||||
living_area_fraction=living_area_fraction_value,
|
||||
control_temperature_adjustment_c=0.0,
|
||||
control_temperature_adjustment_c=_control_temperature_adjustment_c(main),
|
||||
thermal_mass_parameter_kj_per_m2_k=_DEFAULT_THERMAL_MASS_PARAMETER_KJ_PER_M2_K,
|
||||
main_heating_efficiency=eff,
|
||||
hot_water_kwh_per_yr=hw_kwh,
|
||||
|
|
|
|||
|
|
@ -3095,6 +3095,131 @@ def test_sap_10_2_table_11_electric_storage_secondary_fraction_dispatches_per_ta
|
|||
)
|
||||
|
||||
|
||||
def test_sap_10_2_table_4e_temperature_adjustment_applied_to_adjusted_mit_per_table_9c_step_8() -> None:
|
||||
"""SAP 10.2 Table 4e (PDF p.171-173) "Heating system controls":
|
||||
|
||||
3. The 'Temperature adjustment' modifies the mean internal
|
||||
temperature and is added to worksheet (92)m.
|
||||
|
||||
Per Table 9c step 8: "Apply adjustment to the mean internal
|
||||
temperature from Table 4e, where appropriate". The adjusted
|
||||
(93)m drives the §8 heat loss rate calc → SH demand.
|
||||
|
||||
Table 4e adjustments per control code:
|
||||
|
||||
Group 0 — No heating system:
|
||||
2699: +0.3
|
||||
Group 1 — Boilers with radiators/UFH:
|
||||
2101, 2102: +0.6 (No thermo / programmer-only)
|
||||
2103..2113: 0
|
||||
Group 2 — Heat pumps with radiators/UFH:
|
||||
2201, 2202: +0.3
|
||||
2203..2210: 0
|
||||
Group 3 — Heat networks:
|
||||
2301, 2302: +0.3
|
||||
2303..2314: 0
|
||||
Group 4 — Electric storage:
|
||||
2401 (Manual charge): +0.7
|
||||
2402 (Automatic charge): +0.4
|
||||
2403 (Celect): +0.4
|
||||
2404 (HHR controls): 0
|
||||
Group 5 — Warm air:
|
||||
2501, 2502: +0.3
|
||||
2503..2506: 0
|
||||
Group 6 — Room heaters:
|
||||
2601: +0.3
|
||||
2602..2605: 0
|
||||
Group 7 — Other systems:
|
||||
2701, 2702: +0.3
|
||||
2703..2706: 0
|
||||
|
||||
Pre-slice the cascade hardcoded `control_temperature_adjustment_c
|
||||
=0.0` at all three call sites of `mean_internal_temperature_
|
||||
monthly` and `space_heating_section_with_results`. Cert electric 3
|
||||
(corpus 001431, SAP code 401 + main_heating_control 2401 "Manual
|
||||
charge control") exposes the gap: worksheet (93) MIT_adjusted Jan
|
||||
= 19.8897 = (92) MIT 19.1897 + 0.7000; cascade (93) Jan = 19.21
|
||||
(= (92) with no adjustment). The missing +0.7 K on internal
|
||||
temperature drives a ~10% undercount in §8 SH demand
|
||||
(cascade 10083 kWh vs worksheet 11088 kWh annual).
|
||||
"""
|
||||
# Arrange — route corpus electric variants through the Elmhurst
|
||||
# extractor → mapper chain and exercise the cascade's
|
||||
# `_control_temperature_adjustment_c` dispatch via the public
|
||||
# `cert_to_inputs` interface. Each variant lodges a distinct
|
||||
# `main_heating_control` (Table 4e code) so the adjustment
|
||||
# dispatch is observable end-to-end.
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from backend.documents_parser.elmhurst_extractor import ElmhurstSiteNotesExtractor
|
||||
from datatypes.epc.domain.mapper import EpcPropertyDataMapper
|
||||
from domain.sap10_calculator.rdsap.cert_to_inputs import (
|
||||
_control_temperature_adjustment_c, # pyright: ignore[reportPrivateUsage]
|
||||
)
|
||||
|
||||
def _epc_for(variant: str):
|
||||
corpus = (
|
||||
Path(__file__).parents[4]
|
||||
/ "sap worksheets/heating systems examples"
|
||||
/ variant
|
||||
)
|
||||
summary_pdf = next(corpus.glob("Summary_*.pdf"))
|
||||
info = subprocess.run(
|
||||
["pdfinfo", str(summary_pdf)], capture_output=True, text=True, check=True,
|
||||
).stdout
|
||||
pc = int(re.search(r"Pages:\s+(\d+)", info).group(1)) # type: ignore[union-attr]
|
||||
pages: list[str] = []
|
||||
for i in range(1, pc + 1):
|
||||
layout = subprocess.run(
|
||||
["pdftotext", "-layout", "-f", str(i), "-l", str(i),
|
||||
str(summary_pdf), "-"],
|
||||
capture_output=True, text=True, check=True,
|
||||
).stdout
|
||||
tokens: list[str] = []
|
||||
for line in layout.splitlines():
|
||||
if not line.strip():
|
||||
tokens.append("")
|
||||
continue
|
||||
parts = [p for p in re.split(r"\s{2,}", line.strip()) if p]
|
||||
tokens.extend(parts)
|
||||
pages.append("\n".join(tokens))
|
||||
return EpcPropertyDataMapper.from_elmhurst_site_notes(
|
||||
ElmhurstSiteNotesExtractor(pages).extract()
|
||||
)
|
||||
|
||||
epc_e3 = _epc_for("electric 3") # control 2401 → +0.7
|
||||
epc_e5 = _epc_for("electric 5") # control 2402 → +0.4
|
||||
epc_e8 = _epc_for("electric 8") # control 2404 → 0
|
||||
epc_pcdb1 = _epc_for("pcdb 1") # control 2105 → 0
|
||||
|
||||
# Act
|
||||
adj_e3 = _control_temperature_adjustment_c(epc_e3.sap_heating.main_heating_details[0])
|
||||
adj_e5 = _control_temperature_adjustment_c(epc_e5.sap_heating.main_heating_details[0])
|
||||
adj_e8 = _control_temperature_adjustment_c(epc_e8.sap_heating.main_heating_details[0])
|
||||
adj_pcdb1 = _control_temperature_adjustment_c(epc_pcdb1.sap_heating.main_heating_details[0])
|
||||
|
||||
# Assert
|
||||
assert abs(adj_e3 - 0.7) < 1e-9, (
|
||||
f"electric 3 main_heating_control=2401 (Manual charge control): "
|
||||
f"got {adj_e3!r}, want +0.7 per SAP 10.2 Table 4e Group 4"
|
||||
)
|
||||
assert abs(adj_e5 - 0.4) < 1e-9, (
|
||||
f"electric 5 main_heating_control=2402 (Automatic charge control): "
|
||||
f"got {adj_e5!r}, want +0.4 per SAP 10.2 Table 4e Group 4"
|
||||
)
|
||||
assert adj_e8 == 0.0, (
|
||||
f"electric 8 main_heating_control=2404 (HHR controls): "
|
||||
f"got {adj_e8!r}, want 0 per SAP 10.2 Table 4e Group 4"
|
||||
)
|
||||
assert adj_pcdb1 == 0.0, (
|
||||
f"pcdb 1 main_heating_control=2105 (Programmer + ≥2 room "
|
||||
f"thermostats): got {adj_pcdb1!r}, want 0 per SAP 10.2 "
|
||||
f"Table 4e Group 1"
|
||||
)
|
||||
|
||||
|
||||
def test_cylinder_storage_loss_applies_57m_solar_adjustment_per_sap_4_line_7693() -> None:
|
||||
"""SAP 10.2 §4 line 7693 (PDF p.137):
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue