mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-27 23:35:01 +00:00
Lodge the heat-network SAP code and Table 12 community fuel for PasHub community heating 🟩
The extractor now captures "Heating System (Other):" and the mapper resolves it to the Table 4a code (301, community boilers) + Table 12 community fuel (51 via the shared _resolve_community_heating_fuel_code), so is_heat_network_main routes the DLF / standing-charge / 80%-efficiency branch instead of pricing the dwelling as an ordinary mains-gas boiler (fixture 507644414148: +14.3 over-rate closes by ~8.6). Harness MAE 1.726 -> 1.684; ceiling ratcheted to 1.70. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4d60d79cc9
commit
f45651dbc0
5 changed files with 68 additions and 11 deletions
|
|
@ -595,6 +595,8 @@ class PasHubRdSapSiteNotesExtractor:
|
|||
)
|
||||
or "",
|
||||
system_type=self._get_in(data, "System type:") or "",
|
||||
community_heat_source=self._get_in(data, "Heating System (Other):")
|
||||
or "",
|
||||
product_id=int(self._get_in(data, "Product Id") or 0),
|
||||
manufacturer=self._get_in(data, "Manufacturer") or "",
|
||||
model=self._get_in(data, "Model") or "",
|
||||
|
|
|
|||
|
|
@ -718,7 +718,7 @@ class TestMainHeatingCommunityHeatSource:
|
|||
# Arrange — fixture 1's heating section with the community block
|
||||
# spliced in after its "Fuel:" lodgement.
|
||||
lines = load_text_fixture()
|
||||
idx = lines.index("Fuel:")
|
||||
idx = lines.index("System type:")
|
||||
spliced = (
|
||||
lines[: idx + 2]
|
||||
+ ["Heating System (Other):", "Community heating - boilers only"]
|
||||
|
|
|
|||
|
|
@ -93,7 +93,11 @@ _MIN_WITHIN_HALF: float = 0.18
|
|||
# under-rate closing.
|
||||
# Ratcheted 2.58 -> 2.55 by #1590 bug 3 (PV Connection label -> gov-API int:
|
||||
# "Not connected" no longer silently credited): observed MAE 2.538.
|
||||
_MAX_SAP_MAE: float = 1.75
|
||||
# Ratcheted 1.75 -> 1.70 by the dig-round-2 fixes: RIR surfaces billed with
|
||||
# the surveyed roof insulation + stud-wall commons, and community heating
|
||||
# lodging Table 4a code 301 + Table 12 fuel 51 (507644414148 computes on the
|
||||
# heat-network branch). Observed MAE 1.684.
|
||||
_MAX_SAP_MAE: float = 1.70
|
||||
|
||||
_KNOWN_GAP_REASON = (
|
||||
"pashub `from_site_notes` mapper does not int-code a main-heating "
|
||||
|
|
|
|||
|
|
@ -6191,12 +6191,26 @@ def _map_sap_heating(
|
|||
else None
|
||||
)
|
||||
|
||||
# Heat networks lodge the Table 4a code + Table 12 community fuel from
|
||||
# the community heat-source label; every other system resolves the plain
|
||||
# Table 32 fuel (issue #1590 follow-up, fixture 507644414148).
|
||||
community_sap_code: Optional[int] = None
|
||||
main_fuel: Union[int, str]
|
||||
if main.system_type.lower() in _PASHUB_HEAT_NETWORK_SYSTEM_TYPES:
|
||||
community_sap_code, community_fuel = _pashub_community_heating(
|
||||
main.community_heat_source, fuel_type
|
||||
)
|
||||
main_fuel = community_fuel if community_fuel is not None else fuel_type
|
||||
else:
|
||||
main_fuel = _pashub_main_fuel_code(fuel_type)
|
||||
|
||||
return SapHeating(
|
||||
instantaneous_wwhrs=InstantaneousWwhrs(),
|
||||
main_heating_details=[
|
||||
MainHeatingDetail(
|
||||
has_fghrs=main.flue_gas_heat_recovery_system,
|
||||
main_fuel_type=_pashub_main_fuel_code(fuel_type),
|
||||
main_fuel_type=main_fuel,
|
||||
sap_main_heating_code=community_sap_code,
|
||||
heat_emitter_type=_pashub_heat_emitter_code(main.emitter),
|
||||
emitter_temperature=main.emitter_temperature,
|
||||
fan_flue_present=main.fan_assist,
|
||||
|
|
@ -6265,6 +6279,37 @@ def _pashub_mechanical_ventilation_kind(
|
|||
return _PASHUB_VENTILATION_TYPE_TO_KIND[ventilation_type]
|
||||
|
||||
|
||||
# PasHub community heat-source label ("Heating System (Other)") → the
|
||||
# Table 4a heat-network SAP code that routes `is_heat_network_main` (DLF,
|
||||
# standing charge, 80% heat-source efficiency) and, via the shared Elmhurst
|
||||
# resolver, the Table 12 community fuel. Only the cohort's boilers-only
|
||||
# variant is lodged so far; CHP (302) / heat pump (304) join when observed.
|
||||
_PASHUB_COMMUNITY_HEAT_SOURCE_TO_SAP_CODE: Dict[str, int] = {
|
||||
"Community heating - boilers only": 301,
|
||||
}
|
||||
_PASHUB_COMMUNITY_HEAT_SOURCE_TO_ELMHURST: Dict[str, str] = {
|
||||
"Community heating - boilers only": "Boilers",
|
||||
}
|
||||
|
||||
|
||||
def _pashub_community_heating(
|
||||
heat_source_label: str, fuel_label: str
|
||||
) -> tuple[int, Optional[int]]:
|
||||
"""Resolve a PasHub community-heating lodgement to its (Table 4a SAP
|
||||
code, Table 12 fuel code) at the mapper boundary (ADR-0015), reusing
|
||||
`_resolve_community_heating_fuel_code`. An uncovered heat-source label
|
||||
strict-raises so the dwelling is never silently priced as an ordinary
|
||||
boiler (issue #1590 follow-up, fixture 507644414148)."""
|
||||
if heat_source_label not in _PASHUB_COMMUNITY_HEAT_SOURCE_TO_SAP_CODE:
|
||||
raise UnmappedPasHubLabel("community heat source", heat_source_label)
|
||||
sap_code = _PASHUB_COMMUNITY_HEAT_SOURCE_TO_SAP_CODE[heat_source_label]
|
||||
fuel = _resolve_community_heating_fuel_code(
|
||||
_PASHUB_COMMUNITY_HEAT_SOURCE_TO_ELMHURST[heat_source_label],
|
||||
fuel_label,
|
||||
)
|
||||
return sap_code, fuel
|
||||
|
||||
|
||||
# PasHub surveyed §Water-Heating "Cylinder Size" band → SAP10 cylinder-size
|
||||
# enum (Table 28: 2=Normal/110L, 3=Medium/160L, 4=Large/210L). PasHub renders
|
||||
# the band with its own litre-range suffix (unlike the Elmhurst forms in
|
||||
|
|
|
|||
|
|
@ -1802,10 +1802,14 @@ class TestPasHubMainHeatingControlCoding:
|
|||
)
|
||||
|
||||
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"] = (
|
||||
mh = raw["heating_and_hot_water"]["main_heating"]
|
||||
mh["system_type"] = "Community heating system"
|
||||
# a complete community lodgement (heat source + fuel) so the control
|
||||
# resolution is what's exercised, not the community strict-raise
|
||||
mh["community_heat_source"] = "Community heating - boilers only"
|
||||
mh["fuel"] = "Mains Gas"
|
||||
mh["product_id"] = 0
|
||||
mh["controls"] = (
|
||||
"Charging system linked to use of community heating, room thermostat only"
|
||||
)
|
||||
survey = from_dict(PasHubRdSapSiteNotes, raw)
|
||||
|
|
@ -1823,10 +1827,12 @@ class TestPasHubMainHeatingControlCoding:
|
|||
# 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"
|
||||
mh = raw["heating_and_hot_water"]["main_heating"]
|
||||
mh["system_type"] = "Community heating system"
|
||||
mh["community_heat_source"] = "Community heating - boilers only"
|
||||
mh["fuel"] = "Mains Gas"
|
||||
mh["product_id"] = 0
|
||||
mh["controls"] = "Some novel control"
|
||||
survey = from_dict(PasHubRdSapSiteNotes, raw)
|
||||
|
||||
with pytest.raises(UnmappedPasHubLabel, match="Community heating system"):
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue