Cohort residual slice 1: 000490 secondary heating cascade closes -£104 cost gap

Lodges `secondary_heating_type=691` (Electricity Electric Panel) on
000490 `build_epc()` to match the U985 worksheet's "Secondary Heating:
Electricity Electric Panel, convector or radiant heaters, SAP Code 691,
Efficiency 100%". Pre-fix the cert lodged no secondary system →
`_secondary_fraction` returned 0.0 → all useful space heat routed to
main 1 → main_fuel +1357 kWh over PDF, secondary -1118 under PDF, cost
-£104 under PDF (-12.9% residual).

Post-fix: Table 11 fraction 0.1000 for gas-combi category cascade fires
→ main 1 = 11491.89 kWh, secondary = 1126.21 kWh. Total cost £807.42
vs PDF £807.54 (Δ -£0.12, -0.015%). SAP integer 58 vs PDF 57 (delta 1,
was 6); continuous 57.57 vs 57.40 (delta 0.18).

E2E test updates:
- New worksheet-level pin `result.secondary_heating_fuel_kwh_per_yr ≈
  U985 (215) = 1118.3275` at abs=10 (loose — absorbs the +0.7% upstream
  useful space heating overshoot which propagates 1:1 to (215). Tightens
  to abs=1e-3 when the useful bias closes).
- Per-fixture constant `LINE_215_SECONDARY_HEATING_FUEL_KWH = 1118.3275`.
- 000490 SAP integer ceiling tightened 3 → 1; continuous 3.0 → 0.5.
- Removed xfail on `test_elmhurst_000490_end_to_end_sap_score_currently_
  within_3_points` and `test_000490_cert_to_inputs_fuel_cost_closes_to_
  within_5pct` — both now pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-05-22 09:53:33 +00:00
parent fd9df9e502
commit 607e52a354
3 changed files with 45 additions and 29 deletions

View file

@ -128,6 +128,7 @@ def build_epc() -> EpcPropertyData:
main_heating_data_source=1,
),
],
secondary_heating_type=691,
),
)
@ -265,6 +266,12 @@ SECTION_5_PUMP_AGE_STR: str = "Unknown"
# inputs.lighting_kwh_per_yr on the cost side.
LINE_232_LIGHTING_KWH_PER_YR: float = 171.4217
# Secondary heating fuel kWh (215) — Σ (215)m monthly tuple.
# 000490 lodges secondary heating system "Electricity Electric Panel,
# convector or radiant heaters" (SAP Code 691) at 100% efficiency.
# Table 11 fraction 0.1000 of total space heat goes to secondary.
LINE_215_SECONDARY_HEATING_FUEL_KWH: float = 1118.3275
LINE_66_M_METABOLIC_W: tuple[float, ...] = (128.8087,) * 12
LINE_67_M_LIGHTING_W: tuple[float, ...] = (
24.2665, 21.5533, 17.5283, 13.2701, 9.9195, 8.3745,

View file

@ -56,19 +56,6 @@ _ELMHURST_000474_EXPECTED: Final[ElmhurstExpectedSap] = ElmhurstExpectedSap(
)
@pytest.mark.xfail(
reason=(
"Appendix L closure on 000490: lighting kWh closes 614→171 (spec-faithful "
"U985 (232)=171.4217). Cost drops by ~£60 → £703 vs PDF £807; SAP integer "
"climbs 60→63 → delta widens 3→6. Per the e2e validation philosophy "
"(feedback-e2e-validation-philosophy): don't widen the ceiling, hunt the "
"next broken component. Suspects: fuel pricing for pre-2025-07-01 certs "
"(ADR-0010 §3 Validation Cohort), main heating kWh +2.5% overshoot, "
"Table D1/D2/D3 Ecodesign corrections. Re-enable when those land and "
"SAP integer = PDF integer (delta=0)."
),
strict=True,
)
def test_elmhurst_000490_end_to_end_sap_score_currently_within_3_points() -> None:
"""Mid-terrace combi-gas dwelling with time-clock keep-hot. After the
PCDB Table 105 integration the fixture lodges `main_heating_index_
@ -119,17 +106,19 @@ def test_elmhurst_000490_end_to_end_sap_score_currently_within_3_points() -> Non
# Act
result = Sap10Calculator().calculate(epc)
# Assert
# Assert — secondary heating cascade closed the £104 cost gap; SAP
# integer is now 58 vs PDF 57 (delta 1). The residual delta is from
# the +0.7% upstream useful space heating overshoot — next ticket.
delta = abs(result.sap_score - _ELMHURST_000490_EXPECTED.sap_rating)
assert delta <= 3, (
f"SAP rating delta {delta} exceeds current-state ceiling of 3. "
assert delta <= 1, (
f"SAP rating delta {delta} exceeds current-state ceiling of 1. "
f"Actual={result.sap_score}, expected={_ELMHURST_000490_EXPECTED.sap_rating}."
)
continuous_delta = abs(
result.sap_score_continuous - _ELMHURST_000490_EXPECTED.sap_score_continuous
)
assert continuous_delta <= 3.0, (
f"Continuous SAP delta {continuous_delta:.2f} exceeds ceiling 3.0"
assert continuous_delta <= 0.5, (
f"Continuous SAP delta {continuous_delta:.2f} exceeds ceiling 0.5"
)
@ -221,6 +210,37 @@ def test_elmhurst_end_to_end_lighting_kwh_per_yr_matches_u985_worksheet(
assert result.lighting_kwh_per_yr == pytest.approx(expected_kwh, abs=1e-4)
def test_elmhurst_000490_end_to_end_secondary_heating_fuel_kwh_matches_u985_worksheet() -> None:
"""Component-level e2e pin on `SapResult.secondary_heating_fuel_kwh_per_yr`
for 000490 cert lodges secondary heating system "Electricity Electric
Panel, convector or radiant heaters" (SAP Code 691, 100% efficiency).
Table 11 fraction 0.10 of total space heat goes to the secondary
system (215) = 1118.3275 kWh.
Closes the next 000490 residual after Appendix L: secondary fuel was
silently 0 because build_epc didn't lodge secondary_heating_type, so
`_secondary_fraction` early-returned 0.0 all useful space heat
routed to main 1 main_fuel_kwh +1357 kWh over PDF, secondary -1118
under PDF. Cost gap was £147 secondary missing minus £47 main
overshoot = -£104 (the dominant residual after Appendix L closure).
"""
# Arrange
epc = _w000490.build_epc()
# Act
result = Sap10Calculator().calculate(epc)
# Assert — tolerance abs=10 absorbs the +0.7% (78 kWh) overshoot in
# `result.space_heating_kwh_per_yr` (useful demand) that propagates
# proportionally to (215) = useful × 0.1 / 1.0. The secondary cascade
# 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(
_w000490.LINE_215_SECONDARY_HEATING_FUEL_KWH, abs=10.0
)
def test_elmhurst_000490_end_to_end_kwh_within_15pct() -> None:
"""Per-end-use kWh sanity check for 000490. Closer-fitting than the
SAP score because intermediate values aren't compressed through the

View file

@ -368,17 +368,6 @@ def test_000474_cert_to_inputs_fuel_cost_within_existing_e2e_tolerance() -> None
assert inputs.fuel_cost.total_cost_gbp == pytest.approx(655.6949, rel=0.15)
@pytest.mark.xfail(
reason=(
"Appendix L closure on 000490: lighting kWh closes 614→171 (spec-faithful "
"U985 (232)=171.4217). Cost drops ~£60 → £703 vs PDF £807 (-12.9%). The "
"Appendix L direction is correct; the residual is a non-lighting broken "
"component (suspects per feedback-e2e-validation-philosophy: fuel pricing "
"for pre-2025-07-01 certs, main-heating-fuel +2.5% overshoot, Table D1-3 "
"Ecodesign). Re-enable when next component closes."
),
strict=True,
)
def test_000490_cert_to_inputs_fuel_cost_closes_to_within_5pct() -> None:
"""Cert-round-trip conformance: 000490 mid-terrace combi-gas with PV
(PDF total fuel cost £807.54). Pre-§10a was £706.23 (-12.5%)