mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-27 23:35:01 +00:00
PasHub SAP accuracy: semi-exposed-unheated floor → Table 20, not §5.14 🟩
`_PASHUB_FLOOR_EXPOSURE` mapped the surveyed "Semi Exposed (unheated)" floor label to `is_above_partially_heated_space` (RdSAP §5.14, constant U=0.7). A floor semi-exposed to an enclosed UNHEATED space belongs in §5.13 Table 20 `u_exposed_floor` (band-B as-built U=1.20). §5.14's U=0.7 is reserved for a floor above a *partially* heated space. #1602 transposed the two branches; the too-low U under-counted floor loss on the 9 label-carrying cohort fixtures → a shared over-credit. Triangulated against all three siblings + the calculator docstring: the gov-API code-2 "To unheated space" (mapper.py:4797) and Elmhurst `_is_floor_exposed_to_unheated_space` (mapper.py:5970) both route to Table 20; rdsap_uvalues.py:1416 documents Table 20 as collapsing exposed and semi-exposed-to-unheated into one lookup. One-line data flip, from_site_notes-only (gov-API RdSAP corpus untouched, still 78.9%). Adjacent Ronald St mid-terrace pair (the clustered signal): 10 Ronald 60.35→59.02 (oracle 59), 12 Ronald 60.79→59.36 (oracle 59). Cohort vs DB oracle (portfolio 838 effective_sap) 83.3%→86.9% within-0.5, MAE 0.377→0.348, no built form regresses, all 7 verified dwellings unchanged (none carry the label). pashub harness 79.5%→82.4%, MAE 0.389→0.367; ratchets tightened 0.78→0.82 / 0.40→0.37. pyright zero-new. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
04b7c26f67
commit
4c0ccad882
3 changed files with 46 additions and 15 deletions
|
|
@ -115,7 +115,19 @@ _MANIFEST_PATH = _FIXTURES_DIR / "manifest.json"
|
|||
# built form near zero. Observed 79.5% here; vs the DB SAP-10.2 oracle
|
||||
# (property_baseline_performance.effective_sap_score, portfolio 838) the cohort
|
||||
# moves 62.1% -> 83.3% within-0.5. gov-API RdSAP corpus unchanged (78.9%).
|
||||
_MIN_WITHIN_HALF: float = 0.78
|
||||
# Ratcheted 0.78 -> 0.82 by the 2026-07-15e SEMI-EXPOSED-FLOOR fix: from_site_notes
|
||||
# mis-routed the surveyed "Semi Exposed (unheated)" floor label to RdSAP §5.14
|
||||
# (partially-heated constant U=0.7) when a floor semi-exposed to an enclosed
|
||||
# UNHEATED space belongs in §5.13 Table 20 `u_exposed_floor` (band B as-built
|
||||
# U=1.20) — the gov-API code-2 "To unheated space" and Elmhurst
|
||||
# `_is_floor_exposed_to_unheated_space` siblings both route it to Table 20. The
|
||||
# too-low U under-counted floor loss on the 9 label-carrying cohort fixtures (the
|
||||
# adjacent Ronald St mid-terrace pair among them) → a shared over-credit. A pure
|
||||
# data flip in `_PASHUB_FLOOR_EXPOSURE`, from_site_notes-only (gov-API corpus
|
||||
# untouched, still 78.9%). Observed 82.4% here; vs the DB SAP-10.2 oracle the
|
||||
# cohort moves 83.3% -> 86.9% within-0.5, no built form regresses, all 7 verified
|
||||
# dwellings unchanged (none carry the label).
|
||||
_MIN_WITHIN_HALF: float = 0.82
|
||||
# Ratcheted 2.71 -> 2.64 by #1590 bug 2 (roof "Insulation At: None" -> explicit
|
||||
# zero thickness): observed MAE 2.632 — the two ~7-SAP roof over-raters
|
||||
# (507665533138 / 507670893756) correcting is exactly 14/205 ≈ 0.07 of MAE.
|
||||
|
|
@ -187,7 +199,9 @@ _MIN_WITHIN_HALF: float = 0.78
|
|||
# (percent_draughtproofed + upper-floor +0.25 m + built-form sheltered_sides;
|
||||
# #1601 upper-floor RE-ADJUDICATED, see the within-0.5 note above): observed
|
||||
# MAE 0.389.
|
||||
_MAX_SAP_MAE: float = 0.40
|
||||
# Ratcheted 0.40 -> 0.37 by the 2026-07-15e semi-exposed-floor fix (see the
|
||||
# within-0.5 note above): observed MAE 0.367.
|
||||
_MAX_SAP_MAE: float = 0.37
|
||||
|
||||
_KNOWN_GAP_REASON = (
|
||||
"pashub `from_site_notes` mapper does not int-code a main-heating "
|
||||
|
|
|
|||
|
|
@ -6191,9 +6191,16 @@ _PASHUB_FLOOR_EXPOSURE: Final[Dict[str, tuple[bool, bool]]] = {
|
|||
# "Exposed Floor" → open to external air / over enclosed unheated space →
|
||||
# RdSAP 10 §5.13 Table 20 `u_exposed_floor` cascade (is_exposed_floor).
|
||||
"Exposed Floor": (True, False),
|
||||
# "Semi Exposed (unheated)" → above a space heated to a lesser extent →
|
||||
# RdSAP 10 §5.14 constant U=0.7 (is_above_partially_heated_space).
|
||||
"Semi Exposed (unheated)": (False, True),
|
||||
# "Semi Exposed (unheated)" → floor semi-exposed to an enclosed UNHEATED
|
||||
# space → RdSAP 10 §5.13 Table 20 `u_exposed_floor` (is_exposed_floor),
|
||||
# NOT §5.14. Table 20 "collapses exposed (to outside air) and semi-exposed
|
||||
# (to enclosed unheated space) into the same lookup" (rdsap_uvalues.py:1416;
|
||||
# band B as-built U=1.20). Mirrors the gov-API code-2 "To unheated space"
|
||||
# (semi-exposed → Table 20, mapper.py:4797) and the Elmhurst
|
||||
# `_is_floor_exposed_to_unheated_space` sibling (mapper.py:5970). §5.14's
|
||||
# constant U=0.7 is reserved for a floor above a *partially* heated space
|
||||
# (`is_above_partially_heated_space`); the #1602 routing transposed the two.
|
||||
"Semi Exposed (unheated)": (True, False),
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -508,12 +508,16 @@ class TestWallDryLinedMapping:
|
|||
class TestFloorExposureMapping:
|
||||
"""A surveyed "Floor type" of "Semi Exposed (unheated)" or "Exposed Floor"
|
||||
must set the SapFloorDimension exposure flags on the part's LOWEST storey
|
||||
(floor==0), so the floor-U cascade routes through RdSAP §5.14 (U=0.7,
|
||||
above partially heated space) / §5.13 Table 20 (u_exposed_floor) instead of
|
||||
the default BS EN ISO 13370 ground-floor formula. `_map_floor_dimensions`
|
||||
previously never set them — the exposure was silently dropped and every
|
||||
semi-exposed/exposed floor billed as an on-soil ground floor (#1602). The
|
||||
gov-API/Elmhurst siblings already flag the ground SapFloorDimension."""
|
||||
(floor==0), so the floor-U cascade routes through RdSAP §5.13 Table 20
|
||||
(`u_exposed_floor`) — which covers BOTH a floor exposed to outside air AND
|
||||
one semi-exposed to an enclosed unheated space — instead of the default
|
||||
BS EN ISO 13370 ground-floor formula. `_map_floor_dimensions` previously
|
||||
never set them — the exposure was silently dropped and every semi-exposed/
|
||||
exposed floor billed as an on-soil ground floor (#1602). §5.14's constant
|
||||
U=0.7 is reserved for a floor above a *partially* heated space, which the
|
||||
PAS Hub survey does not lodge; #1602 mis-routed "Semi Exposed (unheated)"
|
||||
there and this corrects it to Table 20. The gov-API/Elmhurst siblings
|
||||
already flag the ground SapFloorDimension to Table 20."""
|
||||
|
||||
@staticmethod
|
||||
def _ground(part: SapBuildingPart) -> SapFloorDimension:
|
||||
|
|
@ -523,7 +527,13 @@ class TestFloorExposureMapping:
|
|||
def _upper(part: SapBuildingPart) -> SapFloorDimension:
|
||||
return next(fd for fd in part.sap_floor_dimensions if fd.floor == 1)
|
||||
|
||||
def test_semi_exposed_sets_above_partially_heated_on_ground_floor(self) -> None:
|
||||
def test_semi_exposed_unheated_sets_exposed_floor_on_ground_floor(self) -> None:
|
||||
# "Semi Exposed (unheated)" sits above an enclosed UNHEATED space →
|
||||
# RdSAP 10 §5.13 Table 20 `u_exposed_floor` (is_exposed_floor), NOT
|
||||
# §5.14's constant U=0.7 (reserved for a *partially* heated space
|
||||
# below). Mirrors the gov-API code-2 "To unheated space" and the
|
||||
# Elmhurst `_is_floor_exposed_to_unheated_space` siblings; corrects
|
||||
# the #1602 routing that transposed the two branches.
|
||||
# Arrange
|
||||
data = load("pashub_rdsap_site_notes_example1.json")
|
||||
data["building_construction"]["floor"]["floor_type"] = "Semi Exposed (unheated)"
|
||||
|
|
@ -532,10 +542,10 @@ class TestFloorExposureMapping:
|
|||
# Act
|
||||
result = EpcPropertyDataMapper.from_site_notes(survey)
|
||||
|
||||
# Assert — §5.14 flag on the ground floor only, exposed flag clear.
|
||||
# Assert — Table 20 exposed flag on the ground floor only.
|
||||
ground = self._ground(result.sap_building_parts[0])
|
||||
assert ground.is_above_partially_heated_space is True
|
||||
assert ground.is_exposed_floor is False
|
||||
assert ground.is_exposed_floor is True
|
||||
assert ground.is_above_partially_heated_space is False
|
||||
|
||||
def test_exposed_floor_sets_exposed_on_ground_floor(self) -> None:
|
||||
# Arrange
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue