diff --git a/backend/documents_parser/tests/test_summary_pdf_mapper_chain.py b/backend/documents_parser/tests/test_summary_pdf_mapper_chain.py index b3cb0d89..18e68a60 100644 --- a/backend/documents_parser/tests/test_summary_pdf_mapper_chain.py +++ b/backend/documents_parser/tests/test_summary_pdf_mapper_chain.py @@ -925,6 +925,118 @@ def test_api_001479_full_chain_sap_matches_worksheet_pdf_exactly() -> None: assert abs(result.sap_score_continuous - worksheet_unrounded_sap) < 1e-4 +# ============================================================================ +# Layer 4 chain tests — 7-cert ASHP cohort +# ============================================================================ +# These pin the API → from_api_response → cert_to_inputs → +# calculate_sap_from_inputs cascade against each cert's Elmhurst dr87 +# worksheet unrounded SAP. Tolerance is 0.07 (NOT 1e-4 like the boiler +# cohort above) — see HANDOVER_CERT_0380_MIT_CASCADE.md for the +# investigation: BRE web confirmed max_output_kw matches cascade +# exactly (4.39 / 3.933), cascade (39) annual HLC matches worksheet +# at 4 dp, but back-solving worksheet η_space implies ~0.15% drift +# in Elmhurst's internal interpolation precision (likely a vendor +# rounding convention not in the public SAP 10.2 spec). The 7 certs +# cluster within +0.030..+0.060 SAP — this is the spec-precision +# floor for the publicly-documented cascade. +# +# At rounded (integer SAP) precision, all 7 cascade integers match +# the lodged values exactly (residual = 0, pinned in +# `_GOLDEN_EXPECTATIONS`). + +_API_0350_JSON = ( + Path(__file__).parents[3] + / "domain/sap10_calculator/rdsap/tests/fixtures/golden" + / "0350-2968-2650-2796-5255.json" +) +_API_3800_JSON = ( + Path(__file__).parents[3] + / "domain/sap10_calculator/rdsap/tests/fixtures/golden" + / "3800-8515-0922-3398-3563.json" +) +_API_9285_JSON = ( + Path(__file__).parents[3] + / "domain/sap10_calculator/rdsap/tests/fixtures/golden" + / "9285-3062-0205-7766-7200.json" +) + +_ASHP_COHORT_CHAIN_TOLERANCE: float = 0.07 +"""SAP-precision floor for the 7-cert ASHP cohort — see handover.""" + + +def test_api_0380_full_chain_sap_within_spec_floor_of_worksheet() -> None: + # Mitsubishi PUZ-WM50VHA PCDB 104568, semi-detached bungalow age D. + doc = json.loads(_API_0380_JSON.read_text()) + epc = EpcPropertyDataMapper.from_api_response(doc) + result = calculate_sap_from_inputs( + cert_to_inputs(epc, prices=SAP_10_2_SPEC_PRICES) + ) + assert abs(result.sap_score_continuous - 88.5104) < _ASHP_COHORT_CHAIN_TOLERANCE + + +def test_api_0350_full_chain_sap_within_spec_floor_of_worksheet() -> None: + # Mitsubishi PUZ-WM50VHA PCDB 104568. + doc = json.loads(_API_0350_JSON.read_text()) + epc = EpcPropertyDataMapper.from_api_response(doc) + result = calculate_sap_from_inputs( + cert_to_inputs(epc, prices=SAP_10_2_SPEC_PRICES) + ) + assert abs(result.sap_score_continuous - 84.1367) < _ASHP_COHORT_CHAIN_TOLERANCE + + +def test_api_2225_full_chain_sap_within_spec_floor_of_worksheet() -> None: + # Mitsubishi PUZ-WM50VHA PCDB 104568, with PV. Slice 102f-prep.8 + # closed the shower_outlets=None default. + doc = json.loads(_API_2225_JSON.read_text()) + epc = EpcPropertyDataMapper.from_api_response(doc) + result = calculate_sap_from_inputs( + cert_to_inputs(epc, prices=SAP_10_2_SPEC_PRICES) + ) + assert abs(result.sap_score_continuous - 88.7921) < _ASHP_COHORT_CHAIN_TOLERANCE + + +def test_api_2636_full_chain_sap_within_spec_floor_of_worksheet() -> None: + # Mitsubishi PUZ-WM50VHA PCDB 104568, with cantilever + alt wall. + # Slice 102f-prep.9 (cantilever) + 102f-prep.10 (alt-wall openings). + doc = json.loads(_API_2636_JSON.read_text()) + epc = EpcPropertyDataMapper.from_api_response(doc) + result = calculate_sap_from_inputs( + cert_to_inputs(epc, prices=SAP_10_2_SPEC_PRICES) + ) + assert abs(result.sap_score_continuous - 86.2641) < _ASHP_COHORT_CHAIN_TOLERANCE + + +def test_api_3800_full_chain_sap_within_spec_floor_of_worksheet() -> None: + # Mitsubishi PUZ-WM50VHA PCDB 104568. + doc = json.loads(_API_3800_JSON.read_text()) + epc = EpcPropertyDataMapper.from_api_response(doc) + result = calculate_sap_from_inputs( + cert_to_inputs(epc, prices=SAP_10_2_SPEC_PRICES) + ) + assert abs(result.sap_score_continuous - 86.1458) < _ASHP_COHORT_CHAIN_TOLERANCE + + +def test_api_9285_full_chain_sap_within_spec_floor_of_worksheet() -> None: + # Mitsubishi PUZ-WM50VHA PCDB 104568. + doc = json.loads(_API_9285_JSON.read_text()) + epc = EpcPropertyDataMapper.from_api_response(doc) + result = calculate_sap_from_inputs( + cert_to_inputs(epc, prices=SAP_10_2_SPEC_PRICES) + ) + assert abs(result.sap_score_continuous - 84.1369) < _ASHP_COHORT_CHAIN_TOLERANCE + + +def test_api_9418_full_chain_sap_within_spec_floor_of_worksheet() -> None: + # Daikin Altherma EDLQ05CAV3 PCDB 102421, heating_duration_code='24' + # (continuous, all days at Th). Slice 102f-prep.7 closed Table N4. + doc = json.loads(_API_9418_JSON.read_text()) + epc = EpcPropertyDataMapper.from_api_response(doc) + result = calculate_sap_from_inputs( + cert_to_inputs(epc, prices=SAP_10_2_SPEC_PRICES) + ) + assert abs(result.sap_score_continuous - 84.6305) < _ASHP_COHORT_CHAIN_TOLERANCE + + # ============================================================================ # Mapper-vs-hand-built EpcPropertyData diff tests # ============================================================================