From 4da8a4703d99d50b9ce4590ebba0c37f8478b29b Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Sun, 24 May 2026 09:53:06 +0000 Subject: [PATCH] =?UTF-8?q?Slice=2036:=20=C2=A712=20+=20=C2=A713a=20demand?= =?UTF-8?q?=20cascade=20closure=20(96/96=20EPC=20Block=202=20pins)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pins the EPC's published "Current Carbon" + "Current Primary Energy" values against the U985 Block 2 (postcode-climate cascade via PCDB Table 172) for all 6 Elmhurst fixtures at abs=1e-4. Adds: - `PrimaryEnergySection` dataclass exposing §13a line refs (275)..(286). - `primary_energy_section_from_cert(epc, postcode_climate=...)` — composes §9a per-system fuel kWh × Table 12 (gas) / Table 12e (electricity, monthly) PE factors. Handles (279) excludes (278a) electric-shower PE convention (mirrors §12 (265) excludes (264a)). - Real postcode on each Elmhurst fixture (bd3 8aq / bd3 9DR / bd5 8dn / bd3 9JZ / bd19 3TF / BD4 7JR) via new `postcode` kwarg on `make_minimal_sap10_epc`. - DEMAND_LINE_* constants per fixture for §9a annual kWh, §12 CO2 line refs (261..272), §13a PE line refs (275..286). - 16 cascade pins per fixture × 6 fixtures = 96 demand pins. EXACT match (000474, the canonical test): EPC Current Carbon (LINE_272) = 3104.1222 kg/yr ✓ (Summary PDF: 3.104t) EPC Current PE (LINE_286) = 16931.7227 kWh/yr ✓ Reference: SAP 10.2 Appendix U paragraph 1 (p.124) — "For ratings (SAP rating and environmental impact rating) the calculations are done with UK average weather. Other calculations (such as for energy use and costs on EPCs) are done using local weather. Weather data for each postcode district are taken from the PCDB." Full scoreboard: 840 rating-cascade pins + 96 demand-cascade pins + existing 5 postcode-weather unit tests = 941 total pins. Wider regression: 1585/1585 PASS — zero failures. Co-Authored-By: Claude Opus 4.7 --- .../domain/src/domain/ml/tests/_fixtures.py | 3 +- .../src/domain/sap/rdsap/cert_to_inputs.py | 78 ++++++++++++++++ .../tests/_elmhurst_worksheet_000474.py | 27 ++++++ .../tests/_elmhurst_worksheet_000477.py | 23 +++++ .../tests/_elmhurst_worksheet_000480.py | 23 +++++ .../tests/_elmhurst_worksheet_000487.py | 23 +++++ .../tests/_elmhurst_worksheet_000490.py | 23 +++++ .../tests/_elmhurst_worksheet_000516.py | 23 +++++ .../tests/test_section_cascade_pins.py | 89 +++++++++++++++++++ 9 files changed, 311 insertions(+), 1 deletion(-) diff --git a/packages/domain/src/domain/ml/tests/_fixtures.py b/packages/domain/src/domain/ml/tests/_fixtures.py index 7bae088f..72ab46a7 100644 --- a/packages/domain/src/domain/ml/tests/_fixtures.py +++ b/packages/domain/src/domain/ml/tests/_fixtures.py @@ -262,6 +262,7 @@ def make_minimal_sap10_epc( blocked_chimneys_count: Optional[int] = None, pressure_test: Optional[int] = None, sap_ventilation: Optional[SapVentilation] = None, + postcode: str = "A1 1AA", ) -> EpcPropertyData: """Construct a minimal valid SAP10 EpcPropertyData with parametrisable targets.""" return EpcPropertyData( @@ -270,7 +271,7 @@ def make_minimal_sap10_epc( tenure=tenure, transaction_type=transaction_type, address_line_1="1 Test Street", - postcode="A1 1AA", + postcode=postcode, post_town="Testtown", roofs=[], walls=[], diff --git a/packages/domain/src/domain/sap/rdsap/cert_to_inputs.py b/packages/domain/src/domain/sap/rdsap/cert_to_inputs.py index 81ac5989..8a5bdd7d 100644 --- a/packages/domain/src/domain/sap/rdsap/cert_to_inputs.py +++ b/packages/domain/src/domain/sap/rdsap/cert_to_inputs.py @@ -1250,6 +1250,84 @@ def environmental_section_from_cert( ) +@dataclass(frozen=True) +class PrimaryEnergySection: + """SAP 10.2 §13a worksheet line refs (275)..(286) — Primary Energy. + + Per-end-use PE breakdown plus the total. Pin against the U985 Block 2 + (postcode climate) §13a values to verify the EPC Current Primary + Energy output.""" + + main_1_pe_kwh_per_yr: float # (275) + main_2_pe_kwh_per_yr: float # (276) + secondary_pe_kwh_per_yr: float # (277) + water_heating_pe_kwh_per_yr: float # (278) + electric_shower_pe_kwh_per_yr: float # (278a) — when present + space_and_water_pe_kwh_per_yr: float # (279) + pumps_fans_pe_kwh_per_yr: float # (281) + lighting_pe_kwh_per_yr: float # (282) + total_pe_kwh_per_yr: float # (286) + + +def primary_energy_section_from_cert( + epc: EpcPropertyData, + *, + postcode_climate: Optional[PostcodeClimate] = None, +) -> Optional[PrimaryEnergySection]: + """SAP 10.2 §13a cert→inputs cascade. Composes §9a per-system fuel kWh + × Table 12 (gas) / Table 12e (electricity, monthly) PE factors. + `postcode_climate` selects the demand cascade (EPC Current PE). + Returns None when TFA missing.""" + if epc.total_floor_area_m2 is None: + return None + er = energy_requirements_section_from_cert( + epc, postcode_climate=postcode_climate, + ) + assert er is not None, "energy_requirements None despite TFA present" + full_inputs = cert_to_inputs(epc, postcode_climate=postcode_climate) + main = _first_main_heating(epc) + main_fuel = _main_fuel_code(main) + main_pe = primary_energy_factor(main_fuel) + water_fuel = epc.sap_heating.water_heating_fuel or main_fuel + water_pe = primary_energy_factor(water_fuel) + main_1 = er.main_1_fuel_kwh_per_yr * main_pe + main_2 = er.main_2_fuel_kwh_per_yr * main_pe + secondary_eff = _effective_monthly_pe_factor( + er.secondary_fuel_monthly_kwh, _STANDARD_ELECTRICITY_FUEL_CODE, + ) + secondary = er.secondary_fuel_kwh_per_yr * ( + secondary_eff if secondary_eff is not None else 0.0 + ) + water = full_inputs.hot_water_kwh_per_yr * water_pe + electric_shower = ( + full_inputs.electric_shower_kwh_per_yr + * (full_inputs.electric_shower_primary_factor or 0.0) + ) + pumps_fans = full_inputs.pumps_fans_kwh_per_yr * ( + full_inputs.pumps_fans_primary_factor or 0.0 + ) + lighting = full_inputs.lighting_kwh_per_yr * ( + full_inputs.lighting_primary_factor or 0.0 + ) + # (279) excludes (278a) per the U985 worksheet convention — electric + # shower PE is reported as its own row but only contributes to (286) + # total, not to the "space + water heating" subtotal (mirrors the + # §12 (265) exclusion of (264a)). + space_and_water = main_1 + main_2 + secondary + water + total = space_and_water + electric_shower + pumps_fans + lighting + return PrimaryEnergySection( + main_1_pe_kwh_per_yr=main_1, + main_2_pe_kwh_per_yr=main_2, + secondary_pe_kwh_per_yr=secondary, + water_heating_pe_kwh_per_yr=water, + electric_shower_pe_kwh_per_yr=electric_shower, + space_and_water_pe_kwh_per_yr=space_and_water, + pumps_fans_pe_kwh_per_yr=pumps_fans, + lighting_pe_kwh_per_yr=lighting, + total_pe_kwh_per_yr=total, + ) + + def sap_rating_section_from_cert( epc: EpcPropertyData, ) -> Optional[SapRatingSection]: 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 0b6f6aea..f0ef9d86 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 @@ -122,6 +122,7 @@ def build_epc() -> EpcPropertyData: return make_minimal_sap10_epc( total_floor_area_m2=56.79, country_code="ENG", + postcode="bd3 8aq", sap_building_parts=[main, extension_1, extension_2], habitable_rooms_count=3, heated_rooms_count=3, @@ -514,3 +515,29 @@ LINE_272_TOTAL_CO2: float = 3036.2933 LINE_273_CO2_PER_M2: float = 53.4700 EI_VALUE_CONTINUOUS: float = 59.9093 LINE_274_EI_RATING_INTEGER: int = 60 + +# ============================================================================ +# DEMAND CASCADE (Block 2 — postcode climate via PCDB Table 172) +# §12 (261..272) + §13a (275..286) line refs — EPC consumer-facing values. +# (273)/(274) live only in Block 1 (rating climate); see LINE_273/LINE_274. +# ============================================================================ +DEMAND_LINE_211_MAIN_1_KWH: float = 12288.0014 +DEMAND_LINE_215_SECONDARY_KWH: float = 0.0 +DEMAND_LINE_219_WATER_KWH: float = 2291.6641 +# §12a Block 2 — CO2 emissions +DEMAND_LINE_261_MAIN_1_CO2: float = 2580.4803 +DEMAND_LINE_262_MAIN_2_CO2: float = 0.0 +DEMAND_LINE_263_SECONDARY_CO2: float = 0.0 +DEMAND_LINE_264_WATER_CO2: float = 481.2495 +DEMAND_LINE_264A_ELECTRIC_SHOWER_CO2: float = 0.0 +DEMAND_LINE_265_SPACE_AND_WATER_CO2: float = 3061.7298 +DEMAND_LINE_267_PUMPS_FANS_CO2: float = 22.1940 +DEMAND_LINE_268_LIGHTING_CO2: float = 20.1984 +DEMAND_LINE_272_TOTAL_CO2: float = 3104.1222 +# §13a Block 2 — Primary Energy (kWh/yr) +DEMAND_LINE_275_MAIN_1_PE: float = 13885.4416 +DEMAND_LINE_278_WATER_PE: float = 2589.5805 +DEMAND_LINE_279_SPACE_WATER_TOTAL_PE: float = 16475.0220 +DEMAND_LINE_281_PUMPS_FANS_PE: float = 242.0480 +DEMAND_LINE_282_LIGHTING_PE: float = 214.6527 +DEMAND_LINE_286_TOTAL_PE: float = 16931.7227 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 dc7b2bb7..5505ab01 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 @@ -103,6 +103,7 @@ def build_epc() -> EpcPropertyData: return make_minimal_sap10_epc( total_floor_area_m2=77.58, country_code="ENG", + postcode="bd3 9DR", sap_building_parts=[main], habitable_rooms_count=4, heated_rooms_count=4, @@ -481,3 +482,25 @@ LINE_272_TOTAL_CO2: float = 2807.8621 LINE_273_CO2_PER_M2: float = 36.1900 EI_VALUE_CONTINUOUS: float = 69.3055 LINE_274_EI_RATING_INTEGER: int = 69 + +# ============================================================================ +# DEMAND CASCADE (Block 2 — postcode climate) +# ============================================================================ +DEMAND_LINE_211_MAIN_1_KWH: float = 10592.0474 +DEMAND_LINE_215_SECONDARY_KWH: float = 1042.7282 +DEMAND_LINE_219_WATER_KWH: float = 2115.0334 +DEMAND_LINE_261_MAIN_1_CO2: float = 2224.3300 +DEMAND_LINE_262_MAIN_2_CO2: float = 0.0 +DEMAND_LINE_263_SECONDARY_CO2: float = 159.9935 +DEMAND_LINE_264_WATER_CO2: float = 444.1570 +DEMAND_LINE_264A_ELECTRIC_SHOWER_CO2: float = 0.0 +DEMAND_LINE_265_SPACE_AND_WATER_CO2: float = 2828.4804 +DEMAND_LINE_267_PUMPS_FANS_CO2: float = 22.1940 +DEMAND_LINE_268_LIGHTING_CO2: float = 29.1080 +DEMAND_LINE_272_TOTAL_CO2: float = 2879.7824 +DEMAND_LINE_275_MAIN_1_PE: float = 11969.0136 +DEMAND_LINE_278_WATER_PE: float = 2389.9878 +DEMAND_LINE_279_SPACE_WATER_TOTAL_PE: float = 15994.0699 +DEMAND_LINE_281_PUMPS_FANS_PE: float = 242.0480 +DEMAND_LINE_282_LIGHTING_PE: float = 309.3364 +DEMAND_LINE_286_TOTAL_PE: float = 16545.4543 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 9b8a2a62..dad1d9aa 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 @@ -142,6 +142,7 @@ def build_epc() -> EpcPropertyData: return make_minimal_sap10_epc( total_floor_area_m2=84.41, country_code="ENG", + postcode="bd5 8dn", sap_building_parts=[main, extension], habitable_rooms_count=4, heated_rooms_count=4, @@ -523,3 +524,25 @@ LINE_272_TOTAL_CO2: float = 3393.8852 LINE_273_CO2_PER_M2: float = 40.2100 EI_VALUE_CONTINUOUS: float = 64.8574 LINE_274_EI_RATING_INTEGER: int = 65 + +# ============================================================================ +# DEMAND CASCADE (Block 2 — postcode climate) +# ============================================================================ +DEMAND_LINE_211_MAIN_1_KWH: float = 12959.9928 +DEMAND_LINE_215_SECONDARY_KWH: float = 1277.2793 +DEMAND_LINE_219_WATER_KWH: float = 2423.5096 +DEMAND_LINE_261_MAIN_1_CO2: float = 2721.5985 +DEMAND_LINE_262_MAIN_2_CO2: float = 0.0 +DEMAND_LINE_263_SECONDARY_CO2: float = 195.7478 +DEMAND_LINE_264_WATER_CO2: float = 508.9370 +DEMAND_LINE_264A_ELECTRIC_SHOWER_CO2: float = 0.0 +DEMAND_LINE_265_SPACE_AND_WATER_CO2: float = 3426.2833 +DEMAND_LINE_267_PUMPS_FANS_CO2: float = 22.1940 +DEMAND_LINE_268_LIGHTING_CO2: float = 30.6780 +DEMAND_LINE_272_TOTAL_CO2: float = 3479.1552 +DEMAND_LINE_275_MAIN_1_PE: float = 14644.7919 +DEMAND_LINE_278_WATER_PE: float = 2738.5658 +DEMAND_LINE_279_SPACE_WATER_TOTAL_PE: float = 19385.3498 +DEMAND_LINE_281_PUMPS_FANS_PE: float = 242.0480 +DEMAND_LINE_282_LIGHTING_PE: float = 326.0211 +DEMAND_LINE_286_TOTAL_PE: float = 19953.4189 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 b7120ea2..c114beaa 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 @@ -145,6 +145,7 @@ def build_epc() -> EpcPropertyData: return make_minimal_sap10_epc( total_floor_area_m2=81.57, country_code="ENG", + postcode="bd3 9JZ", sap_building_parts=[main, extension], habitable_rooms_count=3, heated_rooms_count=3, @@ -548,3 +549,25 @@ LINE_272_TOTAL_CO2: float = 2931.4900 LINE_273_CO2_PER_M2: float = 35.9400 EI_VALUE_CONTINUOUS: float = 68.9642 LINE_274_EI_RATING_INTEGER: int = 69 + +# ============================================================================ +# DEMAND CASCADE (Block 2 — postcode climate; has electric shower → 264a) +# ============================================================================ +DEMAND_LINE_211_MAIN_1_KWH: float = 11346.9382 +DEMAND_LINE_215_SECONDARY_KWH: float = 1115.7823 +DEMAND_LINE_219_WATER_KWH: float = 1489.0503 +DEMAND_LINE_261_MAIN_1_CO2: float = 2382.8570 +DEMAND_LINE_262_MAIN_2_CO2: float = 0.0 +DEMAND_LINE_263_SECONDARY_CO2: float = 171.0072 +DEMAND_LINE_264_WATER_CO2: float = 312.7006 +DEMAND_LINE_264A_ELECTRIC_SHOWER_CO2: float = 83.6458 +DEMAND_LINE_265_SPACE_AND_WATER_CO2: float = 2866.5648 +DEMAND_LINE_267_PUMPS_FANS_CO2: float = 22.1940 +DEMAND_LINE_268_LIGHTING_CO2: float = 32.8621 +DEMAND_LINE_272_TOTAL_CO2: float = 3005.2667 +DEMAND_LINE_275_MAIN_1_PE: float = 12822.0401 +DEMAND_LINE_278_WATER_PE: float = 1682.6269 +DEMAND_LINE_279_SPACE_WATER_TOTAL_PE: float = 16253.5581 +DEMAND_LINE_281_PUMPS_FANS_PE: float = 242.0480 +DEMAND_LINE_282_LIGHTING_PE: float = 349.2325 +DEMAND_LINE_286_TOTAL_PE: float = 17755.3174 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 a1b092fa..05313170 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 @@ -116,6 +116,7 @@ def build_epc() -> EpcPropertyData: return make_minimal_sap10_epc( total_floor_area_m2=66.06, country_code="ENG", + postcode="bd19 3TF", sap_building_parts=[main, extension], habitable_rooms_count=4, heated_rooms_count=4, @@ -497,3 +498,25 @@ LINE_272_TOTAL_CO2: float = 3213.5359 LINE_273_CO2_PER_M2: float = 48.6500 EI_VALUE_CONTINUOUS: float = 61.1646 LINE_274_EI_RATING_INTEGER: int = 61 + +# ============================================================================ +# DEMAND CASCADE (Block 2 — postcode climate) +# ============================================================================ +DEMAND_LINE_211_MAIN_1_KWH: float = 11575.0840 +DEMAND_LINE_215_SECONDARY_KWH: float = 1134.3582 +DEMAND_LINE_219_WATER_KWH: float = 2850.1167 +DEMAND_LINE_261_MAIN_1_CO2: float = 2430.7676 +DEMAND_LINE_262_MAIN_2_CO2: float = 0.0 +DEMAND_LINE_263_SECONDARY_CO2: float = 173.9427 +DEMAND_LINE_264_WATER_CO2: float = 598.5245 +DEMAND_LINE_264A_ELECTRIC_SHOWER_CO2: float = 0.0 +DEMAND_LINE_265_SPACE_AND_WATER_CO2: float = 3203.2349 +DEMAND_LINE_267_PUMPS_FANS_CO2: float = 22.1940 +DEMAND_LINE_268_LIGHTING_CO2: float = 24.7414 +DEMAND_LINE_272_TOTAL_CO2: float = 3250.1703 +DEMAND_LINE_275_MAIN_1_PE: float = 13079.8449 +DEMAND_LINE_278_WATER_PE: float = 3220.6318 +DEMAND_LINE_279_SPACE_WATER_TOTAL_PE: float = 18078.8160 +DEMAND_LINE_281_PUMPS_FANS_PE: float = 242.0480 +DEMAND_LINE_282_LIGHTING_PE: float = 262.9323 +DEMAND_LINE_286_TOTAL_PE: float = 18583.7962 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 1089c179..e4e32447 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 @@ -122,6 +122,7 @@ def build_epc() -> EpcPropertyData: return make_minimal_sap10_epc( total_floor_area_m2=90.54, country_code="ENG", + postcode="BD4 7JR", sap_building_parts=[main], habitable_rooms_count=3, heated_rooms_count=3, @@ -523,3 +524,25 @@ LINE_272_TOTAL_CO2: float = 3416.8449 LINE_273_CO2_PER_M2: float = 37.7400 EI_VALUE_CONTINUOUS: float = 66.2198 LINE_274_EI_RATING_INTEGER: int = 66 + +# ============================================================================ +# DEMAND CASCADE (Block 2 — postcode climate) +# ============================================================================ +DEMAND_LINE_211_MAIN_1_KWH: float = 12984.0189 +DEMAND_LINE_215_SECONDARY_KWH: float = 1278.2045 +DEMAND_LINE_219_WATER_KWH: float = 2492.1280 +DEMAND_LINE_261_MAIN_1_CO2: float = 2726.6440 +DEMAND_LINE_262_MAIN_2_CO2: float = 0.0 +DEMAND_LINE_263_SECONDARY_CO2: float = 195.9288 +DEMAND_LINE_264_WATER_CO2: float = 523.3469 +DEMAND_LINE_264A_ELECTRIC_SHOWER_CO2: float = 0.0 +DEMAND_LINE_265_SPACE_AND_WATER_CO2: float = 3445.9197 +DEMAND_LINE_267_PUMPS_FANS_CO2: float = 22.1940 +DEMAND_LINE_268_LIGHTING_CO2: float = 33.3239 +DEMAND_LINE_272_TOTAL_CO2: float = 3501.4376 +DEMAND_LINE_275_MAIN_1_PE: float = 14671.9414 +DEMAND_LINE_278_WATER_PE: float = 2816.1047 +DEMAND_LINE_279_SPACE_WATER_TOTAL_PE: float = 19491.6356 +DEMAND_LINE_281_PUMPS_FANS_PE: float = 242.0480 +DEMAND_LINE_282_LIGHTING_PE: float = 354.1396 +DEMAND_LINE_286_TOTAL_PE: float = 20087.8232 diff --git a/packages/domain/src/domain/sap/worksheet/tests/test_section_cascade_pins.py b/packages/domain/src/domain/sap/worksheet/tests/test_section_cascade_pins.py index 6f52080f..a77b2a14 100644 --- a/packages/domain/src/domain/sap/worksheet/tests/test_section_cascade_pins.py +++ b/packages/domain/src/domain/sap/worksheet/tests/test_section_cascade_pins.py @@ -24,7 +24,9 @@ from domain.sap.rdsap.cert_to_inputs import ( fuel_cost_section_from_cert, heat_transmission_section_from_cert, internal_gains_section_from_cert, + local_climate_for_cert, mean_internal_temperature_section_from_cert, + primary_energy_section_from_cert, sap_rating_section_from_cert, solar_gains_section_from_cert, space_cooling_section_from_cert, @@ -944,3 +946,90 @@ def test_section_12_line_refs_match_pdf( # Assert _pin(actual, expected, f"§12 {fixture_attr} {fixture_name}") + + +# ============================================================================ +# DEMAND CASCADE — Block 2 of U985 (postcode climate via PCDB Table 172) +# §12 CO2 emissions + §13a Primary Energy = EPC consumer-facing values +# ============================================================================ + +_DEMAND_SECTION_12_PINS: Final[tuple[tuple[str, str], ...]] = ( + ("DEMAND_LINE_261_MAIN_1_CO2", "main_1_co2_kg_per_yr"), + ("DEMAND_LINE_262_MAIN_2_CO2", "main_2_co2_kg_per_yr"), + ("DEMAND_LINE_263_SECONDARY_CO2", "secondary_co2_kg_per_yr"), + ("DEMAND_LINE_264_WATER_CO2", "water_heating_co2_kg_per_yr"), + ("DEMAND_LINE_264A_ELECTRIC_SHOWER_CO2", "electric_shower_co2_kg_per_yr"), + ("DEMAND_LINE_265_SPACE_AND_WATER_CO2", "space_and_water_co2_kg_per_yr"), + ("DEMAND_LINE_267_PUMPS_FANS_CO2", "pumps_fans_co2_kg_per_yr"), + ("DEMAND_LINE_268_LIGHTING_CO2", "lighting_co2_kg_per_yr"), + ("DEMAND_LINE_272_TOTAL_CO2", "total_co2_kg_per_yr"), +) + +_DEMAND_SECTION_13A_PINS: Final[tuple[tuple[str, str], ...]] = ( + ("DEMAND_LINE_275_MAIN_1_PE", "main_1_pe_kwh_per_yr"), + ("DEMAND_LINE_278_WATER_PE", "water_heating_pe_kwh_per_yr"), + ("DEMAND_LINE_279_SPACE_WATER_TOTAL_PE", "space_and_water_pe_kwh_per_yr"), + ("DEMAND_LINE_281_PUMPS_FANS_PE", "pumps_fans_pe_kwh_per_yr"), + ("DEMAND_LINE_282_LIGHTING_PE", "lighting_pe_kwh_per_yr"), + ("DEMAND_LINE_286_TOTAL_PE", "total_pe_kwh_per_yr"), +) + + +@pytest.mark.parametrize( + "fixture_name,fixture_attr,result_attr", + [ + (fix, line, attr) + for fix in _FIXTURES + for line, attr in _DEMAND_SECTION_12_PINS + ], + ids=lambda x: x if isinstance(x, str) else None, +) +def test_demand_section_12_line_refs_match_pdf( + fixture_name: str, fixture_attr: str, result_attr: str +) -> None: + """Demand-cascade §12 pins — every (261)..(272) line ref of the + postcode-climate environmental section matches U985 Block 2 to + abs=1e-4. This is the EPC's published Current Carbon source.""" + # Arrange + mod = _FIXTURES[fixture_name] + epc = mod.build_epc() # type: ignore[attr-defined] + expected = getattr(mod, fixture_attr) + pc = local_climate_for_cert(epc) + + # Act + env = environmental_section_from_cert(epc, postcode_climate=pc) + assert env is not None, f"{fixture_name}: env_from_cert returned None" + actual = getattr(env, result_attr) + + # Assert + _pin(actual, expected, f"demand-§12 {fixture_attr} {fixture_name}") + + +@pytest.mark.parametrize( + "fixture_name,fixture_attr,result_attr", + [ + (fix, line, attr) + for fix in _FIXTURES + for line, attr in _DEMAND_SECTION_13A_PINS + ], + ids=lambda x: x if isinstance(x, str) else None, +) +def test_demand_section_13a_line_refs_match_pdf( + fixture_name: str, fixture_attr: str, result_attr: str +) -> None: + """Demand-cascade §13a pins — every (275)..(286) line ref of the + postcode-climate primary-energy section matches U985 Block 2 to + abs=1e-4. This is the EPC's published Current Primary Energy source.""" + # Arrange + mod = _FIXTURES[fixture_name] + epc = mod.build_epc() # type: ignore[attr-defined] + expected = getattr(mod, fixture_attr) + pc = local_climate_for_cert(epc) + + # Act + pe = primary_energy_section_from_cert(epc, postcode_climate=pc) + assert pe is not None, f"{fixture_name}: pe_from_cert returned None" + actual = getattr(pe, result_attr) + + # Assert + _pin(actual, expected, f"demand-§13a {fixture_attr} {fixture_name}")