mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
fix(ventilation): use lodged extract-fan count when known, not max(lodged, age default) (RdSAP 10 §4.1 Table 5, PDF p.28)
Table 5 reads "Number of extract fans if known; if number is unknown: [age-band default]" — the default is an UNKNOWN-fallback, NOT a floor. The cascade applied `max(lodged, table_5_default)`, flooring a genuinely-lodged count up to the age-band minimum: e.g. an age H-M dwelling lodging 2 extract fans was billed at the 6-8-room default of 3, over-counting ventilation line (8) and the heat-loss coefficient. Fixed to `lodged if lodged > 0 else default` (a lodged 0 is the Elmhurst/RdSAP "unknown" form → default; any positive count is taken literally). Surfaced by Khalim's Elmhurst stress worksheet (simulated case 46): this was its last ventilation residual — our Jan effective ACH 9.14 -> 9.0748 (exact match to the accredited worksheet), SAP 29 -> 30 = Elmhurst, cost £1496 vs £1493. Corpus IMPROVED: within-0.5 71.6% -> 72.5%, MAE 0.819 -> 0.815 (the max-flooring over-counted ventilation on every cert lodging fans below its age default). Floor ratcheted 0.71 -> 0.72. pyright not installed locally. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
34e52a893c
commit
a9632937d5
3 changed files with 57 additions and 6 deletions
|
|
@ -4987,16 +4987,23 @@ 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)`.
|
||||
# RdSAP 10 §4.1 Table 5 (PDF p.28) — extract fans: "Number of extract
|
||||
# fans if known; if number is unknown: [age-band default]." The default
|
||||
# is an UNKNOWN-fallback, NOT a floor: a genuinely-lodged count is used
|
||||
# as-is even when it is below the age-band default (e.g. a band H-M
|
||||
# dwelling lodging 2 fans is NOT bumped to the 3-fan default). The
|
||||
# Elmhurst Summary / RdSAP convention renders "0" as the form for
|
||||
# unknown, so a lodged 0 falls back to the default; any positive count
|
||||
# is taken literally. (Was `max(lodged, default)`, which over-applied
|
||||
# the default as a minimum and over-counted ventilation.)
|
||||
age_band = _dwelling_age_band(epc) or ""
|
||||
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)
|
||||
intermittent_fans = (
|
||||
vc.intermittent_fans if vc.intermittent_fans > 0 else 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 {}
|
||||
|
|
|
|||
|
|
@ -1609,6 +1609,40 @@ def test_ventilation_from_cert_applies_table_5_default_when_lodged_zero() -> Non
|
|||
)
|
||||
|
||||
|
||||
def test_ventilation_from_cert_uses_lodged_fans_below_age_default_not_floored() -> None:
|
||||
# Arrange — RdSAP 10 §4.1 Table 5 (PDF p.28): "Number of extract fans if
|
||||
# known; if unknown: [age-band default]." The default is an UNKNOWN-
|
||||
# fallback, NOT a floor — a genuinely-lodged positive count is used
|
||||
# as-is even when below the age default. An age-H 6-habitable-room
|
||||
# dwelling has a 3-fan default, but a cert lodging 2 fans must use 2,
|
||||
# not be floored up to 3. (Was `max(lodged, default)` → 3, over-counting
|
||||
# ventilation; surfaced on simulated case 46 where it inflated (8) by
|
||||
# one fan = 0.055 ACH and pushed SAP 30 → 29.)
|
||||
age_h_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='H',
|
||||
)
|
||||
epc = make_minimal_sap10_epc(
|
||||
total_floor_area_m2=_TYPICAL_TFA_M2,
|
||||
habitable_rooms_count=6, # age H-M, 6-8 rooms → Table 5 default 3
|
||||
region_code="1",
|
||||
sap_building_parts=[age_h_part],
|
||||
sap_ventilation=SapVentilation(extract_fans_count=2), # lodged 2 < 3
|
||||
)
|
||||
|
||||
# Act
|
||||
v = ventilation_from_cert(epc)
|
||||
|
||||
# Assert — (8) openings ACH uses the lodged 2 fans (20 m³/h), not 3.
|
||||
from domain.sap10_calculator.rdsap.cert_to_inputs import dimensions_from_cert
|
||||
vol = dimensions_from_cert(epc).volume_m3
|
||||
assert abs(v.openings_ach - 20.0 / vol) <= 1e-6
|
||||
assert abs(v.openings_ach - 30.0 / vol) > 1e-6
|
||||
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -183,7 +183,17 @@ _CORPUS = Path(
|
|||
# degenerate dwelling no longer emits a negative SAP. Removed a -12.3 outlier
|
||||
# (cert 422000111926, lodged at the floor of 1, was computing -11.3): within-0.5
|
||||
# 70.2% -> 70.3%, MAE 0.845 -> 0.833.
|
||||
_MIN_WITHIN_HALF_SAP = 0.71
|
||||
# EXTRACT-FAN DEFAULT IS UNKNOWN-FALLBACK, NOT A FLOOR (RdSAP 10 §4.1 Table 5,
|
||||
# PDF p.28). Table 5 reads "Number of extract fans if known; if unknown:
|
||||
# [age-band default]". The cascade applied `max(lodged, age_default)`, flooring
|
||||
# a genuinely-lodged count up to the age-band minimum (e.g. an age H-M dwelling
|
||||
# lodging 2 fans billed at the 3-fan default), over-counting ventilation line
|
||||
# (8) and the HLC. Fixed to `lodged if lodged > 0 else default` (a lodged 0 is
|
||||
# the Elmhurst/RdSAP "unknown" form → default; any positive count is literal).
|
||||
# within-0.5 71.6% -> 72.5%, MAE 0.819 -> 0.815. Surfaced by Khalim's Elmhurst
|
||||
# stress worksheet (simulated case 46): closed its last ventilation residual
|
||||
# (our Jan ACH 9.14 -> 9.0748 exact; SAP 29 -> 30 = accredited Elmhurst).
|
||||
_MIN_WITHIN_HALF_SAP = 0.72
|
||||
_MAX_SAP_MAE = 0.82
|
||||
_MAX_CO2_MAE_TONNES = 0.09 # t CO2 / yr vs co2_emissions_current
|
||||
_MAX_PE_PER_M2_MAE = 3.7 # kWh / m2 / yr vs energy_consumption_current
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue