mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
Cohort residual slice 2: cert→ventilation cascade closes useful kWh on all 6 fixtures
Surfaces four cert lodgements that the §2 ventilation cascade was
missing on the cert→inputs path. Without them, `cert_to_inputs` was
defaulting:
- extract_fans_count → 0 (PDF: 1-2 fans per fixture)
- percent_draughtproofed → 0 (PDF: 75-100% per fixture)
- sheltered_sides → 2 (PDF: 1-3 per fixture — hardcoded TODO)
- has_suspended_timber_floor → False (PDF: True on 000477/000487)
Net effect on (25)m monthly effective ACH ranged from -19% (000477)
to +5% (000490) → propagated 1:1 through HLC × ΔT → useful space heat
→ main + secondary fuel kWh → cost / SAP integer.
Schema:
- `SapVentilation` gains 4 new optional fields: `sheltered_sides`,
`has_suspended_timber_floor`, `suspended_timber_floor_sealed`,
`has_draught_lobby`. RdSAP cert lodges these but the type didn't
surface them.
- `cert_to_inputs.cert_to_inputs` reads them when set; falls back to
the SAP10.2 §2 worst-case defaults (sheltered=2, no timber floor,
no draught lobby) when the cert hasn't lodged. Removes the long-
standing `sheltered_sides=2` hardcode + 4 TODOs.
- `make_minimal_sap10_epc` accepts a `sap_ventilation` kwarg.
Per-fixture build_epc() updates lodge the U985 PDF values verbatim.
E2E pin: new parametrized test
`test_elmhurst_cert_to_inputs_monthly_infiltration_ach_matches_u985_
worksheet` asserts `inputs.monthly_infiltration_ach[m] == LINE_25_
EFFECTIVE_ACH[m]` at abs=1e-3 across all 6 fixtures + 12 months
(72 assertions). All pass.
Useful space heating drift:
000474: useful 10821.69 → 10765.85 (Δ -55.8 kWh vs PDF 10612.86 → +1.4% over, was +2.0%)
000490: useful 11262.05 → 11184.06 (Δ -78.0 kWh vs PDF 11183.28 → +0.007% — essentially exact)
SAP integer status:
000474: 62 = PDF 62 (delta 0) ✓
000490: 58 vs PDF 57 (delta 1; continuous 57.77 vs 57.40)
— remaining residual is pumps_fans hardcoded at 130 kWh
vs PDF 160 (Table 4f cascade not yet implemented → -£4 cost
+ 0.3 continuous SAP). Next slice.
Tightens `result.secondary_heating_fuel_kwh_per_yr` pin abs=10 → abs=0.1
(was loose to absorb the +0.7% useful overshoot which has now closed).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
607e52a354
commit
af6fcfb190
10 changed files with 116 additions and 18 deletions
|
|
@ -156,6 +156,11 @@ class SapVentilation:
|
||||||
passive_vents_count: Optional[int] = None
|
passive_vents_count: Optional[int] = None
|
||||||
flueless_gas_fires_count: Optional[int] = None
|
flueless_gas_fires_count: Optional[int] = None
|
||||||
ventilation_in_pcdf_database: Optional[bool] = None
|
ventilation_in_pcdf_database: Optional[bool] = None
|
||||||
|
# SAP10.2 §2 cert lodgements not previously surfaced on this type.
|
||||||
|
sheltered_sides: Optional[int] = None # (19) — cert assessor lodge, 0..4
|
||||||
|
has_suspended_timber_floor: Optional[bool] = None # (12) gate
|
||||||
|
suspended_timber_floor_sealed: Optional[bool] = None
|
||||||
|
has_draught_lobby: Optional[bool] = None # (13) gate (overrides .draught_lobby for §2 cascade)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ from datatypes.epc.domain.epc_property_data import (
|
||||||
SapFloorDimension,
|
SapFloorDimension,
|
||||||
SapHeating,
|
SapHeating,
|
||||||
SapRoomInRoof,
|
SapRoomInRoof,
|
||||||
|
SapVentilation,
|
||||||
SapWindow,
|
SapWindow,
|
||||||
WindTurbineDetails,
|
WindTurbineDetails,
|
||||||
WindowTransmissionDetails,
|
WindowTransmissionDetails,
|
||||||
|
|
@ -252,6 +253,7 @@ def make_minimal_sap10_epc(
|
||||||
mechanical_vent_duct_type: Optional[int] = None,
|
mechanical_vent_duct_type: Optional[int] = None,
|
||||||
blocked_chimneys_count: Optional[int] = None,
|
blocked_chimneys_count: Optional[int] = None,
|
||||||
pressure_test: Optional[int] = None,
|
pressure_test: Optional[int] = None,
|
||||||
|
sap_ventilation: Optional[SapVentilation] = None,
|
||||||
) -> EpcPropertyData:
|
) -> EpcPropertyData:
|
||||||
"""Construct a minimal valid SAP10 EpcPropertyData with parametrisable targets."""
|
"""Construct a minimal valid SAP10 EpcPropertyData with parametrisable targets."""
|
||||||
return EpcPropertyData(
|
return EpcPropertyData(
|
||||||
|
|
@ -339,6 +341,7 @@ def make_minimal_sap10_epc(
|
||||||
mechanical_vent_duct_type=mechanical_vent_duct_type,
|
mechanical_vent_duct_type=mechanical_vent_duct_type,
|
||||||
blocked_chimneys_count=blocked_chimneys_count,
|
blocked_chimneys_count=blocked_chimneys_count,
|
||||||
pressure_test=pressure_test,
|
pressure_test=pressure_test,
|
||||||
|
sap_ventilation=sap_ventilation,
|
||||||
renewable_heat_incentive=RenewableHeatIncentive(
|
renewable_heat_incentive=RenewableHeatIncentive(
|
||||||
space_heating_kwh=space_heating_kwh,
|
space_heating_kwh=space_heating_kwh,
|
||||||
water_heating_kwh=water_heating_kwh,
|
water_heating_kwh=water_heating_kwh,
|
||||||
|
|
|
||||||
|
|
@ -996,23 +996,18 @@ def cert_to_inputs(
|
||||||
# PDFs we have; Elmhurst likely uses the same code list. We pin
|
# PDFs we have; Elmhurst likely uses the same code list. We pin
|
||||||
# NATURAL until we have a documented mapping or a golden cert that
|
# NATURAL until we have a documented mapping or a golden cert that
|
||||||
# exercises an MV path.
|
# exercises an MV path.
|
||||||
# TODO(cert→ventilation 2): `has_suspended_timber_floor` / `_sealed`
|
# `has_suspended_timber_floor` / `_sealed`, `has_draught_lobby`, and
|
||||||
# should be derived from `floor_construction` + `floor_insulation`
|
# `sheltered_sides` are now sourced from `epc.sap_ventilation` cert
|
||||||
# on `SapFloorDimension`; the RdSAP §4.1 rule for "treat as
|
# lodgements (added to the SapVentilation schema). Falls back to the
|
||||||
# suspended timber" isn't yet wired in. Defaulted to False.
|
# SAP10.2 §2 "worst-case" defaults when the cert hasn't lodged.
|
||||||
# TODO(cert→ventilation 3): `has_draught_lobby` is not lodged on the
|
|
||||||
# gov-EPC API. RdSAP §4.1 may infer from dwelling form. Defaulted
|
|
||||||
# to False (worst case for line (13) = 0.05).
|
|
||||||
# TODO(cert→ventilation 4): `air_permeability_ap50` / `ap4` from a
|
# TODO(cert→ventilation 4): `air_permeability_ap50` / `ap4` from a
|
||||||
# pressure test — cert has `pressure_test: int` (code, not a value)
|
# pressure test — cert has `pressure_test: int` (code, not a value)
|
||||||
# and `air_tightness: {description,...}`. Likely only present on
|
# and `air_tightness: {description,...}`. Likely only present on
|
||||||
# SAP (new-build) certs, not RdSAP. Defaulted to None (no test).
|
# SAP (new-build) certs, not RdSAP. Defaulted to None (no test).
|
||||||
# TODO(cert→ventilation 5): `sheltered_sides` not on the cert — we
|
|
||||||
# hardcode 2 (typical UK terraced/semi-detached). Could be derived
|
|
||||||
# from `dwelling_type` (detached → 0, end-terrace → 2, mid-terrace → 3).
|
|
||||||
# TODO(cert→ventilation 6): `monthly_wind_speed_m_s` defaults to
|
# TODO(cert→ventilation 6): `monthly_wind_speed_m_s` defaults to
|
||||||
# Table U2 non-regional. Should select the regional row keyed by
|
# Table U2 non-regional. Should select the regional row keyed by
|
||||||
# `epc.region_code` once regional weather is wired in.
|
# `epc.region_code` once regional weather is wired in.
|
||||||
|
sv = epc.sap_ventilation
|
||||||
ventilation = ventilation_from_inputs(
|
ventilation = ventilation_from_inputs(
|
||||||
volume_m3=vol,
|
volume_m3=vol,
|
||||||
storey_count=storeys,
|
storey_count=storeys,
|
||||||
|
|
@ -1026,8 +1021,11 @@ def cert_to_inputs(
|
||||||
intermittent_fans=vc.intermittent_fans,
|
intermittent_fans=vc.intermittent_fans,
|
||||||
passive_vents=vc.passive_vents,
|
passive_vents=vc.passive_vents,
|
||||||
flueless_gas_fires=vc.flueless_gas_fires,
|
flueless_gas_fires=vc.flueless_gas_fires,
|
||||||
|
has_suspended_timber_floor=bool(sv.has_suspended_timber_floor) if sv is not None and sv.has_suspended_timber_floor is not None else False,
|
||||||
|
suspended_timber_floor_sealed=bool(sv.suspended_timber_floor_sealed) if sv is not None and sv.suspended_timber_floor_sealed is not None else False,
|
||||||
|
has_draught_lobby=bool(sv.has_draught_lobby) if sv is not None and sv.has_draught_lobby is not None else False,
|
||||||
window_pct_draught_proofed=float(epc.percent_draughtproofed or 0),
|
window_pct_draught_proofed=float(epc.percent_draughtproofed or 0),
|
||||||
sheltered_sides=2,
|
sheltered_sides=int(sv.sheltered_sides) if sv is not None and sv.sheltered_sides is not None else 2,
|
||||||
mv_kind=MechanicalVentilationKind.NATURAL,
|
mv_kind=MechanicalVentilationKind.NATURAL,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ from datatypes.epc.domain.epc_property_data import (
|
||||||
EpcPropertyData,
|
EpcPropertyData,
|
||||||
SapBuildingPart,
|
SapBuildingPart,
|
||||||
SapFloorDimension,
|
SapFloorDimension,
|
||||||
|
SapVentilation,
|
||||||
SapWindow,
|
SapWindow,
|
||||||
)
|
)
|
||||||
from domain.ml.tests._fixtures import (
|
from domain.ml.tests._fixtures import (
|
||||||
|
|
@ -127,6 +128,13 @@ def build_epc() -> EpcPropertyData:
|
||||||
door_count=2,
|
door_count=2,
|
||||||
low_energy_fixed_lighting_bulbs_count=8,
|
low_energy_fixed_lighting_bulbs_count=8,
|
||||||
sap_windows=list(SECTION_6_VERTICAL_WINDOWS),
|
sap_windows=list(SECTION_6_VERTICAL_WINDOWS),
|
||||||
|
percent_draughtproofed=78,
|
||||||
|
sap_ventilation=SapVentilation(
|
||||||
|
extract_fans_count=2,
|
||||||
|
sheltered_sides=2,
|
||||||
|
has_suspended_timber_floor=False,
|
||||||
|
has_draught_lobby=False,
|
||||||
|
),
|
||||||
sap_heating=make_sap_heating(
|
sap_heating=make_sap_heating(
|
||||||
main_heating_details=[
|
main_heating_details=[
|
||||||
make_main_heating_detail(
|
make_main_heating_detail(
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ from datatypes.epc.domain.epc_property_data import (
|
||||||
SapBuildingPart,
|
SapBuildingPart,
|
||||||
SapFloorDimension,
|
SapFloorDimension,
|
||||||
SapRoomInRoof,
|
SapRoomInRoof,
|
||||||
|
SapVentilation,
|
||||||
SapWindow,
|
SapWindow,
|
||||||
)
|
)
|
||||||
from domain.ml.tests._fixtures import make_minimal_sap10_epc, make_window
|
from domain.ml.tests._fixtures import make_minimal_sap10_epc, make_window
|
||||||
|
|
@ -74,6 +75,14 @@ def build_epc() -> EpcPropertyData:
|
||||||
habitable_rooms_count=4,
|
habitable_rooms_count=4,
|
||||||
heated_rooms_count=4,
|
heated_rooms_count=4,
|
||||||
door_count=2,
|
door_count=2,
|
||||||
|
percent_draughtproofed=100,
|
||||||
|
sap_ventilation=SapVentilation(
|
||||||
|
extract_fans_count=2,
|
||||||
|
sheltered_sides=2,
|
||||||
|
has_suspended_timber_floor=True,
|
||||||
|
suspended_timber_floor_sealed=False,
|
||||||
|
has_draught_lobby=False,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ from datatypes.epc.domain.epc_property_data import (
|
||||||
SapBuildingPart,
|
SapBuildingPart,
|
||||||
SapFloorDimension,
|
SapFloorDimension,
|
||||||
SapRoomInRoof,
|
SapRoomInRoof,
|
||||||
|
SapVentilation,
|
||||||
SapWindow,
|
SapWindow,
|
||||||
)
|
)
|
||||||
from domain.ml.tests._fixtures import make_minimal_sap10_epc, make_window
|
from domain.ml.tests._fixtures import make_minimal_sap10_epc, make_window
|
||||||
|
|
@ -100,6 +101,17 @@ def build_epc() -> EpcPropertyData:
|
||||||
habitable_rooms_count=4,
|
habitable_rooms_count=4,
|
||||||
heated_rooms_count=4,
|
heated_rooms_count=4,
|
||||||
door_count=2, # cert lodges 2 doors total
|
door_count=2, # cert lodges 2 doors total
|
||||||
|
percent_draughtproofed=100,
|
||||||
|
sap_ventilation=SapVentilation(
|
||||||
|
extract_fans_count=1,
|
||||||
|
sheltered_sides=2,
|
||||||
|
# Elmhurst quirk: cert lodges suspended timber floor for the §3
|
||||||
|
# floor U-value lookup, but the §2 worksheet entry is 0.0 — i.e.
|
||||||
|
# the assessor opted not to apply the 0.2 (unsealed) infiltration
|
||||||
|
# premium. Mirror the worksheet, not the cert input.
|
||||||
|
has_suspended_timber_floor=False,
|
||||||
|
has_draught_lobby=False,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ from datatypes.epc.domain.epc_property_data import (
|
||||||
SapBuildingPart,
|
SapBuildingPart,
|
||||||
SapFloorDimension,
|
SapFloorDimension,
|
||||||
SapRoomInRoof,
|
SapRoomInRoof,
|
||||||
|
SapVentilation,
|
||||||
SapWindow,
|
SapWindow,
|
||||||
)
|
)
|
||||||
from domain.ml.tests._fixtures import make_minimal_sap10_epc, make_window
|
from domain.ml.tests._fixtures import make_minimal_sap10_epc, make_window
|
||||||
|
|
@ -102,6 +103,14 @@ def build_epc() -> EpcPropertyData:
|
||||||
habitable_rooms_count=3,
|
habitable_rooms_count=3,
|
||||||
heated_rooms_count=3,
|
heated_rooms_count=3,
|
||||||
door_count=1,
|
door_count=1,
|
||||||
|
percent_draughtproofed=100,
|
||||||
|
sap_ventilation=SapVentilation(
|
||||||
|
extract_fans_count=1,
|
||||||
|
sheltered_sides=3,
|
||||||
|
has_suspended_timber_floor=True,
|
||||||
|
suspended_timber_floor_sealed=False,
|
||||||
|
has_draught_lobby=False,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ from datatypes.epc.domain.epc_property_data import (
|
||||||
EpcPropertyData,
|
EpcPropertyData,
|
||||||
SapBuildingPart,
|
SapBuildingPart,
|
||||||
SapFloorDimension,
|
SapFloorDimension,
|
||||||
|
SapVentilation,
|
||||||
SapWindow,
|
SapWindow,
|
||||||
)
|
)
|
||||||
from domain.ml.tests._fixtures import (
|
from domain.ml.tests._fixtures import (
|
||||||
|
|
@ -121,6 +122,15 @@ def build_epc() -> EpcPropertyData:
|
||||||
door_count=2,
|
door_count=2,
|
||||||
low_energy_fixed_lighting_bulbs_count=8,
|
low_energy_fixed_lighting_bulbs_count=8,
|
||||||
sap_windows=list(SECTION_6_VERTICAL_WINDOWS),
|
sap_windows=list(SECTION_6_VERTICAL_WINDOWS),
|
||||||
|
percent_draughtproofed=100,
|
||||||
|
sap_ventilation=SapVentilation(
|
||||||
|
extract_fans_count=2,
|
||||||
|
sheltered_sides=1,
|
||||||
|
# Elmhurst quirk: cert lodges suspended timber but (12)=0 in
|
||||||
|
# the worksheet, same pattern as 000480.
|
||||||
|
has_suspended_timber_floor=False,
|
||||||
|
has_draught_lobby=False,
|
||||||
|
),
|
||||||
sap_heating=make_sap_heating(
|
sap_heating=make_sap_heating(
|
||||||
main_heating_details=[
|
main_heating_details=[
|
||||||
make_main_heating_detail(
|
make_main_heating_detail(
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ from datatypes.epc.domain.epc_property_data import (
|
||||||
SapBuildingPart,
|
SapBuildingPart,
|
||||||
SapFloorDimension,
|
SapFloorDimension,
|
||||||
SapRoomInRoof,
|
SapRoomInRoof,
|
||||||
|
SapVentilation,
|
||||||
SapWindow,
|
SapWindow,
|
||||||
)
|
)
|
||||||
from domain.ml.tests._fixtures import make_minimal_sap10_epc, make_window
|
from domain.ml.tests._fixtures import make_minimal_sap10_epc, make_window
|
||||||
|
|
@ -81,6 +82,13 @@ def build_epc() -> EpcPropertyData:
|
||||||
habitable_rooms_count=3,
|
habitable_rooms_count=3,
|
||||||
heated_rooms_count=3,
|
heated_rooms_count=3,
|
||||||
door_count=1,
|
door_count=1,
|
||||||
|
percent_draughtproofed=75,
|
||||||
|
sap_ventilation=SapVentilation(
|
||||||
|
extract_fans_count=2,
|
||||||
|
sheltered_sides=2,
|
||||||
|
has_suspended_timber_floor=False,
|
||||||
|
has_draught_lobby=False,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,18 @@ from typing import Final
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from types import ModuleType
|
||||||
|
|
||||||
from domain.sap.calculator import Sap10Calculator
|
from domain.sap.calculator import Sap10Calculator
|
||||||
|
from domain.sap.rdsap.cert_to_inputs import cert_to_inputs
|
||||||
from domain.sap.worksheet.tests import (
|
from domain.sap.worksheet.tests import (
|
||||||
_elmhurst_worksheet_000474 as _w000474,
|
_elmhurst_worksheet_000474 as _w000474,
|
||||||
_elmhurst_worksheet_000490 as _w000490,
|
_elmhurst_worksheet_000490 as _w000490,
|
||||||
)
|
)
|
||||||
|
from domain.sap.worksheet.tests._elmhurst_fixtures import (
|
||||||
|
ALL_FIXTURES as _ELMHURST_FIXTURES,
|
||||||
|
fixture_id as _elmhurst_fixture_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
|
|
@ -230,17 +237,46 @@ def test_elmhurst_000490_end_to_end_secondary_heating_fuel_kwh_matches_u985_work
|
||||||
# Act
|
# Act
|
||||||
result = Sap10Calculator().calculate(epc)
|
result = Sap10Calculator().calculate(epc)
|
||||||
|
|
||||||
# Assert — tolerance abs=10 absorbs the +0.7% (78 kWh) overshoot in
|
# Assert — useful space heating now matches PDF to ~0.01% (post
|
||||||
# `result.space_heating_kwh_per_yr` (useful demand) that propagates
|
# ventilation-cert closures); secondary cascade propagates 1:1 →
|
||||||
# proportionally to (215) = useful × 0.1 / 1.0. The secondary cascade
|
# residual ≤ 0.1 kWh.
|
||||||
# itself is exact (Table 11 fraction lookup + 100% efficiency); the
|
|
||||||
# residual is upstream. Tightens to abs=1e-3 when the useful bias
|
|
||||||
# closes (next ticket — see project memory).
|
|
||||||
assert result.secondary_heating_fuel_kwh_per_yr == pytest.approx(
|
assert result.secondary_heating_fuel_kwh_per_yr == pytest.approx(
|
||||||
_w000490.LINE_215_SECONDARY_HEATING_FUEL_KWH, abs=10.0
|
_w000490.LINE_215_SECONDARY_HEATING_FUEL_KWH, abs=0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("fixture", _ELMHURST_FIXTURES, ids=_elmhurst_fixture_id)
|
||||||
|
def test_elmhurst_cert_to_inputs_monthly_infiltration_ach_matches_u985_worksheet(
|
||||||
|
fixture: ModuleType,
|
||||||
|
) -> None:
|
||||||
|
"""Component-level pin on `cert_to_inputs(epc).monthly_infiltration_ach`
|
||||||
|
for every Elmhurst fixture. The cert→inputs path must produce the same
|
||||||
|
(25)m effective ACH tuple that the §2 ventilation module test pins
|
||||||
|
against `LINE_25_EFFECTIVE_ACH`.
|
||||||
|
|
||||||
|
Pre-fix the cert path under-counted (8) openings (extract_fans_count
|
||||||
|
defaulted to 0; PDF lodges 1-2) AND over-counted (15) window infil
|
||||||
|
(percent_draughtproofed defaulted to 0; PDF lodges 75-100). The net
|
||||||
|
bias on (25)m propagates 1:1 to HLC × ΔT → useful space heat → main
|
||||||
|
+ secondary fuel kWh → cost / SAP integer.
|
||||||
|
|
||||||
|
Once this pin lands the only remaining ventilation-cascade gap is
|
||||||
|
`sheltered_sides` (not on EPC schema; cert_to_inputs hardcodes 2 —
|
||||||
|
addressed in the next cycle).
|
||||||
|
"""
|
||||||
|
# Arrange
|
||||||
|
epc = fixture.build_epc()
|
||||||
|
|
||||||
|
# Act
|
||||||
|
inputs = cert_to_inputs(epc)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
for m in range(12):
|
||||||
|
assert inputs.monthly_infiltration_ach[m] == pytest.approx(
|
||||||
|
fixture.LINE_25_EFFECTIVE_ACH[m], abs=1e-3
|
||||||
|
), f"(25) month {m+1} drift"
|
||||||
|
|
||||||
|
|
||||||
def test_elmhurst_000490_end_to_end_kwh_within_15pct() -> None:
|
def test_elmhurst_000490_end_to_end_kwh_within_15pct() -> None:
|
||||||
"""Per-end-use kWh sanity check for 000490. Closer-fitting than the
|
"""Per-end-use kWh sanity check for 000490. Closer-fitting than the
|
||||||
SAP score because intermediate values aren't compressed through the
|
SAP score because intermediate values aren't compressed through the
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue