diff --git a/backend/epc_api/json_samples/real_life_examples/SAP-Schema-16.0/uprn_10070004512/elmhurst_summary.pdf b/backend/epc_api/json_samples/real_life_examples/SAP-Schema-16.0/uprn_10070004512/elmhurst_summary.pdf new file mode 100644 index 00000000..eddc4314 Binary files /dev/null and b/backend/epc_api/json_samples/real_life_examples/SAP-Schema-16.0/uprn_10070004512/elmhurst_summary.pdf differ diff --git a/backend/epc_api/json_samples/real_life_examples/SAP-Schema-16.0/uprn_10070004512/elmhurst_worksheet.pdf b/backend/epc_api/json_samples/real_life_examples/SAP-Schema-16.0/uprn_10070004512/elmhurst_worksheet.pdf new file mode 100644 index 00000000..b71391c8 Binary files /dev/null and b/backend/epc_api/json_samples/real_life_examples/SAP-Schema-16.0/uprn_10070004512/elmhurst_worksheet.pdf differ diff --git a/backend/epc_api/json_samples/real_life_examples/SAP-Schema-16.0/uprn_10070004512/epc.json b/backend/epc_api/json_samples/real_life_examples/SAP-Schema-16.0/uprn_10070004512/epc.json new file mode 100644 index 00000000..63dbcfab --- /dev/null +++ b/backend/epc_api/json_samples/real_life_examples/SAP-Schema-16.0/uprn_10070004512/epc.json @@ -0,0 +1,281 @@ +{ + "uprn": 10070004512, + "roofs": [ + { + "description": "(another dwelling above)", + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + } + ], + "walls": [ + { + "description": "Cavity wall, filled cavity", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "floors": [ + { + "description": "To external air, no insulation (assumed)", + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + } + ], + "status": "entered", + "windows": [ + { + "description": "Fully double glazed", + "energy_efficiency_rating": 3, + "environmental_efficiency_rating": 3 + } + ], + "lighting": { + "description": "Low energy lighting in all fixed outlets", + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + }, + "postcode": "BR1 4QF", + "hot_water": { + "description": "Electric immersion, off-peak", + "energy_efficiency_rating": 3, + "environmental_efficiency_rating": 1 + }, + "post_town": "BROMLEY", + "created_at": "2012-04-20 10:59:22.000000", + "door_count": 1, + "glazed_area": 1, + "region_code": 14, + "report_type": 2, + "sap_heating": { + "wwhrs": { + "rooms_with_bath_and_or_shower": 1, + "rooms_with_mixer_shower_no_bath": 0, + "rooms_with_bath_and_mixer_shower": 0 + }, + "cylinder_size": 1, + "water_heating_code": 903, + "water_heating_fuel": 29, + "main_heating_details": [ + { + "has_fghrs": "N", + "main_fuel_type": 29, + "heat_emitter_type": 0, + "main_heating_number": 1, + "main_heating_control": 2401, + "main_heating_category": 7, + "main_heating_fraction": 1, + "sap_main_heating_code": 402, + "main_heating_data_source": 2 + } + ], + "immersion_heating_type": 1, + "has_fixed_air_conditioning": "false" + }, + "sap_version": 9.91, + "schema_type": "SAP-Schema-16.0", + "uprn_source": "Energy Assessor", + "country_code": "EAW", + "main_heating": [ + { + "description": "Electric storage heaters", + "energy_efficiency_rating": 3, + "environmental_efficiency_rating": 1 + } + ], + "dwelling_type": "Ground-floor flat", + "language_code": 1, + "property_type": 2, + "address_line_1": "54a, Boyland Road", + "schema_version": "LIG-16.0", + "assessment_type": "RdSAP", + "completion_date": "2012-04-20", + "inspection_date": "2012-04-20", + "extensions_count": 0, + "measurement_type": 1, + "sap_flat_details": { + "level": 1, + "top_storey": "N", + "flat_location": 0, + "heat_loss_corridor": 0 + }, + "total_floor_area": 33, + "transaction_type": 3, + "conservatory_type": 1, + "heated_room_count": 2, + "registration_date": "2012-04-20", + "restricted_access": 1, + "sap_energy_source": { + "main_gas": "N", + "meter_type": 1, + "photovoltaic_supply": { + "percent_roof_area": 0 + }, + "wind_turbines_count": 0, + "wind_turbines_terrain_type": 1 + }, + "secondary_heating": { + "description": "Portable electric heaters (assumed)", + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + }, + "sap_building_parts": [ + { + "identifier": "Main Dwelling", + "wall_dry_lined": "N", + "floor_heat_loss": 1, + "roof_construction": 3, + "wall_construction": 4, + "building_part_number": 1, + "sap_floor_dimensions": [ + { + "floor": 0, + "room_height": 2.42, + "floor_insulation": 1, + "total_floor_area": 33.24, + "floor_construction": 0, + "heat_loss_perimeter": 10.39 + } + ], + "wall_insulation_type": 2, + "construction_age_band": "B", + "wall_thickness_measured": "N", + "roof_insulation_location": "ND", + "roof_insulation_thickness": "ND" + } + ], + "low_energy_lighting": 100, + "solar_water_heating": "N", + "bedf_revision_number": 321, + "habitable_room_count": 2, + "heating_cost_current": { + "value": 303, + "currency": "GBP" + }, + "insulated_door_count": 0, + "co2_emissions_current": { + "value": 2.9, + "quantity": "tonnes per year" + }, + "energy_rating_average": 60, + "energy_rating_current": 66, + "lighting_cost_current": { + "value": 23, + "currency": "GBP" + }, + "main_heating_controls": [ + { + "description": "Manual charge control", + "energy_efficiency_rating": 2, + "environmental_efficiency_rating": 2 + } + ], + "multiple_glazing_type": 3, + "open_fireplaces_count": 0, + "has_hot_water_cylinder": "false", + "heating_cost_potential": { + "value": 190, + "currency": "GBP" + }, + "hot_water_cost_current": { + "value": 106, + "currency": "GBP" + }, + "mechanical_ventilation": 0, + "percent_draughtproofed": 100, + "suggested_improvements": [ + { + "sequence": 1, + "typical_saving": { + "value": 86, + "currency": "GBP" + }, + "indicative_cost": "\u00a3800 - \u00a31,200", + "improvement_type": "W", + "improvement_details": { + "improvement_number": 47 + }, + "improvement_category": 5, + "energy_performance_rating": 73, + "environmental_impact_rating": 58 + }, + { + "sequence": 2, + "typical_saving": { + "value": 27, + "currency": "GBP" + }, + "indicative_cost": "\u00a3600 - \u00a3800", + "improvement_type": "L", + "improvement_details": { + "improvement_number": 25 + }, + "improvement_category": 5, + "energy_performance_rating": 75, + "environmental_impact_rating": 60 + } + ], + "co2_emissions_potential": { + "value": 2.1, + "quantity": "tonnes per year" + }, + "energy_rating_potential": 75, + "lighting_cost_potential": { + "value": 23, + "currency": "GBP" + }, + "alternative_improvements": [ + { + "sequence": 1, + "typical_saving": { + "value": 24, + "currency": "GBP" + }, + "improvement_type": "J2", + "improvement_details": { + "improvement_number": 54 + }, + "improvement_category": 6, + "energy_performance_rating": 76, + "environmental_impact_rating": 93 + }, + { + "sequence": 2, + "typical_saving": { + "value": 84, + "currency": "GBP" + }, + "improvement_type": "Z1", + "improvement_details": { + "improvement_number": 51 + }, + "improvement_category": 6, + "energy_performance_rating": 80, + "environmental_impact_rating": 81 + } + ], + "hot_water_cost_potential": { + "value": 106, + "currency": "GBP" + }, + "renewable_heat_incentive": { + "water_heating": 1434, + "space_heating_existing_dwelling": 4064 + }, + "seller_commission_report": "Y", + "energy_consumption_current": 497, + "has_fixed_air_conditioning": "false", + "multiple_glazed_proportion": 100, + "calculation_software_version": 4.1, + "energy_consumption_potential": 365, + "environmental_impact_current": 47, + "fixed_lighting_outlets_count": 5, + "current_energy_efficiency_band": "D", + "environmental_impact_potential": 60, + "has_heated_separate_conservatory": "false", + "potential_energy_efficiency_band": "C", + "co2_emissions_current_per_floor_area": { + "value": 88, + "quantity": "kg/m2 per year" + }, + "low_energy_fixed_lighting_outlets_count": 5 +} \ No newline at end of file diff --git a/scripts/hyde/build_10070004512.py b/scripts/hyde/build_10070004512.py new file mode 100644 index 00000000..2210242a --- /dev/null +++ b/scripts/hyde/build_10070004512.py @@ -0,0 +1,288 @@ +"""Elmhurst build for UPRN 10070004512 (SAP-Schema-16.0, GROUND-FLOOR FLAT, +band B, cavity FILLED, ELECTRIC STORAGE HEATERS (SAP 402 SEB, manual charge +control CSA/2401) + electric immersion off-peak (Economy-7 Dual meter) with a +cylinder (size 1), roof = another dwelling above, floor to EXTERNAL AIR, double +glazed, TFA 33.24, window 4.88 m². Engine 66 = lodged 66. + +P1 of the modelling_e2e corpus validation — the built_form fix cert (16.0 omitted +built_form; mapper derives it from dwelling_type → flat→modal 4). built_form is +ML-only so SAP-neutral; engine reproduces lodged exactly. Storage-heater build +(see build_10022893721.py). Engine models NO secondary (sap_heating. +secondary_heating_type is None) → secondary present=No to match. Run: + DISPLAY=:99 python scripts/hyde/build_10070004512.py +""" +from __future__ import annotations +import sys +import elmhurst_lib as E + +DIM = "TabContainer_TabPanelMain_WebUserControlDimensionsMain_" +WALL = ("TabContainer_TabPanelMain_InnerTabContainerMain_" + "TabPanelExternalWallMain_WebUserControlWallMain_") +ROOF = "TabContainer_TabPanelMain_WebUserControlRoofMain_" +FLOOR = "TabContainer_TabPanelMain_WebUserControlFloorsMain_" +WP = "TabContainer_TabPanelWindowsPanel_" +DP = "TabContainer_TabPanelDoorsPanel_" +VP = "TabContainer_TabPanelVentilationPanel_" +APT = "TabContainer_TabPanelAirPressureTest_" +LP = "TabContainer_TabPanelLighting_" +MV = "TabContainer_TabPanelMechVent_" +WH = "TabContainer_TabPanelWaterHeating_" + + +def _pick(page, suffix, contains): + val = page.evaluate( + """(a)=>{const s=document.getElementById(a[0]);if(!s)return null; + for(const o of s.options){if(o.text.toLowerCase().includes(a[1].toLowerCase()))return o.value;}return null;}""", + [f"{E.FP}{suffix}", contains]) + if val is not None: + E.set_select(page, suffix, val) + return val + + +def _options(page, suffix): + return page.evaluate( + """(id)=>{const s=document.getElementById(id);if(!s)return []; + return Array.from(s.options).map(o=>o.text);}""", f"{E.FP}{suffix}") + + +def property_description(page): + E.goto(page, "PropertyDescription", "WebFormPropertyDescription.aspx") + E.set_select(page, "DropDownListPropertyType1", "F Flat") + _pick(page, "DropDownListPropertyType2", "mid-terrace") # built_form 4 + E.set_text(page, "TextBoxStoreys", "1") + E.set_text(page, "TextBoxHabitableRooms", "2") + E.set_text(page, "TextBoxHeatedHabitableRooms", "2") + print("date ->", _pick(page, "DropDownListDateBuiltMain", "1900-1929")) # band B + E.set_select(page, "DropDownListDateBuiltFirst", "") + E.set_select(page, "DropDownListRoomInRoofMain", "") + E.save_close(page) + + +def flats(page): + E.goto(page, "Flats", "WebFormFlats.aspx") + E.set_select(page, "DropDownListPositionOfFlat", "Ground Floor") + E.set_text(page, "TextBoxFloor", "0") + E.set_select(page, "RadioButtonListFlatCoridor", "None") + E.save_close(page) + + +def dimensions(page): + E.goto(page, "Dimensions", "WebFormDimensions.aspx") + E.set_text(page, f"{DIM}TextBoxFloorAreaLowestFloor", "33.24") + E.set_text(page, f"{DIM}TextBoxRoomHeightLowestFloor", "2.42") + E.set_text(page, f"{DIM}TextBoxWallPerimeterLowestFloor", "10.39") + # 16.0 lodges no party_wall_length; a flat's side party walls are unmodelled by + # the engine. Try 0 (Elmhurst may require non-zero — adjust if Recommendations + # complains). + E.set_text(page, f"{DIM}TextBoxPartyWallLengthLowestFloor", "0") + E.save_close(page) + + +def walls(page): + E.goto(page, "Walls", "WebFormWalls.aspx") + E.set_select(page, f"{WALL}DropDownListType", "CA Cavity") + page.wait_for_timeout(400) + print("insulation ->", _pick(page, f"{WALL}DropDownListInsulation", "filled")) + E.save_close(page) + + +def roofs(page): + E.goto(page, "Roofs", "WebFormRoofs.aspx") + _pick(page, f"{ROOF}DropDownListType", "another dwelling above") + E.save_close(page) + + +def floors(page): + E.goto(page, "Floors", "WebFormFloors.aspx") + # Floor is "to external air" — that is the LOCATION/exposure, not a TYPE. + _pick(page, f"{FLOOR}DropDownListLocation", "external air") # E To external air + page.wait_for_timeout(400) + _pick(page, f"{FLOOR}DropDownListType", "solid") # construction; U from exposure + ins = page.locator(f"#{E.FP}{FLOOR}DropDownListInsulation") + if ins.count(): + E.set_select(page, f"{FLOOR}DropDownListInsulation", "A As built") + E.save_close(page) + + +def openings(page): + E.goto(page, "Openings", "WebFormOpenings.aspx") + E.click_tab(page, "TabContainer_TabPanelWindowsPanel") + _add_window(page, 4.88, "North", _glazing(page)) + _delete_zero_rows(page) + E.click_tab(page, "TabContainer_TabPanelDoorsPanel") + E.set_text(page, f"{DP}TextBoxDoors", "1") + E.set_text(page, f"{DP}TextBoxDoorsInsulated", "0") + E.set_text(page, f"{DP}TextBoxDraughtProofedDoors", "0") + E.save_close(page) + + +def _glazing(page): + for needle in ("unknown install date", "before 2002", "pre 2002"): + for opt in _options(page, f"{WP}DropDownListExtGlazing"): + low = opt.lower() + if needle in low and "triple" not in low and "single" not in low and "known data" not in low: + return opt + return "Double post or during 2022" + + +def _add_window(page, area, orientation, glazing): + print("glazing ->", glazing) + E.set_select(page, f"{WP}DropDownListExtGlazing", glazing) + page.wait_for_timeout(400) + ft = page.locator(f"#{E.FP}{WP}DropDownListExtFrameType") + if ft.count(): + ft.select_option("PVC") + gg = page.locator(f"#{E.FP}{WP}DropDownListExtGlazingGap") + if gg.count(): + gg.select_option("12 mm") + wid = f"{E.FP}{WP}TextBoxExtWidth" + page.evaluate( + """(a)=>{const e=document.getElementById(a[0]);if(e){e.value=a[1]; + e.dispatchEvent(new Event('input',{bubbles:true})); + e.dispatchEvent(new Event('change',{bubbles:true})); + e.dispatchEvent(new Event('blur',{bubbles:true}));}}""", [wid, str(area)]) + page.locator(f"#{E.FP}{WP}TextBoxExtHeight").fill("1.00") + page.locator(f"#{E.FP}{WP}DropDownListExtOrientation").select_option(orientation) + page.locator(f"#{E.FP}{WP}DropDownListExtBuildingPartId").select_option("Main") + page.locator(f"#{E.FP}{WP}DropDownListExtLocation").select_option("External wall") + page.wait_for_timeout(300) + before = E.window_row_count(page) + page.evaluate("(id)=>{const e=document.getElementById(id); if(e)e.click();}", f"{E.FP}{WP}ButtonAddWindow") + for _ in range(25): + page.wait_for_timeout(200) + if E.window_row_count(page) > before: + break + + +def _grid_rows(page): + return page.evaluate( + """()=>{const t=document.querySelector("[id*=GridViewExtendedWidows]"); + if(!t)return[];return Array.from(t.querySelectorAll('tr')).slice(1) + .map(r=>Array.from(r.querySelectorAll('td')).map(c=>c.innerText.trim()));}""") + + +def _delete_zero_rows(page): + g = 0 + while g < 6 and E.window_row_count(page) > 1: + g += 1 + rows = _grid_rows(page) + bad = next((i for i, c in enumerate(rows) if len(c) > 1 and c[1] in ("0.00", "0", "0.0")), None) + if bad is None: + break + _delete_row(page, bad) + page.wait_for_timeout(400) + + +def _delete_row(page, idx): + before = E.window_row_count(page) + btn = page.evaluate( + """(i)=>{const b=document.querySelectorAll("[id*='GridViewExtendedWidows_DeleteButton_']");return b[i]?b[i].id:null;}""", idx) + if not btn: + return + page.evaluate("(id)=>{const e=document.getElementById(id); if(e)e.click();}", btn) + page.wait_for_selector(f"#{E.FP}DeleteWindowDialog_LinkButtonYes", state="visible", timeout=5000) + page.evaluate("(id)=>{const e=document.getElementById(id); if(e)e.click();}", f"{E.FP}DeleteWindowDialog_LinkButtonYes") + for _ in range(20): + page.wait_for_timeout(200) + if E.window_row_count(page) < before: + break + + +def ventilation(page): + E.goto(page, "VentilationAndCooling", "WebFormVentilationAndCooling.aspx") + E.click_tab(page, "TabContainer_TabPanelVentilationPanel") + E.set_text(page, f"{VP}TextBoxIntermittentFans", "0") + cool = page.locator(f"#{E.FP}{VP}CheckBoxFixedSpaceCooling") + if cool.count() and cool.is_checked(): + E.commit(page, cool.uncheck) + E.click_tab(page, "TabContainer_TabPanelMechVent") + mv = page.locator(f"#{E.FP}{MV}CheckBoxMechanicalVentilation") + if mv.count() and mv.is_checked(): + E.commit(page, mv.uncheck) + E.click_tab(page, "TabContainer_TabPanelAirPressureTest") + E.set_select(page, f"{APT}DropDownListTestMethod", "Not available") + E.click_tab(page, "TabContainer_TabPanelLighting") + E.set_text(page, f"{LP}TextBoxLightsTotal", "5") + E.set_text(page, f"{LP}TextBoxLedLightsTotal", "5") # 100% low energy + E.set_text(page, f"{LP}TextBoxCflLightsTotal", "0") + E.save_close(page) + + +def space_heating(page): + # Electric storage heaters (SAP 402 = SEB), manual charge control (SAP 2401 = + # CSA). Two passes: clear bound PCDB boiler first, then set the SAP-table code. + E.goto(page, "SpaceHeating", "WebFormSpaceHeating.aspx") + page.wait_for_timeout(1000) + rid = f"{E.MH1}TextBoxPCDFBoilerReference" + ref = page.locator(f"#{rid}").input_value() + if ref not in ("0", ""): + print(f"clearing bound PCDB boiler {ref} -> 0 (rerun space_heating)") + page.evaluate("""(rid)=>{const r=document.getElementById(rid);r.value='0'; + r.dispatchEvent(new Event('change',{bubbles:true}));}""", rid) + page.wait_for_timeout(500) + E.save_close(page) + return + E.set_heating_dialog(page, "TabContainer_TabPanelMainHeating1_WebUserControlMainHeating1_ButtonMainHeatingCode", + "^Electric", "^Electric", "Storage", "SEB Modern slimline") + print("code:", page.locator(f"#{E.MH1}TextBoxMainHeatingCode").input_value()) + E.set_heating_dialog(page, "TabContainer_TabPanelMainHeating1_WebUserControlMainHeating1_ButtonMainHeatingControls", + "Storage Radiator", "CSA Manual charge control") + print("control:", page.locator(f"#{E.MH1}TextBoxMainHeatingControls").input_value()) + E.set_select(page, "DropDownListSecondaryHeatingPresent", "No") + # Economy-7 Dual meter (cert meter_type 1) — hidden Meters sub-tab. + E.click_tab(page, "TabContainer_TabPanelMeters") + E.set_select(page, "TabContainer_TabPanelMeters_RadioButtonListElectricityType", "Dual") + print("meter:", page.locator("#ContentBody_ContentPlaceHolder1_TabContainer_TabPanelMeters_RadioButtonListElectricityType").input_value()) + E.save_close(page) + + +def water_heating(page): + # Electric immersion off-peak (Dual) WITH cylinder (size 1 = small). The + # immersion code REQUIRES a cylinder. + E.goto(page, "WaterHeating", "WebFormWaterHeating.aspx") + E.click_tab(page, "TabContainer_TabPanelWaterHeating") + page.wait_for_timeout(600) + E.set_heating_dialog(page, f"{WH}ButtonWaterHeatingCode", + "Water Heater", "^Electric", "Immersion") + print("water code:", page.locator(f"#{E.FP}{WH}TextBoxWaterHeatingCode").input_value()) + cid = f"{E.FP}{WH}CheckBoxHotWaterCylinder" + cyl = page.locator(f"#{cid}") + if cyl.count() and not cyl.is_checked(): + try: + with page.expect_navigation(wait_until="load", timeout=8000): + page.evaluate( + """(id)=>{const c=document.getElementById(id);if(c){c.checked=true; + c.dispatchEvent(new Event('click',{bubbles:true})); + c.dispatchEvent(new Event('change',{bubbles:true}));}}""", cid) + except Exception: + page.wait_for_timeout(2000) + print("cylinder present:", cyl.is_checked() if cyl.count() else "n/a") + print("cyl sizes:", _options(page, f"{WH}DropDownListCylinderSize")) + _pick(page, f"{WH}DropDownListCylinderSize", "small") or \ + E.set_select(page, f"{WH}DropDownListCylinderSize", "Normal") + E.set_select(page, f"{WH}DropDownListInsulated", "Foam") + isuf = f"{WH}DropDownListInsulationThickness" + if page.locator(f"#{E.FP}{isuf}").count(): + _pick(page, isuf, "Unknown") or E.set_select(page, isuf, "25 mm") + imm = page.locator(f"#{E.FP}{WH}RadioButtonListImmersionHeater") + if imm.count(): + E.set_select(page, f"{WH}RadioButtonListImmersionHeater", "Dual") + E.save_close(page) + + +_ORDER = ["property_description", "flats", "dimensions", "walls", "roofs", + "floors", "openings", "ventilation", "space_heating", "water_heating"] + + +def main(): + if len(sys.argv) < 2 or sys.argv[1] not in _ORDER: + print("usage: build_10070004512.py <" + "|".join(_ORDER) + ">") + return 2 + with E.session() as (ctx, page): + globals()[sys.argv[1]](page) + print("done:", sys.argv[1], "->", page.url) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/domain/sap10_calculator/test_real_cert_sap_accuracy.py b/tests/domain/sap10_calculator/test_real_cert_sap_accuracy.py index 56989c71..df9a58e1 100644 --- a/tests/domain/sap10_calculator/test_real_cert_sap_accuracy.py +++ b/tests/domain/sap10_calculator/test_real_cert_sap_accuracy.py @@ -603,6 +603,28 @@ _EXPECTATIONS: Final[tuple[RealCertExpectation, ...]] = ( "engine prices 100% at the low rate; see elmhurst_worksheet.pdf (243-246)" ), ), + # UPRN 10070004512 → cert 8742-6624-9300-2780-4926. SAP-Schema-16.0, GROUND- + # FLOOR FLAT, band B, cavity FILLED, ELECTRIC STORAGE HEATERS (SAP 402 SEB, + # manual charge CSA/2401) + electric immersion off-peak (Economy-7 Dual meter) + # with a small cylinder (size 1), roof = another dwelling above, floor to + # EXTERNAL AIR, double glazed, TFA 33.24. Engine 66 = lodged 66 EXACTLY. + # This is the modelling_e2e built_form fix cert: 16.0 omits `built_form`, which + # RdSapSchema17_1 requires; the mapper derives it from dwelling_type (flat → + # modal 4). built_form is ML-only (the SAP calculator never reads it) so the fix + # is SAP-NEUTRAL — the engine reproduces the lodged score regardless. Built in + # Elmhurst RdSAP10 (evidence saved: elmhurst_summary.pdf / elmhurst_worksheet.pdf): + # worksheet SAP 54, engine on Elmhurst's own parsed inputs 53 ≈ 54 → calculator + # faithful. The engine 66 vs Elmhurst 54 (+12) is an input/build gap dominated by + # HOT WATER (engine 1272 vs Elmhurst 1948 kWh): the cert lodges a size-1 (small) + # cylinder but Elmhurst's RdSAP entry has no "Small" option (Normal/110 L is the + # smallest), forcing a larger cylinder + more storage loss; plus the reduced-field + # 16.0 floor/party-wall defaults. PINNED to the observed engine 66 (= lodged 66). + RealCertExpectation( + schema="SAP-Schema-16.0", + sample="uprn_10070004512", + cert_num="8742-6624-9300-2780-4926", + sap_score=66, + ), )