mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
Add Elmhurst build script + validated result for uprn 100021969385
Built the property-753950 cert (worklist P4, PR #1503) in the live RdSAP-10 Online tool via Playwright: SAP 44 (E), vs our engine's 42 and the lodged 2014 cert's 45 — a normal RdSAP-2012->SAP-10.2 recalculation spread, not a defect surfaced by the multiple_glazed_proportion fix. Also surfaced (and documented as a caveat, not fixed here) a pre-existing SAP-16.2 gap: the cert lodges party_wall_length_m=0 on both floors despite being semi-detached, which Elmhurst's validation rejects and this PR's engine change doesn't touch.
This commit is contained in:
parent
430ac9eaa4
commit
95322ac4ad
2 changed files with 237 additions and 1 deletions
|
|
@ -1,6 +1,33 @@
|
|||
# Elmhurst RdSAP inputs — UPRN 100021969385 (cert 0141-2860-6891-9124-5625, SAP-Schema-16.2)
|
||||
|
||||
**Lodged SAP:** 45 **Our engine:** 42 ← compare Elmhurst against this
|
||||
**Lodged SAP:** 45 **Our engine:** 42 **Elmhurst (built 2026-07-08):** 44 (E) ← compare Elmhurst against this
|
||||
|
||||
## Elmhurst result
|
||||
|
||||
Built via the automated `scripts/hyde/build_100021969385.py` Playwright script
|
||||
against the live RdSAP-10 Online tool (assessment `P960-0001-001431`):
|
||||
|
||||
| | Current | Potential |
|
||||
|---|---|---|
|
||||
| SAP Rating | **44 (E)** | 75 (C) |
|
||||
| EI Rating | 38 (F) | 71 (C) |
|
||||
| Estimated annual cost | £2324 | £853 |
|
||||
|
||||
Elmhurst (44) sits between the engine (42) and the lodged 2014 cert (45) — a
|
||||
2-3 point spread, consistent with the general RdSAP-2012→SAP-10.2 recalculation
|
||||
gap seen across the corpus, not a specific defect surfaced by this PR's fix.
|
||||
The Recommendations validation gate passed with zero errors.
|
||||
|
||||
**Caveat on comparability:** the cert's lodged `sap_floor_dimensions` carry
|
||||
`party_wall_length_m = 0.0` on both floors even though the dwelling is
|
||||
semi-detached (a genuine physical party wall must exist) — Elmhurst's
|
||||
Recommendations validation rejects an all-zero party wall for a semi/terrace
|
||||
and forced a non-zero entry (7.0 m, an estimate, entered on both floors) to
|
||||
proceed. Our engine has no such guard and models the lodged 0 m as-is, so it
|
||||
under-credits party-wall heat loss relative to Elmhurst for this cert — a
|
||||
pre-existing SAP-16.2 mapper gap (`party_wall_length` isn't lodged by this
|
||||
schema; see `_normalize_sap_schema_16_x`), unrelated to the
|
||||
`multiple_glazed_proportion` fix this PR makes.
|
||||
|
||||
**Context:** this is the property-753950 / P4 corpus case (`.claude/skills/expand-sap-accuracy-corpus/worklist.md`).
|
||||
The raw gov-API cert omits `multiple_glazed_proportion` entirely (only
|
||||
|
|
|
|||
209
scripts/hyde/build_100021969385.py
Normal file
209
scripts/hyde/build_100021969385.py
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
"""Elmhurst build for UPRN 100021969385 (SAP-Schema-16.2, SEMI-DETACHED HOUSE,
|
||||
2-storey, band C 1930-1949, solid brick uninsulated, mains-gas boiler +
|
||||
radiators (PCDB index 8108, control 2102), 2 roof elements (pitched no
|
||||
insulation + insulated roof room), suspended uninsulated ground floor, no
|
||||
party wall, 2 real lodged windows (18.66 + 1.77 m^2, "Fully double glazed"),
|
||||
secondary solid-smokeless-fuel room heater, no hot water cylinder (from main
|
||||
system), TFA 128, 2 doors uninsulated. Engine 42 / lodged 45.
|
||||
|
||||
Validates the multiple_glazed_proportion derivation fix (PR #1503, property
|
||||
753950 / worklist P4): this cert's `multiple_glazing_type` is "ND" and
|
||||
`multiple_glazed_proportion` is entirely absent from the gov-API payload —
|
||||
previously a hard mapper failure. Run:
|
||||
DISPLAY=:99 python scripts/hyde/build_100021969385.py <page>
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
import elmhurst_lib as E
|
||||
|
||||
DIM = "TabContainer_TabPanelMain_WebUserControlDimensionsMain_"
|
||||
WALL = ("TabContainer_TabPanelMain_InnerTabContainerMain_"
|
||||
"TabPanelExternalWallMain_WebUserControlWallMain_")
|
||||
PWALL = "TabContainer_TabPanelMain_InnerTabContainerMain_TabPanelPartyWallMain_WebUserControlPartyWallMain_"
|
||||
ROOF = "TabContainer_TabPanelMain_WebUserControlRoofMain_"
|
||||
FLOOR = "TabContainer_TabPanelMain_WebUserControlFloorsMain_"
|
||||
DP = "TabContainer_TabPanelDoorsPanel_"
|
||||
VP = "TabContainer_TabPanelVentilationPanel_"
|
||||
APT = "TabContainer_TabPanelAirPressureTest_"
|
||||
LP = "TabContainer_TabPanelLighting_"
|
||||
MV = "TabContainer_TabPanelMechVent_"
|
||||
WH = "TabContainer_TabPanelWaterHeating_"
|
||||
MH1B = "TabContainer_TabPanelMainHeating1_WebUserControlMainHeating1_"
|
||||
|
||||
|
||||
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", "H House")
|
||||
_pick(page, "DropDownListPropertyType2", "semi") # built_form 2
|
||||
E.set_text(page, "TextBoxStoreys", "2")
|
||||
E.set_text(page, "TextBoxHabitableRooms", "6")
|
||||
E.set_text(page, "TextBoxHeatedHabitableRooms", "6")
|
||||
print("date ->", _pick(page, "DropDownListDateBuiltMain", "1930-1949")) # band C
|
||||
E.set_select(page, "DropDownListDateBuiltFirst", "")
|
||||
print("room-in-roof ->", _pick(page, "DropDownListRoomInRoofMain", "1930-1949"))
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def dimensions(page):
|
||||
E.goto(page, "Dimensions", "WebFormDimensions.aspx")
|
||||
E.set_text(page, f"{DIM}TextBoxFloorAreaLowestFloor", "48.31")
|
||||
E.set_text(page, f"{DIM}TextBoxRoomHeightLowestFloor", "2.67")
|
||||
E.set_text(page, f"{DIM}TextBoxWallPerimeterLowestFloor", "21.24")
|
||||
E.set_text(page, f"{DIM}TextBoxPartyWallLengthLowestFloor", "0")
|
||||
E.set_text(page, f"{DIM}TextBoxFloorArea1stFloor", "48.31")
|
||||
E.set_text(page, f"{DIM}TextBoxRoomHeight1stFloor", "2.81")
|
||||
E.set_text(page, f"{DIM}TextBoxWallPerimeter1stFloor", "21.24")
|
||||
E.set_text(page, f"{DIM}TextBoxPartyWallLength1stFloor", "0")
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def walls(page):
|
||||
E.goto(page, "Walls", "WebFormWalls.aspx")
|
||||
print("wall type ->", _pick(page, f"{WALL}DropDownListType", "solid brick"))
|
||||
page.wait_for_timeout(400)
|
||||
print("insulation ->", _pick(page, f"{WALL}DropDownListInsulation", "as built"))
|
||||
# No party wall lodged (party_wall_length_m = 0 on both floors).
|
||||
pw = _pick(page, f"{PWALL}DropDownListPartyWallType", "determine") or \
|
||||
_pick(page, f"{PWALL}DropDownListPartyWallType", "none")
|
||||
print("party wall ->", pw)
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def roofs(page):
|
||||
# Two lodged roof elements: "Pitched, no insulation (assumed)" (main) and
|
||||
# "Roof room(s), insulated". Enter the main roof as no-insulation pitched;
|
||||
# the insulated roof-room element is flagged via Property Description's
|
||||
# Room-in-Roof age band (set above) rather than a 2nd roof row (Elmhurst's
|
||||
# main-roof tab is single-element).
|
||||
E.goto(page, "Roofs", "WebFormRoofs.aspx")
|
||||
print("roof type ->", _pick(page, f"{ROOF}DropDownListType", "access to loft"))
|
||||
print("insulation at ->", _pick(page, f"{ROOF}DropDownListInsulationAt", "joists"))
|
||||
print("thickness ->", _pick(page, f"{ROOF}DropDownListThickness", "no insulation") or
|
||||
_pick(page, f"{ROOF}DropDownListThickness", "0 mm"))
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def floors(page):
|
||||
E.goto(page, "Floors", "WebFormFloors.aspx")
|
||||
print("location ->", _pick(page, f"{FLOOR}DropDownListLocation", "ground"))
|
||||
print("floor type ->", _pick(page, f"{FLOOR}DropDownListType", "suspended"))
|
||||
print("insulation ->", _pick(page, f"{FLOOR}DropDownListInsulation", "as built"))
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def openings(page):
|
||||
E.goto(page, "Openings", "WebFormOpenings.aspx")
|
||||
E.click_tab(page, "TabContainer_TabPanelWindowsPanel")
|
||||
# windows[].description = "Fully double glazed", no install-date band lodged.
|
||||
glazing = "Double post or during 2022"
|
||||
for opt in _options(page, "TabContainer_TabPanelWindowsPanel_DropDownListExtGlazing"):
|
||||
low = opt.lower()
|
||||
if "unknown install date" in low and "single" not in low and "triple" not in low:
|
||||
glazing = opt
|
||||
break
|
||||
print("glazing ->", glazing)
|
||||
E.set_single_window(page, 18.66 + 1.77, orientation="South", glazing=glazing)
|
||||
E.click_tab(page, "TabContainer_TabPanelDoorsPanel")
|
||||
E.set_text(page, f"{DP}TextBoxDoors", "2")
|
||||
E.set_text(page, f"{DP}TextBoxDoorsInsulated", "0")
|
||||
E.set_text(page, f"{DP}TextBoxDraughtProofedDoors", "2")
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def ventilation(page):
|
||||
E.goto(page, "VentilationAndCooling", "WebFormVentilationAndCooling.aspx")
|
||||
E.click_tab(page, "TabContainer_TabPanelVentilationPanel")
|
||||
E.set_text(page, f"{VP}TextBoxIntermittentFans", "0")
|
||||
E.set_text(page, f"{VP}TextBoxOpenChimneys", "2")
|
||||
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", "14")
|
||||
E.set_text(page, f"{LP}TextBoxLedLightsTotal", "5")
|
||||
E.set_text(page, f"{LP}TextBoxCflLightsTotal", "0")
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def space_heating(page):
|
||||
# Mains gas boiler + radiators, exact PCDB index 8108, controls SAP 2102.
|
||||
E.goto(page, "SpaceHeating", "WebFormSpaceHeating.aspx")
|
||||
page.wait_for_timeout(1000)
|
||||
E.clear_main_heating_code(page)
|
||||
desc = E.set_pcdb_boiler(page, 8108)
|
||||
print("boiler resolved ->", desc)
|
||||
E.set_heating_dialog(page, f"{MH1B}ButtonMainHeatingControls",
|
||||
"^Boilers", "^Standard", "programmer, room thermostat")
|
||||
print("control:", page.locator(f"#{E.MH1}TextBoxMainHeatingControls").input_value())
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def secondary(page):
|
||||
# Lodged secondary: "Room heaters, smokeless fuel".
|
||||
E.goto(page, "SpaceHeating", "WebFormSpaceHeating.aspx")
|
||||
page.wait_for_timeout(600)
|
||||
E.set_select(page, "DropDownListSecondaryHeatingPresent", "Yes")
|
||||
page.wait_for_timeout(900)
|
||||
E.set_heating_dialog(page, "ButtonSecondaryHeatingCode",
|
||||
"Solid", "Room Heater", "smokeless")
|
||||
tb = page.locator(f"#{E.FP}TextBoxSecondaryHeatingCode")
|
||||
print("secondary code:", tb.input_value() if tb.count() else "?")
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def water_heating(page):
|
||||
# has_hot_water_cylinder = false -> from the main heating system, no cylinder.
|
||||
E.goto(page, "WaterHeating", "WebFormWaterHeating.aspx")
|
||||
E.click_tab(page, "TabContainer_TabPanelWaterHeating")
|
||||
page.wait_for_timeout(400)
|
||||
E.clear_hot_water_cylinder(page)
|
||||
E.set_heating_dialog(page, f"{WH}ButtonWaterHeatingCode",
|
||||
"From Space Heating", "From the primary heating system")
|
||||
print("water code:", page.locator(f"#{E.FP}{WH}TextBoxWaterHeatingCode").input_value())
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
_ORDER = ["property_description", "dimensions", "walls", "roofs", "floors",
|
||||
"openings", "ventilation", "space_heating", "secondary",
|
||||
"water_heating"]
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2 or sys.argv[1] not in _ORDER + ["all"]:
|
||||
print("usage: build_100021969385.py <" + "|".join(_ORDER + ["all"]) + ">")
|
||||
return 2
|
||||
with E.session() as (ctx, page):
|
||||
if sys.argv[1] == "all":
|
||||
for step in _ORDER:
|
||||
print(f"=== {step} ===")
|
||||
globals()[step](page)
|
||||
else:
|
||||
globals()[sys.argv[1]](page)
|
||||
print("done:", sys.argv[1], "->", page.url)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Loading…
Add table
Reference in a new issue