mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-19 08:53:17 +00:00
MainFuelType had no individual wood-logs member — only "biomass (community)" — so the LLM classifier funnelled "Solid Fuel: Wood Logs" into the community fuel, inventing a community heat network the dwelling isn't on (and mislabelling the connection). main_fuel had no deterministic guard at all, so nothing caught it. Verified against domain/sap10_calculator/docs/specs: RdSAP 10 Specification Table 32 lists "wood logs" as a solid fuel (code 20, 0.028 kgCO2e/kWh); the calculator's input scheme (the gov EPC API fuel enum) codes it 6 -> Table 32 20 (sap_efficiencies._API_TO_TABLE32), and water_heating_overlay already pins the same fuel to 6. So _FUEL_CODES["wood logs"] = 6 is confirmed, not guessed. Adds MainFuelType.WOOD_LOGS + the _FUEL_CODES entry, a main_fuel_guard mirroring water_heating_guard (claims the "wood log" token; dual fuel keeps its own member since it has no "wood log" substring), and wires main_fuel through a GuardedColumnClassifier so the live path is deterministic. Applied the scoped backfill to portfolio 796 (Hyde): 21 rows off "biomass (community)" -> "wood logs". property_overrides (TEXT) only; the classifier-cache pgEnum member is deferred to the FE Drizzle migration. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
146 lines
5 KiB
Python
146 lines
5 KiB
Python
"""The Landlord-Override main-fuel → heating Simulation Overlay mapping.
|
|
|
|
A main-fuel value resolves to the RdSAP `main_fuel_type` int code the calculator
|
|
reads from the dwelling's primary heating system; the overlay is whole-dwelling.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from domain.epc.property_overrides.main_fuel_type import MainFuelType
|
|
from domain.epc.property_overlays.main_fuel_overlay import fuel_overlay_for
|
|
from domain.modelling.scoring.overlay_applicator import apply_simulations
|
|
from tests.domain.sap10_calculator.worksheet._elmhurst_worksheet_000490 import (
|
|
build_epc,
|
|
)
|
|
|
|
|
|
def test_mains_gas_overlays_the_primary_fuel() -> None:
|
|
# Act
|
|
simulation = fuel_overlay_for("mains gas", 0)
|
|
|
|
# Assert — mains gas (not community) is RdSAP main_fuel code 26.
|
|
assert simulation is not None
|
|
assert simulation.heating is not None
|
|
assert simulation.heating.main_fuel_type == 26
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("main_fuel_value", "code"),
|
|
[
|
|
("electricity", 29),
|
|
("LPG (bulk)", 27),
|
|
("oil", 28),
|
|
("house coal", 33),
|
|
],
|
|
)
|
|
def test_fuels_decode_to_their_modern_not_community_codes(
|
|
main_fuel_value: str, code: int
|
|
) -> None:
|
|
# Act
|
|
simulation = fuel_overlay_for(main_fuel_value, 0)
|
|
|
|
# Assert
|
|
assert simulation is not None
|
|
assert simulation.heating is not None
|
|
assert simulation.heating.main_fuel_type == code
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("main_fuel_value", "code"),
|
|
[
|
|
("bottled LPG", 3),
|
|
("LPG special condition", 17),
|
|
("electricity (community)", 25),
|
|
("biomass (community)", 31),
|
|
("dual fuel (mineral and wood)", 10),
|
|
("smokeless coal", 15),
|
|
# Wood logs — API fuel enum 6 (RdSAP Table 32 code 20). An individual
|
|
# solid-fuel dwelling, distinct from biomass (community) 31.
|
|
("wood logs", 6),
|
|
],
|
|
)
|
|
def test_more_fuels_decode_to_their_codes(main_fuel_value: str, code: int) -> None:
|
|
# Act
|
|
simulation = fuel_overlay_for(main_fuel_value, 0)
|
|
|
|
# Assert
|
|
assert simulation is not None
|
|
assert simulation.heating is not None
|
|
assert simulation.heating.main_fuel_type == code
|
|
|
|
|
|
def test_community_mains_gas_is_a_distinct_fuel_code() -> None:
|
|
# Act
|
|
simulation = fuel_overlay_for("mains gas (community)", 0)
|
|
|
|
# Assert — community mains gas is code 20, distinct from 26 (not community).
|
|
assert simulation is not None
|
|
assert simulation.heating is not None
|
|
assert simulation.heating.main_fuel_type == 20
|
|
|
|
|
|
def test_mains_gas_fuel_sets_the_mains_gas_connection_flag() -> None:
|
|
# A "mains gas" fuel means the dwelling has a mains-gas connection, so the
|
|
# overlay must set sap_energy_source.gas_connection_available too — not only the fuel code.
|
|
# Without it the effective EPC says "fuel = mains gas" yet gas_connection_available=False,
|
|
# which suppresses the gas-boiler-upgrade path and wrongly offers HHRSH
|
|
# storage (the off-gas path). (Property 728513.)
|
|
simulation = fuel_overlay_for("mains gas", 0)
|
|
assert simulation is not None
|
|
assert simulation.heating is not None
|
|
assert simulation.heating.gas_connection_available is True
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"main_fuel_value",
|
|
["electricity", "LPG (bulk)", "bottled LPG", "oil", "house coal"],
|
|
)
|
|
def test_non_mains_gas_fuel_leaves_the_mains_gas_flag_unchanged(
|
|
main_fuel_value: str,
|
|
) -> None:
|
|
# Only an explicit "mains gas" fuel asserts a mains-gas connection. An
|
|
# electric (or LPG/oil) main fuel does NOT tell us there is no gas supply
|
|
# (the dwelling could still have a gas hob), so the flag is left None
|
|
# ("leave the baseline unchanged") rather than cleared to False.
|
|
simulation = fuel_overlay_for(main_fuel_value, 0)
|
|
assert simulation is not None
|
|
assert simulation.heating is not None
|
|
assert simulation.heating.gas_connection_available is None
|
|
|
|
|
|
@pytest.mark.parametrize("main_fuel_value", ["Unknown", "", "no heating or hot water"])
|
|
def test_unresolvable_fuel_produces_no_overlay(main_fuel_value: str) -> None:
|
|
# Act
|
|
simulation = fuel_overlay_for(main_fuel_value, 0)
|
|
|
|
# Assert
|
|
assert simulation is None
|
|
|
|
|
|
def test_fuel_override_remaps_the_primary_systems_fuel_on_the_epc() -> None:
|
|
# Arrange — a landlord correction that the dwelling runs on electricity.
|
|
baseline = build_epc()
|
|
overlay = fuel_overlay_for("electricity", 0)
|
|
assert overlay is not None
|
|
|
|
# Act
|
|
result = apply_simulations(baseline, [overlay])
|
|
|
|
# Assert — the calculator reads the primary fuel from main_heating_details[0].
|
|
assert result.sap_heating.main_heating_details[0].main_fuel_type == 29
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"member", [m for m in MainFuelType if m is not MainFuelType.UNKNOWN]
|
|
)
|
|
def test_every_resolvable_fuel_value_decodes_to_a_code(member: MainFuelType) -> None:
|
|
# A classifier emits a MainFuelType value; if the overlay can't decode it the
|
|
# override silently no-ops. Every non-UNKNOWN member must resolve.
|
|
|
|
# Act
|
|
simulation = fuel_overlay_for(member.value, 0)
|
|
|
|
# Assert
|
|
assert simulation is not None
|