mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
test(accuracy): corpus-1000 campaign ledger + C001 4510053280 pinned 75 (lodged-software PSR gap)
Setup for the RdSAP-21.0.1 corpus gauge campaign (74.2% within-0.5, SAP MAE 0.721 at HEAD, 1000/1000 computed): - scripts/corpus_1000/build_worklist.py — runs all 1000 corpus certs through the gauge's own path (from_api_response -> Sap10Calculator), ranks |engine - lodged| descending, clusters by dwelling signature, and (re)writes scripts/corpus_1000/worklist.md preserving per-cert statuses and notes. 258 certs outside 0.5 in 249 clusters. - C001 (worst cert, Δ +23.9): uprn 4510053280, ground-floor flat 47 m² on ASHP PCDB 100053 (Mitsubishi Ecodan 5 kW, PSR table 0.2-2.0). Dwelling PSR 2.031 -> SAP 10.2 N2 footnotes 44/45 heat-pump extension (reciprocal interpolation toward 100% at 2x largest PSR) = 305% space efficiency, which accredited Elmhurst also applies (golden case 56, record 100061). The LODGED software instead treated the out-of-range record as invalid and billed 100% direct electric + standard schedule (reproduces lodged CO2 to 1%). Lodged-software methodology gap, worklist ⚠, engine untouched; observed engine 75 pinned in test_real_cert_sap_accuracy. Gauge floors unchanged (no engine change). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
fad2264def
commit
b9f1f428b9
4 changed files with 2079 additions and 0 deletions
|
|
@ -0,0 +1,299 @@
|
|||
{
|
||||
"uprn": 4510053280,
|
||||
"roofs": [
|
||||
{
|
||||
"description": "(another dwelling above)",
|
||||
"energy_efficiency_rating": 0,
|
||||
"environmental_efficiency_rating": 0
|
||||
}
|
||||
],
|
||||
"walls": [
|
||||
{
|
||||
"description": "Solid brick, as built, no insulation (assumed)",
|
||||
"energy_efficiency_rating": 2,
|
||||
"environmental_efficiency_rating": 2
|
||||
},
|
||||
{
|
||||
"description": "Solid brick, as built, partial insulation (assumed)",
|
||||
"energy_efficiency_rating": 3,
|
||||
"environmental_efficiency_rating": 3
|
||||
}
|
||||
],
|
||||
"floors": [
|
||||
{
|
||||
"description": "Solid, no insulation (assumed)",
|
||||
"energy_efficiency_rating": 0,
|
||||
"environmental_efficiency_rating": 0
|
||||
}
|
||||
],
|
||||
"status": "entered",
|
||||
"tenure": 2,
|
||||
"window": {
|
||||
"description": "Full secondary glazing",
|
||||
"energy_efficiency_rating": 2,
|
||||
"environmental_efficiency_rating": 2
|
||||
},
|
||||
"lighting": {
|
||||
"description": "Good lighting efficiency",
|
||||
"energy_efficiency_rating": 4,
|
||||
"environmental_efficiency_rating": 4
|
||||
},
|
||||
"postcode": "NE1 4DG",
|
||||
"hot_water": {
|
||||
"description": "From main system, no cylinder thermostat",
|
||||
"energy_efficiency_rating": 1,
|
||||
"environmental_efficiency_rating": 4
|
||||
},
|
||||
"post_town": "NEWCASTLE UPON TYNE",
|
||||
"built_form": "NR",
|
||||
"created_at": "2026-05-24 12:41:32",
|
||||
"door_count": 0,
|
||||
"region_code": 1,
|
||||
"report_type": 2,
|
||||
"sap_heating": {
|
||||
"number_baths": 1,
|
||||
"cylinder_size": 1,
|
||||
"number_baths_wwhrs": 0,
|
||||
"water_heating_code": 901,
|
||||
"water_heating_fuel": 29,
|
||||
"secondary_fuel_type": 29,
|
||||
"main_heating_details": [
|
||||
{
|
||||
"has_fghrs": "N",
|
||||
"main_fuel_type": 29,
|
||||
"heat_emitter_type": 1,
|
||||
"emitter_temperature": 0,
|
||||
"main_heating_number": 1,
|
||||
"main_heating_control": 2203,
|
||||
"main_heating_category": 4,
|
||||
"main_heating_fraction": 1,
|
||||
"mcs_installed_heat_pump": "false",
|
||||
"central_heating_pump_age": 0,
|
||||
"main_heating_data_source": 1,
|
||||
"main_heating_index_number": 100053
|
||||
}
|
||||
],
|
||||
"immersion_heating_type": "NA",
|
||||
"secondary_heating_type": 691,
|
||||
"has_fixed_air_conditioning": "false"
|
||||
},
|
||||
"sap_version": 10.2,
|
||||
"sap_windows": [
|
||||
{
|
||||
"orientation": 4,
|
||||
"window_type": 1,
|
||||
"glazing_type": 4,
|
||||
"window_width": 1.3,
|
||||
"window_height": 1.46,
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
},
|
||||
{
|
||||
"orientation": 4,
|
||||
"window_type": 1,
|
||||
"glazing_type": 4,
|
||||
"window_width": 1.3,
|
||||
"window_height": 1.46,
|
||||
"draught_proofed": "true",
|
||||
"window_location": 0,
|
||||
"window_wall_type": 1,
|
||||
"permanent_shutters_present": "N",
|
||||
"permanent_shutters_insulated": "N"
|
||||
}
|
||||
],
|
||||
"schema_type": "RdSAP-Schema-21.0.1",
|
||||
"uprn_source": "Energy Assessor",
|
||||
"country_code": "ENG",
|
||||
"main_heating": [
|
||||
{
|
||||
"description": "Air source heat pump, radiators, electric",
|
||||
"energy_efficiency_rating": 4,
|
||||
"environmental_efficiency_rating": 5
|
||||
}
|
||||
],
|
||||
"air_tightness": {
|
||||
"description": "(not tested)",
|
||||
"energy_efficiency_rating": 0,
|
||||
"environmental_efficiency_rating": 0
|
||||
},
|
||||
"dwelling_type": "Ground-floor flat",
|
||||
"language_code": 1,
|
||||
"pressure_test": 4,
|
||||
"property_type": 2,
|
||||
"address_line_1": "64 Waterloo Street",
|
||||
"assessment_type": "RdSAP",
|
||||
"completion_date": "2026-05-24",
|
||||
"inspection_date": "2026-05-22",
|
||||
"extensions_count": 0,
|
||||
"measurement_type": 1,
|
||||
"sap_flat_details": {
|
||||
"level": 1,
|
||||
"top_storey": "N",
|
||||
"storey_count": 3,
|
||||
"flat_location": 0,
|
||||
"heat_loss_corridor": 2,
|
||||
"unheated_corridor_length": 3.5
|
||||
},
|
||||
"total_floor_area": 47,
|
||||
"transaction_type": 8,
|
||||
"conservatory_type": 1,
|
||||
"has_draught_lobby": "true",
|
||||
"heated_room_count": 2,
|
||||
"registration_date": "2026-05-24",
|
||||
"sap_energy_source": {
|
||||
"mains_gas": "N",
|
||||
"meter_type": 2,
|
||||
"pv_connection": 0,
|
||||
"photovoltaic_supply": {
|
||||
"none_or_no_details": {
|
||||
"percent_roof_area": 0
|
||||
}
|
||||
},
|
||||
"wind_turbines_count": 0,
|
||||
"gas_smart_meter_present": "false",
|
||||
"is_dwelling_export_capable": "false",
|
||||
"wind_turbines_terrain_type": 1,
|
||||
"electricity_smart_meter_present": "false"
|
||||
},
|
||||
"secondary_heating": {
|
||||
"description": "Room heaters, electric",
|
||||
"energy_efficiency_rating": 0,
|
||||
"environmental_efficiency_rating": 0
|
||||
},
|
||||
"lzc_energy_sources": [
|
||||
9
|
||||
],
|
||||
"sap_building_parts": [
|
||||
{
|
||||
"identifier": "Main Dwelling",
|
||||
"wall_dry_lined": "N",
|
||||
"wall_thickness": 520,
|
||||
"floor_heat_loss": 7,
|
||||
"roof_construction": 3,
|
||||
"wall_construction": 3,
|
||||
"building_part_number": 1,
|
||||
"sap_floor_dimensions": [
|
||||
{
|
||||
"floor": 0,
|
||||
"room_height": {
|
||||
"value": 2.32,
|
||||
"quantity": "metres"
|
||||
},
|
||||
"floor_insulation": 1,
|
||||
"total_floor_area": {
|
||||
"value": 47,
|
||||
"quantity": "square metres"
|
||||
},
|
||||
"party_wall_length": {
|
||||
"value": 8,
|
||||
"quantity": "metres"
|
||||
},
|
||||
"floor_construction": 1,
|
||||
"heat_loss_perimeter": {
|
||||
"value": 14.15,
|
||||
"quantity": "metres"
|
||||
}
|
||||
}
|
||||
],
|
||||
"wall_insulation_type": 4,
|
||||
"construction_age_band": "B",
|
||||
"sap_alternative_wall_1": {
|
||||
"wall_area": 8.12,
|
||||
"sheltered_wall": "Y",
|
||||
"wall_dry_lined": "N",
|
||||
"wall_thickness": 520,
|
||||
"wall_construction": 3,
|
||||
"wall_insulation_type": 4,
|
||||
"wall_thickness_measured": "Y",
|
||||
"wall_insulation_thickness": "NI"
|
||||
},
|
||||
"party_wall_construction": 0,
|
||||
"wall_thickness_measured": "Y",
|
||||
"roof_insulation_location": "ND",
|
||||
"roof_insulation_thickness": "ND",
|
||||
"wall_insulation_thickness": "NI",
|
||||
"floor_insulation_thickness": "NI"
|
||||
}
|
||||
],
|
||||
"solar_water_heating": "N",
|
||||
"habitable_room_count": 2,
|
||||
"heating_cost_current": {
|
||||
"value": 278,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"insulated_door_count": 0,
|
||||
"co2_emissions_current": 0.8,
|
||||
"energy_rating_average": 60,
|
||||
"energy_rating_current": 51,
|
||||
"lighting_cost_current": {
|
||||
"value": 39,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"main_heating_controls": [
|
||||
{
|
||||
"description": "Room thermostat only",
|
||||
"energy_efficiency_rating": 2,
|
||||
"environmental_efficiency_rating": 2
|
||||
}
|
||||
],
|
||||
"has_hot_water_cylinder": "false",
|
||||
"heating_cost_potential": {
|
||||
"value": 272,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"hot_water_cost_current": {
|
||||
"value": 1295,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"mechanical_ventilation": 0,
|
||||
"percent_draughtproofed": 100,
|
||||
"suggested_improvements": [
|
||||
{
|
||||
"sequence": 1,
|
||||
"typical_saving": {
|
||||
"value": 68,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"indicative_cost": "\u00a3220 - \u00a3250",
|
||||
"improvement_type": "G",
|
||||
"improvement_details": {
|
||||
"improvement_number": 16
|
||||
},
|
||||
"improvement_category": 5,
|
||||
"energy_performance_rating": 53,
|
||||
"environmental_impact_rating": 88
|
||||
}
|
||||
],
|
||||
"co2_emissions_potential": 0.8,
|
||||
"energy_rating_potential": 53,
|
||||
"lighting_cost_potential": {
|
||||
"value": 39,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"schema_version_original": "21.0.1",
|
||||
"hot_water_cost_potential": {
|
||||
"value": 1233,
|
||||
"currency": "GBP"
|
||||
},
|
||||
"renewable_heat_incentive": {
|
||||
"water_heating": 5072.68,
|
||||
"space_heating_existing_dwelling": 2884.0
|
||||
},
|
||||
"draughtproofed_door_count": 0,
|
||||
"energy_consumption_current": 190,
|
||||
"has_fixed_air_conditioning": "false",
|
||||
"multiple_glazed_proportion": 100,
|
||||
"calculation_software_version": "5.02r0344",
|
||||
"energy_consumption_potential": 182,
|
||||
"environmental_impact_current": 88,
|
||||
"current_energy_efficiency_band": "E",
|
||||
"environmental_impact_potential": 88,
|
||||
"has_heated_separate_conservatory": "false",
|
||||
"potential_energy_efficiency_band": "E",
|
||||
"co2_emissions_current_per_floor_area": 18,
|
||||
"low_energy_fixed_lighting_bulbs_count": 6,
|
||||
"incandescent_fixed_lighting_bulbs_count": 0
|
||||
}
|
||||
243
scripts/corpus_1000/build_worklist.py
Normal file
243
scripts/corpus_1000/build_worklist.py
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
"""Build the RdSAP-21.0.1 corpus campaign ledger (scripts/corpus_1000/worklist.md).
|
||||
|
||||
Runs all 1000 corpus certs through the gauge's own path
|
||||
(from_api_response -> Sap10Calculator.calculate, mirroring
|
||||
tests/infrastructure/epc_client/test_sap_accuracy_corpus.py), ranks by
|
||||
|engine - lodged| descending, attaches a short dwelling signature per cert,
|
||||
and groups same-signature certs into named clusters.
|
||||
|
||||
USAGE
|
||||
-----
|
||||
PYTHONPATH=/workspaces/model python scripts/corpus_1000/build_worklist.py
|
||||
|
||||
Rewrites worklist.md in place, PRESERVING existing per-cert statuses
|
||||
([x] / 🔧 / ⚠ / ⛔ and trailing `<-` notes) keyed by uprn, so it doubles as
|
||||
the re-rank step after each fix lands.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
|
||||
from datatypes.epc.domain.mapper import EpcPropertyDataMapper
|
||||
from domain.sap10_calculator.calculator import Sap10Calculator
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
CORPUS = ROOT / "backend/epc_api/json_samples/RdSAP-Schema-21.0.1/corpus.jsonl"
|
||||
WORKLIST = ROOT / "scripts/corpus_1000/worklist.md"
|
||||
|
||||
# Table 12 fuel names (abbreviated) for the signature column.
|
||||
_FUEL = {
|
||||
1: "gas", 2: "gas-bulk", 3: "biogas", 4: "biogas", 7: "bLPG", 9: "LPG",
|
||||
17: "LPG-sc", 10: "oil", 15: "bioliquid", 16: "B30K", 18: "rapeseed",
|
||||
58: "biodiesel", 51: "housecoal", 20: "coal-anth", 21: "smokeless",
|
||||
22: "anthracite", 23: "manuf-smokeless", 12: "wood-log", 13: "wood-pellet",
|
||||
75: "wood-pellet-bag", 71: "wood-chip", 48: "dual-mineral-wood",
|
||||
26: "gas", 25: "elec", 29: "elec", 30: "elec7hr", 31: "elec10hr",
|
||||
32: "elec18hr", 33: "elec24hr", 34: "elec-heatnet", 35: "elec",
|
||||
36: "elec", 37: "elec", 47: "heatnet", 6: "heatnet",
|
||||
}
|
||||
|
||||
|
||||
def _fuel_name(code: Optional[int]) -> str:
|
||||
if code is None:
|
||||
return "?"
|
||||
return _FUEL.get(code, f"fuel{code}")
|
||||
|
||||
|
||||
def _lead(desc: Optional[str], n: int = 3) -> str:
|
||||
"""First n comma/word tokens of a register description, lowercased."""
|
||||
if not desc:
|
||||
return "?"
|
||||
return " ".join(desc.lower().replace(",", " ").split()[:n])
|
||||
|
||||
|
||||
def signature(doc: dict[str, Any]) -> tuple[str, str]:
|
||||
"""(cluster_key, human signature) for one cert."""
|
||||
h = doc.get("sap_heating") or {}
|
||||
mhd = (h.get("main_heating_details") or [{}])
|
||||
m0 = mhd[0] if mhd else {}
|
||||
code = m0.get("sap_main_heating_code")
|
||||
idx = m0.get("main_heating_index_number")
|
||||
fuel = _fuel_name(m0.get("main_fuel_type"))
|
||||
heat = f"h{code or ('pcdb' + str(idx) if idx else '?')}/{fuel}"
|
||||
if len(mhd) > 1:
|
||||
heat += "+2mains"
|
||||
|
||||
walls = doc.get("walls") or []
|
||||
wall = _lead(walls[0].get("description") if walls else None, 2)
|
||||
roofs = doc.get("roofs") or []
|
||||
roof = _lead(roofs[0].get("description") if roofs else None, 2)
|
||||
|
||||
es = doc.get("sap_energy_source") or {}
|
||||
sec = (doc.get("secondary_heating") or {}).get("description") or "None"
|
||||
feats: list[str] = []
|
||||
pv = es.get("photovoltaic_supply") or {}
|
||||
if es.get("pv_connection") not in (0, None) or not pv.get("none_or_no_details"):
|
||||
feats.append("PV")
|
||||
if (es.get("wind_turbines_count") or 0) > 0:
|
||||
feats.append("wind")
|
||||
if sec != "None":
|
||||
feats.append("sec:" + _lead(sec, 2))
|
||||
if (doc.get("extensions_count") or 0) > 0:
|
||||
feats.append(f"ext{doc.get('extensions_count')}")
|
||||
if (doc.get("conservatory_type") or 1) != 1:
|
||||
feats.append(f"consv{doc.get('conservatory_type')}")
|
||||
for bp in doc.get("sap_building_parts") or []:
|
||||
if bp.get("sap_room_in_roof") is not None:
|
||||
feats.append("RR")
|
||||
break
|
||||
mv = doc.get("mechanical_ventilation")
|
||||
if mv not in (0, None):
|
||||
feats.append(f"mv{mv}")
|
||||
if (h.get("water_heating_code") or 901) != 901:
|
||||
feats.append(f"whw{h.get('water_heating_code')}")
|
||||
|
||||
key = f"{heat} | {wall} | {roof}" + (" | " + ",".join(sorted(set(feats))) if feats else "")
|
||||
sig = key
|
||||
return key, sig
|
||||
|
||||
|
||||
_LINE_RE = re.compile(
|
||||
r"^- \[(?P<st>[ x])\] |^- (?P<emoji>🔧|⚠|⛔) "
|
||||
)
|
||||
|
||||
|
||||
def load_existing_statuses() -> dict[str, tuple[str, str]]:
|
||||
"""uprn -> (status token, trailing note) from the current worklist."""
|
||||
out: dict[str, tuple[str, str]] = {}
|
||||
if not WORKLIST.exists():
|
||||
return out
|
||||
for line in WORKLIST.read_text().splitlines():
|
||||
m = re.match(r"^- (?:\[(?P<st>[ x])\]|(?P<emoji>🔧|⚠|⛔)) `(?P<uprn>\w+)`", line)
|
||||
if not m:
|
||||
continue
|
||||
status = f"[{m.group('st')}]" if m.group("st") is not None else m.group("emoji")
|
||||
note = ""
|
||||
if "<-" in line:
|
||||
note = "<-" + line.split("<-", 1)[1]
|
||||
out[m.group("uprn")] = (status, note)
|
||||
return out
|
||||
|
||||
|
||||
def main() -> None:
|
||||
docs = [json.loads(l) for l in CORPUS.read_text().splitlines() if l.strip()]
|
||||
calc = Sap10Calculator()
|
||||
rows: list[dict[str, Any]] = []
|
||||
skipped: list[tuple[str, str]] = []
|
||||
sap_abs: list[float] = []
|
||||
|
||||
for idx, doc in enumerate(docs, 1):
|
||||
# Some corpus docs lodge no top-level uprn; fall back to the corpus
|
||||
# line number so ledger statuses don't collide on "None".
|
||||
uprn = str(doc.get("uprn") or f"line{idx}")
|
||||
lodged = doc.get("energy_rating_current")
|
||||
if lodged is None:
|
||||
skipped.append((uprn, "no lodged SAP"))
|
||||
continue
|
||||
try:
|
||||
epc = EpcPropertyDataMapper.from_api_response(doc)
|
||||
result = calc.calculate(epc)
|
||||
except Exception as e:
|
||||
skipped.append((uprn, f"{type(e).__name__}: {str(e)[:70]}"))
|
||||
continue
|
||||
eng = result.sap_score_continuous
|
||||
delta = eng - lodged
|
||||
sap_abs.append(abs(delta))
|
||||
key, sig = signature(doc)
|
||||
rows.append({
|
||||
"uprn": uprn, "lodged": lodged, "eng": eng, "delta": delta,
|
||||
"key": key, "sig": sig,
|
||||
})
|
||||
|
||||
n = len(sap_abs)
|
||||
within = sum(1 for e in sap_abs if e < 0.5) / n
|
||||
mae = sum(sap_abs) / n
|
||||
|
||||
# Cluster: same signature key, only certs still outside 0.5.
|
||||
clusters: dict[str, list[dict[str, Any]]] = defaultdict(list)
|
||||
for r in rows:
|
||||
if abs(r["delta"]) >= 0.5:
|
||||
clusters[r["key"]].append(r)
|
||||
ranked = sorted(
|
||||
clusters.items(),
|
||||
key=lambda kv: -sum(abs(r["delta"]) for r in kv[1]),
|
||||
)
|
||||
|
||||
prev = load_existing_statuses()
|
||||
|
||||
lines: list[str] = []
|
||||
lines.append("# RdSAP-21.0.1 corpus campaign ledger")
|
||||
lines.append("")
|
||||
lines.append(
|
||||
f"**Gauge:** {n} computed / {len(skipped)} skipped · "
|
||||
f"SAP within-0.5 = **{within:.1%}** · MAE = **{mae:.3f}** "
|
||||
f"(floors in test_sap_accuracy_corpus.py)"
|
||||
)
|
||||
lines.append("")
|
||||
lines.append(
|
||||
"Statuses: `[ ]` todo · `[x]` resolved ≤0.5 · 🔧 fix landed · "
|
||||
"⚠ xfail engine bug · ⛔ unbuildable. "
|
||||
"Line: `uprn · lodged L / eng E / Δ · signature`. "
|
||||
"Rebuild/re-rank: `PYTHONPATH=. python scripts/corpus_1000/build_worklist.py` "
|
||||
"(preserves statuses & `<-` notes)."
|
||||
)
|
||||
lines.append("")
|
||||
lines.append(f"## Clusters (certs with |Δ| ≥ 0.5, ranked by summed |Δ|) — {sum(len(v) for v in clusters.values())} certs in {len(ranked)} clusters")
|
||||
lines.append("")
|
||||
|
||||
for i, (key, members) in enumerate(ranked, 1):
|
||||
members.sort(key=lambda r: -abs(r["delta"]))
|
||||
tot = sum(abs(r["delta"]) for r in members)
|
||||
lines.append(
|
||||
f"### C{i:03d} · Σ|Δ| {tot:.1f} · {len(members)} certs · `{key}`"
|
||||
)
|
||||
for r in members:
|
||||
st, note = prev.get(r["uprn"], ("[ ]", ""))
|
||||
frag = (
|
||||
f"- {st} `{r['uprn']}` · L {r['lodged']} / E {r['eng']:.2f} / "
|
||||
f"Δ {r['delta']:+.2f}"
|
||||
)
|
||||
if note:
|
||||
frag += f" {note}"
|
||||
lines.append(frag)
|
||||
lines.append("")
|
||||
|
||||
if skipped:
|
||||
lines.append(f"## Skipped ({len(skipped)})")
|
||||
for uprn, why in skipped:
|
||||
st, note = prev.get(uprn, ("⛔", ""))
|
||||
lines.append(f"- {st} `{uprn}` · {why} {note}".rstrip())
|
||||
lines.append("")
|
||||
|
||||
resolved = [r for r in rows if abs(r["delta"]) < 0.5]
|
||||
lines.append(f"## Resolved ≤0.5 ({len(resolved)} certs — no action)")
|
||||
lines.append("")
|
||||
lines.append("<details><summary>expand</summary>")
|
||||
lines.append("")
|
||||
for r in sorted(resolved, key=lambda r: -abs(r["delta"])):
|
||||
st, note = prev.get(r["uprn"], ("[x]", ""))
|
||||
if st == "[ ]":
|
||||
st = "[x]"
|
||||
lines.append(
|
||||
f"- {st} `{r['uprn']}` · L {r['lodged']} / E {r['eng']:.2f} / "
|
||||
f"Δ {r['delta']:+.2f} · `{r['key']}` {note}".rstrip()
|
||||
)
|
||||
lines.append("")
|
||||
lines.append("</details>")
|
||||
lines.append("")
|
||||
|
||||
WORKLIST.write_text("\n".join(lines))
|
||||
print(f"wrote {WORKLIST}")
|
||||
print(f"gauge: {n} computed / {len(skipped)} skipped, within-0.5 {within:.1%}, MAE {mae:.3f}")
|
||||
print(f"clusters: {len(ranked)}, worst 5:")
|
||||
for key, members in ranked[:5]:
|
||||
tot = sum(abs(r["delta"]) for r in members)
|
||||
print(f" Σ{tot:7.1f} n={len(members):3d} {key}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
1512
scripts/corpus_1000/worklist.md
Normal file
1512
scripts/corpus_1000/worklist.md
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -699,6 +699,31 @@ _EXPECTATIONS: Final[tuple[RealCertExpectation, ...]] = (
|
|||
cert_num="0018-2049-6232-4431-6934",
|
||||
sap_score=54,
|
||||
),
|
||||
# UPRN 4510053280 (corpus-1000 C001; the corpus doc lodges no cert number).
|
||||
# RdSAP-Schema-21.0.1 native — GROUND-FLOOR FLAT 47 m², band B, solid brick
|
||||
# 520 mm uninsulated (+ 8.12 m² sheltered alternative wall), roof "(another
|
||||
# dwelling above)", ASHP radiators PCDB 100053 (Mitsubishi Ecodan 5 kW,
|
||||
# 2010 record: PSR table 0.2–2.0, η_space 332.8% @ 2.0, η_water 187.5%,
|
||||
# duration "24"), no cylinder, electric room-heater secondary, no PV.
|
||||
# Lodged 51 / engine 74.94 (Δ +23.9, the corpus' worst cert). Dwelling PSR
|
||||
# = 4.62 kW / (94.0 W/K × 24.2 K) = 2.031, just past the record's largest
|
||||
# 2.0 → SAP 10.2 N2 (PDF p.101 footnotes 44/45) heat-pump extension:
|
||||
# reciprocal-interpolate η_space toward 100% at PSR 4.0 → 321% × 0.95 =
|
||||
# 305%. That is what the engine does, and what accredited Elmhurst did on
|
||||
# the SAME record family / same PSR shape (golden case 56, record 100061,
|
||||
# PSR 3.107 → 139.66%). The LODGED software instead treated the
|
||||
# out-of-range record as invalid and billed direct electric: forcing
|
||||
# η_space = η_water = 100% + the standard 9-h schedule reproduces the
|
||||
# lodged CO2 to 1% (0.808 vs 0.800 t) and PE within 6% (178 vs 190),
|
||||
# whereas the engine's spec path gives 0.444 t / 98.8. A lodged-software
|
||||
# methodology gap (worklist ⚠), NOT an engine bug — do not tune. PINNED to
|
||||
# the observed engine 75.
|
||||
RealCertExpectation(
|
||||
schema="RdSAP-Schema-21.0.1",
|
||||
sample="uprn_4510053280",
|
||||
cert_num="uprn-4510053280",
|
||||
sap_score=75,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue