mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
S0380.213: heat-network standing charge (£120) — fixes 9390 cost under-count
Cert 9390 (community mains-gas boiler, API main_fuel_type=20) drew £0 standing charge → fuel cost under-counted → SAP read +4 high (71 vs 67). Root cause: the standing-charge logic (`additional_standing_charges_gbp`) only knows the GAS branch (`_is_gas_code`) and the off-peak-electric branch. A heat-network community fuel is not a Table-32 gas code — EPC 20 = "mains gas (community)" normalises to Table-32 code 20 (biomass), so `_is_gas_code(20)` is False and the standing came out £0. The Summary path masks this because it lodges community gas as Table-32 code 1 (ordinary mains gas), which IS gas-recognised and already draws the £120 gas standing — so the CH1-6 corpus was unaffected while the API path lost the charge. Spec basis (verified against SAP 10.2 spec PDF): - Table 12 (p.191) "Heat networks" row standing charge = £120/yr, note (k). - Note (l): "Include half this value if only DHW is provided by a heat network." - §C3.2 (p.58): the full charge applies when the space heating is also a heat network. Worksheet-validated: simulated case 14 (community boilers + mains gas, space + water) → worksheet (351) Additional standing charges = £120. Fix: new `_heat_network_standing_charge_gbp(epc, main)` returns the heat-network standing (£120 full when the space main is a heat network; £60 when only DHW is on the network) or None otherwise. Applied at both fuel-cost call sites, REPLACING the fuel-based `additional_standing_charges _gbp` for heat-network mains (NOT additive) so a Summary-path community-gas main — already £120 via the gas branch — is not double-counted to £240. The CH1-6 community corpus stays exactly £120 (59 corpus tests pass). 9390 SAP +4 → -2 (cont 65.39 vs lodged 67): the spec-correct £120 standing EXPOSES a separate ~7% demand over-count (also visible as PE 220 vs lodged 205) — a heat-source-efficiency-default / fabric residual, follow-up scope. 9390 is unpinned (retired P2.2 per ADR-0010 §10); helper locked by 2 unit tests. Full suite 2386 passed, 1 skipped. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
f658f7ce71
commit
ee484d9f4a
2 changed files with 100 additions and 8 deletions
|
|
@ -883,6 +883,12 @@ _HEAT_NETWORK_HEAT_SOURCE_EFFICIENCY: Final[dict[int, float]] = {
|
||||||
304: 3.00,
|
304: 3.00,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# SAP 10.2 Table 12 (PDF p.191) "Heat networks" standing charge row =
|
||||||
|
# £120/yr (note (k)). Note (l): "Include half this value if only DHW is
|
||||||
|
# provided by a heat network." §C3.2 (PDF p.58): the full charge applies
|
||||||
|
# when the space heating is also on the heat network.
|
||||||
|
_HEAT_NETWORK_STANDING_CHARGE_GBP: Final[float] = 120.0
|
||||||
|
|
||||||
|
|
||||||
def _is_heat_network_main(main: Optional[MainHeatingDetail]) -> bool:
|
def _is_heat_network_main(main: Optional[MainHeatingDetail]) -> bool:
|
||||||
"""True when the cert's main heating is a heat network — either by
|
"""True when the cert's main heating is a heat network — either by
|
||||||
|
|
@ -1592,6 +1598,35 @@ def _is_community_heating_hw_from_main(epc: EpcPropertyData) -> bool:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _heat_network_standing_charge_gbp(
|
||||||
|
epc: EpcPropertyData, main: Optional[MainHeatingDetail]
|
||||||
|
) -> Optional[float]:
|
||||||
|
"""SAP 10.2 Table 12 note (l) + §C3.2 heat-network standing charge, or
|
||||||
|
None when the dwelling is not on a heat network (caller then falls back
|
||||||
|
to the fuel-based `additional_standing_charges_gbp`).
|
||||||
|
|
||||||
|
A heat network carries the Table 12 £120/yr standing charge regardless
|
||||||
|
of the network fuel — full when the SPACE heating is on the network
|
||||||
|
(§C3.2 "the total standing charge is the normal heat network standing
|
||||||
|
charge"), halved to £60 when ONLY DHW is provided by the heat network
|
||||||
|
(note (l)). This REPLACES the fuel-based gas/off-peak standing for a
|
||||||
|
heat-network main, so it must not be added on top of
|
||||||
|
`additional_standing_charges_gbp` (which would double-count: a
|
||||||
|
Summary-path community-gas main lodges Table-32 code 1 and already
|
||||||
|
draws the £120 gas standing). Worksheet-validated: simulated case 14
|
||||||
|
(community boilers + mains gas, space + water) → (351) = £120.
|
||||||
|
|
||||||
|
The API path under-counted this: an EPC community fuel (e.g. 20 = mains
|
||||||
|
gas community) is not a Table-32 gas code, so `_is_gas_code` returned
|
||||||
|
False and the standing came out £0 — cert 9390 lost the whole £120.
|
||||||
|
"""
|
||||||
|
if _is_heat_network_main(main):
|
||||||
|
return _HEAT_NETWORK_STANDING_CHARGE_GBP
|
||||||
|
if _is_community_heating_hw_from_main(epc):
|
||||||
|
return _HEAT_NETWORK_STANDING_CHARGE_GBP / 2.0
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _main_heating_efficiency(epc: EpcPropertyData) -> float:
|
def _main_heating_efficiency(epc: EpcPropertyData) -> float:
|
||||||
"""SAP 10.2 (206) main system 1 efficiency as a 0..1 fraction.
|
"""SAP 10.2 (206) main system 1 efficiency as a 0..1 fraction.
|
||||||
|
|
||||||
|
|
@ -5885,10 +5920,15 @@ def _fuel_cost(
|
||||||
table_32_unit_price_p_per_kwh(60) * _PENCE_TO_GBP
|
table_32_unit_price_p_per_kwh(60) * _PENCE_TO_GBP
|
||||||
)
|
)
|
||||||
|
|
||||||
standing = additional_standing_charges_gbp(
|
heat_network_standing = _heat_network_standing_charge_gbp(epc, main)
|
||||||
main_fuel_code=main_fuel_code,
|
standing = (
|
||||||
water_heating_fuel_code=water_heating_fuel_code,
|
heat_network_standing
|
||||||
tariff=tariff,
|
if heat_network_standing is not None
|
||||||
|
else additional_standing_charges_gbp(
|
||||||
|
main_fuel_code=main_fuel_code,
|
||||||
|
water_heating_fuel_code=water_heating_fuel_code,
|
||||||
|
tariff=tariff,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Worksheet display convention: when a row's kWh is zero (no main 2, no
|
# Worksheet display convention: when a row's kWh is zero (no main 2, no
|
||||||
|
|
@ -6610,10 +6650,15 @@ def cert_to_inputs(
|
||||||
epc, hw_monthly_kwh_for_factors, _rdsap_tariff(epc),
|
epc, hw_monthly_kwh_for_factors, _rdsap_tariff(epc),
|
||||||
)
|
)
|
||||||
_hw_extra_standing = 0.0
|
_hw_extra_standing = 0.0
|
||||||
standing_charges_total = additional_standing_charges_gbp(
|
_heat_network_standing = _heat_network_standing_charge_gbp(epc, main)
|
||||||
main_fuel_code=_main_fuel_code(main),
|
standing_charges_total = (
|
||||||
water_heating_fuel_code=_water_heating_fuel_code(epc),
|
_heat_network_standing
|
||||||
tariff=_rdsap_tariff(epc),
|
if _heat_network_standing is not None
|
||||||
|
else additional_standing_charges_gbp(
|
||||||
|
main_fuel_code=_main_fuel_code(main),
|
||||||
|
water_heating_fuel_code=_water_heating_fuel_code(epc),
|
||||||
|
tariff=_rdsap_tariff(epc),
|
||||||
|
)
|
||||||
) + _hw_extra_standing
|
) + _hw_extra_standing
|
||||||
|
|
||||||
# SAP 10.2 Appendix C §C3.2 (PDF p.51) — heat-network distribution
|
# SAP 10.2 Appendix C §C3.2 (PDF p.51) — heat-network distribution
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ from domain.sap10_calculator.rdsap.cert_to_inputs import (
|
||||||
_heat_network_distribution_electricity, # pyright: ignore[reportPrivateUsage]
|
_heat_network_distribution_electricity, # pyright: ignore[reportPrivateUsage]
|
||||||
_heat_network_dlf, # pyright: ignore[reportPrivateUsage]
|
_heat_network_dlf, # pyright: ignore[reportPrivateUsage]
|
||||||
_heat_network_factor_fuel_code, # pyright: ignore[reportPrivateUsage]
|
_heat_network_factor_fuel_code, # pyright: ignore[reportPrivateUsage]
|
||||||
|
_heat_network_standing_charge_gbp, # pyright: ignore[reportPrivateUsage]
|
||||||
_is_electric_main, # pyright: ignore[reportPrivateUsage]
|
_is_electric_main, # pyright: ignore[reportPrivateUsage]
|
||||||
_is_heat_network_electric_main, # pyright: ignore[reportPrivateUsage]
|
_is_heat_network_electric_main, # pyright: ignore[reportPrivateUsage]
|
||||||
_is_electric_water, # pyright: ignore[reportPrivateUsage]
|
_is_electric_water, # pyright: ignore[reportPrivateUsage]
|
||||||
|
|
@ -5873,3 +5874,49 @@ def test_non_heat_network_biomass_fuel_not_translated() -> None:
|
||||||
|
|
||||||
# Assert — unchanged (raw code, biomass factor preserved).
|
# Assert — unchanged (raw code, biomass factor preserved).
|
||||||
assert code == 20
|
assert code == 20
|
||||||
|
|
||||||
|
|
||||||
|
def test_heat_network_space_and_water_standing_charge_is_full_120() -> None:
|
||||||
|
# Arrange — a heat-network SPACE main (SAP code 301) carries the full
|
||||||
|
# Table 12 (PDF p.191) heat-network standing charge of £120/yr per
|
||||||
|
# §C3.2 ("the total standing charge is the normal heat network standing
|
||||||
|
# charge" when space heating is on the network). Worksheet-validated:
|
||||||
|
# case 14 (community boilers + mains gas, space + water) → (351) £120.
|
||||||
|
# The epc is not consulted on this branch (heat-network space main wins
|
||||||
|
# first), so a minimal epc suffices.
|
||||||
|
epc = _typical_semi_detached_epc()
|
||||||
|
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,
|
||||||
|
sap_main_heating_code=301,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Act
|
||||||
|
standing = _heat_network_standing_charge_gbp(epc, main)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert standing is not None
|
||||||
|
assert abs(standing - 120.0) <= 1e-9
|
||||||
|
|
||||||
|
|
||||||
|
def test_non_heat_network_main_returns_none_so_caller_uses_fuel_standing() -> None:
|
||||||
|
# Arrange — a non-heat-network gas-boiler main must NOT draw the
|
||||||
|
# heat-network standing branch; the helper returns None so the caller
|
||||||
|
# falls back to the fuel-based `additional_standing_charges_gbp`.
|
||||||
|
epc = _typical_semi_detached_epc()
|
||||||
|
main = MainHeatingDetail(
|
||||||
|
has_fghrs=False,
|
||||||
|
main_fuel_type=26, # mains gas (not community)
|
||||||
|
heat_emitter_type=1,
|
||||||
|
emitter_temperature=1,
|
||||||
|
main_heating_control=2106,
|
||||||
|
main_heating_category=2,
|
||||||
|
sap_main_heating_code=102,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Act / Assert
|
||||||
|
assert _heat_network_standing_charge_gbp(epc, main) is None
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue