mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
cert_to_inputs: wire §4 worksheet orchestrator into HW kWh derivation
Replaces the legacy `predicted_hot_water_kwh` cascade with a call into
`water_heating_from_cert` for the modal combi-gas-mains population. The
new helper `_hot_water_fuel_kwh_per_yr` chains the §4 cascade end-to-end
(occupancy → daily hot water → energy content → distribution + combi
loss → (62)m total → (64)m output) then divides by water-heater
efficiency to land annual fuel kWh — the slot CalculatorInputs expects.
Section-by-section validation across all 6 Elmhurst fixtures shows:
§1 dimensions exact (≤ 1e-4) on all 6
§2 ventilation exact (≤ 1e-4) on all 6
§3 heat trans exact on non-RR (000474, 000490) within 0.04 W/K
(display-rounding); RR fixtures under-count per the
formal SapRoomInRoof sub-area deferral.
§4 hot water exact on the 2 fixtures with LINE_42/LINE_64 lodged
(000474 PCDB override + 000490 cascade-default); 4 RR
fixtures emit plausible orchestrator values.
End-to-end SAP impact (legacy → new):
000490 57=57 (cont 56.72 → 56.92, closer to worksheet 57.40)
000474 55→56 (cont 55.39 → 55.59, expected 62, still 6pt under)
Caveats / future slices:
- Cold water source defaults to mains (no domain-model field yet).
- Shower flow rate defaults to 7 L/min vented (no shower_outlet_type
plumbing yet); both fixtures actually lodge this so no false drift.
- Cylinder + solar + WWHRS / PV / FGHRS branches default to zero.
- PCDB Table 3b combi loss not implemented; orchestrator accepts a
`combi_loss_monthly_kwh_override` for now but cert_to_inputs always
falls to Table 3a row "time-clock keep-hot".
- water_efficiency variable misnamed "pct" — it's a decimal (0.0-1.0).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
171cb97c6e
commit
3c2f975c6d
1 changed files with 98 additions and 17 deletions
|
|
@ -68,6 +68,10 @@ from domain.sap.worksheet.ventilation import (
|
|||
MechanicalVentilationKind,
|
||||
ventilation_from_inputs,
|
||||
)
|
||||
from domain.sap.worksheet.water_heating import (
|
||||
TABLE_J1_TCOLD_FROM_MAINS_C,
|
||||
water_heating_from_cert,
|
||||
)
|
||||
|
||||
|
||||
# RdSAP 10 Table 27 — fraction of total floor area treated as the
|
||||
|
|
@ -741,6 +745,95 @@ def _ventilation_counts(vent: Optional[SapVentilation]) -> _VentilationCounts:
|
|||
)
|
||||
|
||||
|
||||
# SAP 10.2 Table J4 — default mixer-shower flow rate for an existing
|
||||
# dwelling with a vented hot water system (the existing-dwelling minimum).
|
||||
# Both validation worksheets (000474 + 000490) lodge this value. Combi-
|
||||
# pumped showers (11 L/min) and instantaneous-electric showers (handled
|
||||
# via line (64a)m, not here) need shower-outlet-type plumbing in a later
|
||||
# slice.
|
||||
_SHOWER_FLOW_VENTED_L_PER_MIN: Final[float] = 7.0
|
||||
|
||||
|
||||
def _mixer_shower_flow_rates_from_cert(
|
||||
epc: EpcPropertyData,
|
||||
) -> tuple[float, ...]:
|
||||
"""Pull mixer-shower flow rates from the cert.
|
||||
|
||||
The cert lodges flow rate per shower outlet (Elmhurst worksheets
|
||||
show "Vented hot water system, 7.00"). The domain model doesn't
|
||||
surface that field yet; until it does, every cert defaults to the
|
||||
SAP10.2 Table J4 "Vented hot water system" row at 7 L/min — the
|
||||
existing-dwelling minimum and what both validation fixtures
|
||||
(000474 + 000490) actually lodge. Combi-pumped showers (11 L/min)
|
||||
or electric showers (handled via (64a)m, not here) will need
|
||||
proper shower-outlet-type plumbing in a later slice.
|
||||
"""
|
||||
return (_SHOWER_FLOW_VENTED_L_PER_MIN,)
|
||||
|
||||
|
||||
def _has_bath_from_cert(epc: EpcPropertyData) -> bool:
|
||||
"""True iff cert lodges ≥ 1 bath. `number_baths is None` is treated as
|
||||
bath present (modal UK lodging — bathless dwellings are rare and
|
||||
typically explicitly lodged as 0)."""
|
||||
n = epc.sap_heating.number_baths
|
||||
return n is None or n >= 1
|
||||
|
||||
|
||||
def _hot_water_fuel_kwh_per_yr(
|
||||
*,
|
||||
epc: EpcPropertyData,
|
||||
water_efficiency_pct: float,
|
||||
is_instantaneous: bool,
|
||||
primary_age: Optional[str],
|
||||
) -> float:
|
||||
"""Annual hot water FUEL kWh (the slot calculator.CalculatorInputs
|
||||
expects). Wires the SAP10.2 §4 worksheet orchestrator into the cert→
|
||||
inputs adapter.
|
||||
|
||||
For combi gas (the dominant population) the orchestrator handles the
|
||||
full Appendix J cascade including Table 3a row "time-clock keep-hot"
|
||||
combi loss. Cylinder + solar + WWHRS / PV diverter / FGHRS branches
|
||||
default to zero — extension slices will populate them as needed.
|
||||
|
||||
Annual output (Σ (64)m) is divided by `water_efficiency_pct / 100`
|
||||
to convert delivered heat to fuel kWh, mirroring the worksheet's
|
||||
(219) line. Falls back to legacy `predicted_hot_water_kwh` if the
|
||||
TFA is missing (the orchestrator requires it for occupancy).
|
||||
"""
|
||||
if epc.total_floor_area_m2 is None:
|
||||
return predicted_hot_water_kwh(
|
||||
total_floor_area_m2=epc.total_floor_area_m2,
|
||||
seasonal_efficiency_water=water_efficiency_pct,
|
||||
cylinder_size=None if is_instantaneous else _int_or_none(epc.sap_heating.cylinder_size),
|
||||
cylinder_insulation_thickness_mm=(
|
||||
None if is_instantaneous else epc.sap_heating.cylinder_insulation_thickness_mm
|
||||
),
|
||||
cylinder_insulation_type=(
|
||||
None if is_instantaneous
|
||||
else _int_or_none(epc.sap_heating.cylinder_insulation_type)
|
||||
),
|
||||
age_band=None if is_instantaneous else primary_age,
|
||||
has_wwhrs=False,
|
||||
has_solar_water_heating=epc.solar_water_heating,
|
||||
)
|
||||
result = water_heating_from_cert(
|
||||
epc=epc,
|
||||
mixer_shower_flow_rates_l_per_min=_mixer_shower_flow_rates_from_cert(epc),
|
||||
has_bath=_has_bath_from_cert(epc),
|
||||
# Cold water source isn't on the domain model yet; default to mains
|
||||
# (the dominant UK lodging — 95%+). Header-tank dwellings will need
|
||||
# a domain-model field + plumb-through in a future slice.
|
||||
cold_water_temps_c=TABLE_J1_TCOLD_FROM_MAINS_C,
|
||||
low_water_use=False,
|
||||
)
|
||||
if water_efficiency_pct <= 0:
|
||||
return 0.0
|
||||
# `water_efficiency_pct` is misnamed in the calling code — the value
|
||||
# is a decimal (0.0–1.0), not a percent. Divide the orchestrator's
|
||||
# delivered-heat output by the decimal efficiency to land fuel kWh.
|
||||
return result.output_kwh_per_yr / water_efficiency_pct
|
||||
|
||||
|
||||
def cert_to_inputs(
|
||||
epc: EpcPropertyData, *, prices: PriceTable = SAP_10_2_SPEC_PRICES
|
||||
) -> CalculatorInputs:
|
||||
|
|
@ -848,23 +941,11 @@ def cert_to_inputs(
|
|||
# = q_generated, matching the per-kWh-generated unit price.
|
||||
water_eff = 1.0 / _heat_network_dlf(primary_age)
|
||||
is_instantaneous = epc.sap_heating.water_heating_code in _INSTANTANEOUS_WATER_CODES
|
||||
hw_kwh = predicted_hot_water_kwh(
|
||||
total_floor_area_m2=epc.total_floor_area_m2,
|
||||
seasonal_efficiency_water=water_eff,
|
||||
# Instantaneous HW systems (codes 907/909) have no cylinder and
|
||||
# no primary circuit — pass None for both to suppress storage
|
||||
# and primary losses in the demand model.
|
||||
cylinder_size=None if is_instantaneous else _int_or_none(epc.sap_heating.cylinder_size),
|
||||
cylinder_insulation_thickness_mm=(
|
||||
None if is_instantaneous else epc.sap_heating.cylinder_insulation_thickness_mm
|
||||
),
|
||||
cylinder_insulation_type=(
|
||||
None if is_instantaneous
|
||||
else _int_or_none(epc.sap_heating.cylinder_insulation_type)
|
||||
),
|
||||
age_band=None if is_instantaneous else primary_age,
|
||||
has_wwhrs=False,
|
||||
has_solar_water_heating=epc.solar_water_heating,
|
||||
hw_kwh = _hot_water_fuel_kwh_per_yr(
|
||||
epc=epc,
|
||||
water_efficiency_pct=water_eff,
|
||||
is_instantaneous=is_instantaneous,
|
||||
primary_age=primary_age,
|
||||
)
|
||||
lighting_kwh = predicted_lighting_kwh(
|
||||
total_floor_area_m2=epc.total_floor_area_m2,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue