diff --git a/packages/domain/src/domain/ml/tests/_fixtures.py b/packages/domain/src/domain/ml/tests/_fixtures.py index 171af42b..f653fee6 100644 --- a/packages/domain/src/domain/ml/tests/_fixtures.py +++ b/packages/domain/src/domain/ml/tests/_fixtures.py @@ -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, diff --git a/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000474.py b/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000474.py index 3e237527..8e539a81 100644 --- a/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000474.py +++ b/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000474.py @@ -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, +) diff --git a/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000477.py b/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000477.py index a9f030ea..6cbf4021 100644 --- a/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000477.py +++ b/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000477.py @@ -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, +) diff --git a/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000480.py b/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000480.py index dd14e6ae..f25c049e 100644 --- a/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000480.py +++ b/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000480.py @@ -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, +) diff --git a/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000487.py b/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000487.py index deb6a15e..bde9b043 100644 --- a/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000487.py +++ b/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000487.py @@ -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, +) diff --git a/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000490.py b/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000490.py index 721447c0..554caf90 100644 --- a/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000490.py +++ b/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000490.py @@ -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, +) diff --git a/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000516.py b/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000516.py index 05c460a3..9067eb6d 100644 --- a/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000516.py +++ b/packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000516.py @@ -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, +) diff --git a/packages/domain/src/domain/sap/worksheet/tests/test_solar_gains.py b/packages/domain/src/domain/sap/worksheet/tests/test_solar_gains.py index becdaa4f..ad464894 100644 --- a/packages/domain/src/domain/sap/worksheet/tests/test_solar_gains.py +++ b/packages/domain/src/domain/sap/worksheet/tests/test_solar_gains.py @@ -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}"