mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
fix(climate): compute EPC CO2/PE on the postcode demand cascade (SAP 10.2 Appendix U p.124)
The SAP/EI rating is computed on UK-average weather (Appendix U Tables U1-U3 region 0) so ratings are nationally comparable, but Appendix U paragraph 1 (PDF p.124) requires that "other calculations (such as for energy use and costs on EPCs) are done using local weather. Weather data for each postcode district are taken from the PCDB". `Sap10Calculator. calculate` ran ONE cascade (UK-average) and fed it to SAP, CO2 AND primary energy, so every cert's EPC-displayed CO2/PE were computed on the wrong climate. Because most of England is warmer than the UK-average, this systematically OVER-counted heating demand on the emissions/PE outputs. The two cascades (`cert_to_inputs` rating, `cert_to_demand_inputs` postcode) already existed; this wires the demand cascade into the production entry point and grafts its CO2/PE onto the rating result (SAP unchanged). The corpus gauge's longstanding +5% CO2/PE over-estimate was mostly this climate bug, NOT (as previously diagnosed) per-cert mapper fidelity: CO2 MAE 0.26 -> 0.12 t/yr (bias +0.18 -> +0.04) PE MAE 13.6 -> 3.8 kWh/m2 (bias +9.0 -> +0.24) SAP within-0.5 = 69.7% (rating cascade, unchanged) Worksheet-validated to 1e-4 on simulated case 45 (heat-pump ground-floor flat, postcode W6): the P960 prints the current dwelling twice — Block 1 on UK-average weather (SAP 60.5318, CO2 692.13) and Block 2 on postcode weather (CO2 626.78, PE 6581.59). Both reproduce exactly. Added a tracked case-45 Summary fixture + two-cascade cascade pin as a permanent guard, and ratcheted the corpus CO2/PE ceilings to 0.13 / 4.2. The e2e Elmhurst suite (Block-1 line refs) now pins the rating cascade directly; the two Vaillant overlay snapshots refreshed to demand-cascade CO2/PE. pyright not installed in this codespace (strict gate not run locally); change is type-trivial (dataclasses.replace over SapResult). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
26106505be
commit
fc7c4d2d3b
7 changed files with 234 additions and 20 deletions
BIN
backend/documents_parser/tests/fixtures/Summary_001431_case45.pdf
vendored
Normal file
BIN
backend/documents_parser/tests/fixtures/Summary_001431_case45.pdf
vendored
Normal file
Binary file not shown.
|
|
@ -42,7 +42,7 @@ Appendix L + U. RdSAP10 Table 32 (p.95) for fuel prices/CO2/PE factors.
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field, replace
|
||||||
from typing import Final, Optional, TYPE_CHECKING
|
from typing import Final, Optional, TYPE_CHECKING
|
||||||
|
|
||||||
from domain.sap10_calculator.climate.appendix_u import external_temperature_c
|
from domain.sap10_calculator.climate.appendix_u import external_temperature_c
|
||||||
|
|
@ -863,6 +863,25 @@ class Sap10Calculator(SapCalculator):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def calculate(self, epc: "EpcPropertyData") -> SapResult:
|
def calculate(self, epc: "EpcPropertyData") -> SapResult:
|
||||||
from domain.sap10_calculator.rdsap.cert_to_inputs import cert_to_inputs
|
# SAP 10.2 Appendix U paragraph 1 (p.124): the SAP and EI ratings are
|
||||||
|
# computed on UK-average climate (so ratings are nationally
|
||||||
|
# comparable), but "other calculations (such as for energy use and
|
||||||
|
# costs on EPCs) are done using local weather" — the EPC-displayed
|
||||||
|
# CO2 emissions and primary energy use postcode-district weather from
|
||||||
|
# the PCDB. So we run two climate cascades and graft the demand
|
||||||
|
# cascade's CO2/PE onto the rating cascade's SAP result. (Worked
|
||||||
|
# example: simulated case 45 — rating SAP 60.53/CO2 692.13 on
|
||||||
|
# UK-average; demand CO2 626.78/PE 6581.59 on the W6 postcode.)
|
||||||
|
from domain.sap10_calculator.rdsap.cert_to_inputs import (
|
||||||
|
cert_to_demand_inputs,
|
||||||
|
cert_to_inputs,
|
||||||
|
)
|
||||||
|
|
||||||
return calculate_sap_from_inputs(cert_to_inputs(epc))
|
rating = calculate_sap_from_inputs(cert_to_inputs(epc))
|
||||||
|
demand = calculate_sap_from_inputs(cert_to_demand_inputs(epc))
|
||||||
|
return replace(
|
||||||
|
rating,
|
||||||
|
co2_kg_per_yr=demand.co2_kg_per_yr,
|
||||||
|
primary_energy_kwh_per_yr=demand.primary_energy_kwh_per_yr,
|
||||||
|
primary_energy_kwh_per_m2=demand.primary_energy_kwh_per_m2,
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -686,12 +686,15 @@ def test_ashp_overlay_scores_the_vaillant_end_state_from_a_gas_boiler() -> None:
|
||||||
# dwelling's baseline fabric and so the ASHP end-state SAP. Still a snapshot
|
# dwelling's baseline fabric and so the ASHP end-state SAP. Still a snapshot
|
||||||
# of the Vaillant overlay's own output, validated transitively by the
|
# of the Vaillant overlay's own output, validated transitively by the
|
||||||
# system-boiler pin below (which reproduces a real Vaillant cert at delta 0).
|
# system-boiler pin below (which reproduces a real Vaillant cert at delta 0).
|
||||||
|
# CO2/PE are the postcode DEMAND cascade now that `Sap10Calculator.
|
||||||
|
# calculate` computes EPC emissions/PE on local weather (SAP 10.2
|
||||||
|
# Appendix U p.124); SAP is unchanged (UK-average rating cascade).
|
||||||
_assert_overlay_scores(
|
_assert_overlay_scores(
|
||||||
before,
|
before,
|
||||||
option.overlay,
|
option.overlay,
|
||||||
sap=51.99820176096402,
|
sap=51.99820176096402,
|
||||||
co2=1268.4645083243888,
|
co2=1065.7593506066496,
|
||||||
pe=13080.20756425629,
|
pe=10995.781557709413,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -715,12 +718,14 @@ def test_ashp_overlay_scores_the_vaillant_end_state_from_a_gas_boiler_instant_hw
|
||||||
# boiler-1 pin above); the same merge also resolved this cert's main-fuel
|
# boiler-1 pin above); the same merge also resolved this cert's main-fuel
|
||||||
# mapper gap (§14.2 mains-gas derivation), so its raw before now baselines —
|
# mapper gap (§14.2 mains-gas derivation), so its raw before now baselines —
|
||||||
# see `test_gas_boiler_instant_hw_before_baselines`.
|
# see `test_gas_boiler_instant_hw_before_baselines`.
|
||||||
|
# CO2/PE are the postcode DEMAND cascade now (see the boiler-1 pin above);
|
||||||
|
# SAP is unchanged (UK-average rating cascade).
|
||||||
_assert_overlay_scores(
|
_assert_overlay_scores(
|
||||||
before,
|
before,
|
||||||
option.overlay,
|
option.overlay,
|
||||||
sap=39.00740809309464,
|
sap=39.00740809309464,
|
||||||
co2=2248.6089062232704,
|
co2=1845.8588018295509,
|
||||||
pe=23094.10189037302,
|
pe=18944.42568846759,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
"""Mapper-driven cascade pin against the Elmhurst P960-0001-001431
|
||||||
|
"simulated case 45" worksheet — a ~47 m² GROUND-FLOOR FLAT heated by an
|
||||||
|
air-source HEAT PUMP (PCDB 100053 ECODAN, radiators, MCS=No) with a
|
||||||
|
WHC-903 electric-immersion DHW and a 110 L cylinder, postcode W6 9BF
|
||||||
|
(SAP Region "Thames Valley").
|
||||||
|
|
||||||
|
Case 45 is the 1e-4 oracle for the SAP 10.2 Appendix U (PDF p.124) TWO-
|
||||||
|
CLIMATE-CASCADE split. The P960 prints the current dwelling TWICE:
|
||||||
|
|
||||||
|
* Block 1 — "11a. SAP rating / 12a. CO2" — computed on UK-AVERAGE
|
||||||
|
weather (Appendix U Tables U1-U3 region 0). Drives the SAP/EI rating.
|
||||||
|
Space-heat demand (98c) = 7333.79; SAP value (258) = 60.5318 (-> 61);
|
||||||
|
total CO2 (272) = 692.13.
|
||||||
|
* Block 2 — "CALCULATION OF EPC COSTS, EMISSIONS AND PRIMARY ENERGY" —
|
||||||
|
computed on POSTCODE-DISTRICT weather (PCDB Table 172, W6). Drives the
|
||||||
|
EPC-displayed figures. Space-heat demand (98c) = 5921.05; total CO2
|
||||||
|
(272) = 626.78; total primary energy (286) = 6581.59.
|
||||||
|
|
||||||
|
Per Appendix U paragraph 1: "Other calculations (such as for energy use
|
||||||
|
and costs on EPCs) are done using local weather." `Sap10Calculator.
|
||||||
|
calculate` therefore runs both cascades and grafts the demand cascade's
|
||||||
|
CO2/PE onto the rating cascade's SAP — this fixture pins BOTH.
|
||||||
|
|
||||||
|
Like the other `_elmhurst_worksheet_001431_case*` fixtures it does NOT
|
||||||
|
hand-build the EpcPropertyData: it routes the Summary PDF through
|
||||||
|
ElmhurstSiteNotesExtractor + from_elmhurst_site_notes so the pin exercises
|
||||||
|
the WHOLE extractor + mapper + calculator pipeline.
|
||||||
|
|
||||||
|
Source: user-simulated PDFs at `sap worksheets/golden fixture debugging/
|
||||||
|
simulated case 45/`. The Summary is mirrored into the tracked
|
||||||
|
`backend/documents_parser/tests/fixtures/Summary_001431_case45.pdf` so the
|
||||||
|
test runs without depending on the unstaged workspace.
|
||||||
|
|
||||||
|
Per [[feedback-zero-error-strict]]: pins are abs <= 1e-4 against the PDF.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Final
|
||||||
|
|
||||||
|
from backend.documents_parser.elmhurst_extractor import ElmhurstSiteNotesExtractor
|
||||||
|
from datatypes.epc.domain.epc_property_data import EpcPropertyData
|
||||||
|
from datatypes.epc.domain.mapper import EpcPropertyDataMapper
|
||||||
|
|
||||||
|
# parents[0]=worksheet/, [1]=sap10_calculator/, [2]=domain/, [3]=tests/,
|
||||||
|
# [4]=repo root.
|
||||||
|
_SUMMARY_PDF: Final[Path] = (
|
||||||
|
Path(__file__).resolve().parents[4]
|
||||||
|
/ "backend" / "documents_parser" / "tests" / "fixtures"
|
||||||
|
/ "Summary_001431_case45.pdf"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Block 1 — UK-average RATING cascade (`cert_to_inputs`).
|
||||||
|
RATING_SPACE_HEATING_KWH: Final[float] = 7333.7892 # (98c)
|
||||||
|
RATING_SAP_CONTINUOUS: Final[float] = 60.5318 # (258) un-rounded
|
||||||
|
RATING_SAP_INTEGER: Final[int] = 61 # (258)
|
||||||
|
RATING_CO2_KG_PER_YR: Final[float] = 692.1287 # (272)
|
||||||
|
|
||||||
|
# Block 2 — POSTCODE-district DEMAND cascade (`cert_to_demand_inputs`).
|
||||||
|
DEMAND_SPACE_HEATING_KWH: Final[float] = 5921.0486 # (98c)
|
||||||
|
DEMAND_CO2_KG_PER_YR: Final[float] = 626.7797 # (272)
|
||||||
|
DEMAND_PRIMARY_ENERGY_KWH: Final[float] = 6581.5936 # (286)
|
||||||
|
|
||||||
|
|
||||||
|
def _summary_pdf_to_textract_style_pages(pdf_path: Path) -> list[str]:
|
||||||
|
"""Convert a Summary PDF into the per-page text format the
|
||||||
|
ElmhurstSiteNotesExtractor expects (label/value token sequences).
|
||||||
|
Mirror of the helper in the other `_elmhurst_worksheet_*` fixtures.
|
||||||
|
"""
|
||||||
|
info = subprocess.run(
|
||||||
|
["pdfinfo", str(pdf_path)], capture_output=True, text=True, check=True,
|
||||||
|
).stdout
|
||||||
|
m = re.search(r"Pages:\s+(\d+)", info)
|
||||||
|
if m is None:
|
||||||
|
raise RuntimeError(f"Could not parse page count from {pdf_path}")
|
||||||
|
page_count = int(m.group(1))
|
||||||
|
pages: list[str] = []
|
||||||
|
for i in range(1, page_count + 1):
|
||||||
|
layout = subprocess.run(
|
||||||
|
[
|
||||||
|
"pdftotext", "-layout", "-f", str(i), "-l", str(i),
|
||||||
|
str(pdf_path), "-",
|
||||||
|
],
|
||||||
|
capture_output=True, text=True, check=True,
|
||||||
|
).stdout
|
||||||
|
tokens: list[str] = []
|
||||||
|
for line in layout.splitlines():
|
||||||
|
if not line.strip():
|
||||||
|
tokens.append("")
|
||||||
|
continue
|
||||||
|
parts = [p for p in re.split(r"\s{2,}", line.strip()) if p]
|
||||||
|
tokens.extend(parts)
|
||||||
|
pages.append("\n".join(tokens))
|
||||||
|
return pages
|
||||||
|
|
||||||
|
|
||||||
|
def build_epc() -> EpcPropertyData:
|
||||||
|
"""Route the simulated case-45 Summary through extractor + mapper.
|
||||||
|
No hand-built EpcPropertyData — the extractor and mapper are part of
|
||||||
|
the test target. This module is a pin PROVIDER (build_epc + constants);
|
||||||
|
the collected assertions live in `test_section_cascade_pins`."""
|
||||||
|
pages = _summary_pdf_to_textract_style_pages(_SUMMARY_PDF)
|
||||||
|
site_notes = ElmhurstSiteNotesExtractor(pages).extract()
|
||||||
|
return EpcPropertyDataMapper.from_elmhurst_site_notes(site_notes)
|
||||||
|
|
@ -24,7 +24,10 @@ from typing import Final
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from domain.sap10_calculator.calculator import Sap10Calculator
|
from domain.sap10_calculator.calculator import (
|
||||||
|
Sap10Calculator,
|
||||||
|
calculate_sap_from_inputs,
|
||||||
|
)
|
||||||
from domain.sap10_calculator.rdsap.cert_to_inputs import (
|
from domain.sap10_calculator.rdsap.cert_to_inputs import (
|
||||||
cert_to_inputs,
|
cert_to_inputs,
|
||||||
water_heating_section_from_cert,
|
water_heating_section_from_cert,
|
||||||
|
|
@ -338,8 +341,13 @@ def test_sap_result_pin(fixture_name: str, field_name: str) -> None:
|
||||||
epc = _FIXTURE_MODULES[fixture_name].build_epc()
|
epc = _FIXTURE_MODULES[fixture_name].build_epc()
|
||||||
expected = getattr(pin, field_name)
|
expected = getattr(pin, field_name)
|
||||||
|
|
||||||
# Act
|
# Act — these pins are the worksheet's Block-1 (energy-rating) line refs,
|
||||||
result = Sap10Calculator().calculate(epc)
|
# i.e. the UK-average RATING cascade. `Sap10Calculator.calculate` now
|
||||||
|
# grafts the postcode DEMAND cascade's CO2/PE onto the result (SAP 10.2
|
||||||
|
# Appendix U p.124), so the rating-cascade fields are pinned via
|
||||||
|
# `cert_to_inputs` directly; the demand cascade is pinned separately
|
||||||
|
# (corpus gauge + simulated case 45 Block-2 pins).
|
||||||
|
result = calculate_sap_from_inputs(cert_to_inputs(epc))
|
||||||
actual = getattr(result, field_name)
|
actual = getattr(result, field_name)
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ from tests.domain.sap10_calculator.worksheet import (
|
||||||
_elmhurst_worksheet_001431_case21 as _w001431_case21,
|
_elmhurst_worksheet_001431_case21 as _w001431_case21,
|
||||||
_elmhurst_worksheet_001431_case43 as _w001431_case43,
|
_elmhurst_worksheet_001431_case43 as _w001431_case43,
|
||||||
_elmhurst_worksheet_001431_case44 as _w001431_case44,
|
_elmhurst_worksheet_001431_case44 as _w001431_case44,
|
||||||
|
_elmhurst_worksheet_001431_case45 as _w001431_case45,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -491,6 +492,67 @@ def test_case44_blower_door_pressure_test_matches_pdf() -> None:
|
||||||
_pin(vent.effective_monthly_ach[0], 0.5812, "§2 (25) Jan case44")
|
_pin(vent.effective_monthly_ach[0], 0.5812, "§2 (25) Jan case44")
|
||||||
|
|
||||||
|
|
||||||
|
def test_case45_heat_pump_two_climate_cascade_matches_pdf() -> None:
|
||||||
|
"""Simulated case 45 (heat-pump ground-floor flat, postcode W6) is the
|
||||||
|
1e-4 oracle for the SAP 10.2 Appendix U (p.124) two-climate-cascade
|
||||||
|
split. The P960 prints the current dwelling twice:
|
||||||
|
|
||||||
|
* Block 1 ("11a SAP rating / 12a CO2") on UK-AVERAGE weather (region
|
||||||
|
0): space heat (98c) 7333.79, SAP (258) 60.5318, CO2 (272) 692.13.
|
||||||
|
* Block 2 ("EPC COSTS, EMISSIONS AND PRIMARY ENERGY") on POSTCODE
|
||||||
|
weather (PCDB Table 172, W6): space heat (98c) 5921.05, CO2 (272)
|
||||||
|
626.78, primary energy (286) 6581.59.
|
||||||
|
|
||||||
|
The SAP/EI rating reads the rating cascade; the EPC-displayed CO2/PE
|
||||||
|
read the demand cascade. Pins both ends at abs=1e-4."""
|
||||||
|
# Arrange
|
||||||
|
from domain.sap10_calculator.calculator import calculate_sap_from_inputs
|
||||||
|
from domain.sap10_calculator.rdsap.cert_to_inputs import cert_to_demand_inputs
|
||||||
|
|
||||||
|
epc = _w001431_case45.build_epc()
|
||||||
|
# The split only exists because the postcode resolves to local weather.
|
||||||
|
assert local_climate_for_cert(epc) is not None
|
||||||
|
|
||||||
|
# Act — both climate cascades from the one cert.
|
||||||
|
rating = calculate_sap_from_inputs(cert_to_inputs(epc))
|
||||||
|
demand = calculate_sap_from_inputs(cert_to_demand_inputs(epc))
|
||||||
|
|
||||||
|
# Assert — Block 1 (UK-average rating cascade).
|
||||||
|
_pin(
|
||||||
|
rating.space_heating_kwh_per_yr,
|
||||||
|
_w001431_case45.RATING_SPACE_HEATING_KWH,
|
||||||
|
"(98c) rating case45",
|
||||||
|
)
|
||||||
|
_pin(
|
||||||
|
rating.sap_score_continuous,
|
||||||
|
_w001431_case45.RATING_SAP_CONTINUOUS,
|
||||||
|
"(258) rating case45",
|
||||||
|
)
|
||||||
|
assert rating.sap_score == _w001431_case45.RATING_SAP_INTEGER
|
||||||
|
_pin(
|
||||||
|
rating.co2_kg_per_yr,
|
||||||
|
_w001431_case45.RATING_CO2_KG_PER_YR,
|
||||||
|
"(272) rating case45",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Assert — Block 2 (postcode demand cascade).
|
||||||
|
_pin(
|
||||||
|
demand.space_heating_kwh_per_yr,
|
||||||
|
_w001431_case45.DEMAND_SPACE_HEATING_KWH,
|
||||||
|
"(98c) demand case45",
|
||||||
|
)
|
||||||
|
_pin(
|
||||||
|
demand.co2_kg_per_yr,
|
||||||
|
_w001431_case45.DEMAND_CO2_KG_PER_YR,
|
||||||
|
"(272) demand case45",
|
||||||
|
)
|
||||||
|
_pin(
|
||||||
|
demand.primary_energy_kwh_per_yr,
|
||||||
|
_w001431_case45.DEMAND_PRIMARY_ENERGY_KWH,
|
||||||
|
"(286) demand case45",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_case6_main_2_emitter_and_control_extracted() -> None:
|
def test_case6_main_2_emitter_and_control_extracted() -> None:
|
||||||
"""Simulated case 6's §14.1 Main Heating2 lodges its OWN emitter
|
"""Simulated case 6's §14.1 Main Heating2 lodges its OWN emitter
|
||||||
("Underfloor Heating") and control ("SAP code 2110, ...") — the two
|
("Underfloor Heating") and control ("SAP code 2110, ...") — the two
|
||||||
|
|
|
||||||
|
|
@ -30,11 +30,7 @@ from typing import Any
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from datatypes.epc.domain.mapper import EpcPropertyDataMapper
|
from datatypes.epc.domain.mapper import EpcPropertyDataMapper
|
||||||
from domain.sap10_calculator.calculator import calculate_sap_from_inputs
|
from domain.sap10_calculator.calculator import Sap10Calculator
|
||||||
from domain.sap10_calculator.rdsap.cert_to_inputs import (
|
|
||||||
SAP_10_2_SPEC_PRICES,
|
|
||||||
cert_to_inputs,
|
|
||||||
)
|
|
||||||
|
|
||||||
_CORPUS = Path(
|
_CORPUS = Path(
|
||||||
"backend/epc_api/json_samples/RdSAP-Schema-21.0.1/corpus.jsonl"
|
"backend/epc_api/json_samples/RdSAP-Schema-21.0.1/corpus.jsonl"
|
||||||
|
|
@ -127,10 +123,25 @@ _CORPUS = Path(
|
||||||
# "ground floor" floor_type as exposed (worksheet-validated to 1e-4 on simulated
|
# "ground floor" floor_type as exposed (worksheet-validated to 1e-4 on simulated
|
||||||
# case 45: floor (28a) 0 -> 25.38 W/K, fabric (33) 75.6 -> 101.01) -> 69.5% ->
|
# case 45: floor (28a) 0 -> 25.38 W/K, fabric (33) 75.6 -> 101.01) -> 69.5% ->
|
||||||
# 69.7% (MAE 0.859 -> 0.854). Pinned in test_heat_transmission.
|
# 69.7% (MAE 0.859 -> 0.854). Pinned in test_heat_transmission.
|
||||||
|
# POSTCODE DEMAND CASCADE (SAP 10.2 Appendix U paragraph 1, p.124): the
|
||||||
|
# CO2/PE over-estimate diagnosed above as "per-cert mapper/demand fidelity"
|
||||||
|
# was largely a CLIMATE-cascade bug. The SAP/EI rating is computed on
|
||||||
|
# UK-average weather (Tables U1-U3 region 0), but EPC-displayed energy use,
|
||||||
|
# CO2 emissions and primary energy use POSTCODE-DISTRICT weather from PCDB
|
||||||
|
# Table 172 — "other calculations (such as for energy use and costs on EPCs)
|
||||||
|
# are done using local weather". We were feeding the UK-average demand to all
|
||||||
|
# three outputs, so warm-region certs (most of England, warmer than the
|
||||||
|
# UK-average) over-counted heating demand → CO2/PE high. `Sap10Calculator.
|
||||||
|
# calculate` now grafts the demand cascade's CO2/PE onto the rating cascade's
|
||||||
|
# SAP. Across the corpus this moved CO2 MAE 0.26 -> 0.12 t/yr (bias +0.18 ->
|
||||||
|
# +0.04) and PE MAE 13.6 -> 3.8 kWh/m2/yr (bias +9.0 -> +0.24); SAP unchanged
|
||||||
|
# (rating cascade). Worksheet-validated to 1e-4 on simulated case 45 (rating
|
||||||
|
# CO2 692.13; demand CO2 626.78, PE 6581.59). The residual PE/CO2 spread is
|
||||||
|
# now the genuine per-cert mapper-fidelity tail.
|
||||||
_MIN_WITHIN_HALF_SAP = 0.695
|
_MIN_WITHIN_HALF_SAP = 0.695
|
||||||
_MAX_SAP_MAE = 0.86
|
_MAX_SAP_MAE = 0.86
|
||||||
_MAX_CO2_MAE_TONNES = 0.30 # t CO2 / yr vs co2_emissions_current
|
_MAX_CO2_MAE_TONNES = 0.13 # t CO2 / yr vs co2_emissions_current
|
||||||
_MAX_PE_PER_M2_MAE = 14.0 # kWh / m2 / yr vs energy_consumption_current
|
_MAX_PE_PER_M2_MAE = 4.2 # kWh / m2 / yr vs energy_consumption_current
|
||||||
|
|
||||||
|
|
||||||
def _load_corpus() -> list[dict[str, Any]]:
|
def _load_corpus() -> list[dict[str, Any]]:
|
||||||
|
|
@ -155,8 +166,12 @@ def test_api_path_sap_accuracy_on_rdsap_21_0_1_corpus(
|
||||||
co2_signed_errs_t: list[float] = [] # our − lodged, tonnes/yr
|
co2_signed_errs_t: list[float] = [] # our − lodged, tonnes/yr
|
||||||
pe_signed_errs: list[float] = [] # our − lodged, kWh/m²/yr
|
pe_signed_errs: list[float] = [] # our − lodged, kWh/m²/yr
|
||||||
skipped = 0
|
skipped = 0
|
||||||
|
_calculator = Sap10Calculator()
|
||||||
|
|
||||||
# Act — run the API → EpcPropertyData → calculator pipeline per cert.
|
# Act — run the API → EpcPropertyData → calculator pipeline per cert.
|
||||||
|
# `Sap10Calculator.calculate` runs both climate cascades (SAP 10.2
|
||||||
|
# Appendix U p.124): the SAP rating on UK-average weather, CO2/PE on
|
||||||
|
# postcode-district weather — exactly the two figures the EPC lodges.
|
||||||
for doc in corpus:
|
for doc in corpus:
|
||||||
lodged_sap = doc.get("energy_rating_current")
|
lodged_sap = doc.get("energy_rating_current")
|
||||||
if lodged_sap is None:
|
if lodged_sap is None:
|
||||||
|
|
@ -164,9 +179,7 @@ def test_api_path_sap_accuracy_on_rdsap_21_0_1_corpus(
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
epc = EpcPropertyDataMapper.from_api_response(doc)
|
epc = EpcPropertyDataMapper.from_api_response(doc)
|
||||||
result = calculate_sap_from_inputs(
|
result = _calculator.calculate(epc)
|
||||||
cert_to_inputs(epc, prices=SAP_10_2_SPEC_PRICES)
|
|
||||||
)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
# A mapper / calculator raise is a coverage gap tracked elsewhere
|
# A mapper / calculator raise is a coverage gap tracked elsewhere
|
||||||
# (eval_api_sap_accuracy.py); here we gauge the certs that compute.
|
# (eval_api_sap_accuracy.py); here we gauge the certs that compute.
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue