§6 slice 4: 6 Elmhurst fixtures conform on (83) + (84) to ≤5e-3 W

Adds SECTION_6_VERTICAL_WINDOWS, SECTION_6_ROOF_WINDOWS,
SECTION_6_ROOFLIGHTS, LINE_83_M_TOTAL_SOLAR_W, LINE_84_M_TOTAL_GAINS_W
to each of the 6 _elmhurst_worksheet_*.py fixtures, plus an
ALL_FIXTURES-parametrised end-to-end test in test_solar_gains.py.

144 assertions GREEN (12 months × 2 lines × 6 fixtures) at abs=5e-3 W:
  - (83) total solar gains via solar_gains_from_cert
  - (84) = §5 LINE_73_M_TOTAL_INTERNAL_GAINS_W + (83) — cross-checks
    §5 conformance and §6 orchestrator in one go.

000516 exercises the roof window path (1.18 m² NE at 45° pitch, Z=1.0).
000474/000477/000487 carry mixed glazing types (g⊥=0.72 + g⊥=0.76 within
the same fixture) — verifies _g_perpendicular respects per-window
manufacturer-declared values.

`_build_section_6_epc(fixture)` is local to the test (handover §11):
fixture build_epc()s stay untouched. make_window gains a convenience
`solar_transmittance` shortcut so fixture literals stay readable.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-05-20 20:42:59 +00:00
parent d56fef4d62
commit 377caea20a
8 changed files with 234 additions and 8 deletions

View file

@ -164,8 +164,20 @@ def make_window(
permanent_shutters_present: Union[bool, str] = False,
frame_material: Optional[str] = "PVC",
window_transmission_details: Optional[WindowTransmissionDetails] = None,
solar_transmittance: Optional[float] = None,
u_value: float = 2.8,
) -> SapWindow:
"""Build a SapWindow with SAP10 defaults; override the fields the test cares about."""
"""Build a SapWindow with SAP10 defaults; override the fields the test cares about.
`solar_transmittance` is a shortcut that builds a
`WindowTransmissionDetails(u_value, data_source=1, solar_transmittance)`
when `window_transmission_details` isn't supplied directly — keeps
Elmhurst §6 fixture literals tight.
"""
if window_transmission_details is None and solar_transmittance is not None:
window_transmission_details = WindowTransmissionDetails(
u_value=u_value, data_source=1, solar_transmittance=solar_transmittance,
)
return SapWindow(
frame_material=frame_material,
glazing_gap=glazing_gap,

View file

@ -26,8 +26,10 @@ from datatypes.epc.domain.epc_property_data import (
EpcPropertyData,
SapBuildingPart,
SapFloorDimension,
SapWindow,
)
from domain.ml.tests._fixtures import make_minimal_sap10_epc
from domain.ml.tests._fixtures import make_minimal_sap10_epc, make_window
from domain.sap.worksheet.solar_gains import RoofWindowInput, RooflightInput
from domain.sap.worksheet.ventilation import MechanicalVentilationKind
from domain.sap.worksheet.water_heating import TABLE_J1_TCOLD_FROM_MAINS_C
@ -275,3 +277,29 @@ LINE_73_M_TOTAL_INTERNAL_GAINS_W: tuple[float, ...] = (
446.3699, 444.6484, 431.0234, 405.3696, 380.1328, 350.1810,
336.5702, 339.0214, 352.0014, 382.2118, 410.3693, 434.9896,
)
# ============================================================================
# §6 Solar gains — cert-derived inputs + expected outputs
# ============================================================================
# 5 wall windows across 2 glazing types (Manufacturer-declared g⊥):
# "Double between 2002" g=0.72: E 3.74, SE 0.50, NW 1.98
# "Double with unknown" g=0.76: E 3.74, NW 1.76
# All PVC frame. No roof windows, no rooflights.
SECTION_6_VERTICAL_WINDOWS: tuple[SapWindow, ...] = (
make_window(orientation=3, width=3.74, height=1.0, solar_transmittance=0.72),
make_window(orientation=4, width=0.50, height=1.0, solar_transmittance=0.72),
make_window(orientation=8, width=1.98, height=1.0, solar_transmittance=0.72),
make_window(orientation=3, width=3.74, height=1.0, solar_transmittance=0.76),
make_window(orientation=8, width=1.76, height=1.0, solar_transmittance=0.76),
)
SECTION_6_ROOF_WINDOWS: tuple[RoofWindowInput, ...] = ()
SECTION_6_ROOFLIGHTS: tuple[RooflightInput, ...] = ()
LINE_83_M_TOTAL_SOLAR_W: tuple[float, ...] = (
74.2861, 144.8943, 240.3371, 357.4289, 446.8944, 462.0286,
437.9571, 369.7981, 281.3970, 172.1314, 92.4825, 61.2179,
)
LINE_84_M_TOTAL_GAINS_W: tuple[float, ...] = (
520.6560, 589.5427, 671.3605, 762.7985, 827.0272, 812.2096,
774.5274, 708.8195, 633.3985, 554.3432, 502.8518, 496.2075,
)

View file

@ -23,8 +23,10 @@ from datatypes.epc.domain.epc_property_data import (
SapBuildingPart,
SapFloorDimension,
SapRoomInRoof,
SapWindow,
)
from domain.ml.tests._fixtures import make_minimal_sap10_epc
from domain.ml.tests._fixtures import make_minimal_sap10_epc, make_window
from domain.sap.worksheet.solar_gains import RoofWindowInput, RooflightInput
from domain.sap.worksheet.ventilation import MechanicalVentilationKind
from domain.sap.worksheet.water_heating import TABLE_J1_TCOLD_FROM_MAINS_C
@ -212,3 +214,27 @@ LINE_73_M_TOTAL_INTERNAL_GAINS_W: tuple[float, ...] = (
537.2284, 535.1563, 517.9364, 486.2482, 454.3577, 418.6634,
401.8987, 404.7492, 421.3137, 457.4575, 492.3897, 522.7868,
)
# ============================================================================
# §6 Solar gains — cert-derived inputs + expected outputs
# ============================================================================
# 3 wall windows across 2 glazing types (Manufacturer-declared g⊥):
# "Double between 2002" g=0.72: E 1.28, W 6.76
# "Double with unknown" g=0.76: W 1.17
# All PVC frame. No roof windows, no rooflights.
SECTION_6_VERTICAL_WINDOWS: tuple[SapWindow, ...] = (
make_window(orientation=3, width=1.28, height=1.0, solar_transmittance=0.72),
make_window(orientation=7, width=6.76, height=1.0, solar_transmittance=0.72),
make_window(orientation=7, width=1.17, height=1.0, solar_transmittance=0.76),
)
SECTION_6_ROOF_WINDOWS: tuple[RoofWindowInput, ...] = ()
SECTION_6_ROOFLIGHTS: tuple[RooflightInput, ...] = ()
LINE_83_M_TOTAL_SOLAR_W: tuple[float, ...] = (
63.6246, 124.4632, 204.9732, 298.9411, 366.3636, 375.0383,
357.0518, 306.7022, 238.3923, 147.6861, 79.3324, 52.3218,
)
LINE_84_M_TOTAL_GAINS_W: tuple[float, ...] = (
600.8530, 659.6195, 722.9096, 785.1894, 820.7212, 793.7017,
758.9505, 711.4514, 659.7060, 605.1436, 571.7221, 575.1085,
)

View file

@ -24,8 +24,10 @@ from datatypes.epc.domain.epc_property_data import (
SapBuildingPart,
SapFloorDimension,
SapRoomInRoof,
SapWindow,
)
from domain.ml.tests._fixtures import make_minimal_sap10_epc
from domain.ml.tests._fixtures import make_minimal_sap10_epc, make_window
from domain.sap.worksheet.solar_gains import RoofWindowInput, RooflightInput
from domain.sap.worksheet.ventilation import MechanicalVentilationKind
from domain.sap.worksheet.water_heating import TABLE_J1_TCOLD_FROM_MAINS_C
@ -244,3 +246,24 @@ LINE_73_M_TOTAL_INTERNAL_GAINS_W: tuple[float, ...] = (
574.7298, 572.6278, 554.4769, 521.1014, 487.3662, 450.0743,
432.3496, 435.2074, 452.6494, 490.4110, 527.2724, 559.3723,
)
# ============================================================================
# §6 Solar gains — cert-derived inputs + expected outputs
# ============================================================================
# 2 wall windows, both Manufacturer g⊥=0.76 / PVC: NE 8.74, SW 1.80.
# No roof windows, no rooflights.
SECTION_6_VERTICAL_WINDOWS: tuple[SapWindow, ...] = (
make_window(orientation=2, width=8.74, height=1.0, solar_transmittance=0.76),
make_window(orientation=6, width=1.80, height=1.0, solar_transmittance=0.76),
)
SECTION_6_ROOF_WINDOWS: tuple[RoofWindowInput, ...] = ()
SECTION_6_ROOFLIGHTS: tuple[RooflightInput, ...] = ()
LINE_83_M_TOTAL_SOLAR_W: tuple[float, ...] = (
60.7732, 115.5953, 190.2388, 289.4797, 373.3152, 392.2015,
369.1410, 303.2958, 224.0851, 136.4060, 74.9915, 50.5862,
)
LINE_84_M_TOTAL_GAINS_W: tuple[float, ...] = (
635.5030, 688.2231, 744.7156, 810.5810, 860.6814, 842.2758,
801.4906, 738.5032, 676.7345, 626.8170, 602.2639, 609.9585,
)

View file

@ -22,8 +22,10 @@ from datatypes.epc.domain.epc_property_data import (
SapBuildingPart,
SapFloorDimension,
SapRoomInRoof,
SapWindow,
)
from domain.ml.tests._fixtures import make_minimal_sap10_epc
from domain.ml.tests._fixtures import make_minimal_sap10_epc, make_window
from domain.sap.worksheet.solar_gains import RoofWindowInput, RooflightInput
from domain.sap.worksheet.water_heating import TABLE_J1_TCOLD_FROM_MAINS_C
# RdSAP wall_construction code seen in the cert→worksheet mapping. The
@ -259,3 +261,26 @@ LINE_73_M_TOTAL_INTERNAL_GAINS_W: tuple[float, ...] = (
548.8580, 546.8173, 529.7869, 499.9224, 468.4056, 433.5905,
417.8752, 420.2523, 436.8085, 472.1723, 505.4898, 534.4324,
)
# ============================================================================
# §6 Solar gains — cert-derived inputs + expected outputs
# ============================================================================
# 2 wall windows, both South-facing, 2 glazing types (Manufacturer g⊥):
# "Double with unknown" g=0.76: S 0.77
# "Double between 2002" g=0.72: S 6.69
# All PVC frame. No roof windows, no rooflights.
SECTION_6_VERTICAL_WINDOWS: tuple[SapWindow, ...] = (
make_window(orientation=5, width=0.77, height=1.0, solar_transmittance=0.76),
make_window(orientation=5, width=6.69, height=1.0, solar_transmittance=0.72),
)
SECTION_6_ROOF_WINDOWS: tuple[RoofWindowInput, ...] = ()
SECTION_6_ROOFLIGHTS: tuple[RooflightInput, ...] = ()
LINE_83_M_TOTAL_SOLAR_W: tuple[float, ...] = (
122.5143, 200.6468, 255.5884, 288.8704, 301.0208, 289.6916,
283.0463, 274.8772, 266.9923, 216.4164, 145.2212, 105.8636,
)
LINE_84_M_TOTAL_GAINS_W: tuple[float, ...] = (
671.3723, 747.4641, 785.3754, 788.7928, 769.4264, 723.2821,
700.9215, 695.1295, 703.8008, 688.5887, 650.7110, 640.2960,
)

View file

@ -28,8 +28,10 @@ from datatypes.epc.domain.epc_property_data import (
EpcPropertyData,
SapBuildingPart,
SapFloorDimension,
SapWindow,
)
from domain.ml.tests._fixtures import make_minimal_sap10_epc
from domain.ml.tests._fixtures import make_minimal_sap10_epc, make_window
from domain.sap.worksheet.solar_gains import RoofWindowInput, RooflightInput
from domain.sap.worksheet.ventilation import MechanicalVentilationKind
from domain.sap.worksheet.water_heating import TABLE_J1_TCOLD_FROM_MAINS_C
@ -255,3 +257,25 @@ LINE_73_M_TOTAL_INTERNAL_GAINS_W: tuple[float, ...] = (
505.8067, 503.7906, 488.2293, 459.2325, 430.5641, 397.6412,
382.3822, 385.1801, 400.0449, 433.4120, 465.2242, 492.9448,
)
# ============================================================================
# §6 Solar gains — cert-derived inputs + expected outputs
# ============================================================================
# 3 wall windows: NE 0.81, SE 5.52, NW 2.70 — all DG pre-2002 / PVC / 12 mm
# with manufacturer g⊥=0.76. No roof windows, no rooflights.
SECTION_6_VERTICAL_WINDOWS: tuple[SapWindow, ...] = (
make_window(orientation=2, width=0.81, height=1.0, solar_transmittance=0.76),
make_window(orientation=4, width=5.52, height=1.0, solar_transmittance=0.76),
make_window(orientation=8, width=2.70, height=1.0, solar_transmittance=0.76),
)
SECTION_6_ROOF_WINDOWS: tuple[RoofWindowInput, ...] = ()
SECTION_6_ROOFLIGHTS: tuple[RooflightInput, ...] = ()
LINE_83_M_TOTAL_SOLAR_W: tuple[float, ...] = (
89.4795, 157.2665, 228.0608, 304.1703, 360.4042, 366.4669,
349.7056, 306.4273, 254.2093, 177.2863, 108.0591, 76.0043,
)
LINE_84_M_TOTAL_GAINS_W: tuple[float, ...] = (
595.2863, 661.0571, 716.2901, 763.4028, 790.9684, 764.1081,
732.0878, 691.6074, 654.2542, 610.6983, 573.2833, 568.9492,
)

View file

@ -29,8 +29,10 @@ from datatypes.epc.domain.epc_property_data import (
SapBuildingPart,
SapFloorDimension,
SapRoomInRoof,
SapWindow,
)
from domain.ml.tests._fixtures import make_minimal_sap10_epc
from domain.ml.tests._fixtures import make_minimal_sap10_epc, make_window
from domain.sap.worksheet.solar_gains import Orientation, RoofWindowInput, RooflightInput
from domain.sap.worksheet.ventilation import MechanicalVentilationKind
from domain.sap.worksheet.water_heating import TABLE_J1_TCOLD_FROM_MAINS_C
@ -222,3 +224,30 @@ LINE_73_M_TOTAL_INTERNAL_GAINS_W: tuple[float, ...] = (
600.1430, 597.7026, 578.1360, 542.0517, 505.9694, 466.5482,
447.5794, 450.9272, 469.7096, 509.7947, 549.4610, 583.9178,
)
# ============================================================================
# §6 Solar gains — cert-derived inputs + expected outputs
# ============================================================================
# 2 wall windows (Manufacturer g⊥=0.76 / PVC): NE 3.88, SW 4.43.
# 1 roof window (NE 1.18, Manufacturer g⊥=0.76, pitch 45° per RdSAP10
# Table 24 default, Wood frame FF=0.70). No rooflights.
SECTION_6_VERTICAL_WINDOWS: tuple[SapWindow, ...] = (
make_window(orientation=2, width=3.88, height=1.0, solar_transmittance=0.76),
make_window(orientation=6, width=4.43, height=1.0, solar_transmittance=0.76),
)
SECTION_6_ROOF_WINDOWS: tuple[RoofWindowInput, ...] = (
RoofWindowInput(
area_m2=1.18, orientation=Orientation.NE,
g_perpendicular=0.76, frame_factor=0.70,
),
)
SECTION_6_ROOFLIGHTS: tuple[RooflightInput, ...] = ()
LINE_83_M_TOTAL_SOLAR_W: tuple[float, ...] = (
85.4797, 154.2449, 234.3467, 329.9943, 406.2034, 419.4674,
397.6609, 338.2558, 267.0293, 176.7227, 103.9502, 72.1445,
)
LINE_84_M_TOTAL_GAINS_W: tuple[float, ...] = (
685.6226, 751.9476, 812.4827, 872.0460, 912.1729, 886.0156,
845.2404, 789.1829, 736.7389, 686.5174, 653.4111, 656.0623,
)

View file

@ -9,9 +9,15 @@ Appendix U §U3.2 (pages 127-129), Table U4 (latitudes), Table U5
(k1-k9 constants per orientation).
"""
from types import ModuleType
import pytest
from datatypes.epc.domain.epc_property_data import SapWindow, WindowTransmissionDetails
from datatypes.epc.domain.epc_property_data import (
EpcPropertyData,
SapWindow,
WindowTransmissionDetails,
)
from domain.ml.tests._fixtures import make_minimal_sap10_epc, make_window
from domain.sap.worksheet.internal_gains import OvershadingCategory
from domain.sap.worksheet.solar_gains import (
@ -23,6 +29,7 @@ from domain.sap.worksheet.solar_gains import (
window_solar_gain_w,
z_solar_for_overshading,
)
from domain.sap.worksheet.tests._elmhurst_fixtures import ALL_FIXTURES, fixture_id
# Worksheet U985-0001-000490 reference (UK-avg weather, region 0):
@ -240,3 +247,55 @@ def test_out_of_range_region_raises_value_error() -> None:
surface_solar_flux_w_per_m2(
orientation=Orientation.S, pitch_deg=90.0, region=22, month=7,
)
def _build_section_6_epc(fixture: ModuleType) -> EpcPropertyData:
"""Wrap a fixture's base build_epc() with the §6-relevant fields it
doesn't yet carry: per-orientation vertical wall windows lodged on the
epc. Roof windows + rooflights pass through as orchestrator args
because RdSAP cert summaries don't lodge them distinctly. Kept local
to the §6 test so legacy build_epc()s stay pinned for §1-§4 +
e2e SAP-score regressions (handover §11)."""
base = fixture.build_epc()
base.sap_windows = list(fixture.SECTION_6_VERTICAL_WINDOWS)
return base
@pytest.mark.parametrize("fixture", ALL_FIXTURES, ids=[fixture_id(f) for f in ALL_FIXTURES])
def test_solar_gains_from_cert_matches_elmhurst_worksheet_all_fixtures(
fixture: ModuleType,
) -> None:
"""End-to-end §6 orchestrator against every Elmhurst conformance fixture.
Each fixture pins its own §6 input constants
(SECTION_6_VERTICAL_WINDOWS, SECTION_6_ROOF_WINDOWS,
SECTION_6_ROOFLIGHTS) + worksheet outputs (LINE_83_M, LINE_84_M). The
test composes an EPC from the cert-shape windows and drives
solar_gains_from_cert at region 0 (UK-avg, the SAP rating pass).
(84) reconciles via the §5 LINE_73_M total internal gains plus our
new (83), checking both halves of the worksheet sum.
"""
# Arrange
epc = _build_section_6_epc(fixture)
# Act
result = solar_gains_from_cert(
epc=epc,
region=0,
overshading=OvershadingCategory.AVERAGE,
roof_windows=fixture.SECTION_6_ROOF_WINDOWS,
rooflights=fixture.SECTION_6_ROOFLIGHTS,
)
# Assert
for m in range(12):
assert result.total_solar_gains_monthly_w[m] == pytest.approx(
fixture.LINE_83_M_TOTAL_SOLAR_W[m], abs=5e-3
), f"(83) month {m+1}"
line_84 = (
fixture.LINE_73_M_TOTAL_INTERNAL_GAINS_W[m]
+ result.total_solar_gains_monthly_w[m]
)
assert line_84 == pytest.approx(
fixture.LINE_84_M_TOTAL_GAINS_W[m], abs=5e-3
), f"(84) month {m+1}"