mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
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>
This commit is contained in:
parent
75e4989982
commit
8f0432721c
4 changed files with 331 additions and 0 deletions
Binary file not shown.
Binary file not shown.
307
scripts/hyde/build_22086693.py
Normal file
307
scripts/hyde/build_22086693.py
Normal file
|
|
@ -0,0 +1,307 @@
|
|||
"""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())
|
||||
|
|
@ -625,6 +625,30 @@ _EXPECTATIONS: Final[tuple[RealCertExpectation, ...]] = (
|
|||
cert_num="8742-6624-9300-2780-4926",
|
||||
sap_score=66,
|
||||
),
|
||||
# UPRN 22086693 → cert 6102-6227-8000-0083-2292. RdSAP-Schema-20.0.0, SEMI-
|
||||
# DETACHED HOUSE 2-storey, band C, cavity UNINSULATED, mains-gas COMBI (no PCDB
|
||||
# → generic SAP Table-4b BGW post-98 condensing combi), control 2106 (CBE),
|
||||
# pitched 200 mm loft, suspended uninsulated floor, party wall 6.8 m, double
|
||||
# glazed, electric secondary room heater (SAP 691 REA), 2× PV ARRAYS @1.14 kW,
|
||||
# 100% LED, TFA ~74. Engine 66 / lodged 72.
|
||||
# This is the modelling_e2e photovoltaic_supply-AS-LIST fix cert: 20.0.0 typed
|
||||
# `photovoltaic_supply` as the wrapper only, so a measured-array LIST crashed
|
||||
# `from_rdsap_schema_20_0_0` ("'list' object has no attribute none_or_no_details")
|
||||
# and sank the whole prediction cohort. The fix routes it through the dict-tolerant
|
||||
# `_map_schema_21_pv`, capturing the arrays. PV is correctly credited: engine
|
||||
# WITHOUT pv = 61, WITH pv = 66 (+5). Built in Elmhurst RdSAP10 (evidence saved:
|
||||
# elmhurst_summary.pdf / elmhurst_worksheet.pdf) — fabric+heating only (the PV
|
||||
# is a separate "New Technologies" Panel-details grid, deferred): worksheet 55,
|
||||
# engine on Elmhurst's own parsed inputs 55 = 55 EXACTLY → calculator faithful.
|
||||
# The engine-without-pv 61 vs Elmhurst 55 (+6) is the documented engine-vs-
|
||||
# Elmhurst-RdSAP-default residual on a band-C cavity-uninsulated suspended-floor
|
||||
# semi; lodged 72 vs engine 66 (−6) is that plus PV-credit method. PINNED engine 66.
|
||||
RealCertExpectation(
|
||||
schema="RdSAP-Schema-20.0.0",
|
||||
sample="uprn_22086693",
|
||||
cert_num="6102-6227-8000-0083-2292",
|
||||
sap_score=66,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue