mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
S0380.239: system-build walls take masonry structural infiltration (0.35)
RdSAP 10 §2 (Ventilation, "Walls" row): "Structural infiltration: 0.25 for steel or timber frame or 0.35 for masonry construction ... System build: treated as masonry." `_is_timber_or_steel_frame` wrongly included wall_construction code 6 (system build) alongside code 5 (timber frame), handing system-build dwellings the 0.25 structural ACH instead of 0.35. On the cat-10 room-heater fixture (ref 001431, walls SY System Build → code 6) this under-stated the infiltration rate (18) by exactly 0.10 (0.45 vs worksheet 0.55), dropping the effective air change (25), the ventilation heat loss (38)m = 0.33 × (25)m × (5), and the heat-transfer coefficient (39) — so space-heating demand (98) came out 404 kWh low ((211) 11158.6 vs worksheet 11563.2). Restrict the 0.25 branch to code 5 only; code 6 (and everything else) is masonry at 0.35. Pins the rating-block (38)m ventilation heat loss mean = 83.3613 W/K at abs 1e-4 and asserts the classifier treats the system-build wall as masonry. §4 suite green (2415 passed, 1 skipped); no existing fixture relied on system-build → 0.25. Residual after this slice: SAP +0.03 / cost -£0.95 — a small fabric (33) gap (-0.15 W/K) plus lighting (232) +1.0 kWh remain as separate causes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
cbdee9ec3c
commit
efa10fe6cb
2 changed files with 60 additions and 4 deletions
|
|
@ -31,6 +31,7 @@ from datatypes.epc.domain.mapper import EpcPropertyDataMapper
|
|||
from domain.sap10_calculator.calculator import calculate_sap_from_inputs
|
||||
from domain.sap10_calculator.rdsap.cert_to_inputs import (
|
||||
SAP_10_2_SPEC_PRICES,
|
||||
_is_timber_or_steel_frame, # pyright: ignore[reportPrivateUsage]
|
||||
cert_to_inputs,
|
||||
)
|
||||
|
||||
|
|
@ -44,6 +45,12 @@ _FIXTURE_DIR = (
|
|||
# heater is electric (efficiency (216) = 100 %), so (219) == (64) output.
|
||||
_WORKSHEET_LINE_219_WATER_FUEL_KWH = 1770.2313
|
||||
|
||||
# P960 line ref (38)m "Ventilation heat loss calculated monthly" — rating
|
||||
# block, mean of the 12 printed monthly values
|
||||
# (90.1949 .. 86.1692) / 12. The dwelling is SY System Build (masonry per
|
||||
# RdSAP 10 §2), so the structural infiltration (11) = 0.35 not 0.25.
|
||||
_WORKSHEET_LINE_38_VENT_HEAT_LOSS_MEAN_W_PER_K = 83.3613
|
||||
|
||||
_ABS_TOLERANCE = 0.0001
|
||||
|
||||
|
||||
|
|
@ -99,3 +106,46 @@ def test_electric_room_heater_water_fuel_matches_worksheet_line_219() -> None:
|
|||
|
||||
# Assert
|
||||
assert abs(actual_water_fuel_kwh - expected_water_fuel_kwh) <= _ABS_TOLERANCE
|
||||
|
||||
|
||||
def test_system_build_wall_is_classified_masonry_for_structural_infiltration() -> None:
|
||||
# Arrange — the dwelling's walls are SY System Build (wall_construction
|
||||
# code 6). Per RdSAP 10 §2 (Ventilation, "Walls" row): "System build:
|
||||
# treated as masonry", so it must NOT take the 0.25 steel/timber-frame
|
||||
# structural infiltration — only code 5 (timber frame) does.
|
||||
summary_pdf = next(_FIXTURE_DIR.glob("Summary_*.pdf"))
|
||||
pages = _summary_pdf_to_pages(summary_pdf)
|
||||
site_notes = ElmhurstSiteNotesExtractor(pages).extract()
|
||||
epc = EpcPropertyDataMapper.from_elmhurst_site_notes(site_notes)
|
||||
main_wall_construction_code = epc.sap_building_parts[0].wall_construction
|
||||
|
||||
# Act
|
||||
is_frame = _is_timber_or_steel_frame(epc.sap_building_parts)
|
||||
|
||||
# Assert
|
||||
assert main_wall_construction_code == 6
|
||||
assert is_frame is False
|
||||
|
||||
|
||||
def test_electric_room_heater_ventilation_heat_loss_matches_worksheet_line_38() -> None:
|
||||
# Arrange — with SY System Build treated as masonry the structural
|
||||
# infiltration (11) = 0.35, lifting the effective air change (25) and
|
||||
# the monthly ventilation heat loss (38)m = 0.33 × (25)m × (5) to the
|
||||
# worksheet's rating-block values.
|
||||
summary_pdf = next(_FIXTURE_DIR.glob("Summary_*.pdf"))
|
||||
pages = _summary_pdf_to_pages(summary_pdf)
|
||||
site_notes = ElmhurstSiteNotesExtractor(pages).extract()
|
||||
epc = EpcPropertyDataMapper.from_elmhurst_site_notes(site_notes)
|
||||
expected_vent_heat_loss_w_per_k = _WORKSHEET_LINE_38_VENT_HEAT_LOSS_MEAN_W_PER_K
|
||||
|
||||
# Act
|
||||
rating = calculate_sap_from_inputs(
|
||||
cert_to_inputs(epc, prices=SAP_10_2_SPEC_PRICES),
|
||||
)
|
||||
actual_vent_heat_loss_w_per_k = rating.intermediate["infiltration_w_per_k"]
|
||||
|
||||
# Assert
|
||||
assert (
|
||||
abs(actual_vent_heat_loss_w_per_k - expected_vent_heat_loss_w_per_k)
|
||||
<= _ABS_TOLERANCE
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1393,13 +1393,19 @@ def _climate_source(
|
|||
|
||||
|
||||
def _is_timber_or_steel_frame(parts: list[SapBuildingPart]) -> bool:
|
||||
"""RdSAP 10 §5: wall_construction codes 5 (timber frame) and 6 (system
|
||||
build steel frame) get the lower 0.25 structural ACH; everything else
|
||||
is treated as 0.35 masonry."""
|
||||
"""RdSAP 10 §2 (Ventilation, "Walls" row): "Structural infiltration:
|
||||
0.25 for steel or timber frame or 0.35 for masonry construction ...
|
||||
System build: treated as masonry." So only wall_construction code 5
|
||||
(timber frame) takes the lower 0.25 structural ACH; code 6 (system
|
||||
build) is explicitly masonry (0.35), as is everything else.
|
||||
|
||||
(Park homes also take the timber-frame value per the same spec row,
|
||||
but that is a dwelling-type flag, not a wall_construction code, and is
|
||||
out of scope here.)"""
|
||||
if not parts:
|
||||
return False
|
||||
wc = parts[0].wall_construction
|
||||
return isinstance(wc, int) and wc in (5, 6)
|
||||
return isinstance(wc, int) and wc == 5
|
||||
|
||||
|
||||
def _living_area_fraction_default(habitable_rooms_count: Optional[int]) -> float:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue