Slice S0380.151: RdSAP 10 §4.1 Table 5 — extract-fans age-band default

RdSAP 10 Specification §4.1 Table 5 "Ventilation parameters" (PDF p.28)
verbatim — "Extract fans" entry:

  • Number of extract fans if known
  • If number is unknown:
      Not park home:
        Age bands A to E      all cases             → 0
        Age bands F to G      all cases             → 1
        Age bands H to M      up to 2 hab. rooms    → 1
                              3 to 5 hab. rooms     → 2
                              6 to 8 hab. rooms     → 3
                              more than 8 hab. rooms → 4
      Park home:
        Age band F            all cases             → 0
        Age bands G onwards   all cases             → 2

The Elmhurst Summary §12.0 renders "No. of intermittent extract fans: 0"
as the form for *unknown*; every other §2 chimney/flue line item follows
"number if known, or 0 if not present" and the cascade trusts the lodged
value verbatim. Only extract fans have a non-zero age-band default.

Pre-slice the cascade read the lodged 0 verbatim → cohort-wide -0.044
ACH ventilation deficit (= -2.6 W/K HLC, = -1.2% SH demand, = ~-0.3 SAP
per variant). All 25 cascade-OK corpus variants are age G + 4 habitable
rooms + not park home → Table 5 default = 1 fan.

New helper `_rdsap_extract_fans_default(age_band, habitable_rooms, *,
is_park_home)` + wiring in `ventilation_from_cert` applies
`max(lodged, table_5_default)` so the spec minimum fires when lodging
is below it.

Heating-systems corpus impact (25 cascade-OK variants):

  oil 1, oil pcdb 1/2/3            +0.27..+0.29 → EXACT (<1e-4)
  electric 1, solid fuel 5/6/7/8   +0.28..+0.43 → EXACT
  pcdb 1, ashp                     +0.41 / +0.18 → ±0.02
  electric 3/6/7/8/9, sf 4/9/10/11 +0.39..+0.60 → +0.08..+0.12
  electric 5                       -0.74 → -1.18  (Cluster B over-shoot)
  electric 2                       -0.24 → -0.46  (Cluster C HW gap)
  gshp                             +1.09 → +0.94  (Cluster C HW gap)
  solid fuel 2/3                   +3.08 / +1.76  → +2.77 / +1.31

Cluster A (cohort-wide HLC deficit) is closed. The four remaining open
fronts (Clusters B + C) are now visible without offsetting bugs:
  - Cluster B (Table 9c step 12 R sign): electric 5, solid fuel 2/3
  - Cluster C (HW kWh cascade): gshp + electric 2 (Appendix N3)
                                solid fuel 2/3 (Table 4b HW efficiency)

Golden-fixture re-pins:
  cert 0240 (age J, TFA 118): PE +2.18 → +5.80, CO2 +0.13 → +0.32
  cert 0390-2954 (age F, TFA 360): PE -28.27 → -27.97, CO2 -2.74 → -2.71

Pyright net-zero (44 → 44). Extended handover suite: 893 → 895 pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-01 11:26:53 +00:00
parent a658f73613
commit fb173cdf3f
4 changed files with 183 additions and 32 deletions

View file

@ -219,21 +219,21 @@ class _CorpusExpectation:
# the SH+Sec demand mismatch for electric 3/6/7 (Table 11 fraction
# for codes 401/402) remains the open driver of those SAP residuals.
_EXPECTATIONS: tuple[_CorpusExpectation, ...] = (
_CorpusExpectation(variant='ashp', block='11a', expected_sap_resid=+0.1830, expected_cost_resid_gbp=-4.2166, expected_co2_resid_kg=-1.4283, expected_pe_resid_kwh=-11.8017),
_CorpusExpectation(variant='electric 1', block='11a', expected_sap_resid=+0.3849, expected_cost_resid_gbp=-8.8694, expected_co2_resid_kg=-4.3334, expected_pe_resid_kwh=-40.1603),
_CorpusExpectation(variant='electric 2', block='11a', expected_sap_resid=-0.2430, expected_cost_resid_gbp=+5.5979, expected_co2_resid_kg=+38.7768, expected_pe_resid_kwh=+392.8379),
_CorpusExpectation(variant='electric 3', block='11a', expected_sap_resid=+0.5980, expected_cost_resid_gbp=-13.7793, expected_co2_resid_kg=-13.8238, expected_pe_resid_kwh=-114.7533),
_CorpusExpectation(variant='electric 5', block='11a', expected_sap_resid=-0.7401, expected_cost_resid_gbp=+17.0523, expected_co2_resid_kg=+43.9325, expected_pe_resid_kwh=+338.5315),
_CorpusExpectation(variant='electric 6', block='11a', expected_sap_resid=+0.5156, expected_cost_resid_gbp=-11.8811, expected_co2_resid_kg=-10.1354, expected_pe_resid_kwh=-93.1997),
_CorpusExpectation(variant='electric 7', block='11a', expected_sap_resid=+0.4810, expected_cost_resid_gbp=-11.0832, expected_co2_resid_kg=-8.3964, expected_pe_resid_kwh=-83.9576),
_CorpusExpectation(variant='electric 8', block='11a', expected_sap_resid=+0.4286, expected_cost_resid_gbp=-9.8766, expected_co2_resid_kg=-6.4095, expected_pe_resid_kwh=-70.5744),
_CorpusExpectation(variant='electric 9', block='11a', expected_sap_resid=+0.5673, expected_cost_resid_gbp=-13.0713, expected_co2_resid_kg=-12.3507, expected_pe_resid_kwh=-105.2495),
_CorpusExpectation(variant='gshp', block='11a', expected_sap_resid=+1.0903, expected_cost_resid_gbp=-25.1234, expected_co2_resid_kg=-41.4461, expected_pe_resid_kwh=-454.5023),
_CorpusExpectation(variant='oil 1', block='11a', expected_sap_resid=+0.2902, expected_cost_resid_gbp=-6.6882, expected_co2_resid_kg=-36.6371, expected_pe_resid_kwh=-71.2875),
_CorpusExpectation(variant='oil pcdb 1', block='11a', expected_sap_resid=+0.2728, expected_cost_resid_gbp=-6.2850, expected_co2_resid_kg=-34.4292, expected_pe_resid_kwh=-67.1831),
_CorpusExpectation(variant='oil pcdb 2', block='11a', expected_sap_resid=+0.2728, expected_cost_resid_gbp=-6.2850, expected_co2_resid_kg=-34.4292, expected_pe_resid_kwh=-67.1831),
_CorpusExpectation(variant='oil pcdb 3', block='11a', expected_sap_resid=+0.2729, expected_cost_resid_gbp=-6.2879, expected_co2_resid_kg=-34.4447, expected_pe_resid_kwh=-67.2071),
_CorpusExpectation(variant='pcdb 1', block='11a', expected_sap_resid=+0.4096, expected_cost_resid_gbp=-9.0664, expected_co2_resid_kg=-49.6654, expected_pe_resid_kwh=-92.8147),
_CorpusExpectation(variant='ashp', block='11a', expected_sap_resid=-0.0240, expected_cost_resid_gbp=+0.5536, expected_co2_resid_kg=+7.3267, expected_pe_resid_kwh=+36.3435),
_CorpusExpectation(variant='electric 1', block='11a', expected_sap_resid=-0.0000, expected_cost_resid_gbp=-0.0000, expected_co2_resid_kg=+11.9451, expected_pe_resid_kwh=+48.6605),
_CorpusExpectation(variant='electric 2', block='11a', expected_sap_resid=-0.4584, expected_cost_resid_gbp=+10.5613, expected_co2_resid_kg=+47.8864, expected_pe_resid_kwh=+443.1346),
_CorpusExpectation(variant='electric 3', block='11a', expected_sap_resid=+0.1215, expected_cost_resid_gbp=-2.8003, expected_co2_resid_kg=+6.7227, expected_pe_resid_kwh=-5.9859),
_CorpusExpectation(variant='electric 5', block='11a', expected_sap_resid=-1.1759, expected_cost_resid_gbp=+27.0929, expected_co2_resid_kg=+62.7232, expected_pe_resid_kwh=+438.0333),
_CorpusExpectation(variant='electric 6', block='11a', expected_sap_resid=+0.1081, expected_cost_resid_gbp=-2.4918, expected_co2_resid_kg=+7.3225, expected_pe_resid_kwh=+0.1603),
_CorpusExpectation(variant='electric 7', block='11a', expected_sap_resid=+0.1017, expected_cost_resid_gbp=-2.3444, expected_co2_resid_kg=+7.6424, expected_pe_resid_kwh=+3.0976),
_CorpusExpectation(variant='electric 8', block='11a', expected_sap_resid=+0.0941, expected_cost_resid_gbp=-2.1679, expected_co2_resid_kg=+7.9230, expected_pe_resid_kwh=+6.5824),
_CorpusExpectation(variant='electric 9', block='11a', expected_sap_resid=+0.1199, expected_cost_resid_gbp=-2.7611, expected_co2_resid_kg=+6.8225, expected_pe_resid_kwh=-4.5085),
_CorpusExpectation(variant='gshp', block='11a', expected_sap_resid=+0.9373, expected_cost_resid_gbp=-21.5977, expected_co2_resid_kg=-34.9751, expected_pe_resid_kwh=-418.9168),
_CorpusExpectation(variant='oil 1', block='11a', expected_sap_resid=-0.0000, expected_cost_resid_gbp=-0.0000, expected_co2_resid_kg=+0.0000, expected_pe_resid_kwh=+0.0000),
_CorpusExpectation(variant='oil pcdb 1', block='11a', expected_sap_resid=+0.0000, expected_cost_resid_gbp=+0.0000, expected_co2_resid_kg=-0.0000, expected_pe_resid_kwh=+0.0000),
_CorpusExpectation(variant='oil pcdb 2', block='11a', expected_sap_resid=+0.0000, expected_cost_resid_gbp=+0.0000, expected_co2_resid_kg=-0.0000, expected_pe_resid_kwh=+0.0000),
_CorpusExpectation(variant='oil pcdb 3', block='11a', expected_sap_resid=+0.0000, expected_cost_resid_gbp=+0.0000, expected_co2_resid_kg=+0.0000, expected_pe_resid_kwh=-0.0000),
_CorpusExpectation(variant='pcdb 1', block='11a', expected_sap_resid=-0.0108, expected_cost_resid_gbp=+0.2420, expected_co2_resid_kg=+1.3254, expected_pe_resid_kwh=+5.6974),
# Slice S0380.133 unblocked 10 solid-fuel variants by routing the
# Elmhurst §14.0 "Main Heating EES Code" through the new
# `_ELMHURST_MAIN_HEATING_EES_TO_FUEL_CODE` dict. Pre-slice the
@ -241,16 +241,16 @@ _EXPECTATIONS: tuple[_CorpusExpectation, ...] = (
# cost / CO2 / PE all route via the correct Table 32 fuel code.
# Remaining residuals are likely heating-system efficiency or
# control-type gaps — separate slices.
_CorpusExpectation(variant='solid fuel 2', block='11a', expected_sap_resid=+3.0805, expected_cost_resid_gbp=-70.9797, expected_co2_resid_kg=+41.5584, expected_pe_resid_kwh=-1346.0016),
_CorpusExpectation(variant='solid fuel 3', block='11a', expected_sap_resid=+1.7637, expected_cost_resid_gbp=-40.6395, expected_co2_resid_kg=-441.0048, expected_pe_resid_kwh=-1069.2375),
_CorpusExpectation(variant='solid fuel 4', block='11a', expected_sap_resid=+0.3935, expected_cost_resid_gbp=-9.0668, expected_co2_resid_kg=-86.4442, expected_pe_resid_kwh=-106.8858),
_CorpusExpectation(variant='solid fuel 5', block='11a', expected_sap_resid=+0.2767, expected_cost_resid_gbp=-6.3746, expected_co2_resid_kg=-56.6651, expected_pe_resid_kwh=-41.8008),
_CorpusExpectation(variant='solid fuel 6', block='11a', expected_sap_resid=+0.4703, expected_cost_resid_gbp=-10.8355, expected_co2_resid_kg=-11.6812, expected_pe_resid_kwh=-89.8541),
_CorpusExpectation(variant='solid fuel 7', block='11a', expected_sap_resid=+0.5361, expected_cost_resid_gbp=-12.5193, expected_co2_resid_kg=-87.4488, expected_pe_resid_kwh=-117.8475),
_CorpusExpectation(variant='solid fuel 8', block='11a', expected_sap_resid=+0.3618, expected_cost_resid_gbp=-8.3371, expected_co2_resid_kg=+5.6990, expected_pe_resid_kwh=-89.4580),
_CorpusExpectation(variant='solid fuel 9', block='11a', expected_sap_resid=+0.4898, expected_cost_resid_gbp=-11.2865, expected_co2_resid_kg=+1.6494, expected_pe_resid_kwh=-103.7659),
_CorpusExpectation(variant='solid fuel 10', block='11a', expected_sap_resid=+0.5249, expected_cost_resid_gbp=-12.0942, expected_co2_resid_kg=-0.2410, expected_pe_resid_kwh=-130.1413),
_CorpusExpectation(variant='solid fuel 11', block='11a', expected_sap_resid=+0.4221, expected_cost_resid_gbp=-9.7259, expected_co2_resid_kg=+5.5072, expected_pe_resid_kwh=-92.4917),
_CorpusExpectation(variant='solid fuel 2', block='11a', expected_sap_resid=+2.7654, expected_cost_resid_gbp=-63.7195, expected_co2_resid_kg=+120.3433, expected_pe_resid_kwh=-1241.7357),
_CorpusExpectation(variant='solid fuel 3', block='11a', expected_sap_resid=+1.3086, expected_cost_resid_gbp=-30.1525, expected_co2_resid_kg=-327.2043, expected_pe_resid_kwh=-918.6312),
_CorpusExpectation(variant='solid fuel 4', block='11a', expected_sap_resid=+0.0850, expected_cost_resid_gbp=-1.9582, expected_co2_resid_kg=-9.3050, expected_pe_resid_kwh=-5.7762),
_CorpusExpectation(variant='solid fuel 5', block='11a', expected_sap_resid=+0.0000, expected_cost_resid_gbp=+0.0000, expected_co2_resid_kg=+11.9451, expected_pe_resid_kwh=+48.6604),
_CorpusExpectation(variant='solid fuel 6', block='11a', expected_sap_resid=+0.0000, expected_cost_resid_gbp=+0.0000, expected_co2_resid_kg=+11.9452, expected_pe_resid_kwh=+48.6604),
_CorpusExpectation(variant='solid fuel 7', block='11a', expected_sap_resid=-0.0000, expected_cost_resid_gbp=+0.0000, expected_co2_resid_kg=+11.9451, expected_pe_resid_kwh=+48.6604),
_CorpusExpectation(variant='solid fuel 8', block='11a', expected_sap_resid=-0.0000, expected_cost_resid_gbp=+0.0000, expected_co2_resid_kg=+11.9451, expected_pe_resid_kwh=+48.6604),
_CorpusExpectation(variant='solid fuel 9', block='11a', expected_sap_resid=+0.1072, expected_cost_resid_gbp=-2.4702, expected_co2_resid_kg=+9.6917, expected_pe_resid_kwh=-5.0715),
_CorpusExpectation(variant='solid fuel 10', block='11a', expected_sap_resid=+0.1134, expected_cost_resid_gbp=-2.6121, expected_co2_resid_kg=+9.3131, expected_pe_resid_kwh=-13.9149),
_CorpusExpectation(variant='solid fuel 11', block='11a', expected_sap_resid=+0.0912, expected_cost_resid_gbp=-2.1006, expected_co2_resid_kg=+10.5547, expected_pe_resid_kwh=-0.7387),
)

View file

@ -2651,6 +2651,47 @@ def _ventilation_counts(vent: Optional[SapVentilation]) -> _VentilationCounts:
)
def _rdsap_extract_fans_default(
age_band: str, habitable_rooms: int, *, is_park_home: bool,
) -> int:
"""RdSAP 10 §4.1 Table 5 (PDF p.28) — extract-fans default when the
lodged number is unknown. Spec verbatim:
Not park home:
Age bands A to E: all cases 0
Age bands F to G: all cases 1
Age bands H to M: up to 2 hab. rooms 1
3 to 5 hab. rooms 2
6 to 8 hab. rooms 3
more than 8 hab. rooms 4
Park home:
Age band F: all cases 0
Age bands G onwards: all cases 2
The Elmhurst Summary §12.0 renders "No. of intermittent extract
fans: 0" as the form for *unknown*; every other §2 chimney/flue
item follows "number if known, or 0 if not present" and zero is
literal absence. Only extract fans have a non-zero age-band default
this helper plus a `max(lodged, default)` wiring at the call
site applies the spec when the lodging is below the minimum.
"""
band = age_band.strip().upper() if age_band else ""
if is_park_home:
return 0 if band in {"A", "B", "C", "D", "E", "F"} else 2
if band in {"A", "B", "C", "D", "E"}:
return 0
if band in {"F", "G"}:
return 1
# Age bands H to M scale by habitable rooms
if habitable_rooms <= 2:
return 1
if habitable_rooms <= 5:
return 2
if habitable_rooms <= 8:
return 3
return 4
def water_heating_section_from_cert(
epc: EpcPropertyData,
) -> Optional[WaterHeatingResult]:
@ -3416,6 +3457,19 @@ def ventilation_from_cert(
storeys = max(1, dim.storey_count)
vc = _ventilation_counts(epc.sap_ventilation)
sv = epc.sap_ventilation
# RdSAP 10 §4.1 Table 5 (PDF p.28) — extract-fans default when the
# lodged count is below the age-band minimum. The Elmhurst Summary
# renders "0" as the form for unknown; the worksheet applies the
# default via `max(lodged, table_5_default)`.
age_band = (
epc.sap_building_parts[0].construction_age_band
if epc.sap_building_parts else ""
)
is_park_home = (epc.property_type or "").strip().lower() == "park home"
table_5_fan_default = _rdsap_extract_fans_default(
age_band, epc.habitable_rooms_count, is_park_home=is_park_home,
)
intermittent_fans = max(vc.intermittent_fans, table_5_fan_default)
wind_kwargs: dict[str, tuple[float, ...]] = (
{"monthly_wind_speed_m_s": postcode_climate.monthly_wind_speed_m_per_s}
if postcode_climate is not None else {}
@ -3468,7 +3522,7 @@ def ventilation_from_cert(
closed_fire_chimneys=vc.closed_fire_chimneys,
solid_fuel_boiler_chimneys=vc.solid_fuel_boiler_chimneys,
other_heater_chimneys=vc.other_heater_chimneys,
intermittent_fans=vc.intermittent_fans,
intermittent_fans=intermittent_fans,
passive_vents=vc.passive_vents,
flueless_gas_fires=vc.flueless_gas_fires,
has_suspended_timber_floor=eff_has_susp,

View file

@ -46,6 +46,7 @@ from domain.sap10_calculator.rdsap.cert_to_inputs import (
_is_off_peak_meter, # pyright: ignore[reportPrivateUsage]
_main_floor_u_value, # pyright: ignore[reportPrivateUsage]
_other_fuel_cost_gbp_per_kwh, # pyright: ignore[reportPrivateUsage]
_rdsap_extract_fans_default, # pyright: ignore[reportPrivateUsage]
_pv_overshading_factor, # pyright: ignore[reportPrivateUsage]
_pv_pitch_deg, # pyright: ignore[reportPrivateUsage]
_responsiveness, # pyright: ignore[reportPrivateUsage]
@ -611,6 +612,93 @@ def test_main_floor_u_value_routes_suspended_timber_via_floor_construction_type(
assert sealed is False
def test_rdsap_extract_fans_default_per_table_5() -> None:
# Arrange — RdSAP 10 §4.1 Table 5 (PDF p.28) "Extract fans" default
# when the lodged number is unknown. The Summary §12.0 "No. of
# intermittent extract fans = 0" is the Elmhurst-rendered form of
# "unknown" (lodging form has explicit "if known" vs. "unknown"
# options); every other §2 chimney/flue line item follows "number if
# known, or 0 if not present" and the cascade trusts the lodged
# value verbatim. Only extract fans have a non-zero age-band default
# — A-E = 0, F-G = 1, H-M scales 1..4 by habitable rooms.
#
# Table 5 verbatim (Not park home):
# Age bands A to E: all cases → 0
# Age bands F to G: all cases → 1
# Age bands H to M: up to 2 hab. rooms → 1
# 3 to 5 hab. rooms → 2
# 6 to 8 hab. rooms → 3
# more than 8 hab. rooms → 4
# Park home:
# Age band F: all cases → 0
# Age bands G onwards: all cases → 2
# Act / Assert — exhaustive Table 5 coverage
for age in 'ABCDE':
assert _rdsap_extract_fans_default(age, 4, is_park_home=False) == 0
for age in 'FG':
for hr in (1, 2, 3, 4, 5, 6, 8, 10):
assert _rdsap_extract_fans_default(age, hr, is_park_home=False) == 1
for age in 'HIJKLM':
assert _rdsap_extract_fans_default(age, 1, is_park_home=False) == 1
assert _rdsap_extract_fans_default(age, 2, is_park_home=False) == 1
assert _rdsap_extract_fans_default(age, 3, is_park_home=False) == 2
assert _rdsap_extract_fans_default(age, 5, is_park_home=False) == 2
assert _rdsap_extract_fans_default(age, 6, is_park_home=False) == 3
assert _rdsap_extract_fans_default(age, 8, is_park_home=False) == 3
assert _rdsap_extract_fans_default(age, 9, is_park_home=False) == 4
assert _rdsap_extract_fans_default(age, 12, is_park_home=False) == 4
# Park home rows
assert _rdsap_extract_fans_default('F', 4, is_park_home=True) == 0
for age in 'GHIJKLM':
assert _rdsap_extract_fans_default(age, 4, is_park_home=True) == 2
def test_ventilation_from_cert_applies_table_5_default_when_lodged_zero() -> None:
# Arrange — corpus property 001431 (age G, 4 habitable rooms, semi-
# detached) lodges Summary §12.0 "No. of intermittent extract fans
# = 0" but the Elmhurst worksheet (7a) uses 1 × 10 = 10 m³/h. Per
# RdSAP 10 §4.1 Table 5 (PDF p.28) age G + not-park-home defaults
# to 1 fan when the lodged count is unknown. Pre-slice the cascade
# treated lodged 0 as "explicitly zero" and skipped the default —
# under-counting (8) by 0.044 ACH cohort-wide (= ~1.2% HLC deficit
# = ~0.3 SAP per variant across 25 cascade-OK cohort variants).
base = _typical_semi_detached_epc()
# Override age band on the building part to G; 4 habitable rooms is
# already the default of `_typical_semi_detached_epc`.
age_g_part = make_building_part(
floor_dimensions=[
make_floor_dimension(total_floor_area_m2=45.0, floor=0),
make_floor_dimension(total_floor_area_m2=45.0, floor=1),
],
construction_age_band='G',
)
epc_age_g = make_minimal_sap10_epc(
total_floor_area_m2=_TYPICAL_TFA_M2,
habitable_rooms_count=4,
region_code="1",
sap_building_parts=[age_g_part],
sap_windows=base.sap_windows,
sap_heating=base.sap_heating,
sap_ventilation=SapVentilation(extract_fans_count=0),
)
# Act
v = ventilation_from_cert(epc_age_g)
# Assert — (8) openings ACH must include 1 fan × 10 m³/h ÷ volume.
# Volume = TFA × 2.5 m storey height × 2 storeys; use the cascade's
# own dim.volume_m3 by reading it back.
from domain.sap10_calculator.rdsap.cert_to_inputs import dimensions_from_cert
vol = dimensions_from_cert(epc_age_g).volume_m3
expected_openings_ach = 10.0 / vol # one fan at Table 5 default
assert abs(v.openings_ach - expected_openings_ach) <= 1e-6, (
f"openings ACH {v.openings_ach:.6f} should equal "
f"10 / volume = {expected_openings_ach:.6f} per RdSAP 10 "
f"§4.1 Table 5 age-G default of 1 fan"
)
def test_ventilation_from_cert_passes_lodged_ap4_to_pressure_test_ach_per_sap_10_2_section_2_line_18() -> None:
# Arrange — SAP 10.2 §2 line (17a)/(18) "Air permeability value, AP4
# (m³/h/m²)": when a Pulse pressure test is lodged the cascade must

View file

@ -75,8 +75,8 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = (
cert_number="0240-0200-5706-2365-8010",
actual_sap=73,
expected_sap_resid=-1,
expected_pe_resid_kwh_per_m2=+2.1847,
expected_co2_resid_tonnes_per_yr=+0.1333,
expected_pe_resid_kwh_per_m2=+5.8007,
expected_co2_resid_tonnes_per_yr=+0.3173,
notes=(
"Detached house, TFA 118, age J, oil boiler PCDB-listed + PV + "
"RR on BP[0]. Mapper DOES extract sap_room_in_roof.room_in_roof_"
@ -108,7 +108,12 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = (
"→ -1) and raises PE +1.0211 → +2.5225, CO2 +0.1118 → "
"+0.1395. Residual remains net-positive — the 100 kWh "
"spec figure may need refinement when the dual-main "
"main_heating_fraction split lands (slice candidate)."
"main_heating_fraction split lands (slice candidate). "
"Slice S0380.151 wired RdSAP 10 §4.1 Table 5 (PDF p.28) "
"extract-fans default (age J, 4 hab rooms → 2 fans). "
"Cascade ventilation HLC rises ~0.07 ACH × volume → SH "
"demand rises proportionally; PE +2.5225 → +5.8007, CO2 "
"+0.1395 → +0.3173. SAP integer unchanged at 72."
),
),
_GoldenExpectation(
@ -143,8 +148,8 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = (
cert_number="0390-2954-3640-2196-4175",
actual_sap=60,
expected_sap_resid=+7,
expected_pe_resid_kwh_per_m2=-28.2719,
expected_co2_resid_tonnes_per_yr=-2.7404,
expected_pe_resid_kwh_per_m2=-27.9745,
expected_co2_resid_tonnes_per_yr=-2.7134,
notes=(
"Detached, TFA 360, age F, Firebird oil combi PCDF 9005 "
"(winter eff 86.4%). PCDB record lodges separate_dhw_tests=0 + "
@ -171,7 +176,11 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = (
"for the oil combi Main 1 — cascade pumps_fans +100 kWh/yr, "
"PE residual -28.5027 → -28.0830 (closer to zero), CO2 "
"-2.7481 → -2.7342 (closer to zero). Remaining residual is "
"a fabric or different §4 driver — follow-up slice candidate."
"a fabric or different §4 driver — follow-up slice candidate. "
"Slice S0380.151 wired RdSAP 10 §4.1 Table 5 (PDF p.28) "
"extract-fans default (age F → 1 fan). Cascade ventilation "
"HLC rises ~0.03 ACH × volume; PE -28.0830 → -27.9745 "
"(closer to zero), CO2 -2.7342 → -2.7134."
),
),
_GoldenExpectation(