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:
Khalim Conn-Kowlessar 2026-07-14 22:34:50 +00:00
parent 4d60d79cc9
commit f45651dbc0
5 changed files with 68 additions and 11 deletions

View file

@ -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 "",

View file

@ -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"]

View file

@ -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 "

View file

@ -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

View file

@ -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"):