mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-27 23:35:01 +00:00
Treat a lodged party ceiling as U=0 instead of a pitched roof 🟩
RdSAP 10 item 2-3(h) "another dwelling above" is U=0 (spec p.45: "There is no heat loss through the roof of a building part that has the same dwelling above or another dwelling above"). gov-API `roof_construction = 3` is that code, but the mapper labelled it "Pitched (slates/tiles), no access to loft" (which is code 5), so the per-part party override at heat_transmission.py:1172 never matched and the part fell through to the dwelling_type-label-only has_exposed_roof heuristic. 190 of the 200 corpus code-3 parts sit at index 0 and are already suppressed by `_cert_lodges_exposed_roof` off the roof_insulation_location == "ND" signal. The unmitigated set is the 10 parts at index > 0, where extensions default to exposed and were billed a phantom pitched roof. Also removes a latent PV defect: the bogus "Pitched" prefix satisfied `is_pitched` and divided PV array area by cos(35 deg), inflating kWp. Shipped with the stairwell fix so the corpus pin moves once: within-0.5 78.8% -> 79.6%, SAP MAE 0.626 -> 0.611, PE MAE 2.9 -> 2.7. The roof change alone trades 4 certs out of the +-0.5 band for 2 in (one moving 4.4 SAP closer). Those 4 were investigated and are NOT a masked bug: the corpus error distribution is symmetric (mean +0.076, median +0.03, sigma 1.606; 112 certs above +0.5 vs 102 below), ground-floor flats show 7 positive / 8 negative with no excess over the null, and closing them needs HLC changes of 2.3-5.6% — below the ~8.8% noise floor of the RHI anchor, calibrated on the 76 certs whose SAP is exact (space-heating ratio 1.062 +/- 0.088). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3f6129afc6
commit
da993b5359
3 changed files with 97 additions and 3 deletions
|
|
@ -4607,7 +4607,23 @@ _API_FLOOR_CONSTRUCTION_TO_STR: Dict[int, Optional[str]] = {
|
|||
# here too rather than left half-fixed.
|
||||
_API_ROOF_CONSTRUCTION_TO_STR: Dict[int, Optional[str]] = {
|
||||
1: "Flat",
|
||||
3: "Pitched (slates/tiles), no access to loft",
|
||||
# RdSAP 10 item 2-3(h) "another dwelling above" — U=0 (spec p.45: "There is
|
||||
# no heat loss through the roof of a building part that has the same
|
||||
# dwelling above or another dwelling above"). Previously mislabelled
|
||||
# "Pitched (slates/tiles), no access to loft" (that is code 5), so the
|
||||
# per-part party override at `heat_transmission.py:1172` never matched.
|
||||
# 190 of the 200 corpus code-3 parts sit at index 0 and are already
|
||||
# suppressed by `_cert_lodges_exposed_roof` off the `roof_insulation_location
|
||||
# == "ND"` signal; the unmitigated set is the 10 parts at index > 0, where
|
||||
# extensions default to exposed and were billed a phantom pitched roof.
|
||||
# Also removes a latent PV defect: the bogus "Pitched" prefix satisfied
|
||||
# `is_pitched` (cert_to_inputs.py:3559) and divided PV array area by
|
||||
# cos(35°), inflating kWp. Corpus evidence (1000 certs, 200 code-3 parts):
|
||||
# 200/200 lodge roof_insulation_location "ND", 186/200 lodge the roof
|
||||
# description literally "(another dwelling above)", zero lodge a "no access
|
||||
# to loft" description, and no top-floor dwelling ever lodges it. Codes 7
|
||||
# and 9 were corrected by an earlier slice; 3 was left behind.
|
||||
3: "(another dwelling above)",
|
||||
4: "Pitched (slates/tiles), access to loft",
|
||||
5: "Pitched (vaulted ceiling)",
|
||||
6: None,
|
||||
|
|
|
|||
78
tests/datatypes/epc/domain/test_mapper_roof_party_ceiling.py
Normal file
78
tests/datatypes/epc/domain/test_mapper_roof_party_ceiling.py
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
"""Regression: gov-API `roof_construction = 3` is "another dwelling above"
|
||||
(RdSAP 10 item 2-3(h), U=0), NOT a pitched roof.
|
||||
|
||||
RdSAP 10 item 2-3 "Above the building part" (spec PDF p.72, printed p.71) lists
|
||||
the codes and their treatment; (g) "same dwelling above" and (h) "another
|
||||
dwelling above" are both U=0, and p.45 states it plainly: "There is no heat loss
|
||||
through the roof of a building part that has the same dwelling above or another
|
||||
dwelling above."
|
||||
|
||||
The mapper labelled code 3 "Pitched (slates/tiles), no access to loft", so the
|
||||
per-part party override in `heat_transmission.py` — which matches on the literal
|
||||
string "another dwelling above" in `roof_construction_type` — never fired. The
|
||||
building part then fell through to the dwelling-level `has_exposed_roof`
|
||||
heuristic, which is keyed only on the `dwelling_type` LABEL. Where that label
|
||||
says top-floor (or the part is an extension, billed exposed unconditionally), we
|
||||
invented an entire pitched roof over a lodged party ceiling.
|
||||
|
||||
Evidence that code 3 is a party ceiling, from the committed RdSAP-21.0.1 corpus
|
||||
(1000 certs, 200 code-3 building parts):
|
||||
- 200/200 lodge `roof_insulation_location == "ND"` (not determined — there is
|
||||
no insulation to determine on a party ceiling);
|
||||
- 186/200 lodge the cert's roof description as literally
|
||||
"(another dwelling above)";
|
||||
- dwelling types are exclusively non-top-floor (Mid-floor flat 104,
|
||||
Ground-floor flat 86, Ground-floor maisonette 7, Mid-floor maisonette 1,
|
||||
Mid-terrace house 2) — no top-floor dwelling ever lodges it.
|
||||
|
||||
Codes 7 and 9 were corrected to the party string by an earlier slice; code 3 was
|
||||
left behind.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from datatypes.epc.domain.mapper import EpcPropertyDataMapper
|
||||
|
||||
_FIXTURE = Path("backend/epc_api/json_samples/RdSAP-Schema-21.0.1/epc.json")
|
||||
|
||||
_PARTY_CEILING = "another dwelling above"
|
||||
|
||||
|
||||
def _load_with_roof_construction(code: int) -> dict[str, Any]:
|
||||
data: dict[str, Any] = json.loads(_FIXTURE.read_text())
|
||||
for part in data["sap_building_parts"]:
|
||||
part["roof_construction"] = code
|
||||
return data
|
||||
|
||||
|
||||
def test_roof_construction_3_maps_to_a_party_ceiling() -> None:
|
||||
# Arrange — the lodged part has another dwelling above it (API code 3).
|
||||
data = _load_with_roof_construction(3)
|
||||
|
||||
# Act
|
||||
epc = EpcPropertyDataMapper.from_api_response(data)
|
||||
|
||||
# Assert — carries the party-ceiling marker the per-part override matches on,
|
||||
# so the roof is billed at U=0 rather than as an invented pitched roof.
|
||||
assert epc is not None
|
||||
assert epc.sap_building_parts
|
||||
for part in epc.sap_building_parts:
|
||||
assert _PARTY_CEILING in (part.roof_construction_type or "").lower()
|
||||
|
||||
|
||||
def test_roof_construction_4_remains_a_real_pitched_roof() -> None:
|
||||
# Arrange — code 4 is 2-3(a) "pitched roof, access to loft", a REAL roof.
|
||||
data = _load_with_roof_construction(4)
|
||||
|
||||
# Act
|
||||
epc = EpcPropertyDataMapper.from_api_response(data)
|
||||
|
||||
# Assert — must not be swept up by the code-3 correction.
|
||||
assert epc is not None
|
||||
assert epc.sap_building_parts
|
||||
for part in epc.sap_building_parts:
|
||||
assert _PARTY_CEILING not in (part.roof_construction_type or "").lower()
|
||||
|
|
@ -267,7 +267,7 @@ _CORPUS = Path(
|
|||
# unheated corridor, 3 = unheated stairwell), which was read NOWHERE under
|
||||
# domain/, so every sheltered wall took the corridor 0.5 and every stairwell
|
||||
# flat over-billed its sheltered wall. 23 corpus certs; within-0.5 +1.0pp.
|
||||
_MIN_WITHIN_HALF_SAP = 0.798
|
||||
_MIN_WITHIN_HALF_SAP = 0.796
|
||||
# 0.793 -> 0.789 via the §12 Unknown-meter + dual-electric-immersion off-peak
|
||||
# trigger (RdSAP 10 PDF p.62): Apartment 241 (main 691 + 903 dual immersion)
|
||||
# -5.38 -> -1.05. Worksheet-validated on "simulated case 48" (Elmhurst SAP 57,
|
||||
|
|
@ -393,7 +393,7 @@ _MIN_WITHIN_HALF_SAP = 0.798
|
|||
# The -0.672 overshoot is expected to move again when the C4 plant-
|
||||
# efficiency and C3.2 pumping siblings land. Unit-pinned in
|
||||
# test_cert_to_inputs.
|
||||
_MAX_SAP_MAE = 0.614
|
||||
_MAX_SAP_MAE = 0.611
|
||||
_MAX_CO2_MAE_TONNES = 0.072 # t CO2 / yr vs co2_emissions_current
|
||||
_MAX_PE_PER_M2_MAE = 3.0 # kWh / m2 / yr vs energy_consumption_current
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue