Commit graph

5850 commits

Author SHA1 Message Date
Khalim Conn-Kowlessar
f7c674d4cd docs: handover post S0380.164..169
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 15:40:25 +00:00
Khalim Conn-Kowlessar
318c981a17 Slice S0380.169: EES "NON" → electricity (no-system unblock per SAP 10.2 §A.2.2)
Adds `"NON": 30` to `_ELMHURST_MAIN_HEATING_EES_TO_FUEL_CODE` so the
mapper can derive the main heating fuel for the Elmhurst "no main
heating system" lodging (§14.0 Main Heating EES = NON + SAP code
699 + §14.1 Heating Type = None).

SAP 10.2 §A.2.2: "When no main heating system is identified, the
calculation is for the assumed system consisting of portable electric
heaters." Routes the fuel to Table 32 standard-electricity code 30
(tariff resolved separately from `meter_type` per `_rdsap_tariff`).

Pre-slice the cascade raised `MissingMainFuelType` per S0380.132.
Post-slice the cascade closes most of the way:

  no system: ΔSAP_c +1.18,  Δcost −£27, ΔCO2 −50, ΔPE −562

The residuals are cascade-side (likely §A.2.2 portable-electric
efficiency / responsiveness / control-type defaults differ slightly
from Elmhurst) — pinned at observed values as forcing function for
follow-up.

Moves `no system` out of `_BLOCKED_BY_MISSING_MAIN_FUEL_TYPE` into
`_EXPECTATIONS`. Blocked tier now: 5 community-heating variants.

Tests:
  - test_elmhurst_main_heating_ees_maps_no_system_code_to_electricity
  - corpus pin: no system expected residuals at observed values

916 pass / 0 fail.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 15:40:25 +00:00
Khalim Conn-Kowlessar
1b3e8248d4 Slice S0380.168: Bio-liquid mapper extensions + Table 32 FAME price flip
Mapper extensions (`_ELMHURST_MAIN_HEATING_EES_TO_FUEL_CODE`):

  "BFD": 71,  # HVO        — corpus variant oil 2 (SAP 127)
  "BXE": 73,  # FAME       — corpus variant oil 3 (SAP 128)
  "BXF": 73,  # FAME alt   — corpus variant oil 4 (SAP 129)
  "BZC": 76,  # Bioethanol — corpus variant oil 5 (SAP 126)
  "B3C": 75,  # B30K       — corpus variant oil 6 (SAP 126)

`_ELMHURST_MAIN_FUEL_TO_SAP10` water-side labels:

  "Bio-liquid HVO from used cooking oil": 71,
  "Bio-liquid FAME from animal/vegetable oils": 73,
  "Bioethanol": 76,
  "B30K": 75,

Values are direct Table 32 codes (the bio-liquid codes 71/73/75/76
don't collide with any API enum value so they pass through
`unit_price_p_per_kwh` etc. unchanged). Spec: SAP 10.2 Table 12
(PDF p.189) notes (d)/(e)/(f).

Pre-slice all 5 oil 2-6 variants raised `MissingMainFuelType` per
S0380.132. Post-mapper-extension cascade results:

  oil 2 (HVO):       SAP / cost / CO2 / PE all EXACT first try ✓
  oil 5 (Bioethanol): SAP / cost / CO2 / PE all EXACT first try ✓
  oil 3 (FAME):      SAP +17.34, cost −£398
  oil 4 (FAME alt):  SAP +16.06, cost −£367
  oil 6 (B30K):      SAP +3.05,  cost −£70

Slice S0380.131 had left a deferred TODO in `table_32.py` for FAME
code 73 ("worksheet 7.64 vs spec 5.44 — flipping has no measurable
cascade effect today, deferred until a cert that exercises it
surfaces"). Now exercised — flipping `73: 5.44 → 7.64` closes 85 %
of the oil 3/4 cost gap:

  oil 3 (FAME):      SAP +17.34 → +2.59,  cost −£398 → −£62
  oil 4 (FAME alt):  SAP +16.06 → +2.56,  cost −£367 → −£57

The Elmhurst-engine canonical 7.64 ↔ spec PDF 5.44 divergence is the
same pattern S0380.131 applied to heating oil (code 4: 7.64 → 5.44)
per [[feedback-software-no-special-handling]].

Remaining residuals on oil 3 / oil 4 / oil 6 are cascade-side
(HW kWh under by ~250-900, SH demand small diff, CO2/PE blend
artifacts) — pinned at observed values as forcing functions for
follow-up slices. Open fronts:
  - HW kWh discrepancy on FAME (cascade applies different efficiency
    path than Elmhurst for SAP codes 128/129)
  - B30K (oil 6) Δcost −£70 with prices matching: SH/HW kWh gap

Closures `oil 2` / `oil 5`: ±0.0000 on all 4 metrics. Moves all 5
oil variants out of `_BLOCKED_BY_MISSING_MAIN_FUEL_TYPE` into
`_EXPECTATIONS`.

Blocked tier now: 6 variants (community heating × 5, no system).
Cascade-OK tier: 32 variants (up from 30), 30 EXACT + 3 (oil 3/4/6)
pinned with non-zero residuals + 1 (pcdb 1 SH residual closed in
S0380.165).

Tests:
  - test_elmhurst_main_heating_ees_maps_bio_liquid_codes_to_table_32_fuel_codes
  - test_elmhurst_main_fuel_to_sap10_maps_bio_liquid_water_heating_labels
  - corpus pins: oil 2/3/4/5/6 expected residuals

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 15:40:25 +00:00
Khalim Conn-Kowlessar
a124c2bc68 Slice S0380.167: EES codes WEA/REA/OEA → electricity (electric storage 11-14 unblock)
Adds three Elmhurst EES (Energy Efficiency Standard) codes to
`_ELMHURST_MAIN_HEATING_EES_TO_FUEL_CODE` so the mapper can derive the
main heating fuel for electric storage / direct-acting certs whose
Elmhurst Summary §14.0 does not lodge a "Main Heating Fuel Type"
string (same pattern as the solid-fuel block above):

  "WEA": 30,  # electric warm-air storage
  "REA": 30,  # resistive electric (corpus electric 12 SAP 691)
  "OEA": 30,  # other electric (corpus electric 13/14 SAP 701)

All route to Table 32 standard-electricity code 30; the cascade
resolves the actual price tier (high vs low rate) downstream via
`_rdsap_tariff(epc)` keyed off `meter_type`.

The corpus carries 4 electric-storage variants on the 18-hour tariff:

  electric 11 — WEA + SAP 515 (warm-air electric)
  electric 12 — REA + SAP 691
  electric 13 — OEA + SAP 701
  electric 14 — OEA + SAP 701  (differs from 13 by emitter / controls)

Pre-slice all 4 raised `MissingMainFuelType` per S0380.132. Post-slice
all 4 EXACT on first try across all 4 metrics:

  electric 11: ΔSAP_c +0.0000  Δcost +£0.0000  ΔCO2 −0.0000  ΔPE −0.0000
  electric 12: ΔSAP_c +0.0000  Δcost +£0.0000  ΔCO2 −0.0000  ΔPE −0.0000
  electric 13: ΔSAP_c +0.0000  Δcost −£0.0000  ΔCO2 +0.0000  ΔPE −0.0000
  electric 14: ΔSAP_c +0.0000  Δcost −£0.0000  ΔCO2 +0.0000  ΔPE −0.0000

Closure on first try because the cascade was already wired for the
electric-storage path (SAP 10.2 Table 4a codes 515 / 691 / 701, Table
4e Group 4 storage controls, Table 5a pump-gain wet-gate from S0380.160,
S0380.144 secondary-fraction by sub-row); only the Elmhurst EES → fuel
mapping was missing.

Moves electric 11/12/13/14 out of `_BLOCKED_BY_MISSING_MAIN_FUEL_TYPE`
into `_EXPECTATIONS` at ±0.0000. Blocked tier now: 11 variants
(community heating × 5, no system, oil 2-6).

Tests:
  - test_elmhurst_main_heating_ees_maps_electric_storage_codes_to_electricity
  - corpus pins: electric 11/12/13/14 expected residuals = ±0.0000

Cascade-OK tier: 30 variants (up from 25), all SAP / cost / CO2 / PE
EXACT (< 1e-4) vs Elmhurst worksheet on every metric.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 15:40:25 +00:00
Khalim Conn-Kowlessar
1491899412 Slice S0380.166: Elmhurst "Bulk LPG" label → API code 27 (mapper unblock)
Adds the single missing dict entry that lets cert `pcdb 3` cascade:

  `_ELMHURST_MAIN_FUEL_TO_SAP10["Bulk LPG"] = 27`

API code 27 = "LPG (not community)" — routes via:
  - `API_FUEL_TO_TABLE_12[27] = 2` (SAP 10.2 Table 12 bulk LPG: £62
    standing, 6.74 p/kWh, 0.241 CO2, 1.141 PE; spec PDF p.189)
  - `API_FUEL_TO_TABLE_32[27] = 2` (RdSAP 10 Table 32 bulk LPG: £70
    standing, 7.60 p/kWh; spec PDF p.95)

Pre-slice the mapper produced `main_fuel_type=''` for any Elmhurst
fixture lodging "Bulk LPG" as fuel type, so the cascade strict-raised
`MissingMainFuelType` per S0380.132. The legacy `"LPG bulk"` label
(different word order) maps to API code 6 = wood logs — a pre-existing
oddity unexercised by any live fixture; left untouched per
[[feedback-bigger-slices-for-uniform-work]] (different label, different
fix).

Cascade closure `pcdb 3` (Vokera Linea LPG combi 83.10 %, PCDB index
8262, no cylinder, 18-hour tariff) — EXACT on first try across all 4
metrics:

  cascade  SAP_c = 49.2953    worksheet = 49.2953    Δ = +0.0000
  cascade  cost  = £1165.81   worksheet = £1165.81   Δ = +0.0000
  cascade  CO2   = 3367.95    worksheet = 3367.95    Δ = +0.0000
  cascade  PE    = 13936.60   worksheet = 13936.60   Δ = +0.0000

Closure on first try because the cascade was already fully wired for
the gas/oil/LPG path; the Elmhurst label was the only gap. Moves
pcdb 3 out of `_BLOCKED_BY_MISSING_MAIN_FUEL_TYPE` into `_EXPECTATIONS`
at ±0.0000.

Blocked tier now: 15 variants (community heating × 5, electric storage
11-14, no system, oil 2-6).

Tests:
  - test_elmhurst_main_fuel_to_sap10_maps_bulk_lpg_to_api_code_27
  - corpus pin: pcdb 3 expected residuals = ±0.0000 on all 4 metrics

912 pass / 0 fail; pyright net-zero 43 → 43.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 15:40:25 +00:00
Khalim Conn-Kowlessar
549c0b2a39 Slice S0380.165: §9.4.11 boiler-interlock -5pp applies AFTER Eq D1, not before
SAP 10.2 §9.4.11 (PDF p.30): "The efficiency of gas and liquid fuel
boilers for both space and water heating is reduced by 5% if the
boiler is not interlocked for space and water heating."

S0380.141 had subtracted the -5pp from BOTH `Pwinter` and `Psummer`
PCDB / Table 4b seasonal efficiencies BEFORE running the SAP 10.2
Appendix D §D2.1 (2) Equation D1 monthly cascade. The Elmhurst P960
worksheet for `pcdb 1` (PCDB 716 oil boiler, Pwinter 65 / Psummer 53,
Cylinder Stat=No → no interlock) shows the -5pp is applied to the
η_water,monthly OUTPUT of Eq D1, NOT to its inputs. The two
interpretations diverge because Eq D1's reciprocal weighting (1/η_w
and 1/η_s) is non-linear in η.

Worked example for pcdb 1 Jan (Q_space=1409.77, Q_water=387.86):

  Old cascade:  Eq D1(60, 48, …) = 56.9292 %       (off −0.04 pp)
  Worksheet:    Eq D1(65, 53, …) = 61.9725 %
                                    −5pp = 56.9725 %  ≡ (217)m_jan ✓

Across all 12 months the post-Eq-D1 form matches worksheet (217)m to
1e-4 every month. Cascade HW kWh: 7068.41 → 7063.96 (= worksheet (219)
total exactly), Δ −4.45 kWh.

The spec text "reduced by 5%" does not explicitly state pre- vs post-
Eq D1 ordering. Per [[feedback-software-no-special-handling]] mirror
the Elmhurst engine — the worksheet output is unambiguous.

Changes:
  - `_apply_water_efficiency` gains a `interlock_penalty_pp: float = 0.0`
    kwarg. Eq D1 branch runs on raw (Pwinter, Psummer), then subtracts
    `interlock_penalty_pp / 100` from each monthly efficiency before
    dividing.
  - Caller (`cert_to_inputs` orchestrator) now passes the raw seasonal
    efficiencies in `eq_d1_winter_summer_pct` + the penalty separately.
    The pre-Eq-D1 `eq_d1_winter_summer_pct[0] -= 5` block is removed.
  - SH-side `eff -= 0.05` (line 5349) is unchanged — the SH cascade
    doesn't go through Eq D1, just `(98c)m / eff_sh`.

Closures `pcdb 1`:
  ΔSAP_c −0.0108 → +0.0000 (1e-4)
  Δcost  +£0.24  → +£0.0000
  ΔCO2   +1.33   → +0.0000
  ΔPE    +5.70   → −0.0000

No regressions on the other 25 cascade-OK variants — the gate is
`no_interlock AND eq_d1_winter_summer_pct is not None`, which fires
only when Cylinder Stat=No on a gas/oil boiler cert. The 6 Elmhurst
U985 cohort + cohort-2 Elmhurst fixtures all lodge Cylinder Stat=Yes
(interlock present) → no penalty fires; cohort-1 ASHP certs lodge no
cylinder thermostat at all but route through Appendix N3 instead of
Eq D1. 38 cohort-2 + 9 ASHP golden fixtures all PASS unchanged.

The 41-variant heating-systems corpus cascade-OK tier is now CLOSED:
all 25 variants SAP / cost / CO2 / PE EXACT vs Elmhurst worksheet at
abs < 1e-3 (most < 1e-4). Σ|ΔSAP_c| = 0.0001 (= floating-point noise).

Tests:
  - test_apply_water_efficiency_applies_interlock_penalty_after_equation_d1
  - test_apply_water_efficiency_interlock_penalty_zero_keeps_raw_eq_d1

911 pass / 0 fail; pyright net-zero 43 → 43.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 15:40:25 +00:00
Khalim Conn-Kowlessar
7dbc4c9fcb docs: handover post S0380.164
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 15:40:25 +00:00
Khalim Conn-Kowlessar
3a21f22bb3 Slice S0380.164: Elmhurst-mirror §12.4.4 summer-immersion CO2/PE double-count
SAP 10.2 §12.4.4 (PDF p.36-37): "With open fire back boilers or closed
room heaters with boilers, an alternative system (electric immersion)
may be provided for heating water in summer. In that case water
heating is provided by the boiler for months October to May and by the
alternative system for months June to September."

The spec-literal CO2 / PE formula multiplies summer immersion fuel by
the Table 12d / 12e monthly cascade (per Table 12 footnotes (s) and
(t): "monthly factors in Table 12d/12e should be used in the SAP
worksheet"). The BRE-approved Elmhurst engine adds an extra
`summer_fuel × Table 12 annual electric` term ON TOP of the monthly
cascade for dual-rate tariffs — same Elmhurst-mirror shape as S0380.163
(§8.1) but additive rather than substitutive. Cost is computed
cleanly per spec — the double-count quirk only affects the (264) HW
CO2 and (278) HW PE factor lines.

Worksheet evidence (heating-systems corpus property 001431,
`solid fuel 2` — Table 4a code 158 closed-room-heater + back boiler,
65 % winter η + 100 % summer η, anthracite, 18-hour off-peak tariff):

  (62)m heat        303.12 .. 168.95 .. 175.91 .. 300.40 kWh
  winter fuel (W)   = 2205.80 / 0.65 = 3393.51 kWh anthracite
  summer fuel (S)   = 684.55 / 1.00  =  684.55 kWh immersion
  total fuel        = (219) = 4078.06 kWh

  (264) HW CO2  = 4078.06 × 0.3710 = 1513.15 kg/yr
    = W × 0.395 + S × (0.116 monthly_summer + 0.136 annual)
    = 1340.43 + 79.61 + 93.10 = 1513.14 ✓ within rounding

  (278) HW PE   = 4078.06 × 1.3771 = 5616.04 kWh/yr
    = W × 1.064 + S × (1.429 monthly_summer + 1.501 annual)
    = 3610.69 + 977.84 + 1027.51 = 5616.04 ✓ exact

The +annual term is precisely `S × Table 12 electric factor` and
matches the SF2 corpus pin's ΔCO2 = −93.10 and ΔPE = −1027.51 exactly.
Per [[feedback-software-no-special-handling]] mirror the engine.

Cascade rule (post-slice):

  STANDARD tariff       → winter × anth_annual + Σ wh_summer_m × Table 12d/e
                          (spec-literal, unchanged)
  7h / 10h / 18h / 24h → winter × anth_annual + Σ wh_summer_m × Table 12d/e
                          + S_fuel × Table 12 annual electric (Elmhurst mirror)

Closures `solid fuel 2`:
  ΔCO2 −93.10 → +0.0000 EXACT
  ΔPE  −1027.51 → +0.0000 EXACT
ΔSAP and Δcost remain EXACT (cascade cost path was already correct).

The 41-variant heating-systems corpus is now closed on its 25-variant
cascade-OK tier: all 25 SAP / cost / CO2 / PE EXACT (|Δ| < 1e-3) vs
the Elmhurst worksheet. Only `pcdb 1` carries a sub-tolerance gap
(−0.011 SAP / +5.7 PE — PCDB Eq D1 cascade gap on PCDF index 716, a
separate small slice).

⚠ Single-cert evidence

SF2 is the only §12.4.4 fixture in the corpus (`solid fuel 1` =
code 156 is an empty folder; no other variant exercises a back-boiler
combo with summer immersion). Per the handover ≥2-cert rule for new
§8 divergence rows, this slice was admitted under an explicit
exception: the divergence shares its shape with §8.1 (S0380.163's
Table 12 annual mirror for dual-rate HW), and the math matches the
worksheet to within rounding. The new §8.2 row is tagged with a
"⚠ Single-cert evidence" subsection so future agents know to revisit
if a second §12.4.4 cert worksheet ever diverges from this rule.

Tests:
  - test_section_12_4_4_hw_blend_mirrors_elmhurst_summer_annual_pe_co2_double_count
  - test_section_12_4_4_hw_blend_standard_tariff_keeps_spec_literal_monthly_cascade

909 pass / 0 fail; pyright net-zero 43 → 43.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 15:40:25 +00:00
Khalim Conn-Kowlessar
e2fb18a90f docs: handover post S0380.160..163
Wraps the four slices closing the heating-systems corpus from
Σ|ΔSAP_c| 1.24 → 0 (25/25 cascade-OK variants SAP/cost/CO2/PE
EXACT, except solid fuel 2 summer-immersion-blend artifact).

Highest-leverage next slice: close solid fuel 2 (the only remaining
open variant in the cascade-OK tier) via the S0380.154 blend code
path — likely a parallel Elmhurst-mirror gate for the summer-
immersion CO2/PE factors.

Other open fronts: 16 blocked-tier mapper extensions; pcdb 1 sub-
tolerance -0.011 SAP; cohort-2 golden residuals tightening per
[[feedback-golden-residuals-near-zero]].

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 15:40:25 +00:00
Khalim Conn-Kowlessar
41e50be216 Slice S0380.163: Elmhurst-mirror HW PE/CO2 factor on dual-rate tariffs
SAP 10.2 Table 12 footnote (t) (PDF p.189): "PE factors for grid
electricity vary by month. The average figure given in this table is
therefore not used directly. Instead the monthly factors given in
Table 12e should be used in the SAP worksheet." Footnote (s) says the
same for CO2 / Table 12d. Read literally, monthly factors apply to
every electric end-use including dual-rate HW.

The BRE-approved Elmhurst rdSAP engine doesn't follow that reading
for HW. The 41-variant heating-systems corpus controlled-variable
fixture lodges worksheet (278) "Water heating (low-rate cost)" with
factor **1.5010 PE / 0.136 CO2** (Table 12 annual flat) across every
dual-rate tariff cert, while applying the monthly Table 12e/12d
cascade to lighting (1.5338 winter-weighted) and secondary heating
(1.5715) on the same certs. It's an engine implementation choice,
not a documented spec exception.

Per [[feedback-software-no-special-handling]] the calculator
contract is bit-faithful replication of the engine, not literal
compliance with the spec text. This slice flips cascade
`_hot_water_primary_factor` + `_hot_water_co2_factor_kg_per_kwh` to
accept a `tariff: Tariff` parameter:

  - STANDARD tariff       → Table 12e/12d monthly cascade weighted
                            by HW demand seasonality (unchanged from
                            S0380.71 / .72, matches cohort-1 ASHP
                            standard-tariff worksheet)
  - 7-hour / 10-hour /
    18-hour / 24-hour     → Table 12 annual flat (1.501 / 0.136)
                            matching the Elmhurst worksheet (278)
                            "Water heating (low-rate cost)" row

Per-line walk on electric 3 (18-hour tariff, electric immersion HW,
2384.116 kWh annual):

  worksheet (278) factor = 1.5010
  cascade pre-slice      = 1.5214   delta = +0.0204
  (1.5214 - 1.5010) × 2384.116 = +48.66 kWh/yr PE — EXACT match
  the corpus residual pin.

Same shape for CO2: worksheet 0.1360, cascade pre-slice 0.1410,
delta +0.0050 × 2384.116 = +11.95 kg/yr.

Closures across the 18-variant deferred lighting-PE cohort
(electric 1/2/3/5/6/7/8/9 + solid fuel 4/5/6/7/8/9/10/11 + ashp +
gshp):
  ΔCO2 +6.31 / +11.95 → ±0.0000 EXACT
  ΔPE  +25.51 / +48.66 → ±0.0000 EXACT
  ΔSAP_c / Δcost unchanged at ±0.0000 EXACT (already closed
  pre-slice by S0380.156..162).

All 25 cascade-OK variants in the heating-systems corpus now
SAP / cost / CO2 / PE EXACT vs worksheet on all 4 metrics, with
solid fuel 2 as the only remaining open residual (separate
S0380.154 summer-immersion-blend CO2/PE artifact — deferred).

Documented in
`domain/sap10_calculator/docs/SAP_CALCULATOR.md §8.1
"HW PE/CO2 factors on dual-rate tariffs use Table 12 annual"` —
the master doc now carries a new §8 "Elmhurst-mirrored spec
divergences" section for cases like this. Validation tally
refreshed from stale "930/930" to current "941/941".

No regressions on the 6 Elmhurst U985 fixtures (gas combi
STANDARD tariff — unaffected) or the cohort-1 ASHP certs
(STANDARD tariff — unaffected). The dual-rate gate fires only
on the 4 off-peak tariffs.

Verbatim spec quote retained for reference (SAP 10.2 Table 12
footnote (t), PDF p.189):
  "PE factors for grid electricity vary by month. The average
   figure given in this table is therefore not used directly.
   Instead the monthly factors given in Table 12e should be used
   in the SAP worksheet."

Tests: 907 pass (+1), 0 fail. Pyright net-zero (43 → 43).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 15:40:25 +00:00
Khalim Conn-Kowlessar
4d0e2ed6cf Slice S0380.162: SAP 10.2 Appendix N3.1 default pump gain for electric HPs
SAP 10.2 Appendix N3.1 (PDF p.105) "Circulation pump and fan":
"For electric heat pumps: The electricity used by the water
circulation pump or fan is included within the calculated annual
space and hot water heating efficiency and is not included in
worksheet (230c). **The default heat gain from Table 5a is included
via worksheet (70).**"

This rule applies the Table 5a row "Central heating pump in heated
space" GAIN (3 / 10 / 7 W per pump-age bucket) to electric heat
pumps even though the pump ELECTRICITY is hidden in the COP and
excluded from (230c). The "Not applicable for electric heat pumps
from database" clause in Table 5a footnote a) scopes only to the
PCDB-Table-362 cascade case (Appendix N1.2.1: "For heat pumps held
in the PCDB ... a single water circulation pump serving the heat
emitters is sufficient" — pump kWh AND gain embedded in COP).

S0380.160 over-stripped the gain by zeroing pump_w for every HP
category-4 main, conflating the PCDB-Table-362 case with the Table-4a
default cascade. This slice refines the HP gate in
`_any_main_system_has_central_heating_pump`:
  - Cat 4 HP WITH `main_heating_index_number` lodged (PCDB Table
    362) → continue (skip; pump in COP per N1.2.1);
  - Cat 4 HP with SAP code in `_TABLE_4A_WARM_AIR_SAP_CODES` (Cat 5
    warm-air HPs distribute via ducted air, no water circulation
    pump; warm-air fan handled separately by Table 5a "Warm air
    heating system fans" row, S0380.161) → continue;
  - Otherwise (Cat 4 HP, Table 4a default cascade, water-emitter)
    → apply Table 5a default per Appendix N3.1.

Per-line walk on ashp (SAP code 214 air-to-water HP, Cat 4, no PCDB,
"Post 2013" pump age):
  worksheet (70)[Jan] = 3.0000 W
  cascade pre-slice    = 0.0000 W      delta = -3.000 W
The -3 W winter gain shortfall over-stated cascade (84) Total gains
by -3 W in heating months → cascade SH demand +12.27 kWh/yr
(cascade 9302 vs worksheet 9290), pushing continuous SAP down 0.024
because the cost residual was driven by the +1.5 kWh × 12 month
shortfall flowing through the £0.0741 low-rate cost.

Closures:
  ashp:  ΔSAP -0.0240 → +0.0000 EXACT, Δcost +£0.55 → +£0.00 EXACT
  gshp:  ΔSAP -0.0178 → -0.0000 EXACT, Δcost +£0.41 → -£0.00 EXACT

ΔPE +36 → +25.51 (and ΔCO2 +7.33 → +6.31) — residuals narrow to the
Elmhurst-vs-spec HW PE annual-vs-monthly Table 12e/12d quirk only
(same pattern as the 16-variant lighting-PE deferred cohort,
scaled by HW kWh = 1138 vs 2384 → 25.51 vs 48.66). Cohort
Σ |ΔSAP_c| 0.07 → 0.03; all 25 cascade-OK variants now SAP+cost EXACT.

Cohort-1 (cert 0380 et al.) golden fixtures unaffected — those certs
lodge `main_heating_index_number` (PCDB Table 362) → HP gate skips
correctly → (70) = 0 preserved. Cert 000565 (HP main 1 + gas boiler
main 2) unaffected — wet-boiler branch fires for main 2.

Verbatim spec quote (SAP 10.2 Appendix N3.1, PDF p.105):
  "For electric heat pumps: The electricity used by the water
   circulation pump or fan is included within the calculated annual
   space and hot water heating efficiency and is not included in
   worksheet (230c). The default heat gain from Table 5a is
   included via worksheet (70)."

Tests: 906 pass (+1), 0 fail. Pyright net-zero (35 → 35).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 15:40:25 +00:00
Khalim Conn-Kowlessar
b3196cdcf5 Slice S0380.161: SAP 10.2 Table 5a warm-air fan gain (SFP × 0.04 × V)
SAP 10.2 Table 5a (PDF p.177) row "Warm air heating system fans
a) c)" computes the gain as SFP × 0.04 × V (W). Footnote c) sets
the default SFP to 1.5 W/(l/s) when no PCDB warm-air-unit record
is lodged; footnote a) applies the heating-season-only mask
(zero in summer months). Footnote c) further omits the gain when
the dwelling has balanced whole-house mechanical ventilation
(MVHR / MV) — same omission as the Table 4f kWh-side footnote e).

Pre-slice the cascade's `internal_gains_from_cert` only wired the
central-heating-pump row of Table 5a; the warm-air-fan gain helper
(`warm_air_heating_fan_w`) existed but was unwired. The kWh-side
parallel (Table 4f, 136.35 kWh/yr) was wired in S0380.158 — this
slice closes the symmetry on the gain side.

Per-line walk on electric 2 (SAP code 524 = Cat 5 ASHP with
warm-air distribution, V = 227.25 m³, no balanced MV):

  worksheet (70)[Jan] = 13.6350 W
  cascade (70)[Jan]   = 0.0000 W      delta = -13.635 W
  worksheet (98c)[Jan] = 1600.43 kWh
  cascade (98c)[Jan]  = 1608.12 kWh   delta = +7.69 kWh

13.635 W = 1.5 × 0.04 × 227.25 exactly. The -13.6 W winter gain
shortfall propagates through the §7 utilisation cascade and over-
states cascade SH demand by ~57 kWh/yr (cascade 9483 vs worksheet
9426), under-charging cost by ~£2.50 with opposite sign to the
S0380.156-.158 closures.

Fix: new `_any_main_system_has_warm_air_distribution(epc)` +
`_has_balanced_mechanical_ventilation(epc)` predicates in
`internal_gains.py`, mirroring `cert_to_inputs._TABLE_4A_WARM_AIR_SAP_CODES`
+ `_BALANCED_MV_KIND_NAMES` (kept here as siblings so the worksheet
layer stays free of rdsap deps). Orchestrator wires
`warm_air_heating_fan_w(sfp=1.5, dwelling_volume_m3)` into the
heating-season term of `pumps_fans_monthly_w` when warm-air
distribution is present and balanced MV is not.

Closures electric 2:
  ΔSAP_c -0.1087 → -0.0000 EXACT
  Δcost  +£2.50 → -£0.00 EXACT
  ΔCO2   +16.54 → +11.95 (joins lighting-PE deferred cohort)
  ΔPE    +97.69 → +48.66 (joins lighting-PE deferred cohort)

Electric 2 joins the 15-variant lighting-PE deferred cohort
(electric 1 + electric 3/5/6/7/8/9 + solid fuel 5/6/7/8 + solid
fuel 4/9/10/11 + electric 2) where SAP/cost are EXACT but PE/CO2
carry an Elmhurst-vs-spec MONTHLY-factor offset (cohort uses
Table 12 annual factors on the off-peak HW immersion line; spec
mandates Table 12d/12e monthly per the header).

Verbatim spec quote (SAP 10.2 Table 5a row "Warm air heating
system fans a) c)", PDF p.177):
  "Warm air heating system fans a) c)  SFP × 0.04 × V"
  Footnote c): "SFP is the specific fan power from the database
    record for the warm air unit if applicable; otherwise
    1.5 W/(l/s). These values of SFP include an in-use factor.
    If the heating system is a warm air unit and there is balanced
    whole house mechanical ventilation, the gains for the warm air
    system should not be included."
  Footnote a): "... Set to zero in summer months. ..."

Σ |ΔSAP_c| across 25-variant cohort: 0.18 → 0.07 (~60% reduction).
No regressions on the other 24 variants or any golden fixture —
gate keyed on Table 4a warm-air SAP code frozenset (only electric
2 in the corpus has a code in that set).

Tests: 905 pass (+1), 0 fail. Pyright net-zero (35 → 35).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 15:40:25 +00:00
Khalim Conn-Kowlessar
d793ae8851 Slice S0380.160: SAP 10.2 Table 5a wet-pump gate for central heating gain
SAP 10.2 Table 5a (PDF p.177) row "Central heating pump in heated
space" only applies to mains with a water-loop circulation pump.
Footnote a) names two exclusions verbatim ("Does not apply if a
heating system used solely for domestic hot water. ... Not applicable
for electric heat pumps from database."), and the row's name carries
the implicit third: dry mains with no central heating pump (electric
storage heaters, electric direct-acting, solid-fuel room heaters
without back-boilers) — the row simply doesn't list them.

Pre-slice `internal_gains_from_cert` gated only on Note a) (HP
exclusion), applying `central_heating_pump_w(date_category=...)` to
every non-HP main. The default UNKNOWN-date branch added 7 W of pump
gain to (70)m for every dry-system fixture in the controlled-variable
corpus, even though the worksheet (70)m = 0 every month.

Per-line walk on electric 3 (SAP code 401 "Manual charge control"):

  cascade (73)[Jan] = 640.21 W
  worksheet (73)[Jan] = 633.21 W      delta = +7.00 W
  cascade (70)[Jan] = 7.00 W
  worksheet (70)[Jan] = 0.00 W        Table 5a inapplicable

The +7 W winter-month gain lowered cascade SH demand by ~38 kWh/yr
(cascade 11050 vs worksheet 11088). At Table 32 18-hour low-rate
~7.4 p/kWh that's £2.50/yr under-charging — matching the cluster's
uniform Δcost = -£1.96..-£2.80 pattern. Continuous SAP rose ~+0.10
because cost dominates the ECF.

Fix: new `_any_main_system_has_central_heating_pump(epc)` predicate
in `internal_gains.py`, mirroring `cert_to_inputs._is_wet_boiler_main`
(S0380.149 — Table 4f kWh side). Wet if any non-HP main lodges:
  - sap_main_heating_code in {101-141, 151-161, 191-196} (gas/oil/
    solid-fuel/electric boilers per Table 4a/4b),
  - main_heating_index_number (PCDB Table 322 record),
  - main_heating_category in {1, 2} (RdSAP central heating), OR
  - heat_emitter_type in {1, 3} (radiators / fan-coil per Table 4d).

Dead `_all_main_systems_are_heat_pumps` helper removed (the new
predicate subsumes its role).

Cluster closures (10 variants):
  electric 3:    SAP +0.1215 → -0.0000, cost -£2.80 → -£0.00
  electric 5:    SAP +0.1081 → -0.0000, cost -£2.49 → -£0.00
  electric 6:    SAP +0.1081 → -0.0000, cost -£2.49 → -£0.00
  electric 7:    SAP +0.1017 → -0.0000, cost -£2.34 → -£0.00
  electric 8:    SAP +0.0941 → -0.0000, cost -£2.17 → -£0.00
  electric 9:    SAP +0.1199 → -0.0000, cost -£2.76 → -£0.00
  solid fuel 4:  SAP +0.0850 → -0.0000, cost -£1.96 → -£0.00
  solid fuel 9:  SAP +0.1072 → -0.0000, cost -£2.47 → -£0.00
  solid fuel 10: SAP +0.1134 → +0.0000, cost -£2.61 → -£0.00
  solid fuel 11: SAP +0.0912 → +0.0000, cost -£2.10 → +£0.00

Σ |ΔSAP_c| across 25-variant cohort: 1.24 → 0.18. All 10 cluster
variants now join the lighting-PE +48.66 / CO2 +11.95 deferred
cohort (Elmhurst-vs-spec monthly factor quirk, same shape as
electric 1 + solid fuel 5/6/7/8 from prior closures).

Verbatim spec quote (SAP 10.2 Table 5a row 1, PDF p.177):
  "Central heating pump in heated space, 2013 or later  3 a)"
  "Central heating pump in heated space, 2012 or earlier  10 a)"
  "Central heating pump in heated space, unknown date  7 a)"

The row name ("Central heating pump") gates by construction: dry
systems have no central heating pump and the row's three sub-rows
don't apply.

No regressions on the other 31 variants or any golden fixture; the
6 Elmhurst U985 fixtures lodge PCDB index → the new predicate
returns True → pump_w unchanged.

Tests: 904 pass (+1), 0 fail. Pyright net-zero (35 → 35).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 15:40:25 +00:00
Khalim Conn-Kowlessar
cdc5307ebc docs: handover post S0380.156..159
Captures the per-line walk discipline used to close electric 2 + 5
across four slices (.156 Table 3 WHC=903 primary-loss, .157 Table 2b
note b) WHC=903 ×0.9, .158 Table 4f warm-air heating fans, .159
Table 4a Cat 7 R tariff-aware dispatch). Σ |ΔSAP_c| across the
25-variant heating-systems corpus dropped from 2.87 → 1.21 (58%
reduction). All variants now sit under 0.3 SAP.

Next-slice candidate: the 9-variant cluster at ±0.09..0.12 SAP
(electric 3/5/6/7/8/9 + sf 4/9/10/11) — uniform pattern suggesting
a shared shave-the-residual fix. Worth a per-line walk on one
cluster variant before accepting the prior "Elmhurst-vs-spec quirk"
framing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 15:40:25 +00:00
Khalim Conn-Kowlessar
5d086f3ed1 Slice S0380.159: SAP 10.2 Table 4a R tariff-aware dispatch for electric storage
SAP 10.2 Table 4a (PDF p.166) Cat 7 "Electric storage heaters"
splits the responsiveness R between two sub-tables:

  Off-peak tariff:
    Slimline storage heaters       ... R = 0.2  402
    Convector storage heaters      ... R = 0.2  403
    Slimline + Celect-type control ... R = 0.4  405
    Convector + Celect-type ctrl   ... R = 0.4  406
  24-hour heating tariff:
    Slimline storage heaters       ... R = 0.4  402
    Convector storage heaters      ... R = 0.4  403
    Slimline + Celect-type control ... R = 0.6  405
    Convector + Celect-type ctrl   ... R = 0.6  406

Per SAP 10.2 §12.4.3 (PDF p.36) the 18-hour tariff has electricity
at low rate for 18 hours per day with at most 6h of interruption /
2h max each — operationally equivalent to 24-hour for storage-heater
charging. The cascade therefore routes EIGHTEEN_HOUR + TWENTY_FOUR_
HOUR through the 24-hour Table 4a sub-row.

Pre-slice `_responsiveness` keyed on `sap_main_heating_code` only
and returned R=0.2 for code 402 regardless of tariff. The existing
docstring already flagged the gap:

    402: 0.20,  # Slimline storage heaters (24-hr tariff: 0.40)
    ... "promote to (sap_code, tariff) lookup when 24-hour fixture
    surfaces; until then the off-peak default applies (under-shoots
    R for the 24-hour case)."

Per-line walk on electric 5 (sap_main_heating_code=402 +
meter_type="18 Hour"): cascade T_living (87)[Jan] = 20.1213 vs
worksheet 19.6519, (92)[Jan] = 18.6996 vs worksheet 18.2063, (93)
[Jan] = 19.0996 vs worksheet 18.6063 (cascade +0.4933 K throughout
the cascade). Back-solve from worksheet T_living=19.6519 via the
Table 9b Tsc formula:

  Tsc(R=0.4) = 0.6 × (21-2) + 0.4 × (4.3 + 0.9933 × 705.4/210.23)
             = 11.4 + 0.4 × 7.6325 = 14.4528

  ΔT = 21 - 14.4528 = 6.5472
  u_sum = 0.5 × 6.5472 × (7² + 8²) / (24 × 11.43) = 1.3481
  T_living = 21 - 1.3481 = 19.6519 EXACT match.

Adds:
  - `_CONTINUOUS_CHARGING_TARIFFS: frozenset[Tariff]` = {EIGHTEEN_
    HOUR, TWENTY_FOUR_HOUR} — the tariffs treated as "24-hour
    heating" for Table 4a R selection.
  - `_RESPONSIVENESS_24_HOUR_OVERRIDE_BY_SAP_CODE: dict[int, float]`
    — the override table for codes 402/403/405/406 (404, 407, 409
    keep the same R in both sub-tables).
  - `tariff: Optional[Tariff]` parameter to `_responsiveness`, with
    the override consulted before the off-peak default.
  - Tariff threaded through both call sites of MIT cascade (rating
    + demand paths) via `tariff_from_meter_type`.

Closures electric 5:
  ΔSAP −1.1759 → +0.1081 (91% reduction)
  Δcost +£27.09 → −£2.49
  ΔCO2 +62.72 → +7.30 kg
  ΔPE +438.03 → +0.07 kWh (essentially EXACT)

Electric 5 now joins the same residual cluster as electric 3/6/7/8/
9 (+0.09..+0.12 SAP, −£2..−£3 cost, +£7 CO2) — the cluster that
the prior handovers suspected was a shared shave-the-residual gap.

No regressions on the other 24 cohort variants. Extended handover
suite: 903 pass / 0 fail (was 902 — +1 from the new AAA test).
Pyright net-zero (43 → 43).

Σ |ΔSAP_c| across the 25-variant cohort: 2.30 → 1.24 (~46%
reduction from this slice).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 15:40:25 +00:00
Khalim Conn-Kowlessar
4291193ba8 Slice S0380.158: SAP 10.2 Table 4f warm-air heating system fans
SAP 10.2 Table 4f (PDF p.174) row "Warm air heating system fans"
+ footnote e) — verbatim:

  Warm air heating system fans e)        SFP × 0.4 × V

  e) SFP is the specific fan power from the database record for the
     warm air unit if applicable; otherwise 1.5 W/(l/s). These values
     of SFP include the in-use factor.
     If the heating system is a warm air unit and there is balanced
     whole house mechanical ventilation, the electricity for warm
     air circulation should not be included in addition to the
     electricity for mechanical ventilation. However it is included
     for a warm air system and MEV or PIV from outside.
     V is the volume of the dwelling in m³.

Per Table 4a (PDF p.165-166), warm-air systems are:
  - Category 5: heat pumps with warm-air distribution (codes 521,
    523, 524 electric; 525, 526, 527 gas-fired)
  - Category 9: warm-air systems NOT heat pump (501-511, 520 gas-
    fired; 512-514 liquid-fired; 515 Electricaire electric)

Pre-slice the cascade's `_table_4f_additive_components` docstring
explicitly listed "(230b) Warm-air heating fans + (230c) for warm-
air pump" as "Not yet wired" — every Cat 5 / Cat 9 warm-air corpus
variant resolved `pumps_fans_kwh_per_yr` to 0. For electric 2 (code
524 Cat 5 air-source warm-air HP, no MV, V = 227.25 m³), the P960
worksheet block 11a (249) lodges 136.35 kWh × 13.67 p/kWh = £18.64
where the cascade computed 0.

New `_TABLE_4A_WARM_AIR_SAP_CODES` frozenset (22 codes) + leaf helper
`_table_4f_warm_air_heating_fans_kwh(main, dwelling_volume_m3,
has_balanced_mv)` wired at the orchestrator pumps_fans summation
alongside the existing circulation-pump and gas-flue-fan helpers.
Footnote-e balanced-MV omission reads `epc.sap_ventilation.
mechanical_ventilation_kind` via the new
`_has_balanced_mechanical_ventilation` predicate (returns True for
MVHR / MV; False for MEV / PIV / NATURAL).

Per-line walk evidence: cascade `pumps_fans_kwh_per_yr` = 0.0000 vs
worksheet (249) = 136.3500 = 1.5 × 0.4 × 227.25 exactly. Default SFP
from footnote e matches; PCDB warm-air-unit SFP lookup deferred
until a fixture exercises it.

Closures electric 2:
  pumps_fans_kwh_per_yr: 0 → 136.35 (EXACT match to worksheet)
  ΔSAP +0.7002 → −0.1087 (residual swung past worksheet — the +0.70
    pre-slice was an under-counted-fan offset; spec-correct fix lands
    just past zero, exposing a small upstream SH cascade gap likely
    in the Cat 5 warm-air HP Table 4a SH efficiency or Table 9c MIT
    cascade for warm-air mains — follow-up slice)
  Δcost −£16.14 → +£2.50
  ΔCO2 −2.37 → +16.54 kg
  ΔPE −108.58 → +97.69 kWh

No regressions on the other 24 cohort variants — the warm-air-code
gate fires only when `sap_main_heating_code` is in the new frozenset
and only electric 2 has a warm-air SAP code in the corpus. Extended
handover suite: 902 pass / 0 fail (was 901 — +1 from the new AAA
test). Pyright net-zero (43 → 43).

Σ |ΔSAP_c| across the 25-variant cohort: 2.87 → 2.30 (~20%
reduction from this slice).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 15:40:25 +00:00
Khalim Conn-Kowlessar
8507728237 Slice S0380.157: SAP 10.2 Table 2b note b) WHC=903 electric-immersion guard
SAP 10.2 Table 2b note b) (PDF p.159) — verbatim:

  Multiply Temperature Factor by 0.9 if there is separate time
  control of domestic hot water (boiler systems, warm air systems
  and heat pump systems).

The parenthetical list restricts the rule to systems where the heat
generator (boiler / warm-air / HP) is the device heating the
cylinder. Electric immersion is NOT in that list because the
immersion isn't a heat-generator system feeding DHW — it sits inside
the cylinder. The ×0.9 multiplier reflects shorter cylinder-heating
periods when a boiler / HP / warm-air operates on a separate timer
for DHW vs SH; if the heat generator doesn't feed the cylinder at all
(because the immersion does), there's no such timing effect.

Pre-slice `_separately_timed_dhw` returned True for any Cat 4 HP
main BEFORE consulting WHC (line 3872 `if main.main_heating_category
== 4: return True`). For electric 2 (sap_main_heating_code=524 Cat 5
warm-air ASHP, main_heating_category=4 per Elmhurst mapper, WHC=903
electric immersion + cylinder + cylinder thermostat lodged), the
cat-4 branch fired before the existing `_is_electric_water` check
could route the cert to False. The cascade applied ×0.9 to the
Temperature Factor (53), pulling (55) from 1.2294 → 1.1064 → cascade
annual (56) = 403.87 vs worksheet (56) annual = 448.73.

Same WHC=903 principle as the prior slice S0380.156 (Table 3 zero-
loss list for electric immersion): when HW is independent of the
main heating, main-heating-specific DHW rules don't apply — even
when the main happens to be a HP / boiler / warm-air system.

Fix: new top-of-function `if epc.sap_heating.water_heating_code ==
_WHC_ELECTRIC_IMMERSION: return False` guard in
`_separately_timed_dhw`. Reuses the constant introduced in S0380.156.

Closures electric 2:
  Cylinder (56) storage loss annual 403.87 → 448.73 (matches
  worksheet 1.2294 × 365 = 448.73 EXACT within rounding)
  HW kWh demand 2339.24 → 2384.12 (matches worksheet (62)/(64) =
  2384.116 EXACT)
  ΔSAP +0.8118 → +0.7002
  Δcost −£18.71 → −£16.14
  ΔCO2 −7.21 → −2.37 kg
  ΔPE −161.68 → −108.58 kWh

The remaining +0.70 SAP residual is a separate upstream gap (likely
warm-air-HP SH cascade or Table 4a SH efficiency for code 524) —
follow-up slice.

No regressions on the other 24 cohort variants. Cohort-1 ASHP certs
(Cat 4 HP + WHC=901 = HW from HP + cylinder) keep ×0.9 as before
because their WHC=901 doesn't trigger the new guard. Extended
handover suite: 901 pass / 0 fail (was 900 — +1 from the new AAA
test). Pyright net-zero (43 → 43).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 15:40:25 +00:00
Khalim Conn-Kowlessar
c727b3cb5a Slice S0380.156: SAP 10.2 Table 3 WHC=903 electric-immersion zero-loss guard
SAP 10.2 Table 3 (PDF p.160) verbatim:

  Primary loss is set to zero for the following:
      Electric immersion heater
      Combi boiler ...
      CPSU ...
      Boiler and thermal store within a single casing
      Separate boiler and thermal store connected by no more than 1.5
      m of insulated pipework
      Direct-acting electric boiler
      Heat pump (...) with hot water vessel integral to package

The Elmhurst WHC=903 lodging signals exactly the first row: "HW from
a separate electric immersion heater" — the cylinder is heated by an
immersion element inside the tank, no primary pipework between any
heat generator and the cylinder. The rule is universal: regardless
of what main heating exists for space heating, electric immersion
means no primary circuit means no primary loss.

Pre-slice `_primary_loss_applies` only consulted `water_heating_code`
in the Table 4a wet-boiler branch (codes 151-161 / 191-196). The Cat
4 HP branch returned True unconditionally when no PCDB record was
lodged; the Cat 1/2 boiler branch returned True unconditionally; the
PCDB Table 322 + Table 4b non-PCDB branches likewise. For the
electric 2 corpus variant (sap_main_heating_code=524 Cat 5 warm-air
ASHP, main_heating_category=4 per Elmhurst mapper, no PCDB record,
WHC=903 + cylinder), the Cat-4 branch falsely returned True and the
cascade added ~510 kWh/yr primary loss to a system with no primary
circuit at all.

Per-line walk discipline applied: cascade `water_heating_from_cert`
output dump showed `primary_loss_monthly_kwh_annual = 509.98` while
worksheet (59)m = 0 every month → spec lookup found Table 3 verbatim
"Electric immersion heater" zero-loss line.

Adds `_WHC_ELECTRIC_IMMERSION: Final[int] = 903` constant + a
top-of-function `if water_heating_code == _WHC_ELECTRIC_IMMERSION:
return False` guard that fires before any of the system-type-keyed
branches.

Closures electric 2:
  HW kWh 2849.22 → 2339.24 (matches worksheet (62)/(64) = 2384.12
  within the residual ~45 kWh storage-loss gap)
  ΔSAP −0.4584 → +0.8118 (cascade swung past the worksheet by +1.27
  — the pre-slice 'near-correct' value was offsetting cascade bugs
  per [[feedback-software-no-special-handling]]; the +0.81 residual
  exposes a separate upstream gap to chase in a follow-up slice)
  Δcost +£10.56 → −£18.71
  ΔCO2 +47.89 → −7.21 kg
  ΔPE +443.13 → −161.68 kWh

No regressions on the other 24 cohort variants — only electric 2 has
the (Cat 4 HP, no PCDB, WHC=903) combination in the corpus.
Extended handover suite: 900 pass / 0 fail (was 899 — +1 from the
new AAA test). Pyright net-zero (43 → 43).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 15:40:25 +00:00
Khalim Conn-Kowlessar
f4a759ade3 docs: handover post S0380.153..155
Session landed three spec-clean slices closing four major residuals:

- S0380.153 (Table 3 middle row for solid-fuel boilers): SF3 EXACT all
  4 metrics (+0.30 → -0.0000). Found the rule that solid-fuel boilers
  don't ship with dual programmers per §9.2.4.

- S0380.154 (§12.4.4 back-boiler summer-immersion): SF2 SAP+cost
  EXACT (+1.86 → -0.0000 SAP; -£42.84 → -£0.00 cost). Implemented HW
  fuel kWh split + monthly blend across cost / CO2 / PE / standing.

- S0380.155 (Table 4a HP water-column dispatch): gshp closed ±0.02
  SAP (+0.94 → -0.0178). HW kWh 841 → 1138 matches worksheet exactly.

Σ |ΔSAP_c| 14.5 (session start of S0380.150) → 2.7 = 81% reduction
across 6 slices, two sessions.

Handover doc captures:
- Per-line discipline (walk worksheet before forming hypothesis)
- Elmhurst-vs-spec divergences to defer (lighting-PE +48.66 cluster
  uses Table 12 annual factor; spec Table 12d mandates monthly)
- Ranked open fronts (electric 5 R=0.20 storage MIT, electric 2
  warm-air HP HW, deferred lighting-PE cluster)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 15:40:25 +00:00
Daniel Roth
a68ab01d70 All downloaded PasHub files uploaded to SharePoint property folder 🟩 2026-06-04 15:40:25 +00:00
Daniel Roth
3d2a21d9d9 All downloaded PasHub files uploaded to SharePoint property folder 🟥 2026-06-04 15:40:25 +00:00
Daniel Roth
815173bc79 tweak local trigger 2026-06-04 15:40:25 +00:00
Daniel Roth
35b65d0d8d adjust comment about evidence type 2026-06-04 15:40:25 +00:00
Daniel Roth
5fa6f32644 Service persists mcs_certificate file_type when evidence_category is MCS cert 🟩 2026-06-04 15:40:25 +00:00
Daniel Roth
367f6f6cf9 Downloaded files carry evidence_category as DownloadedFile 🟩 2026-06-04 15:40:25 +00:00
Daniel Roth
1e2b997f2f Downloaded files carry evidence_category as DownloadedFile 🟥 2026-06-04 15:40:25 +00:00
Daniel Roth
e18c300deb MCS cert identified by evidence_category in get_core_file_type 🟩 2026-06-04 15:40:25 +00:00
Daniel Roth
1af1d4a82c MCS cert identified by evidence_category in get_core_file_type 🟥 2026-06-04 15:40:25 +00:00
Daniel Roth
5ed3bf73e8 evidence categories plus typehinting 2026-06-04 15:40:25 +00:00
Daniel Roth
020a24d345 run() returns core and other file paths 🟩 2026-06-04 15:40:25 +00:00
Daniel Roth
d8ec12065f run() returns core and other file paths 🟥 2026-06-04 15:40:25 +00:00
Daniel Roth
0aa6a4fc30 Other files persisted to DB with file_type OTHER 🟩 2026-06-04 15:40:25 +00:00
Daniel Roth
1650762ae2 Other files persisted to DB with file_type OTHER 🟥 2026-06-04 15:40:25 +00:00
Daniel Roth
c86dbeb4a1 Upload other files to S3 when get_other_files is True 🟩 2026-06-04 15:40:25 +00:00
Daniel Roth
098f60ecfd Upload other files to S3 when get_other_files is True 🟥 2026-06-04 15:40:25 +00:00
Daniel Roth
9c38f45c98 tidying for readability 2026-06-04 15:40:25 +00:00
Daniel Roth
c9a2ce4921 Service deletes other-file temp paths after run 🟩 2026-06-04 15:40:25 +00:00
Daniel Roth
f8d2bb8049 Service deletes other-file temp paths after run 🟥 2026-06-04 15:40:25 +00:00
Daniel Roth
49e7b7fea6 Wire service to get_evidence_files_by_job_id; retire get_core_evidence_files_by_job_id 🟪 2026-06-04 15:40:25 +00:00
Daniel Roth
662f6de0ab get_evidence_files_by_job_id downloads other files when include_other=True 🟩 2026-06-04 15:40:25 +00:00
Daniel Roth
c4ffaaa069 get_evidence_files_by_job_id downloads other files when include_other=True 🟥 2026-06-04 15:40:25 +00:00
Daniel Roth
f95b6bdd7d get_evidence_files_by_job_id returns DownloadedFiles with empty other when include_other=False 🟩 2026-06-04 15:40:25 +00:00
Daniel Roth
665dc69ad5 get_evidence_files_by_job_id returns DownloadedFiles with empty other when include_other=False 🟥 2026-06-04 15:40:25 +00:00
Daniel Roth
e7c679e0db Group evidence into core and other via _group_into_core_and_other_files 🟪 2026-06-04 15:40:25 +00:00
Daniel Roth
99229844b5 _select_other_files returns non-core evidence files 🟩 2026-06-04 15:40:25 +00:00
Daniel Roth
db796747d9 _select_other_files returns non-core evidence files 🟥 2026-06-04 15:40:25 +00:00
Daniel Roth
6cb6c8c756 allow for missing deal stage column when triggering sqs from file 2026-06-04 15:40:25 +00:00
Daniel Roth
790e430aff rename local handler trigger script 2026-06-04 15:40:25 +00:00
Jun-te Kim
a4670f8bc0 deploy lambda 2026-06-04 15:32:51 +00:00
Jun-te Kim
261fae2e79 reformatted to be DDD structure 2026-06-04 14:50:04 +00:00