mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Thread appliances + cooking annual kWh onto SapResult for ADR-0014 bills
ADR-0014 BillDerivation prices a per-end-use EnergyBreakdown
(HEATING / HOT_WATER / LIGHTING / PUMPS_FANS / APPLIANCES / COOKING).
SapResult already carried the first four but not appliances or cooking,
so a downstream SapResult→EnergyBreakdown adapter had to stub those two
at 0 kWh — understating the bill by the whole unregulated electricity
load. Surface them so the property_baseline side can wire the sections.
Adds two output-only fields to CalculatorInputs + SapResult, threaded
exactly like lighting_kwh_per_yr:
appliances_kwh_per_yr — SAP 10.2 Appendix L L13/L14/L16a annual E_A
(sum of the §5 (68) monthly appliances kWh)
cooking_kwh_per_yr — SAP 10.2 Appendix L L20 (p.91) ELECTRICITY
estimate E_cook = 138 + 28×N
Both values already existed in cert_to_inputs.py (appliances_monthly_kwh,
cooking_monthly_kwh) — reused, not recomputed.
Fuel attribution: cooking_kwh_per_yr is the L20 ELECTRICITY figure (the
field docstring says so), distinct from the L18 cooking heat GAIN
(35 + 7N W) the §5 internal-gains cascade uses. The bill adapter should
treat cooking as an electricity carrier; a gas-cooker split, if ever
needed, is a separate follow-up.
HARD CONSTRAINT honoured — output-only, zero rating drift. Appliances +
cooking are unregulated and are NOT fed into ECF / total_fuel_cost /
CO2 / primary energy / sap_score. Every golden-fixture, Elmhurst e2e
SapResult pin, section cascade pin, and heating-corpus residual stays
byte-identical (1165 rated pins green). The synthetic CalculatorInputs
fixtures set the new fields non-zero on purpose so the existing cost/PE
reconciliation assertions act as leak detectors.
New focused test asserts both fields are populated (non-zero) and
threaded unchanged onto SapResult, with cooking equal to the L20
electricity figure (138 + 28×occupancy) to 1e-9. pyright net-zero
111 → 111.
Note: 11 pre-existing failures in test_appendix_u.py / test_table_32.py
arrived with the recently absorbed PR and are unrelated to this change
(they fail identically on the clean branch); flagged separately.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
44bf186e22
commit
2f039aeb39
6 changed files with 75 additions and 1 deletions
|
|
@ -178,6 +178,14 @@ class CalculatorInputs:
|
|||
hot_water_kwh_per_yr: float
|
||||
pumps_fans_kwh_per_yr: float
|
||||
lighting_kwh_per_yr: float
|
||||
# Unregulated annual delivered electricity — output-only, NOT fed
|
||||
# into ECF / cost / CO2 / primary energy / sap_score (regulated
|
||||
# energy only). Surfaced for ADR-0014 BillDerivation's APPLIANCES +
|
||||
# COOKING sections. `cooking_kwh_per_yr` is the SAP 10.2 Appendix L
|
||||
# L20 (p.91) ELECTRICITY figure (138 + 28×N), not the L18 cooking
|
||||
# heat gain. `appliances_kwh_per_yr` is the L13/L14/L16a annual E_A.
|
||||
appliances_kwh_per_yr: float
|
||||
cooking_kwh_per_yr: float
|
||||
space_heating_fuel_cost_gbp_per_kwh: float
|
||||
hot_water_fuel_cost_gbp_per_kwh: float
|
||||
other_fuel_cost_gbp_per_kwh: float
|
||||
|
|
@ -357,6 +365,15 @@ class SapResult:
|
|||
hot_water_kwh_per_yr: float
|
||||
pumps_fans_kwh_per_yr: float
|
||||
lighting_kwh_per_yr: float
|
||||
# Unregulated annual delivered electricity for ADR-0014
|
||||
# BillDerivation (APPLIANCES + COOKING sections). Output-only — these
|
||||
# do NOT contribute to ecf / total_fuel_cost_gbp / co2_kg_per_yr /
|
||||
# primary_energy_kwh_per_yr / sap_score. `cooking_kwh_per_yr` is the
|
||||
# SAP 10.2 Appendix L L20 (p.91) ELECTRICITY estimate (138 + 28×N);
|
||||
# the bill adapter should treat it as an electricity carrier (a
|
||||
# gas-cooker split, if ever needed, is a separate follow-up).
|
||||
appliances_kwh_per_yr: float
|
||||
cooking_kwh_per_yr: float
|
||||
primary_energy_kwh_per_yr: float
|
||||
primary_energy_kwh_per_m2: float
|
||||
monthly: tuple[MonthlyEntry, ...]
|
||||
|
|
@ -745,6 +762,8 @@ def calculate_sap_from_inputs(inputs: CalculatorInputs) -> SapResult:
|
|||
hot_water_kwh_per_yr=inputs.hot_water_kwh_per_yr,
|
||||
pumps_fans_kwh_per_yr=inputs.pumps_fans_kwh_per_yr,
|
||||
lighting_kwh_per_yr=inputs.lighting_kwh_per_yr,
|
||||
appliances_kwh_per_yr=inputs.appliances_kwh_per_yr,
|
||||
cooking_kwh_per_yr=inputs.cooking_kwh_per_yr,
|
||||
primary_energy_kwh_per_yr=primary_energy_kwh,
|
||||
primary_energy_kwh_per_m2=primary_energy_per_m2,
|
||||
monthly=monthly,
|
||||
|
|
|
|||
|
|
@ -6163,6 +6163,13 @@ def cert_to_inputs(
|
|||
hot_water_kwh_per_yr=hw_kwh,
|
||||
pumps_fans_kwh_per_yr=pumps_fans_kwh,
|
||||
lighting_kwh_per_yr=lighting_kwh,
|
||||
# Unregulated annual delivered electricity for ADR-0014
|
||||
# BillDerivation — output-only, NOT wired into cost / CO2 / PE.
|
||||
# Appliances: SAP 10.2 Appendix L L13/L14/L16a (sum of the §5
|
||||
# (68) monthly E_A). Cooking: Appendix L L20 (p.91) ELECTRICITY
|
||||
# E_cook = 138 + 28×N, already summed in `cooking_monthly_kwh`.
|
||||
appliances_kwh_per_yr=sum(appliances_monthly_kwh),
|
||||
cooking_kwh_per_yr=sum(cooking_monthly_kwh),
|
||||
space_heating_fuel_cost_gbp_per_kwh=_space_heating_fuel_cost_gbp_per_kwh(
|
||||
main, _rdsap_tariff(epc), prices
|
||||
),
|
||||
|
|
|
|||
|
|
@ -115,6 +115,11 @@ def _baseline_dwelling() -> CalculatorInputs:
|
|||
hot_water_kwh_per_yr=2400.0,
|
||||
pumps_fans_kwh_per_yr=100.0,
|
||||
lighting_kwh_per_yr=600.0,
|
||||
# Non-zero on purpose: unregulated loads that must NOT leak into
|
||||
# cost / CO2 / PE / sap_score (the reconciliation assertions sum
|
||||
# only the regulated end-uses, so a leak would surface here).
|
||||
appliances_kwh_per_yr=2000.0,
|
||||
cooking_kwh_per_yr=200.0,
|
||||
space_heating_fuel_cost_gbp_per_kwh=0.07,
|
||||
hot_water_fuel_cost_gbp_per_kwh=0.07,
|
||||
other_fuel_cost_gbp_per_kwh=0.07,
|
||||
|
|
|
|||
|
|
@ -119,6 +119,11 @@ def _baseline_inputs() -> CalculatorInputs:
|
|||
hot_water_kwh_per_yr=2400.0,
|
||||
pumps_fans_kwh_per_yr=100.0,
|
||||
lighting_kwh_per_yr=600.0,
|
||||
# Non-zero on purpose: these unregulated loads must NOT leak into
|
||||
# cost / CO2 / PE / sap_score. The reconciliation assertions in
|
||||
# this file sum only the regulated end-uses, so a leak surfaces here.
|
||||
appliances_kwh_per_yr=2000.0,
|
||||
cooking_kwh_per_yr=200.0,
|
||||
space_heating_fuel_cost_gbp_per_kwh=0.07,
|
||||
hot_water_fuel_cost_gbp_per_kwh=0.07,
|
||||
other_fuel_cost_gbp_per_kwh=0.07,
|
||||
|
|
|
|||
|
|
@ -25,7 +25,10 @@ from typing import Final
|
|||
import pytest
|
||||
|
||||
from domain.sap10_calculator.calculator import Sap10Calculator
|
||||
from domain.sap10_calculator.rdsap.cert_to_inputs import cert_to_inputs
|
||||
from domain.sap10_calculator.rdsap.cert_to_inputs import (
|
||||
cert_to_inputs,
|
||||
water_heating_section_from_cert,
|
||||
)
|
||||
from domain.sap10_calculator.worksheet.tests import (
|
||||
_elmhurst_worksheet_000474 as _w000474,
|
||||
_elmhurst_worksheet_000477 as _w000477,
|
||||
|
|
@ -220,3 +223,36 @@ def test_elmhurst_cert_to_inputs_monthly_infiltration_ach_matches_u985_worksheet
|
|||
assert inputs.monthly_infiltration_ach[m] == pytest.approx(
|
||||
fixture.LINE_25_EFFECTIVE_ACH[m], abs=1e-3
|
||||
), f"(25) month {m+1} drift"
|
||||
|
||||
|
||||
def test_appliances_and_cooking_kwh_threaded_onto_sap_result() -> None:
|
||||
"""Appliances + cooking annual delivered electricity reach SapResult.
|
||||
|
||||
ADR-0014 BillDerivation prices an APPLIANCES and a COOKING section,
|
||||
so the unregulated electricity loads the rating cascade already
|
||||
computes must be surfaced on SapResult (previously a downstream
|
||||
adapter stubbed them at 0 kWh, understating the bill). Cooking is
|
||||
the SAP 10.2 Appendix L L20 (p.91) ELECTRICITY estimate E_cook =
|
||||
138 + 28 × N (N = assumed occupancy) — distinct from the L18 cooking
|
||||
heat GAIN (35 + 7N W) the §5 internal-gains cascade uses. Appliances
|
||||
is the L13/L14/L16a annual E_A distributed monthly.
|
||||
"""
|
||||
# Arrange — a normal main-only gas-combi cert (non-zero TFA → non-
|
||||
# zero appliances + cooking).
|
||||
fixture = _FIXTURE_MODULES['000516']
|
||||
epc = fixture.build_epc()
|
||||
water_heating = water_heating_section_from_cert(epc)
|
||||
assert water_heating is not None # 000516 has a TFA, so HW is present
|
||||
expected_cooking_kwh = 138.0 + 28.0 * water_heating.occupancy # Appendix L L20
|
||||
|
||||
# Act
|
||||
inputs = cert_to_inputs(epc)
|
||||
result = Sap10Calculator().calculate(epc)
|
||||
|
||||
# Assert — both fields populated and threaded unchanged from
|
||||
# CalculatorInputs onto SapResult; cooking equals the L20 electricity
|
||||
# figure (NOT the larger L18 gain) to 1e-9.
|
||||
assert inputs.appliances_kwh_per_yr > 0.0
|
||||
assert result.appliances_kwh_per_yr == inputs.appliances_kwh_per_yr
|
||||
assert result.cooking_kwh_per_yr == inputs.cooking_kwh_per_yr
|
||||
assert abs(result.cooking_kwh_per_yr - expected_cooking_kwh) <= 1e-9
|
||||
|
|
|
|||
|
|
@ -47,6 +47,8 @@ def _sap_result(
|
|||
hot_water_kwh_per_yr=0.0,
|
||||
pumps_fans_kwh_per_yr=0.0,
|
||||
lighting_kwh_per_yr=0.0,
|
||||
appliances_kwh_per_yr=0.0,
|
||||
cooking_kwh_per_yr=0.0,
|
||||
primary_energy_kwh_per_yr=0.0,
|
||||
primary_energy_kwh_per_m2=primary_energy_kwh_per_m2,
|
||||
monthly=(),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue