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}")