mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Slice S0380.137: extend Table 4a R-dispatch to electric storage / direct-acting / underfloor / ceiling (cluster)
Continuation of S0380.135's Table 4a per-heating-system responsiveness dispatch (`_RESPONSIVENESS_BY_SAP_CODE` in cert_to_inputs.py). The solid-fuel coverage closed 10 corpus variants; this slice extends the dispatch to the electric heating SAP code ranges from SAP 10.2 Table 4a (PDF p.170): 401 Old (large volume) storage heaters R=0.00 402 Slimline storage heaters R=0.20 403 Convector storage heaters R=0.20 404 Fan storage heaters R=0.40 405 Slimline storage heaters + Celect-type ctrl R=0.40 407 Fan storage heaters + Celect-type ctrl R=0.60 408 Integrated storage+direct-acting heater R=0.60 409 High heat retention storage heaters (§9.2.8) R=0.80 421 In concrete slab (off-peak only) R=0.00 422 Integrated (storage+direct-acting) R=0.25 423 Integrated with low off-peak R=0.50 424 In screed above insulation R=0.75 425 In timber floor / immediately below covering R=1.00 515 Electricaire system R=0.75 691 Panel, convector or radiant heaters R=1.00 694 Water- or oil-filled radiators R=1.00 701 Electric ceiling heating R=0.75 A few electric storage codes (402, 403, 405, 407) carry a *different* R value in the 24-hour tariff section of Table 4a vs the off-peak section (e.g. Slimline 402 = R=0.20 off-peak / R=0.40 24-hour). This dict captures the off-peak value as the default because the 24-hour tariff is rare in the corpus (no variant lodges it). If a 24-hour- tariff cert surfaces with one of these codes the dispatch needs to be promoted to a (sap_code, tariff) lookup; until then the off-peak default applies. Heating-systems corpus impact — 6 electric corpus variants re-pinned: variant SAP R ΔSAP was ΔPE was electric 3 401 0.00 +9.43 +14.70 -1059 -3189 electric 5 402 0.20 +6.76 +10.97 -96 -1798 electric 6 404 0.40 +7.82 +10.97 -494 -1770 electric 7 408 0.60 +7.58 +9.68 -428 -1277 electric 8 409 0.80 +5.84 +6.89 +200 -224 electric 9 421 0.00 +6.77 +12.03 +154 -1976 3/6 PE residuals close to ±200 kWh (electric 5/8/9). The remaining +5..+9 SAP residuals across all electric variants suggest a separate shared cascade gap (likely Table 12a high/low-rate fraction or pumps/ fans electric handling — fuel cost is consistently under-counted by ~£100-£220 across the cluster). Queued for follow-up. electric 1 (SAP 191 Direct acting electric boiler) and electric 2 (SAP 524 Air source heat pump) unchanged — both have spec R=1.0 already (matched the Table 4d emitter fallback). Extended handover suite: 880 pass / 0 fail (+1 new AAA test covering the 17 electric R-dispatch entries). Pyright net-zero on touched files (43 → 43). No golden fixture impact — no golden cert lodges a covered electric SAP code via the cascade path that would shift residuals. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
9427354d88
commit
2907b40ed9
3 changed files with 109 additions and 9 deletions
|
|
@ -143,16 +143,28 @@ class _CorpusExpectation:
|
|||
# solid-fuel corpus variants. All 10 re-pinned: 7/10 close to ±220
|
||||
# PE, dual-fuel solid fuel 6 SAP regressed -7.38 → -11.37 (PE
|
||||
# closed +87) — exposed a separate dual-fuel cascade bug.
|
||||
#
|
||||
# Slice S0380.136 fixed the dual-fuel cascade bug — solid fuel 6
|
||||
# closed -11.37 → +1.95 (cost £268 → -£45) by routing
|
||||
# `_is_electric_main` through the canonical T32-first normaliser
|
||||
# instead of a literal {10, 25, 29} ∪ {30..40} mixed-enum check.
|
||||
#
|
||||
# Slice S0380.137 extended the Table 4a R-dispatch to electric storage
|
||||
# / direct-acting / underfloor / ceiling SAP codes (401-409, 421-425,
|
||||
# 515, 691, 694, 701). Six electric corpus variants re-pinned: PE
|
||||
# residuals dropped from -1.3..-3.2k to -1.1k..+200 kWh; SAP
|
||||
# residuals from +6.9..+14.7 to +5.8..+9.4. electric 5/8/9 close to
|
||||
# ±200 PE.
|
||||
_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=-11.8017),
|
||||
_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=+164.9052),
|
||||
_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=+970.7570),
|
||||
_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=-3189.2203),
|
||||
_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=-1797.9601),
|
||||
_CorpusExpectation(variant='electric 6', block='11a', expected_sap_resid=+10.9720, expected_cost_resid_gbp=-252.8131, expected_co2_resid_kg=-209.8689, expected_pe_resid_kwh=-1769.8410),
|
||||
_CorpusExpectation(variant='electric 7', block='11a', expected_sap_resid=+9.6834, expected_cost_resid_gbp=-223.1212, expected_co2_resid_kg=-137.9832, expected_pe_resid_kwh=-1276.9603),
|
||||
_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=-224.4607),
|
||||
_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=-1975.8392),
|
||||
_CorpusExpectation(variant='electric 3', block='11a', expected_sap_resid=+9.4332, expected_cost_resid_gbp=-217.3549, expected_co2_resid_kg=-112.3439, expected_pe_resid_kwh=-1059.2875),
|
||||
_CorpusExpectation(variant='electric 5', block='11a', expected_sap_resid=+6.7642, expected_cost_resid_gbp=-155.8576, expected_co2_resid_kg=-5.3096, expected_pe_resid_kwh=-95.6333),
|
||||
_CorpusExpectation(variant='electric 6', block='11a', expected_sap_resid=+7.8189, expected_cost_resid_gbp=-180.1606, expected_co2_resid_kg=-50.0685, expected_pe_resid_kwh=-494.3960),
|
||||
_CorpusExpectation(variant='electric 7', block='11a', expected_sap_resid=+7.5834, expected_cost_resid_gbp=-174.7323, expected_co2_resid_kg=-31.5507, expected_pe_resid_kwh=-427.5932),
|
||||
_CorpusExpectation(variant='electric 8', block='11a', expected_sap_resid=+5.8386, expected_cost_resid_gbp=-134.5304, expected_co2_resid_kg=+18.2051, expected_pe_resid_kwh=+199.7233),
|
||||
_CorpusExpectation(variant='electric 9', block='11a', expected_sap_resid=+6.7699, expected_cost_resid_gbp=-155.9877, expected_co2_resid_kg=+11.1781, expected_pe_resid_kwh=+154.0936),
|
||||
_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=-454.5023),
|
||||
_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=-1050.4919),
|
||||
_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=-83.8239),
|
||||
|
|
|
|||
|
|
@ -1020,9 +1020,19 @@ def _responsiveness(main: Optional[MainHeatingDetail]) -> float:
|
|||
# These rows override the emitter-based Table 4d lookup because the spec
|
||||
# explicitly lists R against the heating system (the system's intrinsic
|
||||
# response time dominates over the emitter's distribution dynamics).
|
||||
# Slice S0380.135 added the solid-fuel rows (151-161 + 631-636); more
|
||||
# entries are added as fixtures surface them (electric storage / range
|
||||
# cookers / etc.). SAP codes not in this dict fall through to Table 4d.
|
||||
# Slice S0380.135 added the solid-fuel rows; S0380.137 added electric
|
||||
# storage / direct-acting / underfloor / electric ceiling rows. More
|
||||
# entries are added as fixtures surface them. SAP codes not in this
|
||||
# dict fall through to Table 4d.
|
||||
#
|
||||
# A few electric storage codes (402, 403, 405, 407) carry a *different*
|
||||
# R value in the 24-hour tariff section vs the off-peak section (e.g.
|
||||
# Slimline 402 = R=0.2 off-peak / R=0.4 24-hour). This dict captures
|
||||
# the off-peak value as the default because the 24-hour tariff is rare
|
||||
# in the corpus (no variant lodges it). If a 24-hour-tariff cert
|
||||
# surfaces with one of these codes the dispatch needs to be promoted
|
||||
# to a (sap_code, tariff) lookup; until then the off-peak default
|
||||
# applies (under-shoots R for the 24-hour case).
|
||||
_RESPONSIVENESS_BY_SAP_CODE: Final[dict[int, float]] = {
|
||||
# Solid-fuel independent boilers (Table 4a p.169):
|
||||
151: 0.75, # Manual feed independent boiler
|
||||
|
|
@ -1043,6 +1053,30 @@ _RESPONSIVENESS_BY_SAP_CODE: Final[dict[int, float]] = {
|
|||
634: 0.50, # Closed room heater with boiler (no radiators)
|
||||
635: 0.75, # Stove (pellet fired)
|
||||
636: 0.75, # Stove (pellet fired) with boiler (no radiators)
|
||||
# Electric storage heaters off-peak tariff (Table 4a p.170):
|
||||
401: 0.00, # Old (large volume) storage heaters
|
||||
402: 0.20, # Slimline storage heaters (24-hr tariff: 0.40)
|
||||
403: 0.20, # Convector storage heaters (24-hr tariff: 0.40)
|
||||
404: 0.40, # Fan storage heaters
|
||||
405: 0.40, # Slimline storage heaters with Celect-type control
|
||||
# (24-hr tariff: 0.60)
|
||||
407: 0.60, # Fan storage heaters with Celect-type control
|
||||
# (24-hr tariff: 0.60 — same)
|
||||
408: 0.60, # Integrated storage+direct-acting heater
|
||||
409: 0.80, # High heat retention storage heaters (§9.2.8)
|
||||
# Electric underfloor heating off-peak / standard tariffs:
|
||||
421: 0.00, # In concrete slab (off-peak only)
|
||||
422: 0.25, # Integrated (storage+direct-acting)
|
||||
423: 0.50, # Integrated (storage+direct-acting) low off-peak
|
||||
424: 0.75, # In screed above insulation
|
||||
425: 1.00, # In timber floor / immediately below floor covering
|
||||
# Electric warm air:
|
||||
515: 0.75, # Electricaire system
|
||||
# Electric direct-acting room heaters (Table 4a p.170):
|
||||
691: 1.00, # Panel, convector or radiant heaters
|
||||
694: 1.00, # Water- or oil-filled radiators
|
||||
# Electric ceiling heating (Table 4a Group 7 dispatch):
|
||||
701: 0.75,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1185,6 +1185,60 @@ def test_responsiveness_solid_fuel_sap_code_160_returns_0p50_per_table_4a() -> N
|
|||
assert abs(_responsiveness(_solid_fuel_main(102)) - 1.0) <= 1e-9
|
||||
|
||||
|
||||
def test_responsiveness_electric_storage_sap_codes_use_table_4a_off_peak_rows() -> None:
|
||||
# Arrange — SAP 10.2 Table 4a (PDF p.170, electric storage / direct-
|
||||
# acting / underfloor / ceiling rows):
|
||||
#
|
||||
# SAP code Description R (off-peak)
|
||||
# -------- ----------------------------------------- ------------
|
||||
# 401 Old (large volume) storage heaters 0.00
|
||||
# 402 Slimline storage heaters 0.20
|
||||
# 404 Fan storage heaters 0.40
|
||||
# 408 Integrated storage+direct-acting 0.60
|
||||
# 409 High heat retention storage (§9.2.8) 0.80
|
||||
# 421 In concrete slab (off-peak only) 0.00
|
||||
# 422 Integrated (storage+direct-acting) 0.25
|
||||
# 423 Integrated low off-peak 0.50
|
||||
# 424 In screed above insulation 0.75
|
||||
# 515 Electricaire system 0.75
|
||||
# 691 Panel/convector/radiant heaters 1.00
|
||||
# 701 Electric ceiling heating 0.75
|
||||
#
|
||||
# S0380.137 closes the same pattern as S0380.135 (solid-fuel) for
|
||||
# electric heating SAP codes — pre-slice the cascade ignored the
|
||||
# Table 4a per-system R and used Table 4d emitter R=1.0 (radiators)
|
||||
# everywhere, over-estimating heating system response and under-
|
||||
# estimating demand by ~5-15% across the electric corpus cluster.
|
||||
# See `_RESPONSIVENESS_BY_SAP_CODE` for the off-peak vs 24-hour
|
||||
# tariff caveat on codes 402/403/405.
|
||||
|
||||
def _electric_main(sap_code: int) -> MainHeatingDetail:
|
||||
return MainHeatingDetail(
|
||||
has_fghrs=False, main_fuel_type=30, heat_emitter_type=1,
|
||||
emitter_temperature=1, main_heating_control=2105,
|
||||
main_heating_category=2, sap_main_heating_code=sap_code,
|
||||
)
|
||||
|
||||
# Act / Assert — full electric storage / direct-acting / UFH coverage
|
||||
assert abs(_responsiveness(_electric_main(401)) - 0.00) <= 1e-9
|
||||
assert abs(_responsiveness(_electric_main(402)) - 0.20) <= 1e-9
|
||||
assert abs(_responsiveness(_electric_main(403)) - 0.20) <= 1e-9
|
||||
assert abs(_responsiveness(_electric_main(404)) - 0.40) <= 1e-9
|
||||
assert abs(_responsiveness(_electric_main(405)) - 0.40) <= 1e-9
|
||||
assert abs(_responsiveness(_electric_main(407)) - 0.60) <= 1e-9
|
||||
assert abs(_responsiveness(_electric_main(408)) - 0.60) <= 1e-9
|
||||
assert abs(_responsiveness(_electric_main(409)) - 0.80) <= 1e-9
|
||||
assert abs(_responsiveness(_electric_main(421)) - 0.00) <= 1e-9
|
||||
assert abs(_responsiveness(_electric_main(422)) - 0.25) <= 1e-9
|
||||
assert abs(_responsiveness(_electric_main(423)) - 0.50) <= 1e-9
|
||||
assert abs(_responsiveness(_electric_main(424)) - 0.75) <= 1e-9
|
||||
assert abs(_responsiveness(_electric_main(425)) - 1.00) <= 1e-9
|
||||
assert abs(_responsiveness(_electric_main(515)) - 0.75) <= 1e-9
|
||||
assert abs(_responsiveness(_electric_main(691)) - 1.00) <= 1e-9
|
||||
assert abs(_responsiveness(_electric_main(694)) - 1.00) <= 1e-9
|
||||
assert abs(_responsiveness(_electric_main(701)) - 0.75) <= 1e-9
|
||||
|
||||
|
||||
def test_heat_emitter_code_dispatch_table_4d_full_coverage() -> None:
|
||||
# Arrange — SAP 10.2 Table 4d responsiveness by Elmhurst-mapper
|
||||
# heat_emitter_type code (per `_ELMHURST_HEAT_EMITTER_TO_SAP10` at
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue