fix(ventilation): map API mechanical_ventilation_index_number for MEV fan electricity

Follow-up to the §2 MV-kind slice. Once MEV dwellings stopped
under-stating their ventilation HEAT loss, a +0.9 SAP over-rate residual
remained — the MEV FAN ELECTRICITY (§5 Table 4f line (230a),
`SFPav × 1.22 × V`, PCDB Tables 322 decentralised-MEV + 329 in-use
factors). `_mev_decentralised_kwh_per_yr_from_cert` already composes it,
but reads `epc.mechanical_ventilation_index_number` +
`epc.mechanical_vent_duct_type`, and the API builder
(`from_rdsap_schema_21_0_1`) never set either — so `pcdf_id is None`
short-circuited the fan energy to 0 on every API cert (the Summary/
Elmhurst path set them, so cert 000565 already billed it).

Wire both schema fields through the 21.0.1 API construction (the corpus
schema). Eval: the 9 MEV certs carrying a PCDB index closed +0.90 ->
+0.13 signed (fan electricity now billed); headline within-0.5 55.01% ->
55.12%, mean|err| 1.233 -> 1.232, 909 computed / 0 raises. Only those 9
certs move (clean diff). The 11 index-less MEV certs still sit at +1.36 —
they need the SAP Table 4h DEFAULT specific fan power (no PCDB record), a
separate slice.

New end-to-end test + fixture (cert 1300, Titon-class dMEV index 500777,
Flexible duct): from_api_response preserves the index + duct type and
(230a) resolves to a positive fan-energy contribution. Goldens + full
calc/epc regression green; pyright net-zero.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-08 23:16:34 +00:00
parent dfba20babf
commit e7af6fda66
3 changed files with 38 additions and 0 deletions

View file

@ -1559,6 +1559,18 @@ class EpcPropertyDataMapper:
open_chimneys_count=schema.open_chimneys_count,
insulated_door_count=schema.insulated_door_count,
draughtproofed_door_count=schema.draughtproofed_door_count,
# Mechanical ventilation PCDB plumbing — feeds the §5 Table 4f
# line (230a) decentralised-MEV fan electricity
# (`SFPav × 1.22 × V`, PCDB Tables 322/329). Without the index
# the cascade's `_mev_decentralised_kwh_per_yr_from_cert`
# short-circuits to 0, leaving the MEV fan running cost off the
# bill (the +0.9 SAP over-rate residual on the MEV cohort once
# the §2 ventilation heat-loss was fixed). Duct type selects the
# Table 329 in-use factor (1=Flexible / 2=Rigid).
mechanical_ventilation_index_number=(
schema.mechanical_ventilation_index_number
),
mechanical_vent_duct_type=schema.mechanical_vent_duct_type,
# Lighting
led_fixed_lighting_bulbs_count=schema.led_fixed_lighting_bulbs_count,
cfl_fixed_lighting_bulbs_count=schema.cfl_fixed_lighting_bulbs_count,

File diff suppressed because one or more lines are too long

View file

@ -983,6 +983,31 @@ def test_api_to_domain_mapper_preserves_main_heating_index_number(
assert abs(inputs.main_heating_efficiency - expected_winter_eff) <= 1e-3
def test_api_mapper_preserves_mechanical_ventilation_pcdb_index_for_mev_fan_energy() -> None:
# Arrange — cert 1300 is a decentralised-MEV dwelling
# (mechanical_ventilation=2) lodging a PCDB index (500777) + duct type
# (1=Flexible). The API path previously DROPPED
# `mechanical_ventilation_index_number`, so the §5 Table 4f line (230a)
# MEV fan electricity (`SFPav × 1.22 × V`, PCDB Tables 322/329)
# short-circuited to 0 in `_mev_decentralised_kwh_per_yr_from_cert` —
# leaving the fan running cost off the bill (+0.9 SAP over-rate residual
# on the MEV cohort once the §2 ventilation heat loss was fixed).
from domain.sap10_calculator.rdsap.cert_to_inputs import (
_mev_decentralised_kwh_per_yr_from_cert, # pyright: ignore[reportPrivateUsage]
)
doc = _load_cert("1300-7634-0922-3203-3563")
# Act
epc = EpcPropertyDataMapper.from_api_response(doc)
# Assert — the PCDB pointer + duct type survive the API mapping, and the
# (230a) MEV fan electricity resolves to a positive contribution.
assert epc.mechanical_ventilation_index_number == 500777
assert epc.mechanical_vent_duct_type == 1
assert _mev_decentralised_kwh_per_yr_from_cert(epc) > 0.0
def test_0240_api_wall_type_4_windows_map_to_roof_windows() -> None:
"""Cert 0240 lodges 6 windows with `window_wall_type=4` — the RdSAP
API code for a roof window ("Roof of Room" rooflight / inclined