mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
S0380.212: fix community fuel-code collision in heat-network CO2/PE/cost
Cert 9390-2722-3520 (community mains-gas boiler scheme, sap_main_heating_
code=301, main_fuel_type=20) emitted CO2 0.44 t vs lodged 2.8 t — 6.4x low.
Root cause: the EPC `main_fuel_type` enum and the SAP Table 12 / Table 32
fuel-code numbering COLLIDE in the 18-25 range. Per
`datatypes/epc/domain/epc_codes.csv` (RdSAP-Schema-17.0) EPC fuel
20 = "mains gas (community)", but Table 12/32 code 20 is a solid biomass
fuel (CO2 0.028, PE 1.046, wood-logs price). The factor lookups
(`co2_factor_kg_per_kwh` / `primary_energy_factor` / `unit_price_p_per_kwh`)
check the Table-12/32 dict FIRST, so the EPC community fuel 20 silently
returned the biomass factor instead of translating 20 -> Table 12 code 51
(community mains gas: CO2 0.210, PE 1.130, mains-gas price).
Fix: new `_heat_network_factor_fuel_code(main)` translates the EPC community
fuel to its Table-12 code via `API_FUEL_TO_TABLE_12`, but ONLY for
heat-network mains (`_is_heat_network_main`) — a genuine biomass boiler
(non-community) keeps its raw Table-12 factor. Applied at the five
heat-network factor sites: space-heating CO2 / PE / unit-price and
water-heating (WHC 901) CO2 / PE. The Summary path is unaffected (it maps
"Mains gas - community" to code 1, no collision), so the community-heating
corpus (CH1-6) is untouched.
Worksheet-validated against simulated case 14 (community boilers + mains
gas, SAP code 301): worksheet (367) CO2 factor 0.2100, (467) PE factor
1.1300 — exactly the Table-12 code-51 values the translator now reaches.
9390 CO2 0.44 -> 3.03 t (lodged 2.8; spec-correct factors over the API-only
register residual per [[feedback-worksheet-not-api-reference]]), PE 204 ->
220 (the spec-correct 1.13 factor; the prior 204≈205 match was the
collision coinciding with the register residual). 9390 is unpinned (retired
at P2.2 per ADR-0010 §10); the translator is locked by two unit tests.
REMAINING (separate follow-up): 9390 SAP +4 is a cost-side gap — the
heat-network cost path does not apply the 1/heat_source_eff (1/0.80)
scaling that the CO2/PE paths do, so community fuel cost under-counts.
Suite: 2616 passed, 1 skipped (community corpus green); the 2
test_rdsap_uvalues stone-formula failures are pre-existing (HEAD 58ff7d88).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
c3c44fa3d0
commit
08dd0b4c73
2 changed files with 96 additions and 5 deletions
|
|
@ -1855,6 +1855,37 @@ def _main_fuel_code(main: Optional[MainHeatingDetail]) -> Optional[int]:
|
|||
raise MissingMainFuelType(fuel, main.sap_main_heating_code)
|
||||
|
||||
|
||||
def _heat_network_factor_fuel_code(
|
||||
main: Optional[MainHeatingDetail],
|
||||
) -> Optional[int]:
|
||||
"""Fuel code to feed the Table 12 / Table 32 factor lookups, with the
|
||||
EPC→Table-12 translation applied for heat-network (community) mains.
|
||||
|
||||
The EPC `main_fuel_type` enum and the SAP Table 12 / Table 32 fuel-code
|
||||
numbering COLLIDE in the 18-25 range: `epc_codes.csv` lists
|
||||
20='mains gas (community)', 21='LPG (community)', 22='oil (community)',
|
||||
..., whereas Table 12/32 code 20-25 are solid biomass fuels. The factor
|
||||
lookups (`co2_factor_kg_per_kwh` / `primary_energy_factor` /
|
||||
`unit_price_p_per_kwh`) check the Table-12/32 dict FIRST, so an EPC
|
||||
community fuel 20 silently returns the biomass factor (CO2 0.028, PE
|
||||
1.046, wood-logs price) instead of community mains gas (CO2 0.210, PE
|
||||
1.130, mains-gas price + £120 standing charge).
|
||||
|
||||
Resolution: for a heat-network main, translate the EPC community fuel to
|
||||
its Table-12 code via `API_FUEL_TO_TABLE_12` (20->51) so the lookups hit
|
||||
the heat-network row. NON-heat-network mains are returned unchanged so a
|
||||
genuine biomass boiler (EPC 6 wood logs / 12 biomass, etc.) keeps its raw
|
||||
Table-12 factor. The Summary path is unaffected — it maps
|
||||
"Mains gas - community" to code 1 (no collision). Worksheet-validated:
|
||||
simulated case 14 (community boilers + mains gas, SAP code 301) →
|
||||
(367) CO2 factor 0.2100, (467) PE factor 1.1300.
|
||||
"""
|
||||
fuel = _main_fuel_code(main)
|
||||
if fuel is None or not _is_heat_network_main(main):
|
||||
return fuel
|
||||
return API_FUEL_TO_TABLE_12.get(fuel, fuel)
|
||||
|
||||
|
||||
def _fuel_cost_gbp_per_kwh(
|
||||
main: Optional[MainHeatingDetail], prices: PriceTable
|
||||
) -> float:
|
||||
|
|
@ -1882,7 +1913,10 @@ def _fuel_cost_gbp_per_kwh(
|
|||
)
|
||||
blended_p = chp_frac * chp_price + (1.0 - chp_frac) * boiler_price
|
||||
return blended_p * _PENCE_TO_GBP
|
||||
return prices.unit_price_p_per_kwh(_main_fuel_code(main)) * _PENCE_TO_GBP
|
||||
return (
|
||||
prices.unit_price_p_per_kwh(_heat_network_factor_fuel_code(main))
|
||||
* _PENCE_TO_GBP
|
||||
)
|
||||
|
||||
|
||||
# RdSAP energy_tariff enum (per datatypes/epc/domain/epc_codes.csv):
|
||||
|
|
@ -2816,7 +2850,7 @@ def _main_heating_co2_factor_kg_per_kwh(
|
|||
)
|
||||
if monthly is not None:
|
||||
return monthly * scaling
|
||||
return _co2_factor_kg_per_kwh(main) * scaling
|
||||
return co2_factor_kg_per_kwh(_heat_network_factor_fuel_code(main)) * scaling
|
||||
if tariff is Tariff.STANDARD:
|
||||
monthly = _effective_monthly_co2_factor(
|
||||
main_fuel_monthly_kwh, _STANDARD_ELECTRICITY_FUEL_CODE,
|
||||
|
|
@ -2895,7 +2929,7 @@ def _main_heating_primary_factor(
|
|||
)
|
||||
if monthly is not None:
|
||||
return monthly * scaling
|
||||
return primary_energy_factor(fuel) * scaling
|
||||
return primary_energy_factor(_heat_network_factor_fuel_code(main)) * scaling
|
||||
if tariff is Tariff.STANDARD:
|
||||
monthly = _effective_monthly_pe_factor(
|
||||
main_fuel_monthly_kwh, _STANDARD_ELECTRICITY_FUEL_CODE,
|
||||
|
|
@ -3171,7 +3205,7 @@ def _hot_water_co2_factor_kg_per_kwh(
|
|||
)
|
||||
if monthly is not None:
|
||||
return monthly * scaling
|
||||
return _co2_factor_kg_per_kwh(main) * scaling
|
||||
return co2_factor_kg_per_kwh(_heat_network_factor_fuel_code(main)) * scaling
|
||||
fuel = _water_heating_fuel_code(epc)
|
||||
if fuel is None:
|
||||
return _DEFAULT_CO2_KG_PER_KWH
|
||||
|
|
@ -3235,7 +3269,7 @@ def _hot_water_primary_factor(
|
|||
)
|
||||
if monthly is not None:
|
||||
return monthly * scaling
|
||||
return primary_energy_factor(_main_fuel_code(main)) * scaling
|
||||
return primary_energy_factor(_heat_network_factor_fuel_code(main)) * scaling
|
||||
fuel = _water_heating_fuel_code(epc)
|
||||
if fuel is None:
|
||||
return _DEFAULT_PEF
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ from domain.sap10_calculator.rdsap.cert_to_inputs import (
|
|||
_heat_network_code_302_effective_factor, # pyright: ignore[reportPrivateUsage]
|
||||
_heat_network_distribution_electricity, # pyright: ignore[reportPrivateUsage]
|
||||
_heat_network_dlf, # pyright: ignore[reportPrivateUsage]
|
||||
_heat_network_factor_fuel_code, # pyright: ignore[reportPrivateUsage]
|
||||
_is_electric_main, # pyright: ignore[reportPrivateUsage]
|
||||
_is_heat_network_electric_main, # pyright: ignore[reportPrivateUsage]
|
||||
_is_electric_water, # pyright: ignore[reportPrivateUsage]
|
||||
|
|
@ -5816,3 +5817,59 @@ def test_sap_appendix_d_eq_d1_water_efficiency_monthly_for_non_pcdb_table_4b_boi
|
|||
f"want {expected_hw_fuel!r} per SAP 10.2 Appendix D §D2.1 (2) "
|
||||
f"Equation D1 with Table 4b code 127 (winter 84%, summer 72%)"
|
||||
)
|
||||
|
||||
|
||||
def test_heat_network_community_gas_fuel_translates_epc_20_to_table12_51() -> None:
|
||||
# Arrange — a community mains-gas BOILER main (SAP code 301) lodges
|
||||
# main_fuel_type=20. Per epc_codes.csv (RdSAP-Schema-17.0) EPC fuel 20
|
||||
# is "mains gas (community)", but the SAP Table 12 / Table 32 numbering
|
||||
# uses 20 for a solid biomass fuel — a collision. The factor lookups
|
||||
# check the Table-12 dict first, so co2_factor_kg_per_kwh(20) returns
|
||||
# the biomass 0.028 instead of community mains gas 0.210. The
|
||||
# heat-network fuel-code translator must route EPC 20 → Table 12 51.
|
||||
from domain.sap10_calculator.tables.table_12 import (
|
||||
co2_factor_kg_per_kwh,
|
||||
primary_energy_factor,
|
||||
)
|
||||
|
||||
main = MainHeatingDetail(
|
||||
has_fghrs=False,
|
||||
main_fuel_type=20, # EPC "mains gas (community)"
|
||||
heat_emitter_type=1,
|
||||
emitter_temperature=1,
|
||||
main_heating_control=2306,
|
||||
main_heating_category=6, # heat network
|
||||
sap_main_heating_code=301, # community boilers
|
||||
)
|
||||
|
||||
# Act
|
||||
code = _heat_network_factor_fuel_code(main)
|
||||
|
||||
# Assert — translates to Table 12 code 51 (community mains gas), and the
|
||||
# factor lookups then return the worksheet-validated case-14 values
|
||||
# ((367) CO2 0.2100, (467) PE 1.1300), NOT the collided biomass factors.
|
||||
assert code == 51
|
||||
assert abs(co2_factor_kg_per_kwh(code) - 0.210) <= 1e-9
|
||||
assert abs(primary_energy_factor(code) - 1.130) <= 1e-9
|
||||
assert abs(co2_factor_kg_per_kwh(20) - 0.028) <= 1e-9 # the collided value
|
||||
|
||||
|
||||
def test_non_heat_network_biomass_fuel_not_translated() -> None:
|
||||
# Arrange — a NON-heat-network main lodging the same integer fuel code
|
||||
# must NOT be translated: a genuine biomass boiler keeps its raw
|
||||
# Table-12 factor. The translator only fires for heat-network mains.
|
||||
main = MainHeatingDetail(
|
||||
has_fghrs=False,
|
||||
main_fuel_type=20,
|
||||
heat_emitter_type=1,
|
||||
emitter_temperature=1,
|
||||
main_heating_control=2106,
|
||||
main_heating_category=2, # ordinary boiler, NOT heat network
|
||||
sap_main_heating_code=102,
|
||||
)
|
||||
|
||||
# Act
|
||||
code = _heat_network_factor_fuel_code(main)
|
||||
|
||||
# Assert — unchanged (raw code, biomass factor preserved).
|
||||
assert code == 20
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue