diff --git a/backend/documents_parser/tests/test_heating_systems_corpus.py b/backend/documents_parser/tests/test_heating_systems_corpus.py index 27bbd496..fe62b1a8 100644 --- a/backend/documents_parser/tests/test_heating_systems_corpus.py +++ b/backend/documents_parser/tests/test_heating_systems_corpus.py @@ -219,21 +219,21 @@ class _CorpusExpectation: # the SH+Sec demand mismatch for electric 3/6/7 (Table 11 fraction # for codes 401/402) remains the open driver of those SAP residuals. _EXPECTATIONS: tuple[_CorpusExpectation, ...] = ( - _CorpusExpectation(variant='ashp', block='11a', expected_sap_resid=+0.2418, expected_cost_resid_gbp=-5.5706, expected_co2_resid_kg=-1.4283, expected_pe_resid_kwh=-11.8017), - _CorpusExpectation(variant='electric 1', block='11a', expected_sap_resid=+0.4522, expected_cost_resid_gbp=-10.4203, expected_co2_resid_kg=-4.3334, expected_pe_resid_kwh=-40.1603), - _CorpusExpectation(variant='electric 2', block='11a', expected_sap_resid=-0.1842, expected_cost_resid_gbp=+4.2439, expected_co2_resid_kg=+38.7768, expected_pe_resid_kwh=+392.8379), - _CorpusExpectation(variant='electric 3', block='11a', expected_sap_resid=+0.6568, expected_cost_resid_gbp=-15.1334, expected_co2_resid_kg=-13.8238, expected_pe_resid_kwh=-114.7533), - _CorpusExpectation(variant='electric 5', block='11a', expected_sap_resid=-0.6813, expected_cost_resid_gbp=+15.6982, expected_co2_resid_kg=+43.9325, expected_pe_resid_kwh=+338.5315), - _CorpusExpectation(variant='electric 6', block='11a', expected_sap_resid=+0.5744, expected_cost_resid_gbp=-13.2352, expected_co2_resid_kg=-10.1354, expected_pe_resid_kwh=-93.1997), - _CorpusExpectation(variant='electric 7', block='11a', expected_sap_resid=+0.5398, expected_cost_resid_gbp=-12.4372, expected_co2_resid_kg=-8.3964, expected_pe_resid_kwh=-83.9576), - _CorpusExpectation(variant='electric 8', block='11a', expected_sap_resid=+0.4874, expected_cost_resid_gbp=-11.2307, expected_co2_resid_kg=-6.4095, expected_pe_resid_kwh=-70.5744), - _CorpusExpectation(variant='electric 9', block='11a', expected_sap_resid=+0.6261, expected_cost_resid_gbp=-14.4253, expected_co2_resid_kg=-12.3507, expected_pe_resid_kwh=-105.2495), - _CorpusExpectation(variant='gshp', block='11a', expected_sap_resid=+1.1491, expected_cost_resid_gbp=-26.4775, expected_co2_resid_kg=-41.4461, expected_pe_resid_kwh=-454.5023), - _CorpusExpectation(variant='oil 1', block='11a', expected_sap_resid=+0.4042, expected_cost_resid_gbp=-9.3142, expected_co2_resid_kg=-36.6371, expected_pe_resid_kwh=-71.2875), - _CorpusExpectation(variant='oil pcdb 1', block='11a', expected_sap_resid=+0.3609, expected_cost_resid_gbp=-8.3159, expected_co2_resid_kg=-34.4292, expected_pe_resid_kwh=-67.1831), - _CorpusExpectation(variant='oil pcdb 2', block='11a', expected_sap_resid=+0.3609, expected_cost_resid_gbp=-8.3159, expected_co2_resid_kg=-34.4292, expected_pe_resid_kwh=-67.1831), - _CorpusExpectation(variant='oil pcdb 3', block='11a', expected_sap_resid=+0.3869, expected_cost_resid_gbp=-8.9139, expected_co2_resid_kg=-34.4447, expected_pe_resid_kwh=-67.2071), - _CorpusExpectation(variant='pcdb 1', block='11a', expected_sap_resid=+0.5018, expected_cost_resid_gbp=-11.0973, expected_co2_resid_kg=-49.6654, expected_pe_resid_kwh=-92.8147), + _CorpusExpectation(variant='ashp', block='11a', expected_sap_resid=+0.1830, expected_cost_resid_gbp=-4.2166, expected_co2_resid_kg=-1.4283, expected_pe_resid_kwh=-11.8017), + _CorpusExpectation(variant='electric 1', block='11a', expected_sap_resid=+0.3849, expected_cost_resid_gbp=-8.8694, expected_co2_resid_kg=-4.3334, expected_pe_resid_kwh=-40.1603), + _CorpusExpectation(variant='electric 2', block='11a', expected_sap_resid=-0.2430, expected_cost_resid_gbp=+5.5979, expected_co2_resid_kg=+38.7768, expected_pe_resid_kwh=+392.8379), + _CorpusExpectation(variant='electric 3', block='11a', expected_sap_resid=+0.5980, expected_cost_resid_gbp=-13.7793, expected_co2_resid_kg=-13.8238, expected_pe_resid_kwh=-114.7533), + _CorpusExpectation(variant='electric 5', block='11a', expected_sap_resid=-0.7401, expected_cost_resid_gbp=+17.0523, expected_co2_resid_kg=+43.9325, expected_pe_resid_kwh=+338.5315), + _CorpusExpectation(variant='electric 6', block='11a', expected_sap_resid=+0.5156, expected_cost_resid_gbp=-11.8811, expected_co2_resid_kg=-10.1354, expected_pe_resid_kwh=-93.1997), + _CorpusExpectation(variant='electric 7', block='11a', expected_sap_resid=+0.4810, expected_cost_resid_gbp=-11.0832, expected_co2_resid_kg=-8.3964, expected_pe_resid_kwh=-83.9576), + _CorpusExpectation(variant='electric 8', block='11a', expected_sap_resid=+0.4286, expected_cost_resid_gbp=-9.8766, expected_co2_resid_kg=-6.4095, expected_pe_resid_kwh=-70.5744), + _CorpusExpectation(variant='electric 9', block='11a', expected_sap_resid=+0.5673, expected_cost_resid_gbp=-13.0713, expected_co2_resid_kg=-12.3507, expected_pe_resid_kwh=-105.2495), + _CorpusExpectation(variant='gshp', block='11a', expected_sap_resid=+1.0903, expected_cost_resid_gbp=-25.1234, expected_co2_resid_kg=-41.4461, expected_pe_resid_kwh=-454.5023), + _CorpusExpectation(variant='oil 1', block='11a', expected_sap_resid=+0.2902, expected_cost_resid_gbp=-6.6882, expected_co2_resid_kg=-36.6371, expected_pe_resid_kwh=-71.2875), + _CorpusExpectation(variant='oil pcdb 1', block='11a', expected_sap_resid=+0.2728, expected_cost_resid_gbp=-6.2850, expected_co2_resid_kg=-34.4292, expected_pe_resid_kwh=-67.1831), + _CorpusExpectation(variant='oil pcdb 2', block='11a', expected_sap_resid=+0.2728, expected_cost_resid_gbp=-6.2850, expected_co2_resid_kg=-34.4292, expected_pe_resid_kwh=-67.1831), + _CorpusExpectation(variant='oil pcdb 3', block='11a', expected_sap_resid=+0.2729, expected_cost_resid_gbp=-6.2879, expected_co2_resid_kg=-34.4447, expected_pe_resid_kwh=-67.2071), + _CorpusExpectation(variant='pcdb 1', block='11a', expected_sap_resid=+0.4096, expected_cost_resid_gbp=-9.0664, expected_co2_resid_kg=-49.6654, expected_pe_resid_kwh=-92.8147), # Slice S0380.133 unblocked 10 solid-fuel variants by routing the # Elmhurst §14.0 "Main Heating EES Code" through the new # `_ELMHURST_MAIN_HEATING_EES_TO_FUEL_CODE` dict. Pre-slice the @@ -241,16 +241,16 @@ _EXPECTATIONS: tuple[_CorpusExpectation, ...] = ( # cost / CO2 / PE all route via the correct Table 32 fuel code. # Remaining residuals are likely heating-system efficiency or # control-type gaps — separate slices. - _CorpusExpectation(variant='solid fuel 2', block='11a', expected_sap_resid=+3.1478, expected_cost_resid_gbp=-72.5305, expected_co2_resid_kg=+41.5584, expected_pe_resid_kwh=-1346.0016), - _CorpusExpectation(variant='solid fuel 3', block='11a', expected_sap_resid=+1.8310, expected_cost_resid_gbp=-42.1903, expected_co2_resid_kg=-441.0048, expected_pe_resid_kwh=-1069.2375), - _CorpusExpectation(variant='solid fuel 4', block='11a', expected_sap_resid=+0.4523, expected_cost_resid_gbp=-10.4208, expected_co2_resid_kg=-86.4442, expected_pe_resid_kwh=-106.8858), - _CorpusExpectation(variant='solid fuel 5', block='11a', expected_sap_resid=+0.3440, expected_cost_resid_gbp=-7.9255, expected_co2_resid_kg=-56.6651, expected_pe_resid_kwh=-41.8008), - _CorpusExpectation(variant='solid fuel 6', block='11a', expected_sap_resid=+0.5376, expected_cost_resid_gbp=-12.3864, expected_co2_resid_kg=-11.6812, expected_pe_resid_kwh=-89.8541), - _CorpusExpectation(variant='solid fuel 7', block='11a', expected_sap_resid=+0.6029, expected_cost_resid_gbp=-14.0701, expected_co2_resid_kg=-87.4488, expected_pe_resid_kwh=-117.8475), - _CorpusExpectation(variant='solid fuel 8', block='11a', expected_sap_resid=+0.4291, expected_cost_resid_gbp=-9.8880, expected_co2_resid_kg=+5.6990, expected_pe_resid_kwh=-89.4580), - _CorpusExpectation(variant='solid fuel 9', block='11a', expected_sap_resid=+0.5486, expected_cost_resid_gbp=-12.6405, expected_co2_resid_kg=+1.6494, expected_pe_resid_kwh=-103.7659), - _CorpusExpectation(variant='solid fuel 10', block='11a', expected_sap_resid=+0.5837, expected_cost_resid_gbp=-13.4482, expected_co2_resid_kg=-0.2410, expected_pe_resid_kwh=-130.1413), - _CorpusExpectation(variant='solid fuel 11', block='11a', expected_sap_resid=+0.4809, expected_cost_resid_gbp=-11.0799, expected_co2_resid_kg=+5.5072, expected_pe_resid_kwh=-92.4917), + _CorpusExpectation(variant='solid fuel 2', block='11a', expected_sap_resid=+3.0805, expected_cost_resid_gbp=-70.9797, expected_co2_resid_kg=+41.5584, expected_pe_resid_kwh=-1346.0016), + _CorpusExpectation(variant='solid fuel 3', block='11a', expected_sap_resid=+1.7637, expected_cost_resid_gbp=-40.6395, expected_co2_resid_kg=-441.0048, expected_pe_resid_kwh=-1069.2375), + _CorpusExpectation(variant='solid fuel 4', block='11a', expected_sap_resid=+0.3935, expected_cost_resid_gbp=-9.0668, expected_co2_resid_kg=-86.4442, expected_pe_resid_kwh=-106.8858), + _CorpusExpectation(variant='solid fuel 5', block='11a', expected_sap_resid=+0.2767, expected_cost_resid_gbp=-6.3746, expected_co2_resid_kg=-56.6651, expected_pe_resid_kwh=-41.8008), + _CorpusExpectation(variant='solid fuel 6', block='11a', expected_sap_resid=+0.4703, expected_cost_resid_gbp=-10.8355, expected_co2_resid_kg=-11.6812, expected_pe_resid_kwh=-89.8541), + _CorpusExpectation(variant='solid fuel 7', block='11a', expected_sap_resid=+0.5361, expected_cost_resid_gbp=-12.5193, expected_co2_resid_kg=-87.4488, expected_pe_resid_kwh=-117.8475), + _CorpusExpectation(variant='solid fuel 8', block='11a', expected_sap_resid=+0.3618, expected_cost_resid_gbp=-8.3371, expected_co2_resid_kg=+5.6990, expected_pe_resid_kwh=-89.4580), + _CorpusExpectation(variant='solid fuel 9', block='11a', expected_sap_resid=+0.4898, expected_cost_resid_gbp=-11.2865, expected_co2_resid_kg=+1.6494, expected_pe_resid_kwh=-103.7659), + _CorpusExpectation(variant='solid fuel 10', block='11a', expected_sap_resid=+0.5249, expected_cost_resid_gbp=-12.0942, expected_co2_resid_kg=-0.2410, expected_pe_resid_kwh=-130.1413), + _CorpusExpectation(variant='solid fuel 11', block='11a', expected_sap_resid=+0.4221, expected_cost_resid_gbp=-9.7259, expected_co2_resid_kg=+5.5072, expected_pe_resid_kwh=-92.4917), ) diff --git a/domain/sap10_calculator/rdsap/cert_to_inputs.py b/domain/sap10_calculator/rdsap/cert_to_inputs.py index b89c03bd..9eb0c0d6 100644 --- a/domain/sap10_calculator/rdsap/cert_to_inputs.py +++ b/domain/sap10_calculator/rdsap/cert_to_inputs.py @@ -1985,7 +1985,17 @@ def _other_fuel_cost_gbp_per_kwh( is on an off-peak tariff, applies the Table 12a Grid 2 ALL_OTHER_USES high-rate fraction → blended Table 32 rate. Standard tariff bypasses to the prices table's flat scalar (preserves the - cohort fixture cost cascade at 1e-4).""" + cohort fixture cost cascade at 1e-4). + + SAP 10.2 §12 (PDF p.45) + Appendix F2 (PDF p.63) — for the 18-hour + tariff, "the 18-hour high rate applies to all other electricity + uses" (i.e. fraction = 1.0 at the high rate). Table 12a Grid 2 omits + 18-hour and 24-hour from its 7-hour/10-hour table; for 18-hour the + spec rule is explicit (fraction 1.0 at the high rate per Appendix + F2), so route directly to the 18-hour high rate (Table 32 code 38 = + 13.67 p/kWh). 24-hour heating tariff is a heating-only single-rate + tariff (Table 32 code 35 = 6.61 p/kWh) — non-heating uses fall back + to the standard electricity rate.""" if tariff is Tariff.STANDARD: return prices.standard_electricity_p_per_kwh * _PENCE_TO_GBP try: @@ -1993,6 +2003,9 @@ def _other_fuel_cost_gbp_per_kwh( OtherUse.ALL_OTHER_USES, tariff, ) except NotImplementedError: + if tariff is Tariff.EIGHTEEN_HOUR: + high_rate, _low = _tariff_high_low_rates_p_per_kwh(tariff) + return high_rate * _PENCE_TO_GBP return prices.standard_electricity_p_per_kwh * _PENCE_TO_GBP high_rate, low_rate = _tariff_high_low_rates_p_per_kwh(tariff) blended = high_frac * high_rate + (1.0 - high_frac) * low_rate diff --git a/domain/sap10_calculator/rdsap/tests/test_cert_to_inputs.py b/domain/sap10_calculator/rdsap/tests/test_cert_to_inputs.py index c4498024..3bf835fe 100644 --- a/domain/sap10_calculator/rdsap/tests/test_cert_to_inputs.py +++ b/domain/sap10_calculator/rdsap/tests/test_cert_to_inputs.py @@ -45,6 +45,7 @@ from domain.sap10_calculator.rdsap.cert_to_inputs import ( _is_electric_water, # pyright: ignore[reportPrivateUsage] _is_off_peak_meter, # pyright: ignore[reportPrivateUsage] _main_floor_u_value, # pyright: ignore[reportPrivateUsage] + _other_fuel_cost_gbp_per_kwh, # pyright: ignore[reportPrivateUsage] _pv_overshading_factor, # pyright: ignore[reportPrivateUsage] _pv_pitch_deg, # pyright: ignore[reportPrivateUsage] _responsiveness, # pyright: ignore[reportPrivateUsage] @@ -1398,6 +1399,46 @@ def test_tariff_high_low_rates_full_dispatch_coverage() -> None: assert excinfo.value.field == "tariff_high_low_rates" +def test_other_fuel_cost_for_18_hour_tariff_uses_18_hour_high_rate() -> None: + # Arrange — SAP 10.2 §12 (PDF p.45) and Appendix F2 (PDF p.63) both + # specify that for the 18-hour tariff "the 18-hour high rate applies + # to all other electricity uses" (pumps, fans, lighting). Table 12a + # Grid 2 only lists 7-hour / 10-hour fractions; for 18-hour the spec + # rule is implicit fraction = 1.0 at the high rate per Appendix F2. + # + # Pre-slice the cascade fell through Grid 2's NotImplementedError + # to `prices.standard_electricity_p_per_kwh` (Table 32 code 30 = + # 13.19 p/kWh), under-counting pumps + lighting cost on every + # 18-hour cert by (13.67 − 13.19) × kWh. The 41-variant heating- + # systems corpus all lodges `meter_type='18 Hour'`, so this gap is + # cohort-wide. + # + # Spec verbatim, SAP 10.2 §12 (lines 2280-2283): + # "The 18-hour tariff is only for use with electric CPSUs ... + # The low-rate price applies to space and water heating, while + # electricity for all other purposes is at the high-rate price." + # + # Spec verbatim, SAP 10.2 Appendix F2 (lines 3809-3812): + # "F2 Electric CPSUs using 18-hour electricity tariff. The 18-hour + # low rate applies to all space heating and water heating + # provided by the CPSU. ... The 18-hour high rate applies to all + # other electricity uses." + # + # Table 32 code 38 (18-hour high rate) = 13.67 p/kWh = + # 0.1367 £/kWh. + from domain.sap10_calculator.tables.table_12a import Tariff + + # Act + rate_18h = _other_fuel_cost_gbp_per_kwh(Tariff.EIGHTEEN_HOUR, SAP_10_2_SPEC_PRICES) + + # Assert + assert abs(rate_18h - 0.1367) <= 1e-6, ( + f"18-hour tariff other-uses rate {rate_18h:.6f} £/kWh " + f"should equal Table 32 code 38 high rate 0.1367 £/kWh per " + f"SAP 10.2 §12 / Appendix F2" + ) + + def test_is_off_peak_meter_recognises_bare_18_hour_lodging() -> None: # Arrange — RdSAP 10 §17 page 85 row 10-2 lodges 18-hour meter # as the bare "18-hour" or "18 Hour" form (Elmhurst Summary §14.2