mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Slice S0380.132: strict-raise MissingMainFuelType on empty main_fuel_type
The cascade's `_main_fuel_code` previously returned None when
`MainHeatingDetail.main_fuel_type` was anything other than an int
(empty string, None, or an unmapped string label). The downstream
`table_32.unit_price_p_per_kwh(None)` then silently defaulted to mains
gas (3.48 p/kWh / CO2 0.21 kg/kWh / η 0.45 / PE 1.22) — a misleading
fallback where cost may happen to be close but CO2 / PE / efficiency
are completely wrong for the actual heating system.
Probe of the heating-systems corpus surfaced 26 of 41 controlled-
variable variants with `main_fuel_type=''`:
Community heating 1/2/3/4/6 (Table 4a 301-304) 5
Electric 11/12/13/14 (Table 4a 5xx/6xx/7xx) 4
No system (SAP code 699) 1
Oil 2 (HVO) / oil 3 (FAME) / oil 4 (FAME) /
oil 5 (bioethanol) / oil 6 (B30K) (Table 4b) 5
Solid fuel 2..11 (Table 4a 150-160 + 600-636) 10
pcdb 3 (lodges 'Bulk LPG' string — mapper dict gap) 1
Each pre-slice carried a residual pin in `_EXPECTATIONS` encoding the
broken mains-gas-default state. Solid fuel 8's +0.87 ΔSAP — the
"smallest open residual" the user asked to investigate next — turned
out to be the net of compensating cost/efficiency errors; the CO2
delta was +3525 kg/yr and PE +4103 kWh/yr because the cascade was
costing wood chips as mains gas.
Two changes land together:
1. Add `MissingMainFuelType(ValueError)` to
`domain/sap10_calculator/exceptions.py`. Semantics distinct from
the sibling `UnmappedSapCode` (which is for unmapped int dispatch
codes; this is for "value not resolvable to a SAP fuel code at
all"). The error message names the lodged value + the
`sap_main_heating_code` hint so the upstream mapper fix is
obvious.
2. `_main_fuel_code` in `cert_to_inputs.py` now raises
`MissingMainFuelType` when `main_fuel_type` is not an int.
`main is None` still returns None (genuinely no main heating).
The 26 blocked corpus variants are lifted out of the
`_EXPECTATIONS` residual-pin grid into a new tuple
`_BLOCKED_BY_MISSING_MAIN_FUEL_TYPE` driving a new parametrised test
`test_heating_systems_corpus_blocked_variant_raises_missing_main_fuel_type`
that asserts the raise for each blocked variant. As mapper-side fixes
land (deriving fuel from `sap_main_heating_code` via SAP 10.2 Table
4a/4b/4f, or extending `_ELMHURST_MAIN_FUEL_TO_SAP10`), variants move
back onto the residual-pin grid.
Mirrors the [[reference-unmapped-sap-code]] / [[reference-unmapped-
api-code]] strict-raise pattern: forcing function for spec/mapper
completion at the cascade boundary instead of silently producing
wrong outputs.
Extended handover suite at HEAD post-slice: 875 pass / 0 fail (was
874; +1 from the new `_main_fuel_code` strict-raise unit test;
26 blocked corpus pins replaced 1:1 by 26 assert-on-raise tests).
Pyright net-zero (43 → 43 — all pre-existing `pytest.approx` flags).
No golden fixture impact — every golden cert carries an int
`main_fuel_type`.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
65e2025f96
commit
019f6f3be1
4 changed files with 179 additions and 29 deletions
|
|
@ -38,6 +38,7 @@ import pytest
|
|||
from backend.documents_parser.elmhurst_extractor import ElmhurstSiteNotesExtractor
|
||||
from datatypes.epc.domain.mapper import EpcPropertyDataMapper
|
||||
from domain.sap10_calculator.calculator import calculate_sap_from_inputs
|
||||
from domain.sap10_calculator.exceptions import MissingMainFuelType
|
||||
from domain.sap10_calculator.rdsap.cert_to_inputs import (
|
||||
SAP_10_2_SPEC_PRICES,
|
||||
cert_to_inputs,
|
||||
|
|
@ -82,18 +83,21 @@ class _CorpusExpectation:
|
|||
# price from RdSAP 10 Table 32's published 7.64 p/kWh to the Elmhurst-
|
||||
# worksheet-canonical 5.44 p/kWh. Worst-residual oil ΔSAP −11.63 → +0.42;
|
||||
# pcdb 1 −9.41 → +6.95 (largest remaining oil-cohort gap).
|
||||
#
|
||||
# Slice S0380.132 surfaced 26 variants where the Elmhurst Summary §14.0
|
||||
# "Fuel Type" lodging is absent and the mapper produces
|
||||
# `main_fuel_type=''` (or an unmapped string like 'Bulk LPG'). Before
|
||||
# this slice the cascade silently routed those certs through mains gas
|
||||
# defaults (3.48 p/kWh / 0.21 kg CO2/kWh / η 0.45) — the pre-slice
|
||||
# residual pins encoded that broken state. The cascade now raises
|
||||
# `MissingMainFuelType` for these variants; the corresponding
|
||||
# `_CorpusExpectation` entries were lifted out into
|
||||
# `_BLOCKED_BY_MISSING_MAIN_FUEL_TYPE` (assert-on-raise test) until
|
||||
# each mapper gap is closed and the cert can be moved back onto the
|
||||
# residual-pin grid.
|
||||
_EXPECTATIONS: tuple[_CorpusExpectation, ...] = (
|
||||
_CorpusExpectation(variant='ashp', block='11a', expected_sap_resid=+5.6680, expected_cost_resid_gbp=-130.5995, expected_co2_resid_kg=-1.4283, expected_pe_resid_kwh=+1467.8983),
|
||||
_CorpusExpectation(variant='community heating 1', block='11b', expected_sap_resid=+4.1830, expected_cost_resid_gbp=-96.3816, expected_co2_resid_kg=-786.6453, expected_pe_resid_kwh=-940.7364),
|
||||
_CorpusExpectation(variant='community heating 2', block='11b', expected_sap_resid=+1.1558, expected_cost_resid_gbp=-26.6309, expected_co2_resid_kg=-498.3058, expected_pe_resid_kwh=+636.7545),
|
||||
_CorpusExpectation(variant='community heating 3', block='11b', expected_sap_resid=+4.1830, expected_cost_resid_gbp=-96.3816, expected_co2_resid_kg=+2545.7991, expected_pe_resid_kwh=+11009.4778),
|
||||
_CorpusExpectation(variant='community heating 4', block='11b', expected_sap_resid=+1.1558, expected_cost_resid_gbp=-26.6309, expected_co2_resid_kg=-3465.0640, expected_pe_resid_kwh=-374.6720),
|
||||
_CorpusExpectation(variant='community heating 6', block='11b', expected_sap_resid=-6.8661, expected_cost_resid_gbp=+158.2067, expected_co2_resid_kg=-2002.8867, expected_pe_resid_kwh=+6995.3140),
|
||||
_CorpusExpectation(variant='electric 1', block='11a', expected_sap_resid=+9.6439, expected_cost_resid_gbp=-222.2109, expected_co2_resid_kg=+14.3441, expected_pe_resid_kwh=+2837.1414),
|
||||
_CorpusExpectation(variant='electric 11', block='11a', expected_sap_resid=+18.1002, expected_cost_resid_gbp=-417.0547, expected_co2_resid_kg=+579.6971, expected_pe_resid_kwh=-1067.1863),
|
||||
_CorpusExpectation(variant='electric 12', block='11a', expected_sap_resid=+15.4249, expected_cost_resid_gbp=-355.4117, expected_co2_resid_kg=+620.4563, expected_pe_resid_kwh=-467.5748),
|
||||
_CorpusExpectation(variant='electric 13', block='11a', expected_sap_resid=+18.3886, expected_cost_resid_gbp=-423.7001, expected_co2_resid_kg=+619.3628, expected_pe_resid_kwh=-1129.2285),
|
||||
_CorpusExpectation(variant='electric 14', block='11a', expected_sap_resid=+18.3886, expected_cost_resid_gbp=-423.7001, expected_co2_resid_kg=+619.3628, expected_pe_resid_kwh=-1129.2285),
|
||||
_CorpusExpectation(variant='electric 2', block='11a', expected_sap_resid=+5.8523, expected_cost_resid_gbp=-134.8455, expected_co2_resid_kg=+94.4364, expected_pe_resid_kwh=+2420.9013),
|
||||
_CorpusExpectation(variant='electric 3', block='11a', expected_sap_resid=+14.6973, expected_cost_resid_gbp=-338.6485, expected_co2_resid_kg=-379.1296, expected_pe_resid_kwh=-850.9293),
|
||||
_CorpusExpectation(variant='electric 5', block='11a', expected_sap_resid=+10.9720, expected_cost_resid_gbp=-252.8131, expected_co2_resid_kg=-218.5642, expected_pe_resid_kwh=+540.3309),
|
||||
|
|
@ -102,28 +106,56 @@ _EXPECTATIONS: tuple[_CorpusExpectation, ...] = (
|
|||
_CorpusExpectation(variant='electric 8', block='11a', expected_sap_resid=+6.8875, expected_cost_resid_gbp=-158.6999, expected_co2_resid_kg=-34.9564, expected_pe_resid_kwh=+2113.8303),
|
||||
_CorpusExpectation(variant='electric 9', block='11a', expected_sap_resid=+12.0340, expected_cost_resid_gbp=-277.2813, expected_co2_resid_kg=-255.6076, expected_pe_resid_kwh=+362.4518),
|
||||
_CorpusExpectation(variant='gshp', block='11a', expected_sap_resid=+5.1598, expected_cost_resid_gbp=-118.8901, expected_co2_resid_kg=-41.4461, expected_pe_resid_kwh=+639.1890),
|
||||
_CorpusExpectation(variant='no system', block='11a', expected_sap_resid=+21.9350, expected_cost_resid_gbp=-505.4134, expected_co2_resid_kg=+689.2188, expected_pe_resid_kwh=-2454.8193),
|
||||
_CorpusExpectation(variant='oil 1', block='11a', expected_sap_resid=+2.6578, expected_cost_resid_gbp=-61.2402, expected_co2_resid_kg=-242.2677, expected_pe_resid_kwh=+1259.6587),
|
||||
_CorpusExpectation(variant='oil 2', block='11a', expected_sap_resid=+26.0712, expected_cost_resid_gbp=-600.7179, expected_co2_resid_kg=+2230.1071, expected_pe_resid_kwh=+801.2920),
|
||||
_CorpusExpectation(variant='oil 3', block='11a', expected_sap_resid=+30.9500, expected_cost_resid_gbp=-712.1785, expected_co2_resid_kg=+2859.5796, expected_pe_resid_kwh=+738.4592),
|
||||
_CorpusExpectation(variant='oil 4', block='11a', expected_sap_resid=+28.5927, expected_cost_resid_gbp=-655.6129, expected_co2_resid_kg=+2636.9526, expected_pe_resid_kwh=+701.8340),
|
||||
_CorpusExpectation(variant='oil 5', block='11a', expected_sap_resid=+120.7457, expected_cost_resid_gbp=-6312.0020, expected_co2_resid_kg=+1345.3630, expected_pe_resid_kwh=-2780.6222),
|
||||
_CorpusExpectation(variant='oil 6', block='11a', expected_sap_resid=+24.4087, expected_cost_resid_gbp=-561.8886, expected_co2_resid_kg=-658.8928, expected_pe_resid_kwh=-478.5733),
|
||||
_CorpusExpectation(variant='oil pcdb 1', block='11a', expected_sap_resid=+0.4239, expected_cost_resid_gbp=-9.7668, expected_co2_resid_kg=-35.9551, expected_pe_resid_kwh=+2086.7505),
|
||||
_CorpusExpectation(variant='oil pcdb 2', block='11a', expected_sap_resid=+0.4239, expected_cost_resid_gbp=-9.7668, expected_co2_resid_kg=-35.9551, expected_pe_resid_kwh=+2086.7505),
|
||||
_CorpusExpectation(variant='oil pcdb 3', block='11a', expected_sap_resid=+1.1597, expected_cost_resid_gbp=-26.7204, expected_co2_resid_kg=-53.1709, expected_pe_resid_kwh=+1897.4341),
|
||||
_CorpusExpectation(variant='pcdb 1', block='11a', expected_sap_resid=+6.9521, expected_cost_resid_gbp=-157.6055, expected_co2_resid_kg=-845.8065, expected_pe_resid_kwh=-171.6971),
|
||||
_CorpusExpectation(variant='pcdb 3', block='11a', expected_sap_resid=+27.7563, expected_cost_resid_gbp=-637.0435, expected_co2_resid_kg=-446.3815, expected_pe_resid_kwh=+2097.4553),
|
||||
_CorpusExpectation(variant='solid fuel 10', block='11a', expected_sap_resid=+14.7769, expected_cost_resid_gbp=-340.4814, expected_co2_resid_kg=+1906.2620, expected_pe_resid_kwh=-584.5284),
|
||||
_CorpusExpectation(variant='solid fuel 11', block='11a', expected_sap_resid=+8.4098, expected_cost_resid_gbp=-193.7739, expected_co2_resid_kg=+2262.3481, expected_pe_resid_kwh=+2583.7764),
|
||||
_CorpusExpectation(variant='solid fuel 2', block='11a', expected_sap_resid=+6.0050, expected_cost_resid_gbp=-138.3659, expected_co2_resid_kg=-3718.6886, expected_pe_resid_kwh=+1594.6199),
|
||||
_CorpusExpectation(variant='solid fuel 3', block='11a', expected_sap_resid=+6.1846, expected_cost_resid_gbp=-142.5032, expected_co2_resid_kg=-5877.9595, expected_pe_resid_kwh=+3118.4874),
|
||||
_CorpusExpectation(variant='solid fuel 4', block='11a', expected_sap_resid=+5.0671, expected_cost_resid_gbp=-116.7534, expected_co2_resid_kg=-3215.4585, expected_pe_resid_kwh=+2547.5896),
|
||||
_CorpusExpectation(variant='solid fuel 5', block='11a', expected_sap_resid=+3.7888, expected_cost_resid_gbp=-87.2980, expected_co2_resid_kg=-2725.9268, expected_pe_resid_kwh=+3224.8144),
|
||||
_CorpusExpectation(variant='solid fuel 6', block='11a', expected_sap_resid=+9.2944, expected_cost_resid_gbp=-214.1551, expected_co2_resid_kg=+2174.7565, expected_pe_resid_kwh=+4052.5690),
|
||||
_CorpusExpectation(variant='solid fuel 7', block='11a', expected_sap_resid=+15.1079, expected_cost_resid_gbp=-344.9565, expected_co2_resid_kg=-3711.3064, expected_pe_resid_kwh=+488.1476),
|
||||
_CorpusExpectation(variant='solid fuel 8', block='11a', expected_sap_resid=+0.8707, expected_cost_resid_gbp=-20.0627, expected_co2_resid_kg=+3524.9644, expected_pe_resid_kwh=+4103.0089),
|
||||
_CorpusExpectation(variant='solid fuel 9', block='11a', expected_sap_resid=+15.1593, expected_cost_resid_gbp=-349.2946, expected_co2_resid_kg=+1810.7952, expected_pe_resid_kwh=+168.2046),
|
||||
)
|
||||
|
||||
|
||||
# Variants the mapper currently leaves with `main_fuel_type=''` (no
|
||||
# §14.0 "Fuel Type" lodged) or an unmapped string (pcdb 3 lodges "Bulk
|
||||
# LPG" — Elmhurst label not yet in `_ELMHURST_MAIN_FUEL_TO_SAP10`). The
|
||||
# cascade now strict-raises via `_main_fuel_code` per S0380.132 instead
|
||||
# of silently defaulting to mains gas. Each entry will move back onto
|
||||
# the `_EXPECTATIONS` residual-pin grid once the mapper gap closes.
|
||||
#
|
||||
# Grouped by SAP code range to mirror the mapper-derivation slices the
|
||||
# follow-ups will need:
|
||||
# - Community heating (Table 4a 301-304) ×5
|
||||
# - Electric storage / direct-acting (Table 4a 5xx, 6xx, 7xx) ×4
|
||||
# - "No system" (SAP code 699) ×1
|
||||
# - Liquid-fuel boilers Table 4b non-oil (HVO/FAME/B30K/bioethanol) ×5
|
||||
# - Solid-fuel boilers (Table 4a 150-160, 600-636) ×10
|
||||
# - PCDB-lodged "Bulk LPG" mapper-dict gap ×1
|
||||
_BLOCKED_BY_MISSING_MAIN_FUEL_TYPE: tuple[str, ...] = (
|
||||
'community heating 1',
|
||||
'community heating 2',
|
||||
'community heating 3',
|
||||
'community heating 4',
|
||||
'community heating 6',
|
||||
'electric 11',
|
||||
'electric 12',
|
||||
'electric 13',
|
||||
'electric 14',
|
||||
'no system',
|
||||
'oil 2',
|
||||
'oil 3',
|
||||
'oil 4',
|
||||
'oil 5',
|
||||
'oil 6',
|
||||
'pcdb 3',
|
||||
'solid fuel 10',
|
||||
'solid fuel 11',
|
||||
'solid fuel 2',
|
||||
'solid fuel 3',
|
||||
'solid fuel 4',
|
||||
'solid fuel 5',
|
||||
'solid fuel 6',
|
||||
'solid fuel 7',
|
||||
'solid fuel 8',
|
||||
'solid fuel 9',
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -285,3 +317,34 @@ def test_heating_systems_corpus_residual_matches_pin(
|
|||
f"drifted from pin {expectation.expected_pe_resid_kwh:+.4f} kWh/yr "
|
||||
f"(tolerance ±{_PE_RESID_ABS_TOLERANCE_KWH})"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"variant",
|
||||
_BLOCKED_BY_MISSING_MAIN_FUEL_TYPE,
|
||||
ids=lambda v: v,
|
||||
)
|
||||
def test_heating_systems_corpus_blocked_variant_raises_missing_main_fuel_type(
|
||||
variant: str,
|
||||
) -> None:
|
||||
# Arrange — every variant in `_BLOCKED_BY_MISSING_MAIN_FUEL_TYPE`
|
||||
# has an Elmhurst Summary §14.0 that does not lodge "Fuel Type" (or
|
||||
# lodges a string label the mapper's `_ELMHURST_MAIN_FUEL_TO_SAP10`
|
||||
# doesn't yet recognise). The mapper consequently produces
|
||||
# `MainHeatingDetail.main_fuel_type=''` (or the raw unmapped
|
||||
# string), so the cascade's `_main_fuel_code` strict-raises per
|
||||
# S0380.132 (mirror of [[reference-unmapped-sap-code]] pattern).
|
||||
#
|
||||
# This forcing-function test asserts the raise actually fires for
|
||||
# each blocked variant. As mapper-side fixes land (deriving the
|
||||
# fuel from `sap_main_heating_code` via SAP 10.2 Table 4a/4b/4f,
|
||||
# or extending the Elmhurst label dict), variants move out of this
|
||||
# list and back onto the residual-pin grid in `_EXPECTATIONS`.
|
||||
summary_pdf, _ = _variant_paths(variant)
|
||||
pages = _summary_pdf_to_textract_style_pages(summary_pdf)
|
||||
site_notes = ElmhurstSiteNotesExtractor(pages).extract()
|
||||
epc = EpcPropertyDataMapper.from_elmhurst_site_notes(site_notes)
|
||||
|
||||
# Act / Assert
|
||||
with pytest.raises(MissingMainFuelType):
|
||||
cert_to_inputs(epc, prices=SAP_10_2_SPEC_PRICES)
|
||||
|
|
|
|||
|
|
@ -34,3 +34,30 @@ class UnmappedSapCode(ValueError):
|
|||
)
|
||||
self.field = field
|
||||
self.value = value
|
||||
|
||||
|
||||
class MissingMainFuelType(ValueError):
|
||||
"""The cascade was asked to resolve `MainHeatingDetail.main_fuel_type`
|
||||
but the mapper produced no usable SAP fuel code (None / empty string
|
||||
/ unmapped string label).
|
||||
|
||||
Unlike the Table 4d/4e dispatch sites where "absent" maps to a spec-
|
||||
blessed "assume as-built" default, heating fuel has no defensible
|
||||
default: silently routing to mains gas produces a misleading cascade
|
||||
output where cost may happen to be close but CO2 / PE / efficiency
|
||||
are completely wrong for the actual heating system. The fix is
|
||||
upstream in the mapper — extract the fuel from the appropriate
|
||||
Summary / EPC field, or derive it from `sap_main_heating_code`
|
||||
via SAP 10.2 Table 4a/4b/4f.
|
||||
"""
|
||||
|
||||
def __init__(self, value: object, sap_main_heating_code: object) -> None:
|
||||
super().__init__(
|
||||
f"MainHeatingDetail.main_fuel_type is not resolvable to a SAP "
|
||||
f"fuel code (got {value!r}); sap_main_heating_code="
|
||||
f"{sap_main_heating_code!r}. Fix the mapper to populate "
|
||||
f"main_fuel_type as an int via Summary / EPC fields or via "
|
||||
f"SAP 10.2 Table 4a/4b/4f derivation from the SAP code."
|
||||
)
|
||||
self.value = value
|
||||
self.sap_main_heating_code = sap_main_heating_code
|
||||
|
|
|
|||
|
|
@ -700,7 +700,10 @@ _CONTROL_TYPE_BY_CODE: Final[dict[int, int]] = {
|
|||
}
|
||||
|
||||
|
||||
from domain.sap10_calculator.exceptions import UnmappedSapCode
|
||||
from domain.sap10_calculator.exceptions import (
|
||||
MissingMainFuelType,
|
||||
UnmappedSapCode,
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
|
@ -1006,10 +1009,24 @@ _RESPONSIVENESS_BY_EMITTER_CODE: Final[dict[int, float]] = {
|
|||
|
||||
|
||||
def _main_fuel_code(main: Optional[MainHeatingDetail]) -> Optional[int]:
|
||||
"""Resolve `MainHeatingDetail.main_fuel_type` to a SAP fuel code.
|
||||
|
||||
- `main is None` (no main heating system) → None.
|
||||
- `main_fuel_type` is an int → that code.
|
||||
- `main_fuel_type` is anything else (empty string from a mapper
|
||||
extraction gap, or an unmapped string label like 'Bulk LPG') →
|
||||
raise `MissingMainFuelType`. Heating fuel has no defensible
|
||||
"assume as-built" default (silently routing to mains gas
|
||||
mis-categorises CO2 / PE / efficiency), so the cascade strict-
|
||||
raises to force the mapper-side fix. Mirror of the
|
||||
[[reference-unmapped-sap-code]] strict-raise pattern.
|
||||
"""
|
||||
if main is None:
|
||||
return None
|
||||
fuel = main.main_fuel_type
|
||||
return fuel if isinstance(fuel, int) else None
|
||||
if isinstance(fuel, int):
|
||||
return fuel
|
||||
raise MissingMainFuelType(fuel, main.sap_main_heating_code)
|
||||
|
||||
|
||||
def _fuel_cost_gbp_per_kwh(
|
||||
|
|
|
|||
|
|
@ -33,7 +33,10 @@ from domain.sap10_ml.tests._fixtures import (
|
|||
make_window,
|
||||
)
|
||||
from domain.sap10_calculator.calculator import Sap10Calculator, SapResult
|
||||
from domain.sap10_calculator.exceptions import UnmappedSapCode
|
||||
from domain.sap10_calculator.exceptions import (
|
||||
MissingMainFuelType,
|
||||
UnmappedSapCode,
|
||||
)
|
||||
from domain.sap10_calculator.rdsap.cert_to_inputs import (
|
||||
_has_suspended_timber_floor_per_spec, # pyright: ignore[reportPrivateUsage]
|
||||
_heat_network_dlf, # pyright: ignore[reportPrivateUsage]
|
||||
|
|
@ -978,6 +981,46 @@ def test_cert_to_inputs_raises_unmapped_sap_code_on_unknown_main_heating_control
|
|||
assert excinfo.value.value == 2998
|
||||
|
||||
|
||||
def test_cert_to_inputs_raises_missing_main_fuel_type_on_empty_string() -> None:
|
||||
# Arrange — when the mapper produces a MainHeatingDetail with an
|
||||
# empty-string `main_fuel_type` (Elmhurst Summary §14.0 leaves the
|
||||
# "Fuel Type" line absent for many SAP code ranges — solid-fuel
|
||||
# boilers 150-160, community heating 301-304, electric storage 5xx,
|
||||
# "no system" 699 — leaving only `sap_main_heating_code` as the
|
||||
# fuel hint), the cascade has no defensible default. Silently
|
||||
# routing to mains gas (3.48 p/kWh / CO2 0.21 / η 0.45) produces a
|
||||
# misleading cascade output where cost may happen to be close but
|
||||
# CO2 / PE / efficiency are completely wrong.
|
||||
#
|
||||
# `_main_fuel_code` must raise `MissingMainFuelType` so the upstream
|
||||
# mapper gap surfaces unambiguously at test time. Mirror of
|
||||
# `UnmappedSapCode` strict-raise pattern per
|
||||
# [[reference-unmapped-sap-code]].
|
||||
epc = make_minimal_sap10_epc(
|
||||
total_floor_area_m2=_TYPICAL_TFA_M2,
|
||||
habitable_rooms_count=4,
|
||||
region_code="1",
|
||||
sap_building_parts=[make_building_part(
|
||||
floor_dimensions=[make_floor_dimension(total_floor_area_m2=90.0, floor=0)],
|
||||
)],
|
||||
sap_heating=make_sap_heating(
|
||||
main_heating_details=[
|
||||
MainHeatingDetail(
|
||||
has_fghrs=False, main_fuel_type="", heat_emitter_type=1,
|
||||
emitter_temperature=1, main_heating_control=2106,
|
||||
main_heating_category=4, sap_main_heating_code=160,
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
# Act / Assert
|
||||
with pytest.raises(MissingMainFuelType) as excinfo:
|
||||
cert_to_inputs(epc)
|
||||
assert excinfo.value.value == ""
|
||||
assert excinfo.value.sap_main_heating_code == 160
|
||||
|
||||
|
||||
def test_heat_emitter_code_2_underfloor_in_screed_routes_to_responsiveness_0p75_per_table_4d() -> None:
|
||||
# Arrange — SAP 10.2 Table 4d (PDF p.170, "Heating type and
|
||||
# responsiveness ... depending on heat emitter"):
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue