"""Demand-driven divergence analyzer. Flags corpus certs where the SAP miss is driven by FABRIC/DEMAND (PE & CO2 move with SAP), not cost — the fixable class. Tags nbp (building parts) and room-in-roof so multi-part / RR bugs surface.""" from __future__ import annotations import json from pathlib import Path from datatypes.epc.domain.mapper import EpcPropertyDataMapper from domain.sap10_calculator.calculator import calculate_sap_from_inputs from domain.sap10_calculator.rdsap.cert_to_inputs import ( SAP_10_2_SPEC_PRICES, cert_to_demand_inputs, cert_to_inputs) CORPUS = Path("backend/epc_api/json_samples/RdSAP-Schema-21.0.1/corpus.jsonl") def has_rr(doc): for bp in doc.get("sap_building_parts") or []: r = bp.get("room_in_roof") or bp.get("rooms_in_roof") if r: return True for r in doc.get("roofs") or []: if "room in roof" in (r.get("description") or "").lower(): return True return False rows = [] for line in CORPUS.read_text().splitlines(): doc = json.loads(line) L = doc.get("energy_rating_current"); peL = doc.get("energy_consumption_current") co2L = doc.get("co2_emissions_current") if L is None: continue try: epc = EpcPropertyDataMapper.from_api_response(doc) r = calculate_sap_from_inputs(cert_to_inputs(epc, prices=SAP_10_2_SPEC_PRICES)) d = calculate_sap_from_inputs(cert_to_demand_inputs(epc, prices=SAP_10_2_SPEC_PRICES)) except Exception: continue dsap = r.sap_score_continuous - L if abs(dsap) < 0.5: continue peE = getattr(d, "primary_energy_kwh_per_m2", None) dpe = (peE - peL) if (peE is not None and peL is not None) else None co2E = d.co2_kg_per_yr/1000 if d.co2_kg_per_yr is not None else None dco2 = (co2E - co2L) if (co2E is not None and co2L is not None) else None nbp = len(doc.get("sap_building_parts") or []) rr = has_rr(doc) # demand-driven: PE moves same direction as SAP-miss inverse (PE up -> SAP down) # i.e. dpe and dsap have OPPOSITE signs, and |dpe| is material demand = (dpe is not None and abs(dpe) >= 4 and (dpe > 0) == (dsap < 0)) rows.append((doc.get("uprn"), L, round(r.sap_score_continuous,1), round(dsap,2), round(dpe,1) if dpe is not None else None, round(dco2,3) if dco2 is not None else None, nbp, rr, demand)) demand_rows = [r for r in rows if r[8]] demand_rows.sort(key=lambda x:-abs(x[3])) print(f"diverging certs: {len(rows)} | demand-driven: {len(demand_rows)}") mp = [r for r in demand_rows if r[6] > 1] rr = [r for r in demand_rows if r[7]] print(f" demand+multipart(nbp>1): {len(mp)} | demand+RR: {len(rr)}") print(f"\n=== top demand-driven divergences ===") print(f"{'uprn':13s} {'L':>3} {'E':>5} {'dSAP':>6} {'dPE':>6} {'dCO2':>6} {'nbp':>3} {'RR':>3}") for r in demand_rows[:32]: print(f"{str(r[0]):13s} {r[1]:3d} {r[2]:5.1f} {r[3]:+6.2f} {str(r[4]):>6} {str(r[5]):>6} {r[6]:3d} {'Y' if r[7] else '':>3}")