mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-08-02 21:08:24 +00:00
fix(sap): (203)-weight the dual-main Table 4e MIT adjustment
For a dwelling with two main systems heating different parts, the (93)m Table 4e mean-internal-temperature adjustment was taken from system 1 alone (`details[0]`). SAP 10.2 p.186 requires the (203)-weighted mean of each system's control adjustment — (1 - (203)) for system 1, (203) for system 2 — mirroring the Table 9b responsiveness weighting already applied two lines above. Keyed to system 1, a dwelling led by a small-fraction system (e.g. a 20% HHRSH, adj 0.0 °C, ahead of an 80% manual-charge storage heater, adj +0.7 °C) was modelled ~0.7 °C too cool → space heat under-counted → over-rated. Compute the weighted adjustment at the call site. Shared `cert_to_inputs` path, so it also lifts the accredited gov-API RdSAP corpus (81.5% → 81.7%, guardrail). 5 Edmund Close +2.74 → +0.59; 4 Edmund Close -2.51 → -0.55. LRHA cohort within-0.5 60.2% → 62.1%, MAE 0.496 → 0.445; ratchets re-based to 0.62 / 0.45. Also corrects an oil-combi docstring (462051619031 is a Grant Vortex 10599, not the Worcester 18415). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
138f5688e0
commit
4d9dd9aaf3
2 changed files with 55 additions and 4 deletions
|
|
@ -128,8 +128,24 @@ _MANIFEST_PATH = _FIXTURES_DIR / "manifest.json"
|
|||
# P960 worksheet exactly (5.4175 kWh/day). The 5 oil storage-combis collapse
|
||||
# from +2.0..+4.9 into the ±1.5 spread: within-0.5 55.3% → 58.3%, MAE
|
||||
# 0.675 → 0.531.
|
||||
_MIN_WITHIN_HALF: float = 0.58
|
||||
_MAX_SAP_MAE: float = 0.54
|
||||
# 2026-07-27 (fan-out audit — 3 spec-correct fixes, each with a named test
|
||||
# above): (1) the window channel collapsed MIXED per-window glazing onto
|
||||
# windows[0]'s U (SAP 10.2 (26)-(27) sums each window's own Table 24 U); (2) a
|
||||
# surveyed measured cylinder heat-loss factor was dropped on the PasHub path (an
|
||||
# extractor label mismatch + the mapper never wiring `cylinder_heat_loss`), so
|
||||
# the calc fell back to the Table 2 insulation default (SAP 10.2 §4 (48)-(50)
|
||||
# uses the declared loss); (3) the dual-main Table 4e MIT adjustment was keyed
|
||||
# to system 1 alone, not (203)-weighted across both mains (SAP 10.2 p.186).
|
||||
# Fixes (1)/(3) are symmetric — they correct over- AND under-raters — and (3)
|
||||
# lives in the shared `cert_to_inputs` path, improving the accredited gov-API
|
||||
# RdSAP corpus too (81.5% → 81.7%, guardrail). Combined: within-0.5 58.3% →
|
||||
# 62.1%, MAE 0.531 → 0.445. The residual top-of-cohort is now entirely the
|
||||
# Varsity Rise community-heating cluster (a solar-DHW over-credit + an immersion
|
||||
# fuel-collision), deferred to Elmhurst adjudication (per-cert build sheets
|
||||
# prepared). Floor/ceiling re-baselined to the corrected cohort — coverage
|
||||
# correction is not loosening.
|
||||
_MIN_WITHIN_HALF: float = 0.62
|
||||
_MAX_SAP_MAE: float = 0.45
|
||||
|
||||
_KNOWN_GAP_REASON = (
|
||||
"pashub `from_site_notes` mapper does not int-code a main-heating "
|
||||
|
|
@ -229,7 +245,11 @@ def test_oil_storage_combi_applies_pcdb_store_loss() -> None:
|
|||
The five oil storage-combis (461996268736, 461989738731, 497712315613,
|
||||
497602074839, 462051619031) all collapse from +2..+4.9 into the cohort's
|
||||
normal ±1.5 spread; only this one is pinned (the others' residuals are
|
||||
PasHub-vs-Elmhurst divergence, not the store-loss bug)."""
|
||||
PasHub-vs-Elmhurst divergence, not the store-loss bug). NB the five share
|
||||
the store_type-1 storage-combi treatment but NOT one PCDB product — e.g.
|
||||
462051619031 is a Grant Vortex Outdoor Condensing Combi (PCDB 10599), not
|
||||
the Worcester Greenstar Heatslave II (18415) pinned here; the Table 2 note-b
|
||||
store loss uses each boiler's own PCDB store volume/insulation."""
|
||||
outcome = _evaluate("461996268736")
|
||||
assert outcome.diff is not None
|
||||
assert outcome.diff < 0.5, f"oil storage-combi store loss not applied: diff {outcome.diff:.2f}"
|
||||
|
|
@ -321,6 +341,25 @@ def test_measured_cylinder_loss_overrides_table2() -> None:
|
|||
assert outcome.diff < 0.5, f"measured cylinder loss not applied: diff {outcome.diff:.2f}"
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
"462019682527" not in _BY_ID, reason="dual-main Table-4e fixture not in manifest"
|
||||
)
|
||||
def test_dual_main_table_4e_adjustment_is_fraction_weighted() -> None:
|
||||
"""Regression: for a dwelling with two main systems heating different parts
|
||||
(5 Edmund Close, 462019682527 — a 20% HHRSH, Table 4e adj 0.0 °C, + an 80%
|
||||
manual-charge storage heater, adj +0.7 °C), the (93)m Table 4e mean-internal-
|
||||
temperature adjustment must be the (203)-weighted mean of both systems'
|
||||
control adjustments per SAP 10.2 p.186, not system 1's alone. Keyed to the
|
||||
20% HHRSH the adjustment was 0.0 (dwelling modelled ~0.7 °C too cool → space
|
||||
heat under-counted) → over-rated +2.74. The (203)-weighted +0.56 °C brings
|
||||
it to +0.59. Bound 1.0 guards against regression to the +2.74 mis-weight; the
|
||||
residual is normal cohort spread. NB this fix lives in the shared
|
||||
`cert_to_inputs` path and improves the gov-API RdSAP corpus too (81.5→81.7%)."""
|
||||
outcome = _evaluate("462019682527")
|
||||
assert outcome.diff is not None
|
||||
assert outcome.diff < 1.0, f"Table 4e adjustment not (203)-weighted: diff {outcome.diff:.2f}"
|
||||
|
||||
|
||||
@pytest.mark.skipif(not _FIXTURES, reason="no pashub_accuracy_lrha_wave3 fixtures/manifest")
|
||||
@pytest.mark.parametrize("deal_id", _IDS)
|
||||
def test_pashub_fixture_computes(deal_id: str) -> None:
|
||||
|
|
|
|||
|
|
@ -8405,6 +8405,18 @@ def cert_to_inputs(
|
|||
main_2_responsiveness_value = _responsiveness(
|
||||
_mit_main_2, tariff=_mit_tariff, epc=epc
|
||||
)
|
||||
# SAP 10.2 p.186: the (93)m Table 4e temperature adjustment for a dwelling
|
||||
# with two main systems heating different parts is the (203)-weighted mean
|
||||
# of each system's control adjustment — (1 - (203)) for system 1, (203) for
|
||||
# system 2 — mirroring the Table 9b responsiveness weighting above. Keyed to
|
||||
# system 1 alone it ignores the dominant system's control when the two carry
|
||||
# different Table 4e controls (e.g. a 20% HHRSH + 80% manual-charge storage).
|
||||
control_temperature_adjustment_value = _control_temperature_adjustment_c(main)
|
||||
if _mit_main_2 is not None and main_2_control_type_value is not None:
|
||||
control_temperature_adjustment_value = (
|
||||
(1.0 - main_2_fraction_value) * _control_temperature_adjustment_c(main)
|
||||
+ main_2_fraction_value * _control_temperature_adjustment_c(_mit_main_2)
|
||||
)
|
||||
monthly_total_gains_w = tuple(
|
||||
internal_gains_monthly_w[m] + solar_gains_monthly_w[m] for m in range(12)
|
||||
)
|
||||
|
|
@ -8429,7 +8441,7 @@ def cert_to_inputs(
|
|||
control_type=control_type_value,
|
||||
responsiveness=responsiveness_value,
|
||||
living_area_fraction=living_area_fraction_value,
|
||||
control_temperature_adjustment_c=_control_temperature_adjustment_c(main),
|
||||
control_temperature_adjustment_c=control_temperature_adjustment_value,
|
||||
main_2_control_type=main_2_control_type_value,
|
||||
main_2_fraction=main_2_fraction_value,
|
||||
main_2_responsiveness=main_2_responsiveness_value,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue