Model/scripts/hyde/build_22086693.py
Jun-te Kim 8f0432721c test(accuracy): pin RdSAP-20.0.0 PV semi uprn_22086693 (PV-list fix corpus)
Corpus validation of the modelling_e2e photovoltaic_supply-as-list fix. Cert
6102-6227-8000-0083-2292 (RdSAP-20.0.0 semi, gas combi + 2× 1.14 kW PV arrays)
crashed from_rdsap_schema_20_0_0 on the measured-array list; the fix routes it
through the dict-tolerant _map_schema_21_pv. PV correctly credited: engine 61
(no PV) → 66 (+5). Built in Elmhurst (evidence: epc.json + summary + worksheet,
fabric+heating; the PV "New Technologies" Panel-details grid deferred): worksheet
55 = engine-on-Elmhurst-inputs 55 exactly → calculator faithful. The +6 engine-vs-
Elmhurst base-dwelling residual is the documented RdSAP-default gap (band-C cavity-
uninsulated suspended-floor semi). Pinned engine 66.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 16:13:34 +00:00

307 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Elmhurst build for UPRN 22086693 (RdSAP-Schema-20.0.0, SEMI-DETACHED HOUSE,
2-storey, band C 1930-1949, cavity UNINSULATED, mains-gas COMBI (no PCDB index →
generic SAP Table 4b combi), control 2106 (CBE), pitched 200 mm loft, suspended
uninsulated floor, party wall 6.8 m, double glazed, 2× PV ARRAYS @1.14 kW
(orientations SE/SW, pitch 30°, overshading modest), electric SECONDARY room
heater (SAP 691 REA), TFA ~74, window 11.84 m². Engine 66 / lodged 72.
P2 of the modelling_e2e corpus validation — the photovoltaic_supply-as-list fix
cert. The PV adds +5 (engine 61→66) so it IS credited; the 6 vs lodged is a
fabric/heating gap to localise. NEW build elements: generic (no-PCDB) combi +
the Renewables/PV page. Run:
DISPLAY=:99 python scripts/hyde/build_22086693.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_"
WP = "TabContainer_TabPanelWindowsPanel_"
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", "4")
E.set_text(page, "TextBoxHeatedHabitableRooms", "4")
print("date ->", _pick(page, "DropDownListDateBuiltMain", "1930-1949")) # band C
E.set_select(page, "DropDownListDateBuiltFirst", "")
E.set_select(page, "DropDownListRoomInRoofMain", "")
E.save_close(page)
def dimensions(page):
E.goto(page, "Dimensions", "WebFormDimensions.aspx")
E.set_text(page, f"{DIM}TextBoxFloorAreaLowestFloor", "36.86")
E.set_text(page, f"{DIM}TextBoxRoomHeightLowestFloor", "2.30")
E.set_text(page, f"{DIM}TextBoxWallPerimeterLowestFloor", "13.4")
E.set_text(page, f"{DIM}TextBoxPartyWallLengthLowestFloor", "6.8")
E.set_text(page, f"{DIM}TextBoxFloorArea1stFloor", "36.86")
E.set_text(page, f"{DIM}TextBoxRoomHeight1stFloor", "2.30")
E.set_text(page, f"{DIM}TextBoxWallPerimeter1stFloor", "17.4")
E.set_text(page, f"{DIM}TextBoxPartyWallLength1stFloor", "6.8")
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", "as built")) # uninsulated
# Semi party wall: cavity. Match "masonry filled" → CF (U≈0); avoid loose "filled".
pw = _pick(page, f"{PWALL}DropDownListPartyWallType", "masonry filled") \
or _pick(page, f"{PWALL}DropDownListPartyWallType", "determine")
print("party wall ->", pw)
E.save_close(page)
def roofs(page):
E.goto(page, "Roofs", "WebFormRoofs.aspx")
_pick(page, f"{ROOF}DropDownListType", "access to loft")
_pick(page, f"{ROOF}DropDownListInsulationAt", "joists")
E.set_select(page, f"{ROOF}DropDownListThickness", "200 mm")
E.save_close(page)
def floors(page):
E.goto(page, "Floors", "WebFormFloors.aspx")
E.set_select(page, f"{FLOOR}DropDownListLocation", "G Ground floor")
_pick(page, f"{FLOOR}DropDownListType", "suspended timber")
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, 11.84, "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 fix_window(page):
"""Edit-in-place: overwrite the shared assessment's leftover window width to ours."""
E.goto(page, "Openings", "WebFormOpenings.aspx")
E.click_tab(page, "TabContainer_TabPanelWindowsPanel")
page.wait_for_timeout(800)
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, "11.84"])
print("window width now:", page.locator(f"#{wid}").input_value())
E.save_close(page)
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", "10")
E.set_text(page, f"{LP}TextBoxLedLightsTotal", "10") # 100% LED
E.set_text(page, f"{LP}TextBoxCflLightsTotal", "0")
E.save_close(page)
def space_heating(page):
# Generic (no-PCDB) mains-gas combi. The shared assessment may carry a prior
# cert's storage SEB / PCDB boiler — clear it first if present, then select a
# generic gas combi via the boiler dialog. Two passes if a SAP code is bound.
E.goto(page, "SpaceHeating", "WebFormSpaceHeating.aspx")
page.wait_for_timeout(1000)
# Pass 1: clear a leftover SAP-table MainHeatingCode (e.g. SEB) so the boiler
# search/dialog is usable.
mhc = page.locator(f"#{E.MH1}TextBoxMainHeatingCode")
code = mhc.input_value() if mhc.count() else ""
if code and code not in ("0",):
print(f"clearing leftover MainHeatingCode {code}")
page.evaluate("""(id)=>{const e=document.getElementById(id);if(e){e.value='';
e.dispatchEvent(new Event('change',{bubbles:true}));}}""", f"{E.MH1}TextBoxMainHeatingCode")
page.wait_for_timeout(400)
E.save_close(page)
return
# Generic (no-PCDB) mains-gas boiler via the cascade: Gas → Mains gas →
# Boilers → Post 1998 → Condensing (combi vs regular is set by the water-
# heating page: "from primary" + no cylinder = combi). ~89% SAP Table 4b.
E.set_heating_dialog(page, f"{MH1B}ButtonMainHeatingCode",
"^Gas", "Mains gas", "Boilers", "Post 1998", "Condensing",
"Combi condens") # L6 = BGW Post 98 Combi condens. (SAP 4b ~89%)
print("code:", page.locator(f"#{E.MH1}TextBoxMainHeatingCode").input_value())
E.set_heating_dialog(page, f"{MH1B}ButtonMainHeatingControls",
"^Boilers", "^Standard", "CBE Programmer, room thermostat and TRVs")
print("control:", page.locator(f"#{E.MH1}TextBoxMainHeatingControls").input_value())
E.save_close(page)
def secondary(page):
# Lodged secondary: SAP 691 = REA electric panel/convector/radiant room heater.
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",
"Electric", "Electric", "Room Heater", "REA Panel")
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):
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)
def renewables(page):
# 2× PV arrays @1.14 kW. Discover the Renewables/PV page structure.
for url in ("WebFormRenewables.aspx", "WebFormPhotovoltaics.aspx", "WebFormSolar.aspx"):
try:
E.goto(page, url.replace("WebForm", "").replace(".aspx", ""), url)
page.wait_for_timeout(800)
print("PV page:", url, "->", page.url)
inputs = page.evaluate(
"""()=>Array.from(document.querySelectorAll("[id*=ContentPlaceHolder1] input,[id*=ContentPlaceHolder1] select")).map(e=>e.id.replace('ContentBody_ContentPlaceHolder1_','')).filter(i=>/pv|photov|solar|peak|orient|pitch|oversh|panel/i.test(i)).slice(0,30)""")
print("PV-ish fields:", inputs)
return
except Exception as e:
print(" ", url, "->", type(e).__name__)
_ORDER = ["property_description", "dimensions", "walls", "roofs", "floors",
"openings", "fix_window", "ventilation", "space_heating", "secondary",
"water_heating", "renewables"]
def main():
if len(sys.argv) < 2 or sys.argv[1] not in _ORDER:
print("usage: build_22086693.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())