mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
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>
63 lines
2.7 KiB
Python
63 lines
2.7 KiB
Python
"""Calculator-side strict-raise exception types.
|
|
|
|
Shared across `domain/sap10_calculator/` modules so any cascade-dispatch
|
|
helper can raise a consistent exception when it encounters a SAP/Table
|
|
code outside its dispatch dict. Mirrors the mapper-side
|
|
`UnmappedApiCode` / `UnmappedElmhurstLabel` pattern at
|
|
`datatypes/epc/domain/mapper.py`.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
class UnmappedSapCode(ValueError):
|
|
"""A SAP/Table integer code lodged on the cert that the calculator
|
|
does not yet know how to translate to a dispatch result.
|
|
|
|
Raised by strict cascade-dispatch helpers (Table 4e control codes,
|
|
Table 4d emitter codes, Appendix M PV pitch / overshading, meter →
|
|
tariff, Table 12c heat-network DLF age band, Table 11 secondary
|
|
heating fraction by category, etc.) to surface spec-coverage gaps
|
|
at the cascade boundary instead of silently defaulting to a
|
|
fallback value.
|
|
|
|
Distinguish "lodging absent" (code is None / 0 / "" — cascade
|
|
default OK, spec "assume as-built" applies) from "lodging present
|
|
but unmapped" (raise — fixture exposes a dispatch-dict gap that
|
|
needs an entry).
|
|
"""
|
|
|
|
def __init__(self, field: str, value: object) -> None:
|
|
super().__init__(
|
|
f"unmapped SAP code in {field}: {value!r}; "
|
|
f"add an entry to the corresponding cascade dispatch dict"
|
|
)
|
|
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
|