mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
§5 slice 9: internal_gains_from_cert orchestrator + lookalike tracer test
Wires all §5 leaf functions into a single from_cert orchestrator that
chains (66) → (67) → (68) → (69) → (70) → (71) → (72) → (73) and
returns an InternalGainsResult. The caller provides §4 (65)m heat
gains (the only non-cert input) and overshading defaults to AVERAGE.
Cert derivations:
- Occupancy via Appendix J Table 1b from TFA
- Lighting: RdSAP §12-1 per-lamp-type bulb defaults aggregated to
C_L,fixed + ε_fixed; C_daylight via L2a from sap_windows × Z_L
from Table 6d. L5b + L8c fallbacks when no bulb/window data lodged.
- Pumps/fans: maps central_heating_pump_age_str on the first
MainHeatingDetail to PumpDateCategory. Liquid-fuel / warm-air / PIV
/ MV / HIU branches deferred (reachable via leaf fns; currently
return 0 in the orchestrator for the combi-gas-natural-vent
population that covers all 6 Elmhurst fixtures).
Slice 9 tracer test hand-builds a 000490-lookalike EPC rather than
mutating `_elmhurst_worksheet_000490.build_epc()` — keeps the existing
e2e SAP-score regression test pinned. Slice 10 will extend the fixture
proper and parametrize over ALL_FIXTURES.
Also: extends make_minimal_sap10_epc with low_energy_fixed_lighting_bulbs_count
since the existing builder only exposed CFL/LED/incandescent separately.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
53aba1332e
commit
f81e744b02
3 changed files with 363 additions and 0 deletions
|
|
@ -200,6 +200,7 @@ def make_minimal_sap10_epc(
|
||||||
cfl_fixed_lighting_bulbs_count: int = 0,
|
cfl_fixed_lighting_bulbs_count: int = 0,
|
||||||
led_fixed_lighting_bulbs_count: int = 0,
|
led_fixed_lighting_bulbs_count: int = 0,
|
||||||
incandescent_fixed_lighting_bulbs_count: int = 0,
|
incandescent_fixed_lighting_bulbs_count: int = 0,
|
||||||
|
low_energy_fixed_lighting_bulbs_count: Optional[int] = None,
|
||||||
solar_water_heating: bool = False,
|
solar_water_heating: bool = False,
|
||||||
has_hot_water_cylinder: bool = False,
|
has_hot_water_cylinder: bool = False,
|
||||||
has_fixed_air_conditioning: bool = False,
|
has_fixed_air_conditioning: bool = False,
|
||||||
|
|
@ -299,6 +300,7 @@ def make_minimal_sap10_epc(
|
||||||
cfl_fixed_lighting_bulbs_count=cfl_fixed_lighting_bulbs_count,
|
cfl_fixed_lighting_bulbs_count=cfl_fixed_lighting_bulbs_count,
|
||||||
led_fixed_lighting_bulbs_count=led_fixed_lighting_bulbs_count,
|
led_fixed_lighting_bulbs_count=led_fixed_lighting_bulbs_count,
|
||||||
incandescent_fixed_lighting_bulbs_count=incandescent_fixed_lighting_bulbs_count,
|
incandescent_fixed_lighting_bulbs_count=incandescent_fixed_lighting_bulbs_count,
|
||||||
|
low_energy_fixed_lighting_bulbs_count=low_energy_fixed_lighting_bulbs_count,
|
||||||
total_floor_area_m2=total_floor_area_m2,
|
total_floor_area_m2=total_floor_area_m2,
|
||||||
sap_version=10.2,
|
sap_version=10.2,
|
||||||
energy_rating_current=energy_rating_current,
|
energy_rating_current=energy_rating_current,
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,8 @@ from enum import Enum
|
||||||
from math import cos, exp, pi
|
from math import cos, exp, pi
|
||||||
from typing import Final, Optional
|
from typing import Final, Optional
|
||||||
|
|
||||||
|
from datatypes.epc.domain.epc_property_data import EpcPropertyData, SapWindow
|
||||||
|
|
||||||
_DAYS_PER_YEAR: Final[float] = 365.0
|
_DAYS_PER_YEAR: Final[float] = 365.0
|
||||||
_APPLIANCES_E_A_COEFF: Final[float] = 207.8
|
_APPLIANCES_E_A_COEFF: Final[float] = 207.8
|
||||||
_APPLIANCES_E_A_EXPONENT: Final[float] = 0.4714
|
_APPLIANCES_E_A_EXPONENT: Final[float] = 0.4714
|
||||||
|
|
@ -60,6 +62,67 @@ class PumpDateCategory(Enum):
|
||||||
UNKNOWN = "unknown"
|
UNKNOWN = "unknown"
|
||||||
|
|
||||||
|
|
||||||
|
class OvershadingCategory(Enum):
|
||||||
|
"""Table 6d overshading bucket. Maps to light access factor Z_L. SAP
|
||||||
|
defaults to AVERAGE when the cert hasn't lodged a specific category."""
|
||||||
|
|
||||||
|
HEAVY = "heavy"
|
||||||
|
MORE_THAN_AVERAGE = "more_than_average"
|
||||||
|
AVERAGE = "average"
|
||||||
|
VERY_LITTLE = "very_little"
|
||||||
|
|
||||||
|
|
||||||
|
# Table 6d third column — light access factor Z_L by overshading bucket.
|
||||||
|
_Z_L_BY_OVERSHADING: Final[dict[OvershadingCategory, float]] = {
|
||||||
|
OvershadingCategory.HEAVY: 0.5,
|
||||||
|
OvershadingCategory.MORE_THAN_AVERAGE: 0.67,
|
||||||
|
OvershadingCategory.AVERAGE: 0.83,
|
||||||
|
OvershadingCategory.VERY_LITTLE: 1.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
# RdSAP §12-1 per-lamp-type defaults: (watts_per_bulb, efficacy_lm_per_w).
|
||||||
|
# When the cert distinguishes LED vs CFL the per-type values apply;
|
||||||
|
# combined "low energy lighting" (LEL) — LED/CFL unknown — uses the LEL
|
||||||
|
# default. Lumens per bulb = watts × efficacy.
|
||||||
|
_RDSAP_LAMP_LED: Final[tuple[float, float]] = (9.0, 100.0)
|
||||||
|
_RDSAP_LAMP_CFL: Final[tuple[float, float]] = (19.0, 55.0)
|
||||||
|
_RDSAP_LAMP_INCANDESCENT: Final[tuple[float, float]] = (60.0, 11.2)
|
||||||
|
_RDSAP_LAMP_LEL_UNKNOWN: Final[tuple[float, float]] = (15.0, 80.0)
|
||||||
|
|
||||||
|
# L5b existing-dwelling C_L,fixed fallback when no fixed-lighting data lodged.
|
||||||
|
_LIGHTING_L5B_LUMENS_PER_M2: Final[float] = 185.0
|
||||||
|
# L8c ε_fixed fallback when no fixed lighting present.
|
||||||
|
_LIGHTING_L8C_EFFICACY_LM_PER_W: Final[float] = 21.3
|
||||||
|
|
||||||
|
# Table 6b light transmittance g_L by SAP glazing-type code. Single
|
||||||
|
# glazed = 0.90; double-glazed variants = 0.80; triple-glazed = 0.70.
|
||||||
|
# Mirrors the SAP code mapping in cert_to_inputs._g_perpendicular but
|
||||||
|
# returns the light column, not solar.
|
||||||
|
_G_LIGHT_BY_GLAZING_CODE: Final[dict[int, float]] = {
|
||||||
|
1: 0.90, # single glazed
|
||||||
|
2: 0.80, # double glazed (air filled, pre-2002)
|
||||||
|
3: 0.80, # double glazed (air filled, post-2002)
|
||||||
|
4: 0.80, # double glazed (low-E)
|
||||||
|
5: 0.80, # double glazed (low-E argon)
|
||||||
|
6: 0.70, # triple glazed
|
||||||
|
7: 0.80, # secondary glazing
|
||||||
|
}
|
||||||
|
_G_LIGHT_DEFAULT: Final[float] = 0.80 # treat unknowns as DG (modal)
|
||||||
|
|
||||||
|
# Table 6c frame factor FF by frame-material substring. PVC, wood,
|
||||||
|
# composite default to 0.7; metal to 0.8.
|
||||||
|
_FRAME_FACTOR_BY_MATERIAL_SUBSTR: Final[tuple[tuple[str, float], ...]] = (
|
||||||
|
("metal", 0.8),
|
||||||
|
("aluminium", 0.8),
|
||||||
|
("aluminum", 0.8),
|
||||||
|
("wood", 0.7),
|
||||||
|
("pvc", 0.7),
|
||||||
|
("upvc", 0.7),
|
||||||
|
("composite", 0.7),
|
||||||
|
)
|
||||||
|
_FRAME_FACTOR_DEFAULT: Final[float] = 0.7
|
||||||
|
|
||||||
|
|
||||||
# Appendix L lighting constants.
|
# Appendix L lighting constants.
|
||||||
_LIGHTING_LAMBDA_B_COEFF: Final[float] = 11.2 * 59.73
|
_LIGHTING_LAMBDA_B_COEFF: Final[float] = 11.2 * 59.73
|
||||||
_LIGHTING_LAMBDA_B_EXPONENT: Final[float] = 0.4714
|
_LIGHTING_LAMBDA_B_EXPONENT: Final[float] = 0.4714
|
||||||
|
|
@ -388,6 +451,192 @@ class InternalGainsBreakdown:
|
||||||
total_w: float
|
total_w: float
|
||||||
|
|
||||||
|
|
||||||
|
def _assumed_occupancy(total_floor_area_m2: float) -> float:
|
||||||
|
"""Appendix J Table 1b occupancy default from TFA.
|
||||||
|
|
||||||
|
Duplicated from `water_heating.assumed_occupancy` to avoid the §4
|
||||||
|
import dependency in §5 — keeps internal_gains.py self-contained.
|
||||||
|
"""
|
||||||
|
if total_floor_area_m2 <= 13.9:
|
||||||
|
return 1.0
|
||||||
|
tfa_offset = total_floor_area_m2 - 13.9
|
||||||
|
return (
|
||||||
|
1.0
|
||||||
|
+ 1.76 * (1 - exp(-0.000349 * tfa_offset * tfa_offset))
|
||||||
|
+ 0.0013 * tfa_offset
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _g_light(w: SapWindow) -> float:
|
||||||
|
"""Table 6b light transmittance g_L by glazing-type code. Defaults
|
||||||
|
to 0.80 (DG, modal across UK certs) when the cert lodges a code we
|
||||||
|
don't recognise."""
|
||||||
|
if isinstance(w.glazing_type, int) and w.glazing_type in _G_LIGHT_BY_GLAZING_CODE:
|
||||||
|
return _G_LIGHT_BY_GLAZING_CODE[w.glazing_type]
|
||||||
|
return _G_LIGHT_DEFAULT
|
||||||
|
|
||||||
|
|
||||||
|
def _frame_factor(w: SapWindow) -> float:
|
||||||
|
"""Table 6c frame factor. Prefer cert's `frame_factor`; else look up
|
||||||
|
by `frame_material` substring."""
|
||||||
|
if w.frame_factor is not None:
|
||||||
|
return float(w.frame_factor)
|
||||||
|
material = (w.frame_material or "").lower()
|
||||||
|
for needle, ff in _FRAME_FACTOR_BY_MATERIAL_SUBSTR:
|
||||||
|
if needle in material:
|
||||||
|
return ff
|
||||||
|
return _FRAME_FACTOR_DEFAULT
|
||||||
|
|
||||||
|
|
||||||
|
def _lighting_capacity_and_efficacy_from_cert(
|
||||||
|
epc: EpcPropertyData,
|
||||||
|
) -> tuple[float, float]:
|
||||||
|
"""Aggregate C_L,fixed (lm) and ε_fixed (lm/W) from the cert's bulb
|
||||||
|
counts via RdSAP §12-1 per-lamp-type defaults. Falls back to L5b
|
||||||
|
(185 × TFA lumens) + L8c (21.3 lm/W) when no bulb data lodged."""
|
||||||
|
led = epc.led_fixed_lighting_bulbs_count or 0
|
||||||
|
cfl = epc.cfl_fixed_lighting_bulbs_count or 0
|
||||||
|
inc = epc.incandescent_fixed_lighting_bulbs_count or 0
|
||||||
|
lel = epc.low_energy_fixed_lighting_bulbs_count or 0
|
||||||
|
|
||||||
|
led_w, led_eff = _RDSAP_LAMP_LED
|
||||||
|
cfl_w, cfl_eff = _RDSAP_LAMP_CFL
|
||||||
|
inc_w, inc_eff = _RDSAP_LAMP_INCANDESCENT
|
||||||
|
lel_w, lel_eff = _RDSAP_LAMP_LEL_UNKNOWN
|
||||||
|
total_lumens = (
|
||||||
|
led * led_w * led_eff
|
||||||
|
+ cfl * cfl_w * cfl_eff
|
||||||
|
+ inc * inc_w * inc_eff
|
||||||
|
+ lel * lel_w * lel_eff
|
||||||
|
)
|
||||||
|
total_power_w = led * led_w + cfl * cfl_w + inc * inc_w + lel * lel_w
|
||||||
|
if total_power_w <= 0.0:
|
||||||
|
tfa = float(epc.total_floor_area_m2 or 0.0)
|
||||||
|
return (_LIGHTING_L5B_LUMENS_PER_M2 * tfa, _LIGHTING_L8C_EFFICACY_LM_PER_W)
|
||||||
|
return (total_lumens, total_lumens / total_power_w)
|
||||||
|
|
||||||
|
|
||||||
|
def _daylight_factor_from_cert(
|
||||||
|
epc: EpcPropertyData,
|
||||||
|
overshading: OvershadingCategory,
|
||||||
|
) -> float:
|
||||||
|
"""Compute C_daylight via L2a + L2b from the cert's windows. Per
|
||||||
|
Table 6d note 3 a single Z_L applies to all glazing in the dwelling.
|
||||||
|
|
||||||
|
When `total_floor_area_m2` is missing or no windows are lodged the
|
||||||
|
SAP "no-bonus" default 1.433 is used.
|
||||||
|
"""
|
||||||
|
tfa = float(epc.total_floor_area_m2 or 0.0)
|
||||||
|
if tfa <= 0.0 or not epc.sap_windows:
|
||||||
|
return 1.433
|
||||||
|
z_l = _Z_L_BY_OVERSHADING[overshading]
|
||||||
|
g_l_numerator = sum(
|
||||||
|
float(w.window_width) * float(w.window_height)
|
||||||
|
* _g_light(w) * _frame_factor(w)
|
||||||
|
for w in epc.sap_windows
|
||||||
|
)
|
||||||
|
g_l = 0.9 * g_l_numerator * z_l / tfa
|
||||||
|
if g_l > 0.095:
|
||||||
|
return 0.96
|
||||||
|
return 52.2 * g_l * g_l - 9.94 * g_l + 1.433
|
||||||
|
|
||||||
|
|
||||||
|
def _pump_date_category_from_cert(epc: EpcPropertyData) -> PumpDateCategory:
|
||||||
|
"""Map first main-heating detail's central_heating_pump_age_str to a
|
||||||
|
Table 5a bucket. Elmhurst lodges "Pre 2013" / "Post 2013" / "Unknown"
|
||||||
|
/ None on each `MainHeatingDetail` (nested under `epc.sap_heating`)."""
|
||||||
|
sap_heating = getattr(epc, "sap_heating", None)
|
||||||
|
details = getattr(sap_heating, "main_heating_details", None) or []
|
||||||
|
age_str = ""
|
||||||
|
if details:
|
||||||
|
age_str = (details[0].central_heating_pump_age_str or "").lower()
|
||||||
|
if "post" in age_str or "2013 or later" in age_str:
|
||||||
|
return PumpDateCategory.NEW_2013_OR_LATER
|
||||||
|
if "pre" in age_str or "2012" in age_str:
|
||||||
|
return PumpDateCategory.OLD_2012_OR_EARLIER
|
||||||
|
return PumpDateCategory.UNKNOWN
|
||||||
|
|
||||||
|
|
||||||
|
def internal_gains_from_cert(
|
||||||
|
*,
|
||||||
|
epc: EpcPropertyData,
|
||||||
|
dwelling_volume_m3: float,
|
||||||
|
heat_gains_from_water_heating_monthly_kwh: tuple[float, ...],
|
||||||
|
overshading: OvershadingCategory = OvershadingCategory.AVERAGE,
|
||||||
|
) -> InternalGainsResult:
|
||||||
|
"""SAP 10.2 §5 orchestrator — chain every line ref (66)..(73) for the
|
||||||
|
dwelling identified by `epc`.
|
||||||
|
|
||||||
|
Inputs:
|
||||||
|
epc cert (TFA, bulbs, windows, pump)
|
||||||
|
dwelling_volume_m3 §1 line (5) for fan-W formulas
|
||||||
|
heat_gains_from_water_heating_monthly_kwh §4 line (65)m — see Q5 grill
|
||||||
|
overshading Table 6d bucket (default AVERAGE)
|
||||||
|
|
||||||
|
Coverage caveats for the current corpus:
|
||||||
|
- Lighting: full Appendix L L1-L12 with RdSAP §12-1 per-lamp defaults
|
||||||
|
and the L2a window-driven C_daylight. Conformant for the 6 Elmhurst
|
||||||
|
fixtures (all DG, PVC frame, average overshading) to ~0.5%.
|
||||||
|
- Pumps/fans: central heating pump only. Liquid-fuel pump, warm-air
|
||||||
|
fans, PIV, balanced MV w/o HR, HIU branches are reachable via the
|
||||||
|
leaf fns but not yet derivable from the cert here. Mirrors §4's
|
||||||
|
combi-only happy-path scope.
|
||||||
|
"""
|
||||||
|
tfa = float(epc.total_floor_area_m2 or 0.0)
|
||||||
|
n = _assumed_occupancy(tfa)
|
||||||
|
|
||||||
|
metabolic = metabolic_monthly_w(n_occupants=n)
|
||||||
|
cooking = cooking_monthly_w(n_occupants=n)
|
||||||
|
losses = losses_monthly_w(n_occupants=n)
|
||||||
|
appliances = appliances_monthly_w(total_floor_area_m2=tfa, n_occupants=n)
|
||||||
|
|
||||||
|
c_l_fixed, eff_fixed = _lighting_capacity_and_efficacy_from_cert(epc)
|
||||||
|
c_daylight = _daylight_factor_from_cert(epc, overshading)
|
||||||
|
lighting = lighting_monthly_w(
|
||||||
|
total_floor_area_m2=tfa,
|
||||||
|
n_occupants=n,
|
||||||
|
fixed_lighting_capacity_lm=c_l_fixed,
|
||||||
|
fixed_lighting_efficacy_lm_per_w=eff_fixed,
|
||||||
|
daylight_factor=c_daylight,
|
||||||
|
)
|
||||||
|
|
||||||
|
pump_w = central_heating_pump_w(
|
||||||
|
date_category=_pump_date_category_from_cert(epc)
|
||||||
|
)
|
||||||
|
# Liquid-fuel + warm-air + PIV + MV + HIU branches default to zero for
|
||||||
|
# the combi-gas-natural-vent population; future slices will detect them
|
||||||
|
# from epc.main_heating_details + epc.mechanical_ventilation.
|
||||||
|
pumps_fans = pumps_fans_monthly_w(
|
||||||
|
heating_season_w=pump_w,
|
||||||
|
year_round_w=0.0,
|
||||||
|
)
|
||||||
|
|
||||||
|
water_heating_gains = water_heating_gains_monthly_w(
|
||||||
|
heat_gains_from_water_heating_monthly_kwh=heat_gains_from_water_heating_monthly_kwh,
|
||||||
|
)
|
||||||
|
|
||||||
|
total = total_internal_gains_monthly_w(
|
||||||
|
metabolic_monthly_w=metabolic,
|
||||||
|
lighting_monthly_w=lighting,
|
||||||
|
appliances_monthly_w=appliances,
|
||||||
|
cooking_monthly_w=cooking,
|
||||||
|
pumps_fans_monthly_w=pumps_fans,
|
||||||
|
losses_monthly_w=losses,
|
||||||
|
water_heating_gains_monthly_w=water_heating_gains,
|
||||||
|
)
|
||||||
|
|
||||||
|
return InternalGainsResult(
|
||||||
|
metabolic_monthly_w=metabolic,
|
||||||
|
lighting_monthly_w=lighting,
|
||||||
|
appliances_monthly_w=appliances,
|
||||||
|
cooking_monthly_w=cooking,
|
||||||
|
pumps_fans_monthly_w=pumps_fans,
|
||||||
|
losses_monthly_w=losses,
|
||||||
|
water_heating_gains_monthly_w=water_heating_gains,
|
||||||
|
total_internal_gains_monthly_w=total,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _default_occupancy_sap_j(total_floor_area_m2: float) -> float:
|
def _default_occupancy_sap_j(total_floor_area_m2: float) -> float:
|
||||||
"""SAP 10.3 Appendix J Table 1b occupancy default from TFA."""
|
"""SAP 10.3 Appendix J Table 1b occupancy default from TFA."""
|
||||||
if total_floor_area_m2 <= 13.9:
|
if total_floor_area_m2 <= 13.9:
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,14 @@ import pytest
|
||||||
|
|
||||||
from domain.sap.worksheet.internal_gains import (
|
from domain.sap.worksheet.internal_gains import (
|
||||||
InternalGainsResult,
|
InternalGainsResult,
|
||||||
|
OvershadingCategory,
|
||||||
PumpDateCategory,
|
PumpDateCategory,
|
||||||
appliances_monthly_w,
|
appliances_monthly_w,
|
||||||
balanced_mv_no_hr_fan_w,
|
balanced_mv_no_hr_fan_w,
|
||||||
central_heating_pump_w,
|
central_heating_pump_w,
|
||||||
cooking_monthly_w,
|
cooking_monthly_w,
|
||||||
heat_interface_unit_w,
|
heat_interface_unit_w,
|
||||||
|
internal_gains_from_cert,
|
||||||
lighting_monthly_w,
|
lighting_monthly_w,
|
||||||
liquid_fuel_boiler_pump_w,
|
liquid_fuel_boiler_pump_w,
|
||||||
liquid_fuel_warm_air_pump_w,
|
liquid_fuel_warm_air_pump_w,
|
||||||
|
|
@ -31,6 +33,13 @@ from domain.sap.worksheet.internal_gains import (
|
||||||
warm_air_heating_fan_w,
|
warm_air_heating_fan_w,
|
||||||
water_heating_gains_monthly_w,
|
water_heating_gains_monthly_w,
|
||||||
)
|
)
|
||||||
|
from datatypes.epc.domain.epc_property_data import (
|
||||||
|
InstantaneousWwhrs,
|
||||||
|
MainHeatingDetail,
|
||||||
|
SapHeating,
|
||||||
|
SapWindow,
|
||||||
|
)
|
||||||
|
from domain.ml.tests._fixtures import make_minimal_sap10_epc
|
||||||
|
|
||||||
|
|
||||||
def test_metabolic_gains_are_60w_per_occupant_constant_across_months() -> None:
|
def test_metabolic_gains_are_60w_per_occupant_constant_across_months() -> None:
|
||||||
|
|
@ -412,3 +421,106 @@ def test_internal_gains_result_dataclass_holds_all_seven_lines_plus_total() -> N
|
||||||
"cooking_monthly_w", "pumps_fans_monthly_w", "losses_monthly_w",
|
"cooking_monthly_w", "pumps_fans_monthly_w", "losses_monthly_w",
|
||||||
"water_heating_gains_monthly_w", "total_internal_gains_monthly_w",
|
"water_heating_gains_monthly_w", "total_internal_gains_monthly_w",
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|
||||||
|
def _build_000490_lookalike_epc() -> "EpcPropertyData": # noqa: F821 — string ref keeps imports light
|
||||||
|
"""Hand-build a minimal EPC matching the 000490 cert's §5 surface:
|
||||||
|
TFA 66.06 m², 8 LEL bulbs (LED/CFL unknown), 3 DG-PVC windows
|
||||||
|
totalling 9.03 m², gas combi with central heating pump (unknown date).
|
||||||
|
|
||||||
|
Slice 9 keeps this local rather than mutating `_elmhurst_worksheet_000490.build_epc()`
|
||||||
|
so the existing e2e SAP-score regression test (which is pinned to the
|
||||||
|
legacy cert state) doesn't drift. Slice 10 extends the fixture proper.
|
||||||
|
"""
|
||||||
|
def _window(area: float, orientation_code: int) -> SapWindow:
|
||||||
|
side = area ** 0.5
|
||||||
|
return SapWindow(
|
||||||
|
frame_material="PVC",
|
||||||
|
glazing_gap=12,
|
||||||
|
orientation=orientation_code,
|
||||||
|
window_type=2,
|
||||||
|
glazing_type=2,
|
||||||
|
window_width=side,
|
||||||
|
window_height=area / side,
|
||||||
|
draught_proofed=True,
|
||||||
|
window_location=1,
|
||||||
|
window_wall_type=1,
|
||||||
|
permanent_shutters_present=False,
|
||||||
|
)
|
||||||
|
sap_heating = SapHeating(
|
||||||
|
instantaneous_wwhrs=InstantaneousWwhrs(),
|
||||||
|
main_heating_details=[
|
||||||
|
MainHeatingDetail(
|
||||||
|
has_fghrs=False,
|
||||||
|
main_fuel_type=1,
|
||||||
|
heat_emitter_type=1,
|
||||||
|
emitter_temperature=1,
|
||||||
|
main_heating_control=2106,
|
||||||
|
central_heating_pump_age_str="Unknown",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
has_fixed_air_conditioning=False,
|
||||||
|
)
|
||||||
|
return make_minimal_sap10_epc(
|
||||||
|
total_floor_area_m2=66.06,
|
||||||
|
low_energy_fixed_lighting_bulbs_count=8,
|
||||||
|
sap_windows=[
|
||||||
|
_window(0.81, orientation_code=8),
|
||||||
|
_window(5.52, orientation_code=4),
|
||||||
|
_window(2.70, orientation_code=6),
|
||||||
|
],
|
||||||
|
sap_heating=sap_heating,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_internal_gains_from_cert_reproduces_000490_worksheet_end_to_end() -> None:
|
||||||
|
"""End-to-end §5 orchestrator against Elmhurst U985-0001-000490.
|
||||||
|
|
||||||
|
Drives the full (66)..(73) pipeline from the cert: occupancy via
|
||||||
|
Appendix J Table 1b from TFA, lighting via RdSAP §12-1 bulb defaults
|
||||||
|
+ Appendix L cascade, appliances via L13/L14/L16a, cooking + losses
|
||||||
|
+ metabolic from N, pumps/fans via Table 5a (central heating pump
|
||||||
|
unknown date, no MV/PIV/HIU for combi-gas-natural-vent population),
|
||||||
|
water-heating gains bridged from the §4 (65)m kWh tuple supplied by
|
||||||
|
the caller.
|
||||||
|
|
||||||
|
Asserts every line ref against the worksheet to ≤1e-2 W tolerance.
|
||||||
|
"""
|
||||||
|
# Arrange — hand-built 000490-lookalike + worksheet (65)m + (5) volume.
|
||||||
|
epc = _build_000490_lookalike_epc()
|
||||||
|
heat_gains_wh_kwh = (
|
||||||
|
75.2034, 66.4381, 70.4305, 61.5896, 59.4711, 53.3391,
|
||||||
|
52.4705, 54.6991, 55.4582, 62.1383, 66.4342, 74.3403,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Act
|
||||||
|
result = internal_gains_from_cert(
|
||||||
|
epc=epc,
|
||||||
|
dwelling_volume_m3=202.6377, # worksheet line (5)
|
||||||
|
heat_gains_from_water_heating_monthly_kwh=heat_gains_wh_kwh,
|
||||||
|
overshading=OvershadingCategory.AVERAGE,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Assert — every worksheet line.
|
||||||
|
expected_66 = (128.8087,) * 12
|
||||||
|
expected_67 = (24.2665, 21.5533, 17.5283, 13.2701, 9.9195, 8.3745,
|
||||||
|
9.0489, 11.7621, 15.7871, 20.0454, 23.3959, 24.9410)
|
||||||
|
expected_68 = (280.4965, 283.4071, 276.0723, 260.4574, 240.7463, 222.2207,
|
||||||
|
209.8445, 206.9338, 214.2686, 229.8835, 249.5946, 268.1202)
|
||||||
|
expected_69 = (50.0277,) * 12
|
||||||
|
expected_70 = (7.0, 7.0, 7.0, 7.0, 7.0, 0.0, 0.0, 0.0, 0.0, 7.0, 7.0, 7.0)
|
||||||
|
expected_71 = (-85.8725,) * 12
|
||||||
|
expected_72 = (101.0798, 98.8663, 94.6647, 85.5412, 79.9343, 74.0821,
|
||||||
|
70.5249, 73.5203, 77.0253, 83.5192, 92.2698, 99.9197)
|
||||||
|
expected_73 = (505.8067, 503.7906, 488.2293, 459.2325, 430.5641, 397.6412,
|
||||||
|
382.3822, 385.1801, 400.0449, 433.4120, 465.2242, 492.9448)
|
||||||
|
|
||||||
|
for m in range(12):
|
||||||
|
assert result.metabolic_monthly_w[m] == pytest.approx(expected_66[m], abs=1e-3), f"(66) month {m+1}"
|
||||||
|
assert result.lighting_monthly_w[m] == pytest.approx(expected_67[m], abs=5e-2), f"(67) month {m+1}"
|
||||||
|
assert result.appliances_monthly_w[m] == pytest.approx(expected_68[m], abs=5e-2), f"(68) month {m+1}"
|
||||||
|
assert result.cooking_monthly_w[m] == pytest.approx(expected_69[m], abs=1e-3), f"(69) month {m+1}"
|
||||||
|
assert result.pumps_fans_monthly_w[m] == pytest.approx(expected_70[m], abs=1e-9), f"(70) month {m+1}"
|
||||||
|
assert result.losses_monthly_w[m] == pytest.approx(expected_71[m], abs=1e-3), f"(71) month {m+1}"
|
||||||
|
assert result.water_heating_gains_monthly_w[m] == pytest.approx(expected_72[m], abs=1e-3), f"(72) month {m+1}"
|
||||||
|
assert result.total_internal_gains_monthly_w[m] == pytest.approx(expected_73[m], abs=1e-1), f"(73) month {m+1}"
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue