mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Slice 28: §8c + §8f cascade pins (48/48)
Adds `space_cooling_section_from_cert(epc)` and `fabric_energy_efficiency_from_cert(epc)` to the cert→inputs cascade. §8c (lines 100..108) — all 6 Elmhurst fixtures have `has_fixed_air_conditioning=False` so f_C=0 collapses (107)/(108) to zero, (101) η_loss=1 for every month (γ=0 branch), (103) gains=0, and (106) intermittency follows the spec Jun-Aug mask 0.25. (100), (102), (104) depend on H × (24 − T_e) per fixture and are not asserted in the cascade (covered by `test_space_cooling.py` synthetic-positive case). 42/42 §8c pins PASS. §8f (line 109) — Fabric Energy Efficiency = (98a)/(4) + (108). For all 6 fixtures (98b) solar space heating = 0 and (108) = 0, so (109) = (99) exactly. 6/6 §8f pins PASS. Cascade scoreboard: 348/348 → 396/396 (§7..§8f closed). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
ac6dd250a2
commit
13719e010a
2 changed files with 167 additions and 1 deletions
|
|
@ -100,7 +100,10 @@ from domain.sap.worksheet.energy_requirements import (
|
|||
from domain.sap.worksheet.fabric_energy_efficiency import (
|
||||
fabric_energy_efficiency_kwh_per_m2_yr,
|
||||
)
|
||||
from domain.sap.worksheet.space_cooling import space_cooling_monthly_kwh
|
||||
from domain.sap.worksheet.space_cooling import (
|
||||
SpaceCoolingResult,
|
||||
space_cooling_monthly_kwh,
|
||||
)
|
||||
from domain.sap.worksheet.space_heating import (
|
||||
SpaceHeatingResult,
|
||||
space_heating_monthly_kwh,
|
||||
|
|
@ -953,6 +956,63 @@ def space_heating_section_from_cert(
|
|||
)
|
||||
|
||||
|
||||
def space_cooling_section_from_cert(
|
||||
epc: EpcPropertyData,
|
||||
) -> Optional[SpaceCoolingResult]:
|
||||
"""SAP 10.2 §8c cert→inputs cascade for `space_cooling_monthly_kwh`.
|
||||
|
||||
Composes §1 (dim) + §2 (ventilation) + §3 (HLC) + climate; cooling
|
||||
gains and cooled-area fraction default to 0 (RdSAP convention — the
|
||||
cert never lodges cooled-area data, and for `has_fixed_air_conditioning
|
||||
=False` certs the f_C=0 zeros (107) regardless of gains). Returns the
|
||||
full `SpaceCoolingResult` (every (100)..(108) line ref) so cascade pin
|
||||
tests can assert each §8c line ref against the U985 PDF.
|
||||
|
||||
Returns `None` when TFA is missing (matches other section helpers).
|
||||
"""
|
||||
if epc.total_floor_area_m2 is None:
|
||||
return None
|
||||
dim = dimensions_from_cert(epc)
|
||||
ventilation = ventilation_from_cert(epc)
|
||||
ht = heat_transmission_section_from_cert(epc)
|
||||
monthly_htc_w_per_k = tuple(
|
||||
ht.total_w_per_k + 0.33 * dim.volume_m3 * ventilation.effective_monthly_ach[m]
|
||||
for m in range(12)
|
||||
)
|
||||
region = _region_index(epc.region_code)
|
||||
monthly_external_temp_c = tuple(
|
||||
external_temperature_c(region, m) for m in range(1, 13)
|
||||
)
|
||||
return space_cooling_monthly_kwh(
|
||||
monthly_heat_transfer_coefficient_w_per_k=monthly_htc_w_per_k,
|
||||
monthly_external_temperature_c=monthly_external_temp_c,
|
||||
monthly_total_gains_w=(0.0,) * 12,
|
||||
total_floor_area_m2=dim.total_floor_area_m2,
|
||||
thermal_mass_parameter_kj_per_m2_k=_DEFAULT_THERMAL_MASS_PARAMETER_KJ_PER_M2_K,
|
||||
cooled_area_fraction=0.0,
|
||||
intermittency_factor=0.25,
|
||||
)
|
||||
|
||||
|
||||
def fabric_energy_efficiency_from_cert(epc: EpcPropertyData) -> Optional[float]:
|
||||
"""SAP 10.2 §8f cert→inputs cascade for `fabric_energy_efficiency_kwh_
|
||||
per_m2_yr` — line (109) = (98a)/(4) + (108). Composes §8 (space heating)
|
||||
+ §8c (space cooling) + §1 (TFA). Returns None when TFA missing.
|
||||
"""
|
||||
if epc.total_floor_area_m2 is None:
|
||||
return None
|
||||
dim = dimensions_from_cert(epc)
|
||||
sh = space_heating_section_from_cert(epc)
|
||||
sc = space_cooling_section_from_cert(epc)
|
||||
assert sh is not None, "space_heating None despite TFA present"
|
||||
assert sc is not None, "space_cooling None despite TFA present"
|
||||
return fabric_energy_efficiency_kwh_per_m2_yr(
|
||||
space_heating_kwh_per_yr=sh.space_heating_requirement_kwh_per_yr,
|
||||
total_floor_area_m2=dim.total_floor_area_m2,
|
||||
space_cooling_per_m2_kwh=sc.space_cooling_per_m2_kwh,
|
||||
)
|
||||
|
||||
|
||||
def solar_gains_section_from_cert(epc: EpcPropertyData) -> SolarGainsResult:
|
||||
"""SAP 10.2 §6 cert→inputs cascade for `solar_gains_from_cert`.
|
||||
|
||||
|
|
|
|||
|
|
@ -18,10 +18,12 @@ import pytest
|
|||
|
||||
from domain.sap.rdsap.cert_to_inputs import (
|
||||
cert_to_inputs,
|
||||
fabric_energy_efficiency_from_cert,
|
||||
heat_transmission_section_from_cert,
|
||||
internal_gains_section_from_cert,
|
||||
mean_internal_temperature_section_from_cert,
|
||||
solar_gains_section_from_cert,
|
||||
space_cooling_section_from_cert,
|
||||
space_heating_section_from_cert,
|
||||
ventilation_from_cert,
|
||||
water_heating_section_from_cert,
|
||||
|
|
@ -593,3 +595,107 @@ def test_section_8_scalar_line_refs_match_pdf(
|
|||
|
||||
# Assert
|
||||
_pin(actual, expected, f"§8 {fixture_attr} {fixture_name}")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# §8c Space cooling — LINE_100..LINE_108
|
||||
# ============================================================================
|
||||
# All 6 Elmhurst fixtures have `has_fixed_air_conditioning=False` so f_C=0
|
||||
# and (107)/(108) collapse to zero. (100), (102), (104) depend on H × (24 −
|
||||
# T_e) per fixture, so we don't assert those here (covered by
|
||||
# `test_space_cooling.py` synthetic-positive case); cascade pins target the
|
||||
# spec-collapsed lines: (101) η_loss=1, (103) gains=0, (106) intermittency
|
||||
# mask, (107) cooling kWh=0, (107) annual=0, (108) per-m²=0.
|
||||
|
||||
_SECTION_8C_MONTHLY_PINS: Final[tuple[tuple[str, str], ...]] = (
|
||||
("LINE_101_M_UTILISATION_FACTOR_LOSS", "utilisation_factor_loss_monthly"),
|
||||
("LINE_103_M_COOLING_GAINS_W", "cooling_gains_monthly_w"),
|
||||
("LINE_106_M_INTERMITTENCY_FACTOR", "intermittency_factor_monthly"),
|
||||
("LINE_107_M_SPACE_COOLING_KWH", "space_cooling_monthly_kwh"),
|
||||
)
|
||||
|
||||
_SECTION_8C_SCALAR_PINS: Final[tuple[tuple[str, str], ...]] = (
|
||||
("SECTION_8C_COOLED_AREA_FRACTION", "cooled_area_fraction"),
|
||||
("LINE_107_ANNUAL_KWH", "space_cooling_kwh_per_yr"),
|
||||
("LINE_108_PER_M2_KWH", "space_cooling_per_m2_kwh"),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"fixture_name,fixture_attr,result_attr",
|
||||
[
|
||||
(fix, line, attr)
|
||||
for fix in _FIXTURES
|
||||
for line, attr in _SECTION_8C_MONTHLY_PINS
|
||||
],
|
||||
ids=lambda x: x if isinstance(x, str) else None,
|
||||
)
|
||||
def test_section_8c_monthly_line_refs_match_pdf(
|
||||
fixture_name: str, fixture_attr: str, result_attr: str
|
||||
) -> None:
|
||||
"""§8c monthly pins — every Jan..Dec value of (101)/(103)/(106)/(107)
|
||||
space-cooling lines matches the U985 PDF to abs=1e-4."""
|
||||
# Arrange
|
||||
mod = _FIXTURES[fixture_name]
|
||||
epc = mod.build_epc() # type: ignore[attr-defined]
|
||||
expected = getattr(mod, fixture_attr)
|
||||
|
||||
# Act
|
||||
sc = space_cooling_section_from_cert(epc)
|
||||
assert sc is not None, f"{fixture_name}: space_cooling_from_cert returned None"
|
||||
actual = getattr(sc, result_attr)
|
||||
|
||||
# Assert
|
||||
for m in range(12):
|
||||
_pin(actual[m], expected[m], f"§8c {fixture_attr}[{m+1}] {fixture_name}")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"fixture_name,fixture_attr,result_attr",
|
||||
[
|
||||
(fix, line, attr)
|
||||
for fix in _FIXTURES
|
||||
for line, attr in _SECTION_8C_SCALAR_PINS
|
||||
],
|
||||
ids=lambda x: x if isinstance(x, str) else None,
|
||||
)
|
||||
def test_section_8c_scalar_line_refs_match_pdf(
|
||||
fixture_name: str, fixture_attr: str, result_attr: str
|
||||
) -> None:
|
||||
"""§8c scalar pins — (105) cooled-area fraction + (107) annual Σ +
|
||||
(108) per-m² total match the U985 PDF to abs=1e-4."""
|
||||
# Arrange
|
||||
mod = _FIXTURES[fixture_name]
|
||||
epc = mod.build_epc() # type: ignore[attr-defined]
|
||||
expected = getattr(mod, fixture_attr)
|
||||
|
||||
# Act
|
||||
sc = space_cooling_section_from_cert(epc)
|
||||
assert sc is not None, f"{fixture_name}: space_cooling_from_cert returned None"
|
||||
actual = getattr(sc, result_attr)
|
||||
|
||||
# Assert
|
||||
_pin(actual, expected, f"§8c {fixture_attr} {fixture_name}")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# §8f Fabric Energy Efficiency — LINE_109
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@pytest.mark.parametrize("fixture_name", list(_FIXTURES), ids=lambda x: x)
|
||||
def test_section_8f_line_109_fee_matches_pdf(fixture_name: str) -> None:
|
||||
"""§8f scalar pin — (109) FEE = (98a)/(4) + (108) matches the U985 PDF
|
||||
to abs=1e-4. For all 6 fixtures (98b) solar space heating = 0 and (108)
|
||||
= 0, so (109) = (99)."""
|
||||
# Arrange
|
||||
mod = _FIXTURES[fixture_name]
|
||||
epc = mod.build_epc() # type: ignore[attr-defined]
|
||||
expected = mod.LINE_109_FEE_KWH_PER_M2 # type: ignore[attr-defined]
|
||||
|
||||
# Act
|
||||
actual = fabric_energy_efficiency_from_cert(epc)
|
||||
|
||||
# Assert
|
||||
assert actual is not None, f"{fixture_name}: fee_from_cert returned None"
|
||||
_pin(actual, expected, f"§8f LINE_109_FEE_KWH_PER_M2 {fixture_name}")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue