mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
Source every end use's off-peak high-rate fraction from the cert 🟩
Surface the hot-water (Table 13 / HP-DHW), secondary (direct-acting), main-2 and ALL_OTHER_USES High-Rate Fractions on CalculatorInputs from the same Table 12a helpers the SAP cost path uses, so Bill Derivation's day/night split matches the rating's exactly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b58e27f46e
commit
81d5429b60
2 changed files with 130 additions and 0 deletions
|
|
@ -2819,6 +2819,76 @@ def _hot_water_fuel_cost_gbp_per_kwh(
|
|||
return _fuel_cost_gbp_per_kwh(main, prices)
|
||||
|
||||
|
||||
def _hot_water_high_rate_fraction(
|
||||
water_heating_fuel: Optional[int],
|
||||
main: Optional[MainHeatingDetail],
|
||||
tariff: Tariff,
|
||||
*,
|
||||
water_heating_code: Optional[int] = None,
|
||||
inherit_main_for_community_heating: bool = False,
|
||||
cylinder_volume_l: Optional[float] = None,
|
||||
occupancy_n: Optional[float] = None,
|
||||
immersion_single: Optional[bool] = None,
|
||||
) -> float:
|
||||
"""ADR-0014 Bill Derivation — the hot-water High-Rate Fraction (the day/high-
|
||||
rate share) on an Off-Peak Meter, mirroring `_hot_water_fuel_cost_gbp_per_kwh`
|
||||
branch-for-branch so the bill's day/night HW split matches the rating's:
|
||||
|
||||
- community-heating HW inheriting a (non-electric) main, non-electric HW, or
|
||||
STANDARD tariff → 1.0 (single rate, no split);
|
||||
- HP-DHW (the WHC inherits a PCDB Table 362 heat-pump main) → Table 12a Grid 1
|
||||
WH `ASHP_APP_N` fraction (0.70 at 7-/10-hour);
|
||||
- electric immersion (WHC 903) with a known cylinder + occupancy → the Table
|
||||
13 dual-immersion fraction (§10.5 assumes a DUAL immersion on a dual meter);
|
||||
- any other electric off-peak HW (e.g. heated by a storage main) → 0.0 (the
|
||||
timer charges it wholly at the night/low rate)."""
|
||||
if inherit_main_for_community_heating:
|
||||
return 1.0
|
||||
if not _is_electric_water(water_heating_fuel) or tariff is Tariff.STANDARD:
|
||||
return 1.0
|
||||
if (
|
||||
water_heating_code in _WATER_INHERIT_FROM_MAIN_CODES
|
||||
and main is not None
|
||||
and main.main_heating_index_number is not None
|
||||
and heat_pump_record(main.main_heating_index_number) is not None
|
||||
):
|
||||
return water_heating_high_rate_fraction(Table12aSystem.ASHP_APP_N, tariff)
|
||||
if (
|
||||
water_heating_code == _WHC_ELECTRIC_IMMERSION
|
||||
and cylinder_volume_l is not None
|
||||
and occupancy_n is not None
|
||||
):
|
||||
effective_single = (
|
||||
immersion_single if immersion_single is not None else False
|
||||
)
|
||||
return electric_dhw_high_rate_fraction(
|
||||
cylinder_volume_l=cylinder_volume_l,
|
||||
occupancy_n=occupancy_n,
|
||||
single_immersion=effective_single,
|
||||
tariff=tariff,
|
||||
)
|
||||
return 0.0
|
||||
|
||||
|
||||
def _secondary_high_rate_fraction(epc: EpcPropertyData, tariff: Tariff) -> float:
|
||||
"""ADR-0014 Bill Derivation — the secondary-heating High-Rate Fraction on an
|
||||
Off-Peak Meter. Non-electric or standard-tariff secondary → 1.0. Electric
|
||||
secondary heaters are portable/direct-acting (Table 4a room heaters), so they
|
||||
take the Table 12a Grid 1 `OTHER_DIRECT_ACTING_ELECTRIC` row (1.0 at 7-hour,
|
||||
0.50 at 10-hour) — run on demand, mostly at the day/high rate. Tariffs Table
|
||||
12a omits (18-/24-hour) fall back to 1.0 (high)."""
|
||||
if tariff is Tariff.STANDARD:
|
||||
return 1.0
|
||||
if not is_electric_fuel_code(_secondary_fuel_code(epc)):
|
||||
return 1.0
|
||||
try:
|
||||
return space_heating_high_rate_fraction(
|
||||
Table12aSystem.OTHER_DIRECT_ACTING_ELECTRIC, tariff
|
||||
)
|
||||
except NotImplementedError:
|
||||
return 1.0
|
||||
|
||||
|
||||
def _secondary_fraction(
|
||||
main: Optional[MainHeatingDetail],
|
||||
secondary_heating_type: object,
|
||||
|
|
@ -3408,6 +3478,27 @@ def _other_fuel_cost_gbp_per_kwh(
|
|||
return blended * _PENCE_TO_GBP
|
||||
|
||||
|
||||
def _other_uses_high_rate_fraction(tariff: Tariff) -> float:
|
||||
"""ADR-0014 Bill Derivation — the ALL_OTHER_USES High-Rate Fraction (the
|
||||
day/high-rate share) for lighting / appliances / cooking / pumps on an
|
||||
Off-Peak Meter, mirroring `_other_fuel_cost_gbp_per_kwh`'s tariff handling so
|
||||
the bill's split matches the rating's: STANDARD → 1.0 (single rate); 7-/10-
|
||||
hour → SAP 10.2 Table 12a Grid 2; 18-hour → 1.0 (all other uses bill at the
|
||||
high rate per SAP 10.2 Appendix F2); 24-hour → 1.0 (a heating-only tariff —
|
||||
the Fuel Rates snapshot carries no separate non-heating rate, so other uses
|
||||
bill at the off-peak day rate, a documented approximation for a rare tariff).
|
||||
|
||||
Pumps/fans reuse this fraction; the Table 12a Grid 2 MEV/MVHR `FANS_FOR_MECH_
|
||||
VENT` distinction the SAP cost path applies is a small second-order effect on
|
||||
a small load and is deferred for the bill."""
|
||||
if tariff is Tariff.STANDARD:
|
||||
return 1.0
|
||||
try:
|
||||
return other_use_high_rate_fraction(OtherUse.ALL_OTHER_USES, tariff)
|
||||
except NotImplementedError:
|
||||
return 1.0
|
||||
|
||||
|
||||
def _pumps_fans_fuel_cost_gbp_per_kwh(
|
||||
*,
|
||||
tariff: Tariff,
|
||||
|
|
@ -8164,6 +8255,28 @@ def cert_to_inputs(
|
|||
_main_high_rate_fraction = _main_space_heating_high_rate_fraction(
|
||||
main, _billing_tariff
|
||||
)
|
||||
_main_2_detail = (
|
||||
epc.sap_heating.main_heating_details[1]
|
||||
if epc.sap_heating and len(epc.sap_heating.main_heating_details) > 1
|
||||
else None
|
||||
)
|
||||
_main_2_high_rate_fraction = _main_space_heating_high_rate_fraction(
|
||||
_main_2_detail, _billing_tariff
|
||||
)
|
||||
_secondary_high_rate_frac = _secondary_high_rate_fraction(epc, _billing_tariff)
|
||||
_hw_high_rate_fraction = _hot_water_high_rate_fraction(
|
||||
_water_heating_fuel_code(epc),
|
||||
_water_heating_main(epc),
|
||||
_billing_tariff,
|
||||
water_heating_code=(
|
||||
epc.sap_heating.water_heating_code if epc.sap_heating else None
|
||||
),
|
||||
inherit_main_for_community_heating=_is_community_heating_hw_from_main(epc),
|
||||
cylinder_volume_l=_hot_water_cylinder_volume_l(epc),
|
||||
occupancy_n=wh_result.occupancy if wh_result is not None else None,
|
||||
immersion_single=_immersion_is_single(epc),
|
||||
)
|
||||
_other_uses_fraction = _other_uses_high_rate_fraction(_billing_tariff)
|
||||
|
||||
return CalculatorInputs(
|
||||
dimensions=dim,
|
||||
|
|
@ -8231,6 +8344,11 @@ def cert_to_inputs(
|
|||
hot_water_fuel_code=_water_heating_fuel_code(epc),
|
||||
is_off_peak_meter=_is_off_peak_meter,
|
||||
main_heating_high_rate_fraction=_main_high_rate_fraction,
|
||||
main_2_heating_high_rate_fraction=_main_2_high_rate_fraction,
|
||||
secondary_heating_high_rate_fraction=_secondary_high_rate_frac,
|
||||
hot_water_high_rate_fraction=_hw_high_rate_fraction,
|
||||
pumps_fans_high_rate_fraction=_other_uses_fraction,
|
||||
other_electricity_high_rate_fraction=_other_uses_fraction,
|
||||
space_heating_fuel_cost_gbp_per_kwh=_space_heating_fuel_cost_gbp_per_kwh(
|
||||
main, _rdsap_tariff(epc), prices
|
||||
),
|
||||
|
|
|
|||
|
|
@ -870,6 +870,10 @@ def test_off_peak_storage_cert_surfaces_billing_meter_flag_and_main_fraction() -
|
|||
# Assert — whole-meter off-peak, main heating high-rate fraction 0.20.
|
||||
assert inputs.is_off_peak_meter is True
|
||||
assert inputs.main_heating_high_rate_fraction == 0.20
|
||||
# Lighting / appliances / cooking + pumps bill at the 7-hour Table 12a
|
||||
# Grid 2 ALL_OTHER_USES high-rate fraction (0.90), mostly at the day rate.
|
||||
assert inputs.other_electricity_high_rate_fraction == 0.90
|
||||
assert inputs.pumps_fans_high_rate_fraction == 0.90
|
||||
|
||||
|
||||
def test_off_peak_immersion_unlodged_type_defaults_to_dual_table13_blend() -> None:
|
||||
|
|
@ -919,6 +923,14 @@ def test_off_peak_immersion_unlodged_type_defaults_to_dual_table13_blend() -> No
|
|||
rate = inputs.hot_water_fuel_cost_gbp_per_kwh
|
||||
assert rate > 0.0550 + 1e-6 # NOT the 100%-low-rate bug value (5.50 p/kWh)
|
||||
assert rate < 0.0900 # a small DUAL high-rate fraction, not single (~11 p)
|
||||
# ADR-0014 Bill Derivation surfaces the SAME Table 13 fraction it bills the
|
||||
# HW cost at: a small day-rate share (0 < frac < 0.3), not 0.0 (all-night
|
||||
# fallback) nor 1.0 (single rate). Mirrors the cost-rate split exactly.
|
||||
assert 0.0 < inputs.hot_water_high_rate_fraction < 0.3
|
||||
# Back out the fraction the cost rate used and confirm the surfaced one
|
||||
# matches it: rate_p = frac×15.29 + (1−frac)×5.50.
|
||||
_frac_from_cost = (rate * 100.0 - 5.50) / (15.29 - 5.50)
|
||||
assert inputs.hot_water_high_rate_fraction == pytest.approx(_frac_from_cost)
|
||||
|
||||
|
||||
def test_non_integrated_storage_heater_bills_100_percent_low_rate() -> None:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue