mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-27 23:35:01 +00:00
Close the last PasHub mapper gaps: secondary fuel + Group 3 heat-network control 🟩
Secondary fuel 'Mains gas'/'Mains Gas'->26 and 'House Coal'->11 (Table 32);
main-heating control resolution becomes group-aware with a Table 4e Group 3
map ('Charging system linked to use of community heating, room thermostat
only' -> 2306, the code the accredited Elmhurst certs lodge for this cohort;
all four linked-to-use codes are calculation-identical). All 205 Guinness
fixtures now compute (was 201/4 xfails); ratchet re-baselined to the
full-coverage aggregate (12.2% within-0.5 floor, MAE ceiling 2.71) with the
two newly-computable outliers documented for verification.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
533f33d62b
commit
cab7e3d613
3 changed files with 125 additions and 35 deletions
|
|
@ -63,21 +63,25 @@ _KNOWN_GAPS: tuple[type[Exception], ...] = (UnmappedSapCode, UnmappedPasHubLabel
|
|||
_FIXTURES_DIR = Path(__file__).parent / "fixtures" / "pashub_accuracy"
|
||||
_MANIFEST_PATH = _FIXTURES_DIR / "manifest.json"
|
||||
|
||||
# --- Ratchets (never loosen), mirroring test_sap_accuracy_corpus.py. ----------
|
||||
# First real aggregate: the main-heating control-code mapper fix (#1557) is the
|
||||
# gap that unblocks computation for the cohort. Before it, every fixture raised
|
||||
# `UnmappedSapCode: main_heating_control` (0/205 computed, aggregate xfailed);
|
||||
# with it, 201/205 compute (4 still blocked on the residual fuel-code gap,
|
||||
# e.g. Bulk LPG). Observed on this branch, with the control fix layered on the
|
||||
# full merged sibling stack to date (fabric #1559-#1562 plus secondary-fuel
|
||||
# #1564 and roof/country #1566): SAP within-0.5 = 12.4% (25/201), MAE = 2.560.
|
||||
# The within-0.5 count is one distributional metric that moved *down* a touch
|
||||
# (13.4% -> 12.4%) as those later fixes landed even though MAE fell (2.65 ->
|
||||
# 2.56) and is not loosened relative to the current integrated state — it is set
|
||||
# to what the current codebase actually produces. Ratchet up as later field
|
||||
# fixes land, per #1568.
|
||||
_MIN_WITHIN_HALF: float = 0.124
|
||||
_MAX_SAP_MAE: float = 2.561
|
||||
# --- Ratchets (never loosen at fixed coverage), mirroring test_sap_accuracy_corpus.py.
|
||||
# The last integrated state computed 201/205 (4 blocked on residual
|
||||
# `UnmappedPasHubLabel` gaps) at SAP within-0.5 = 12.4% (25/201), MAE = 2.560.
|
||||
# Closing those last gaps — secondary fuel 'Mains Gas'/'House Coal' and the
|
||||
# Table 4e Group 3 heat-network control 'Charging system linked to use of
|
||||
# community heating, room thermostat only' — brings the cohort to **205/205
|
||||
# computed (0 blocked)**. That is coverage growth, not a loosening: the
|
||||
# within-0.5 *count* is unchanged at 25, so no fixture regressed; the fraction
|
||||
# is 25/205 = 12.2% only because the 4 newly-computable fixtures all sit outside
|
||||
# 0.5. MAE rises 2.560 -> 2.701 because two of those four are large, isolated
|
||||
# divergences NOT caused by the mappings (which land the others at d=-1.7/+7.6):
|
||||
# - 499584755922 d=-33.0 Bulk-LPG main + house-coal secondary (34.0 vs pre_sap 67)
|
||||
# - 507644414148 d=+18.2 community heating — pashub path maps only the control
|
||||
# code, not the full heat-network fuel/flags (70.2 vs 52)
|
||||
# Both are tracked for verification (pre_sap correctness vs a deeper modelling
|
||||
# gap) — see docs/HANDOVER_PASHUB_838_PROBLEM_PROPERTIES.md. Re-baselined to the
|
||||
# full-coverage aggregate; ratchet back DOWN as those two are resolved.
|
||||
_MIN_WITHIN_HALF: float = 0.12
|
||||
_MAX_SAP_MAE: float = 2.71
|
||||
|
||||
_KNOWN_GAP_REASON = (
|
||||
"pashub `from_site_notes` mapper does not int-code a main-heating "
|
||||
|
|
|
|||
|
|
@ -7165,6 +7165,14 @@ _PASHUB_MAIN_FUEL_TO_SAP10: Dict[str, int] = {
|
|||
# not a fuel code.
|
||||
_PASHUB_SECONDARY_FUEL_TO_SAP10: Dict[str, int] = {
|
||||
"Electricity": 30, # standard electricity, Table 32 code 30
|
||||
# Mains-gas secondary heater (e.g. a gas fire): same fuel code the main-fuel
|
||||
# map pins for mains gas (26 — see `_PASHUB_MAIN_FUEL_TO_SAP10`). Both label
|
||||
# cases occur in the cohort's site notes, mirroring the main-fuel map.
|
||||
"Mains gas": 26,
|
||||
"Mains Gas": 26,
|
||||
# Solid-fuel secondary heater burning house coal → Table 32 code 11 (House
|
||||
# coal), the same code the solid-fuel main-heating path pins (SAP 631 → 11).
|
||||
"House Coal": 11,
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -7229,6 +7237,30 @@ _PASHUB_MAIN_HEATING_CONTROL_TO_SAP10: Dict[str, int] = {
|
|||
}
|
||||
|
||||
|
||||
# PasHub `system_type` labels that are SAP 10.2 Table 4e Group 3 heat networks —
|
||||
# the only non-boiler group the cohort surveys. Their control codes live in a
|
||||
# separate map (below) because the same control label carries a different 2xxx
|
||||
# code per group (see `_PASHUB_BOILER_SYSTEM_TYPES`).
|
||||
_PASHUB_HEAT_NETWORK_SYSTEM_TYPES: frozenset[str] = frozenset({
|
||||
"community heating system",
|
||||
})
|
||||
|
||||
|
||||
# PasHub Site-Notes main-heating "Controls" labels for a Table 4e Group 3 heat
|
||||
# network → their SAP code. All four "charging system linked to use of community
|
||||
# heating" codes (2306/2310/2312/2314) are calculation-identical — control_type
|
||||
# 3, DLF 1.00/1.00, temperature_adjustment 0.0 (`cert_to_inputs.py`
|
||||
# `_CONTROL_TYPE_BY_CODE` / DLF / temp-adjustment maps) — so the cohort's
|
||||
# "...room thermostat only" resolves to 2306, the domain's modal heat-network
|
||||
# control code (`domain/epc/property_overlays/main_heating_system_overlay.py`,
|
||||
# `HANDOVER_POST_S0380_176`). Every code here is in `_CONTROL_TYPE_BY_CODE`.
|
||||
_PASHUB_HEAT_NETWORK_CONTROL_TO_SAP10: Dict[str, int] = {
|
||||
"Charging system linked to use of community heating, room thermostat only": (
|
||||
2306
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def _pashub_main_heating_control_code(
|
||||
control_label: str, system_type: str
|
||||
) -> Union[int, str]:
|
||||
|
|
@ -7236,24 +7268,34 @@ def _pashub_main_heating_control_code(
|
|||
Table 4e code at the mapper boundary (ADR-0015). A genuinely blank label is
|
||||
the "no controls lodged" shape and passes through unchanged.
|
||||
|
||||
The resolution is system-aware: the lookup holds only Group 1 (boiler)
|
||||
codes, and the same control label recurs across Table 4e's system groups
|
||||
under different 2xxx codes, so a non-boiler `system_type` strict-raises
|
||||
(naming the system) rather than silently taking a boiler code. A boiler
|
||||
system whose label the lookup does not cover likewise strict-raises — either
|
||||
way the gap is fixed here, never mis-rated or strict-raised deep in the
|
||||
calculator as `UnmappedSapCode`."""
|
||||
The resolution is system-aware: the same control label recurs across Table
|
||||
4e's system groups under different 2xxx codes, so the code is chosen by the
|
||||
system's group — Group 1 boiler (`_PASHUB_MAIN_HEATING_CONTROL_TO_SAP10`) or
|
||||
Group 3 heat network (`_PASHUB_HEAT_NETWORK_CONTROL_TO_SAP10`). Any other
|
||||
system group, or a label the group's lookup does not cover, strict-raises
|
||||
`UnmappedPasHubLabel` (naming the label and system) rather than silently
|
||||
taking a wrong-group code — the gap is fixed here, never mis-rated or
|
||||
strict-raised deep in the calculator as `UnmappedSapCode`."""
|
||||
if not control_label:
|
||||
return control_label
|
||||
if system_type.lower() not in _PASHUB_BOILER_SYSTEM_TYPES:
|
||||
system = system_type.lower()
|
||||
if system in _PASHUB_BOILER_SYSTEM_TYPES:
|
||||
group_map = _PASHUB_MAIN_HEATING_CONTROL_TO_SAP10
|
||||
elif system in _PASHUB_HEAT_NETWORK_SYSTEM_TYPES:
|
||||
group_map = _PASHUB_HEAT_NETWORK_CONTROL_TO_SAP10
|
||||
else:
|
||||
raise UnmappedPasHubLabel(
|
||||
"main heating control",
|
||||
f"{control_label!r} on non-boiler system {system_type!r} "
|
||||
f"(only Table 4e Group 1 boiler codes are mapped)",
|
||||
f"{control_label!r} on unsupported system {system_type!r} "
|
||||
f"(only Table 4e Group 1 boiler + Group 3 heat-network codes "
|
||||
f"are mapped)",
|
||||
)
|
||||
code = _PASHUB_MAIN_HEATING_CONTROL_TO_SAP10.get(control_label)
|
||||
code = group_map.get(control_label)
|
||||
if code is None:
|
||||
raise UnmappedPasHubLabel("main heating control", control_label)
|
||||
raise UnmappedPasHubLabel(
|
||||
"main heating control",
|
||||
f"{control_label!r} on system {system_type!r}",
|
||||
)
|
||||
return code
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -181,6 +181,9 @@ class TestSecondaryFuelTypeCoding:
|
|||
"label, expected_code",
|
||||
[
|
||||
("Electricity", 30), # standard electricity, Table 32 code 30
|
||||
("Mains gas", 26), # mains-gas secondary heater, code 26
|
||||
("Mains Gas", 26), # capitalised site-note variant, same code
|
||||
("House Coal", 11), # solid-fuel secondary, Table 32 house coal
|
||||
("No Secondary Heating", None), # no secondary heater lodged
|
||||
("", None), # no lodging
|
||||
],
|
||||
|
|
@ -1266,18 +1269,19 @@ class TestPasHubMainHeatingControlCoding:
|
|||
# inventory `_CONTROL_TYPE_BY_CODE`; a code that isn't there would
|
||||
# strict-raise `UnmappedSapCode` in `_control_type`. This guards the
|
||||
# mapper against drifting out of sync with the calculator's coverage.
|
||||
from datatypes.epc.domain.mapper import ( # pyright: ignore[reportPrivateUsage]
|
||||
_PASHUB_MAIN_HEATING_CONTROL_TO_SAP10,
|
||||
from datatypes.epc.domain.mapper import (
|
||||
_PASHUB_HEAT_NETWORK_CONTROL_TO_SAP10, # pyright: ignore[reportPrivateUsage]
|
||||
_PASHUB_MAIN_HEATING_CONTROL_TO_SAP10, # pyright: ignore[reportPrivateUsage]
|
||||
)
|
||||
from domain.sap10_calculator.rdsap.cert_to_inputs import ( # pyright: ignore[reportPrivateUsage]
|
||||
_CONTROL_TYPE_BY_CODE,
|
||||
from domain.sap10_calculator.rdsap.cert_to_inputs import (
|
||||
_CONTROL_TYPE_BY_CODE, # pyright: ignore[reportPrivateUsage]
|
||||
)
|
||||
|
||||
unknown = {
|
||||
code
|
||||
for code in _PASHUB_MAIN_HEATING_CONTROL_TO_SAP10.values()
|
||||
if code not in _CONTROL_TYPE_BY_CODE
|
||||
mapped_codes = {
|
||||
*_PASHUB_MAIN_HEATING_CONTROL_TO_SAP10.values(),
|
||||
*_PASHUB_HEAT_NETWORK_CONTROL_TO_SAP10.values(),
|
||||
}
|
||||
unknown = {c for c in mapped_codes if c not in _CONTROL_TYPE_BY_CODE}
|
||||
|
||||
assert not unknown
|
||||
|
||||
|
|
@ -1358,6 +1362,46 @@ class TestPasHubMainHeatingControlCoding:
|
|||
with pytest.raises(UnmappedPasHubLabel, match="Heat pump"):
|
||||
EpcPropertyDataMapper.from_site_notes(survey)
|
||||
|
||||
def test_heat_network_control_maps_to_group3_code(self) -> None:
|
||||
# Arrange — a Table 4e Group 3 heat network (`system_type =
|
||||
# "Community heating system"`). Its control label carries a Group 3
|
||||
# (23xx) code, not the boiler 21xx; the accredited Elmhurst certs for
|
||||
# this same cohort lodge 2306 for it (HANDOVER_POST_S0380_176).
|
||||
from domain.sap10_calculator.rdsap.cert_to_inputs import (
|
||||
_control_type, # pyright: ignore[reportPrivateUsage]
|
||||
)
|
||||
|
||||
raw = load("pashub_rdsap_site_notes_example1.json")
|
||||
raw["heating_and_hot_water"]["main_heating"][
|
||||
"system_type"
|
||||
] = "Community heating system"
|
||||
raw["heating_and_hot_water"]["main_heating"]["controls"] = (
|
||||
"Charging system linked to use of community heating, room thermostat only"
|
||||
)
|
||||
survey = from_dict(PasHubRdSapSiteNotes, raw)
|
||||
|
||||
# Act
|
||||
main = EpcPropertyDataMapper.from_site_notes(
|
||||
survey
|
||||
).sap_heating.main_heating_details[0]
|
||||
|
||||
# Assert — the Group 3 code, and its spec control type (3).
|
||||
assert main.main_heating_control == 2306
|
||||
assert _control_type(main) == 3
|
||||
|
||||
def test_heat_network_unmapped_control_label_strict_raises(self) -> None:
|
||||
# A community-system control label the Group 3 lookup does not cover
|
||||
# strict-raises (naming label + system), never silently mis-rating.
|
||||
raw = load("pashub_rdsap_site_notes_example1.json")
|
||||
raw["heating_and_hot_water"]["main_heating"][
|
||||
"system_type"
|
||||
] = "Community heating system"
|
||||
raw["heating_and_hot_water"]["main_heating"]["controls"] = "Some novel control"
|
||||
survey = from_dict(PasHubRdSapSiteNotes, raw)
|
||||
|
||||
with pytest.raises(UnmappedPasHubLabel, match="Community heating system"):
|
||||
EpcPropertyDataMapper.from_site_notes(survey)
|
||||
|
||||
|
||||
class TestPasHubUnmappedHeatEmitter:
|
||||
"""A PasHub survey whose surveyed main-heating emitter label the mapper does
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue