mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
fix(fuel): strict-raise on unmapped Table-12 factor fuel codes
Tier-1 finding of the silent-fallback audit. The fuel-type helpers fed the
SAP 10.2 Table 12/32 cost/CO2/PE lookups via a silent
`API_FUEL_TO_TABLE_12.get(fuel, fuel)` passthrough at 5 sites
(_heat_network_factor_fuel_code, HW CO2/PE, _secondary_fuel_code, PV). A fuel
code in NEITHER the API enum map NOR the Table-12 numbering passed straight
through to the mains-gas default baked into unit_price_p_per_kwh /
co2_factor_kg_per_kwh / primary_energy_factor (table_12.py:233/274/287,
table_32.py:190) — silently mis-pricing a novel/colliding fuel as grid gas.
This is the class that mis-priced cert 8536's community biomass as
electricity (-17 SAP) before a7761ea8.
New _table_12_factor_fuel_code mirrors .get(fuel, fuel) EXACTLY for every
recognised input (union of the CO2/PE/price/monthly table keys +
API_FUEL_TO_TABLE_12 values) and raises UnmappedSapCode only when the
resolved code is recognised by no table — surfacing the gap loudly per the
strict-raise principle (reference_unmapped_sap_code). Verified behaviour-
preserving: 0/909 corpus certs hit the new raise; eval unchanged at 54.9%
within-0.5 / 909 computed / 0 raises.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
ddb9fdbec5
commit
7878a96900
2 changed files with 82 additions and 9 deletions
|
|
@ -87,7 +87,10 @@ from domain.sap10_calculator.tables.pcdb.postcode_weather import (
|
|||
from domain.sap10_calculator.tables.table_12 import (
|
||||
API_FUEL_TO_TABLE_12,
|
||||
CO2_KG_PER_KWH,
|
||||
CO2_KG_PER_KWH_MONTHLY,
|
||||
PE_FACTOR_MONTHLY,
|
||||
PRIMARY_ENERGY_FACTOR,
|
||||
UNIT_PRICE_P_PER_KWH,
|
||||
_DEFAULT_CO2_KG_PER_KWH, # pyright: ignore[reportPrivateUsage]
|
||||
_DEFAULT_PEF, # pyright: ignore[reportPrivateUsage]
|
||||
co2_monthly_factors_kg_per_kwh,
|
||||
|
|
@ -2040,6 +2043,42 @@ def _main_fuel_code(main: Optional[MainHeatingDetail]) -> Optional[int]:
|
|||
raise MissingMainFuelType(fuel, main.sap_main_heating_code)
|
||||
|
||||
|
||||
# Fuel codes the Table 12 / Table 32 factor & price lookups recognise as a
|
||||
# DIRECT key (vs falling through to their mains-gas default). The union of
|
||||
# every per-fuel column the cost/CO2/PE cascade consumes, plus the values
|
||||
# `API_FUEL_TO_TABLE_12` translates to (all valid Table-12 codes). A code in
|
||||
# this set — or translatable into it via the API enum map — is priced/
|
||||
# factored correctly; a code in NEITHER would silently default to mains gas.
|
||||
_RECOGNISED_TABLE_12_FUEL_CODES: Final[frozenset[int]] = frozenset(
|
||||
set(CO2_KG_PER_KWH)
|
||||
| set(PRIMARY_ENERGY_FACTOR)
|
||||
| set(UNIT_PRICE_P_PER_KWH)
|
||||
| set(CO2_KG_PER_KWH_MONTHLY)
|
||||
| set(PE_FACTOR_MONTHLY)
|
||||
| set(API_FUEL_TO_TABLE_12.values())
|
||||
)
|
||||
|
||||
|
||||
def _table_12_factor_fuel_code(fuel: int) -> int:
|
||||
"""`API_FUEL_TO_TABLE_12.get(fuel, fuel)` with a STRICT tail.
|
||||
|
||||
Returns the Table-12 factor code for the cost / CO2 / PE lookups, with
|
||||
behaviour identical to the prior silent passthrough for every recognised
|
||||
input. The one difference: when the resolved code is neither translatable
|
||||
via the API enum map NOR already a recognised Table-12/32 fuel code, it
|
||||
raises `UnmappedSapCode` instead of passing the unknown code through to
|
||||
the mains-gas default baked into `unit_price_p_per_kwh` /
|
||||
`co2_factor_kg_per_kwh` / `primary_energy_factor` (the silent fuel-
|
||||
collision class — cert 8536's community biomass mis-priced as grid
|
||||
electricity was this pattern). Mirror of the strict-raise principle
|
||||
([[reference-unmapped-sap-code]]).
|
||||
"""
|
||||
code = API_FUEL_TO_TABLE_12.get(fuel, fuel)
|
||||
if code in _RECOGNISED_TABLE_12_FUEL_CODES:
|
||||
return code
|
||||
raise UnmappedSapCode("table_12_factor_fuel", fuel)
|
||||
|
||||
|
||||
def _heat_network_factor_fuel_code(
|
||||
main: Optional[MainHeatingDetail],
|
||||
) -> Optional[int]:
|
||||
|
|
@ -2068,7 +2107,7 @@ def _heat_network_factor_fuel_code(
|
|||
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)
|
||||
return _table_12_factor_fuel_code(fuel)
|
||||
|
||||
|
||||
def _fuel_cost_gbp_per_kwh(
|
||||
|
|
@ -3627,7 +3666,7 @@ def _hot_water_co2_factor_kg_per_kwh(
|
|||
return _DEFAULT_CO2_KG_PER_KWH
|
||||
table_12_code = (
|
||||
fuel if fuel in CO2_KG_PER_KWH
|
||||
else API_FUEL_TO_TABLE_12.get(fuel, fuel)
|
||||
else _table_12_factor_fuel_code(fuel)
|
||||
)
|
||||
if tariff is not Tariff.STANDARD:
|
||||
return co2_factor_kg_per_kwh(table_12_code)
|
||||
|
|
@ -3691,7 +3730,7 @@ def _hot_water_primary_factor(
|
|||
return _DEFAULT_PEF
|
||||
table_12_code = (
|
||||
fuel if fuel in PRIMARY_ENERGY_FACTOR
|
||||
else API_FUEL_TO_TABLE_12.get(fuel, fuel)
|
||||
else _table_12_factor_fuel_code(fuel)
|
||||
)
|
||||
if tariff is not Tariff.STANDARD:
|
||||
return primary_energy_factor(table_12_code)
|
||||
|
|
@ -3725,7 +3764,7 @@ def _secondary_fuel_code(epc: EpcPropertyData) -> int:
|
|||
return _STANDARD_ELECTRICITY_FUEL_CODE
|
||||
if code in CO2_KG_PER_KWH:
|
||||
return code
|
||||
return API_FUEL_TO_TABLE_12.get(code, code)
|
||||
return _table_12_factor_fuel_code(code)
|
||||
|
||||
|
||||
def _secondary_heating_co2_factor_kg_per_kwh(
|
||||
|
|
@ -7098,15 +7137,12 @@ def cert_to_inputs(
|
|||
secondary_fuel_monthly_kwh=energy_requirements_result.secondary_fuel_monthly_kwh,
|
||||
hot_water_monthly_kwh=hot_water_monthly_kwh_for_pv,
|
||||
main_fuel_code_table_12=(
|
||||
API_FUEL_TO_TABLE_12.get(main_fuel, main_fuel)
|
||||
_table_12_factor_fuel_code(main_fuel)
|
||||
if main_fuel is not None else None
|
||||
),
|
||||
secondary_fuel_code_table_12=_secondary_fuel_code(epc),
|
||||
water_heating_fuel_code_table_12=(
|
||||
API_FUEL_TO_TABLE_12.get(
|
||||
epc.sap_heating.water_heating_fuel,
|
||||
epc.sap_heating.water_heating_fuel,
|
||||
)
|
||||
_table_12_factor_fuel_code(epc.sap_heating.water_heating_fuel)
|
||||
if epc.sap_heating.water_heating_fuel is not None else None
|
||||
),
|
||||
# SAP 10.2 Appendix M1 §3a — exclude the low-rate portion of an
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ from domain.sap10_calculator.rdsap.cert_to_inputs import (
|
|||
_heat_network_factor_fuel_code, # pyright: ignore[reportPrivateUsage]
|
||||
_heat_network_standing_charge_gbp, # pyright: ignore[reportPrivateUsage]
|
||||
_main_fuel_code, # pyright: ignore[reportPrivateUsage]
|
||||
_table_12_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]
|
||||
|
|
@ -1663,6 +1664,42 @@ def test_heat_network_unmapped_community_collision_fuel_raises() -> None:
|
|||
_heat_network_community_fuel_code(31, main)
|
||||
|
||||
|
||||
def test_table_12_factor_fuel_unmapped_code_raises() -> None:
|
||||
# Arrange — a fuel code that is neither translatable via
|
||||
# API_FUEL_TO_TABLE_12 nor already a recognised Table-12/32 fuel code.
|
||||
# 998 is a deliberately out-of-range sentinel for "novel/unknown fuel".
|
||||
unmapped_fuel: Final[int] = 998
|
||||
|
||||
# Act / Assert — the gap surfaces loudly instead of passing the code
|
||||
# through to the silent mains-gas default in unit_price_p_per_kwh /
|
||||
# co2_factor_kg_per_kwh / primary_energy_factor (the cert-8536
|
||||
# community-collision class). Strict-raise per [[reference-unmapped-sap-code]].
|
||||
with pytest.raises(UnmappedSapCode) as excinfo:
|
||||
_table_12_factor_fuel_code(unmapped_fuel)
|
||||
|
||||
# Assert — the raised error names the field and the offending value.
|
||||
assert excinfo.value.field == "table_12_factor_fuel"
|
||||
assert excinfo.value.value == unmapped_fuel
|
||||
|
||||
|
||||
def test_table_12_factor_fuel_recognised_codes_preserve_get_semantics() -> None:
|
||||
# Arrange — recognised inputs must behave EXACTLY like the prior
|
||||
# `API_FUEL_TO_TABLE_12.get(fuel, fuel)` passthrough: a gov-API enum is
|
||||
# translated (26 mains-gas-not-community -> Table-12 code 1); a code
|
||||
# already in the Table-12 numbering passes through unchanged (51 = heat-
|
||||
# network mains gas).
|
||||
api_enum_mains_gas: Final[int] = 26
|
||||
table_12_heat_network_gas: Final[int] = 51
|
||||
|
||||
# Act
|
||||
translated: int = _table_12_factor_fuel_code(api_enum_mains_gas)
|
||||
passthrough: int = _table_12_factor_fuel_code(table_12_heat_network_gas)
|
||||
|
||||
# Assert — identical to the old .get(fuel, fuel) results, no behaviour drift.
|
||||
assert translated == 1
|
||||
assert passthrough == 51
|
||||
|
||||
|
||||
def test_responsiveness_solid_fuel_sap_code_160_returns_0p50_per_table_4a() -> None:
|
||||
# Arrange — SAP 10.2 Table 4a (PDF p.169, "Heating systems"):
|
||||
#
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue