mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
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:
parent
a658f73613
commit
fb173cdf3f
4 changed files with 183 additions and 32 deletions
|
|
@ -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),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue