mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
feat(conservatory): §6.1 solar gains + TFA-occupancy (demand-side)
Close the §6.1 conservatory demand cascade per RdSAP 10 §6.1 + Table 25. Solar gains (§6, solar_gains.py) — Table 25 note (PDF p.51): "The orientation of windows in a conservatory is not recorded, thus solar gains are calculated using the default solar flux (East/West orientation, with 20° pitch for roof windows)." The glazed wall bills onto the (76) East line (vertical, average-overshading Z); the glazed roof onto the (82) roof-window line (20° pitch, Z=1.0), both at Table 25 g=0.76, FF=0.70. TFA-occupancy (mapper) — §6.1: the conservatory floor area is added to the dwelling total floor area. TFA drives occupancy → §5 internal gains + §4 hot-water demand, so the non-separated conservatory's floor area now enters `EpcPropertyData.total_floor_area_m2` (the worksheet's (4) = 95.38 carries it). Separated conservatories (§6.2) stay excluded. Pinned against the case-44 P960 demand cascade at abs=1e-4: (73) internal gains 625.1759, (83) solar gains 495.8655, (95) useful gains 1079.6510, (99) space heating per m² 89.8073 — the full §6.1 chain reproduces EXACTLY. The whole-dwelling SAP (72.9517) / CO2 (3241.8656) are not pinned: the case-44 Summary omits the House-Coal secondary heater (SAP 633) the P960 descriptor carries (cf. case 43), so the cascade computes no secondary — the entire residual (+349.77 kg CO2). A Summary-input defect, independent of §6.1; every conservatory-affected line ref is exact. Worksheet harness stays 47/47 0-raised; corpus unchanged (API path; mirror is the next slice). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
fe3bf4eaed
commit
2a4d67e396
4 changed files with 127 additions and 0 deletions
|
|
@ -416,6 +416,18 @@ class EpcPropertyDataMapper:
|
|||
ext.room_in_roof.floor_area_m2
|
||||
for ext in survey.extensions
|
||||
if ext.room_in_roof is not None
|
||||
)
|
||||
# RdSAP 10 §6.1 (PDF p.49) — a non-separated conservatory's
|
||||
# floor area is added to the dwelling total floor area. TFA
|
||||
# drives occupancy → §5 internal gains + §4 hot-water demand,
|
||||
# so it must include the conservatory (the worksheet's (4) =
|
||||
# 95.38 carries it). Separated conservatories (§6.2) are
|
||||
# disregarded.
|
||||
+ (
|
||||
survey.conservatory.floor_area_m2
|
||||
if survey.conservatory is not None
|
||||
and not survey.conservatory.thermally_separated
|
||||
else 0.0
|
||||
),
|
||||
2,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -36,6 +36,10 @@ from math import cos, radians, sin
|
|||
from typing import Final
|
||||
|
||||
from datatypes.epc.domain.epc_property_data import EpcPropertyData, SapWindow
|
||||
from domain.sap10_calculator.worksheet.conservatory import (
|
||||
CONSERVATORY_ROOF_PITCH_DEG,
|
||||
conservatory_geometry,
|
||||
)
|
||||
from domain.sap10_calculator.tables.pcdb.postcode_weather import PostcodeClimate
|
||||
from domain.sap10_calculator.climate.appendix_u import (
|
||||
horizontal_solar_irradiance_w_per_m2,
|
||||
|
|
@ -435,6 +439,48 @@ def solar_gains_from_cert(
|
|||
o: _sum_tuples(*per_orientation[o]) for o in Orientation
|
||||
}
|
||||
|
||||
# RdSAP 10 §6.1 (PDF p.49) + Table 25 note (p.51): "The orientation of
|
||||
# windows in a conservatory is not recorded, thus solar gains are
|
||||
# calculated using the default solar flux (East/West orientation, with
|
||||
# 20° pitch for roof windows)." Average overshading per §7 (Table 6d).
|
||||
# The glazed wall bills onto the (76) East line (vertical, Z=z_vertical);
|
||||
# the glazed roof onto the (82) roof-window line (20° pitch, Z=1.0).
|
||||
cons = conservatory_geometry(epc)
|
||||
if cons is not None:
|
||||
cons_wall_monthly = tuple(
|
||||
window_solar_gain_w(
|
||||
area_m2=cons.glazed_wall_area_m2,
|
||||
surface_flux_w_per_m2=surface_solar_flux_w_per_m2(
|
||||
orientation=Orientation.E, pitch_deg=90.0,
|
||||
region=region, month=m,
|
||||
),
|
||||
g_perpendicular=cons.g_value,
|
||||
frame_factor=cons.frame_factor,
|
||||
overshading_factor=z_vertical,
|
||||
)
|
||||
for m in _MONTHS
|
||||
)
|
||||
cons_roof_monthly = tuple(
|
||||
window_solar_gain_w(
|
||||
area_m2=cons.glazed_roof_area_m2,
|
||||
surface_flux_w_per_m2=surface_solar_flux_w_per_m2(
|
||||
orientation=Orientation.E,
|
||||
pitch_deg=CONSERVATORY_ROOF_PITCH_DEG,
|
||||
region=region, month=m,
|
||||
),
|
||||
g_perpendicular=cons.g_value,
|
||||
frame_factor=cons.frame_factor,
|
||||
overshading_factor=_HORIZONTAL_Z,
|
||||
)
|
||||
for m in _MONTHS
|
||||
)
|
||||
per_orientation_summed[Orientation.E] = _sum_tuples(
|
||||
per_orientation_summed[Orientation.E], cons_wall_monthly,
|
||||
)
|
||||
roof_windows_monthly = _sum_tuples(
|
||||
roof_windows_monthly, cons_roof_monthly,
|
||||
)
|
||||
|
||||
total = _sum_tuples(
|
||||
*per_orientation_summed.values(),
|
||||
roof_windows_monthly,
|
||||
|
|
|
|||
|
|
@ -75,6 +75,31 @@ LINE_31_EXTERNAL_AREA_M2: Final[float] = 294.2900
|
|||
LINE_33_FABRIC_W_PER_K: Final[float] = 207.3274
|
||||
LINE_36_THERMAL_BRIDGING_W_PER_K: Final[float] = 23.5432
|
||||
|
||||
# Demand-side line refs (Jan column, UK-average rating block). These
|
||||
# integrate the WHOLE §6.1 conservatory chain end-to-end:
|
||||
# - (73) internal gains — the conservatory floor area enters TFA (4),
|
||||
# which drives occupancy → §5 appliance/cooking/metabolic gains;
|
||||
# - (83) solar gains — the glazed wall (E/W flux, 90° pitch) + glazed
|
||||
# roof (E/W flux, 20° pitch) at Table 25 g=0.76, FF=0.70;
|
||||
# - (95) useful gains = (84) total gains × the §7 utilisation factor —
|
||||
# matches only when fabric (33), ventilation (38) AND gains (84) all
|
||||
# agree, so it is the single tightest end-to-end conservatory pin;
|
||||
# - (99) space heating per m² = (98c)/(4) — the integrated demand.
|
||||
LINE_73_INTERNAL_GAINS_JAN_W: Final[float] = 625.1759
|
||||
LINE_83_SOLAR_GAINS_JAN_W: Final[float] = 495.8655
|
||||
LINE_95_USEFUL_GAINS_JAN_W: Final[float] = 1079.6510
|
||||
LINE_99_SPACE_HEATING_PER_M2_KWH: Final[float] = 89.8073
|
||||
|
||||
# NB — the full SAP value (72.9517) + (272) CO2 (3241.8656) are NOT pinned
|
||||
# here. The case-44 Summary PDF omits the House-Coal secondary heater
|
||||
# (SAP 633, 60% eff) that the P960 worksheet's descriptor block carries
|
||||
# (the same secondary as case 43); routed through the extractor the
|
||||
# Summary therefore yields NO secondary system, and the residual SAP/CO2
|
||||
# gap is exactly that missing secondary (main+secondary CO2 1927.31 +
|
||||
# 563.92 = 2491.23 vs cascade 2141.46 → +349.77 ≈ the 350 kg deficit).
|
||||
# This is a Summary-input defect, independent of §6.1 — every
|
||||
# conservatory-affected line ref above reproduces the P960 EXACTLY.
|
||||
|
||||
|
||||
def _summary_pdf_to_textract_style_pages(pdf_path: Path) -> list[str]:
|
||||
"""Convert a Summary PDF into the per-page text format the
|
||||
|
|
|
|||
|
|
@ -427,6 +427,50 @@ def test_case44_non_separated_conservatory_fabric_matches_pdf() -> None:
|
|||
)
|
||||
|
||||
|
||||
def test_case44_conservatory_demand_side_matches_pdf() -> None:
|
||||
"""End-to-end §6.1 conservatory demand pin for simulated case 44.
|
||||
Beyond the §3 fabric, the conservatory ripples through the demand
|
||||
cascade: its floor area enters TFA (4) → occupancy → §5 internal
|
||||
gains (73); its glazing contributes §6 solar gains (83) at the
|
||||
default E/W flux (Table 25 g=0.76, FF=0.70, 20° roof pitch); fabric
|
||||
+ ventilation + gains combine into the §7 useful gains (95) and the
|
||||
space-heating demand (99). Every line ref reproduces the P960 to 1e-4.
|
||||
|
||||
The full SAP/CO2 is NOT asserted: the case-44 Summary omits the
|
||||
House-Coal secondary heater the P960 carries (see the provider's NB) —
|
||||
a Summary-input defect downstream of, and independent of, §6.1."""
|
||||
# Arrange
|
||||
epc = _w001431_case44.build_epc()
|
||||
|
||||
# Act
|
||||
ig = internal_gains_section_from_cert(epc)
|
||||
sg = solar_gains_section_from_cert(epc)
|
||||
sh = space_heating_section_from_cert(epc)
|
||||
assert ig is not None # TFA present ⇒ §5 helper returns a result
|
||||
|
||||
# Assert — §5/§6/§7 demand line refs, each at abs=1e-4.
|
||||
_pin(
|
||||
ig.total_internal_gains_monthly_w[0],
|
||||
_w001431_case44.LINE_73_INTERNAL_GAINS_JAN_W,
|
||||
"§5 (73) case44",
|
||||
)
|
||||
_pin(
|
||||
sg.total_solar_gains_monthly_w[0],
|
||||
_w001431_case44.LINE_83_SOLAR_GAINS_JAN_W,
|
||||
"§6 (83) case44",
|
||||
)
|
||||
_pin(
|
||||
sh.useful_gains_monthly_w[0],
|
||||
_w001431_case44.LINE_95_USEFUL_GAINS_JAN_W,
|
||||
"§7 (95) case44",
|
||||
)
|
||||
_pin(
|
||||
sh.space_heating_per_m2_kwh,
|
||||
_w001431_case44.LINE_99_SPACE_HEATING_PER_M2_KWH,
|
||||
"§7 (99) case44",
|
||||
)
|
||||
|
||||
|
||||
def test_case44_blower_door_pressure_test_matches_pdf() -> None:
|
||||
"""Simulated case 44 lodges a Blower Door air-pressure test
|
||||
(§12.2 "Pressure Test Result (AP50) 4.50"). SAP 10.2 §2 (17)-(18):
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue