mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
Resolve electric-underfloor overrides to their own SAP codes dragging electricity 🟥
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e5c112a2d3
commit
ef34f76116
2 changed files with 115 additions and 0 deletions
|
|
@ -0,0 +1,92 @@
|
|||
# Electric underfloor is scored by its own SAP codes; the meter defers to the cert when the tariff is ambiguous
|
||||
|
||||
## Status
|
||||
|
||||
accepted
|
||||
|
||||
## Context
|
||||
|
||||
Electric-underfloor `main_heating_system` overrides had no archetype, so the LLM
|
||||
classifier funnelled them into the nearest wrong bucket (#1376):
|
||||
|
||||
| description | rows | mis-mapped to |
|
||||
| --- | --- | --- |
|
||||
| Electric Underfloor Heating: In concrete slab (off-peak only) | 15 | Direct-acting electric (191), **Single** meter |
|
||||
| Electric Underfloor Heating: In screed above insulation (standard or off peak) | 63 | Direct-acting electric (191), **Single** meter |
|
||||
| Electric Underfloor Heating: Integrated (storage+direct-acting) (off peak) | 12 | Electric storage heaters, old (401) |
|
||||
|
||||
The **meter is the crux**. A Landlord heating override drags an assumed meter from
|
||||
its SAP code (ADR-0035), and the `keep_existing_off_peak_meter` mitigation
|
||||
([overlay_applicator.py](../../domain/modelling/scoring/overlay_applicator.py))
|
||||
protects a cert's lodged off-peak meter — but *only when the overlay's own desired
|
||||
meter is off-peak*. Direct-acting (191) is a **Single**-meter system, so the
|
||||
mitigation never fires and the overlay **forces Single**, overwriting the cert's
|
||||
meter. For off-peak underfloor that bills overnight heat at the peak rate and
|
||||
craters SAP — the exact Class-A meter defect.
|
||||
|
||||
SAP Table 4a has **dedicated electric-underfloor codes** (labels from
|
||||
`cert_to_inputs.py`): **421** in concrete slab (off-peak only), **422** integrated
|
||||
(storage+direct-acting), **424** in screed above insulation (423 integrated-low-
|
||||
off-peak and 425 timber-floor are not in the corpus). Per SAP 10.2 §12, **421/422
|
||||
are off-peak (Rule 2, 7-hour → Dual)**; **424 is not off-peak-implying (Single)**.
|
||||
|
||||
A lodged-meter audit is decisive for the screed case: **51 of 63 (81%)** of the
|
||||
`"…in screed (standard or off peak)"` dwellings lodge an **off-peak** meter (24-hour
|
||||
dual or 7/10-hour), the rest single. The description itself says "standard **or**
|
||||
off peak" — so the archetype genuinely cannot know the tariff; the cert does.
|
||||
Forcing 424/Single would downgrade 81%; forcing Dual would over-credit the ~19% on
|
||||
single.
|
||||
|
||||
## Decision
|
||||
|
||||
Add three electric-underfloor archetypes mapping to their own SAP codes, and treat
|
||||
the meter per what the description actually asserts.
|
||||
|
||||
1. **New archetypes** in `_MAIN_HEATING_CODES`:
|
||||
`"Electric underfloor, in concrete slab (off-peak)"` → **421**,
|
||||
`"Electric underfloor, integrated storage and direct-acting"` → **422**,
|
||||
`"Electric underfloor, in screed above insulation"` → **424**.
|
||||
2. **Electricity fuel (29)** drags for 421/422/424 via `_natural_fuel_for` — electric
|
||||
underfloor is unambiguously electric (ADR-0041), closing the prior no-fuel gap.
|
||||
3. **421 and 422 assert an off-peak Dual meter.** Their descriptions positively
|
||||
claim off-peak ("off-peak only" / "off peak"), and both are already in
|
||||
`_ASSUMED_DUAL_METER_CODES`; the `keep_existing_off_peak_meter` mitigation keeps
|
||||
a more-specific cert off-peak meter and applies Dual to a single-rate cert.
|
||||
4. **424 (screed) defers the meter to the cert.** Because "standard or off peak" is
|
||||
tariff-agnostic and 81% lodge off-peak, `_meter_for` returns **`None`** for 424
|
||||
(a new meter-agnostic set), so the overlay leaves `meter_type` unset and the
|
||||
lodged meter stands — each dwelling scores on its actual tariff. This is a
|
||||
**targeted exception to the "always set the meter, never let it bleed"
|
||||
invariant**: that invariant guards against a switched-*off* storage system
|
||||
leaving a stale Dual meter, but an underfloor override *confirms* underfloor —
|
||||
there is no switch-off, so deferring is faithful, not a bleed.
|
||||
5. **Deterministic `main_heating_guard` + reclassify** (TEXT-first, pgEnum-deferred),
|
||||
reusing the generalized `reclassify_main_heating.py`. All three underfloor
|
||||
descriptions are description-determined, so the guard resolves them directly (no
|
||||
fuel-awareness needed) and the reclassify follows.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The 15 concrete-slab-off-peak rows stop being billed at the peak rate (191/Single
|
||||
→ 421/Dual); the 12 integrated rows get the correct underfloor code (401 → 422,
|
||||
meter already Dual); the 63 screed rows keep their lodged (81% off-peak) meter and
|
||||
score on code 424 with correct screed responsiveness (Table 4d R=0.75). Correct
|
||||
Rebaselining (ADR-0039).
|
||||
- **FE-owned pgEnum additions (Dan):** three new `main_heating_system` values —
|
||||
`"Electric underfloor, in concrete slab (off-peak)"`, `"Electric underfloor,
|
||||
integrated storage and direct-acting"`, `"Electric underfloor, in screed above
|
||||
insulation"`. TEXT read path fixed on merge; cache writes defer to the migration.
|
||||
- `_meter_for` becomes `Optional[str]` (a code may defer the meter). Every existing
|
||||
code keeps its current Single/Dual answer; only 424 returns `None`.
|
||||
- Extends [ADR-0041](0041-landlord-heating-classification-targets-a-complete-modellable-taxonomy.md)
|
||||
and [ADR-0035](0035-coherent-heating-system-synthesis.md); refines the
|
||||
`keep_existing_off_peak_meter` behaviour ADR-0035 introduced.
|
||||
|
||||
### Alternatives rejected
|
||||
|
||||
- **Force Single (424 per spec).** Rejected: downgrades the 81% lodged off-peak,
|
||||
reproducing the meter bug.
|
||||
- **Force Dual for screed.** Rejected: over-credits the ~19% genuinely on single and
|
||||
asserts a tariff the "standard or off peak" description does not claim.
|
||||
- **Map underfloor to Direct-acting (191) / old storage (401) as today.** Rejected:
|
||||
wrong code (responsiveness/meter) and the source of the mis-score.
|
||||
|
|
@ -127,6 +127,29 @@ def test_electric_cpsu_decodes_to_192_electric_on_an_off_peak_meter() -> None:
|
|||
assert simulation.heating.meter_type == "Dual"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("main_heating_value", "code"),
|
||||
[
|
||||
# Electric underfloor funnelled into Direct-acting (191) / old storage (401)
|
||||
# for want of the dedicated SAP Table 4a underfloor codes (ADR-0046).
|
||||
("Electric underfloor, in concrete slab (off-peak)", 421),
|
||||
("Electric underfloor, integrated storage and direct-acting", 422),
|
||||
("Electric underfloor, in screed above insulation", 424),
|
||||
],
|
||||
)
|
||||
def test_electric_underfloor_decodes_to_its_own_code_dragging_electricity(
|
||||
main_heating_value: str, code: int
|
||||
) -> None:
|
||||
# Act
|
||||
simulation = main_heating_overlay_for(main_heating_value, 0)
|
||||
|
||||
# Assert — its own underfloor code, electric (29).
|
||||
assert simulation is not None
|
||||
assert simulation.heating is not None
|
||||
assert simulation.heating.sap_main_heating_code == code
|
||||
assert simulation.heating.main_fuel_type == 29
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("main_heating_value", "code"),
|
||||
[
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue