mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
5394 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a8d6568cbf | Merge branch 'main' of https://github.com/Hestia-Homes/Model into feature/per-cert-mapper-validation | ||
|
|
fa6974bdd9 |
Slice S0380.95: Detailed-RR residual area cascade per RdSAP 10 §3.10.1
RdSAP 10 §3.10.1 (PDF p.24) "Default U-values of the roof rooms":
> "The residual area (area of roof less the floor area of room(s)-in-
> roof) has a U-value from Table 16 : Roof U-values when loft
> insulation thickness is known according to its insulation thickness
> if at least half the area concerned is accessible, otherwise it is
> the default for the age band of the original property or extension."
Plus RdSAP 10 §3.9.1 step (d-e) (PDF p.21-22) — the Simplified A_RR
formula `12.5 × √(A_RR_floor / 1.5)` is the empirical estimator for
the total RR exposed shell; residual = A_RR − Σ lodged walls. The
worksheet applies this same formula to Detailed mode when the lodged
surface set has no roof-going entries (cert 000565 BP[0]:
12.5 × √(45/1.5) − (9.8 + 14.7) = 43.96 ≈ ws 43.97).
Pre-slice the cascade computed residual area ONLY in the Simplified
RR branch (via `_part_geometry`'s `rr_simplified_a_rr_m2` − rr_common
− rr_gable subtractions). The Detailed-RR branch in
`heat_transmission` iterated `rir.detailed_surfaces` and missed the
residual entirely. Cert 000565 routes all 5 BPs through Detailed mode
(the Elmhurst mapper translates Summary "Simplified" lodgements to
`SapRoomInRoofSurface` records when per-surface L×H is present), so
cascade total_external_element_area_m2 was 779.27 m² vs worksheet
(31) = 857.64 m² (Δ −78.37 m² → thermal_bridging cascade −11.76 W/K
under).
Slice span (1 file):
- `heat_transmission.py`: Detailed-RR branch adds residual area via
the §3.9.1 A_RR formula minus wall-going lodgements (gable_wall,
gable_wall_external, common_wall). Residual area contributes to
`rr_detailed_area` (→ part_external_area → (31) → thermal_bridging
multiplier) and to `roof` at `u_rr_default_all_elements`.
- Discriminator: residual fires only when no roof-going surface kinds
(slope, flat_ceiling, stud_wall) are lodged — true Detailed-mode
lodgements (cohort fixture 000516) lodge the entire roof shell
explicitly and have no residual.
Cert 000565 movement (HEAD `78c57c0d` → this slice):
- thermal_bridging_w_per_k: 116.89 → 129.35 ✓ vs ws 128.65 (Δ +0.70)
- total_external_area_m2: 779.27 → 862.34 ✓ vs ws 857.64 (Δ +4.70)
- roof_w_per_k: 34.64 → 63.72 (Δ −16.74 → +12.34)
- sap_score_continuous: 29.02 → 28.07 (Δ +0.51 → −0.44)
- sap_score (integer): 29 → 28 (temp regression
past 28.5 threshold)
- space_heating_kwh: −685 → +533
- main_heating_fuel: −403 → +321
- hot_water_kwh: ✓ 0 EXACT unchanged
Per user direction temporary continuous-SAP drift is acceptable when
fixing real spec-correct sub-component bugs; the absolute continuous-
SAP residual is now −0.44 (was +0.51) — slightly closer to zero
overall. The roof overshoot localises to:
- BP[4] Flat Ceiling 1 "Unknown PUR or PIR" lodgement (cascade 2.30
vs ws 0.15, over by +10.75 W/K) — Elmhurst-specific "Unknown +
known material" convention not yet wired
- BP[1] residual formula gives +3.68 m² over worksheet (Δ +1.29 W/K)
— Detailed-mode residual is spec-ambiguous for extensions with
non-2.45 m RR height; future slice may add a height-aware formula
Cohort safety: discriminator `has_roof_lodgement` filters out true
Detailed-mode lodgements (cohort fixtures 000474/000477/000480/
000487/000490/000516 all lodge slope/flat_ceiling/stud_wall surfaces).
Initial implementation broke 41 cohort pins; the discriminator
restores cohort behaviour exactly. Test baseline: 585 pass + 9
expected `000565` fails (was 585 + 8 — sap_score moved from passing
to failing during the slice's transient overshoot; expected per
user direction).
Pyright net-zero per touched file (test_summary_pdf_mapper_chain.py
13 → 13 preserved; heat_transmission.py 13 → 12 improved by −1).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
6706e76a28
|
Merge pull request #1121 from Hestia-Homes/feature/solar-api-client
GoogleSolarApiClient class |
||
|
|
78c57c0dc7 |
Slice S0380.94: RIR insulation "400+ mm PUR or PIR" extractor + mapper + cascade (RdSAP 10 Table 17 col 3b)
RdSAP 10 §5.11.3 + Table 17 (PDF p.42-43) "Roof room U-values when
insulation thickness is known". Column (3b) "Stud wall — PUR or PIR
optional" 400 mm row → 0.10 W/m²K. Cert 000565 Summary §8.1 BP[2] Ext2
(Detailed) lodges:
Stud Wall 2 2.00 × 2.00 400+ mm PUR or PIR Default U=0.10
Pre-slice three coupled bugs silently dropped the lodgement, routing
the cascade through the uninsulated Table 17 row 0 (U=2.30) — over-
counting Stud Wall 2 by (2.30 − 0.10) × 4 m² = +8.80 W/K on roof:
1. **Extractor regex** `_RIR_INSULATION_THICKNESS_RE = ^\d+\s*mm$`
failed to match the "400+ mm" bucket-cap form (Table 17's largest
tabulated row is annotated with a trailing "+" in the Summary).
2. **Extractor insulation_type allow-list** `("Mineral or EPS",
"PUR", "PIR")` failed to match the disjunction "PUR or PIR" — the
actual Summary form when the assessor doesn't distinguish PUR from
PIR. (Both columns Table 17 column (b) anyway.)
3. **Mapper thickness parser** `_elmhurst_rir_insulation_thickness_mm`
used the same `^\d+\s*mm$` regex — also failed on "400+ mm".
Plus a fourth coupled fix: the cascade's `_is_rigid_foam` checked a
frozenset `{"pur", "pir", "rigid"}` that didn't include the canonical
mapper-side code "rigid_foam" — even if the mapper translated "PUR or
PIR" → "rigid_foam", the cascade would route to column (a) mineral-
wool instead of column (b) rigid-foam.
Slice span (4 layers):
1. **Extractor regex** — `^\d+\+?\s*mm$` matches both "100 mm" and
"400+ mm".
2. **Extractor allow-list** — add "PUR or PIR" alongside individual
"PUR" / "PIR" + "Mineral or EPS".
3. **Mapper** — `_RIR_INSULATION_TYPE_TO_SAP10` canonicalises all
rigid-foam strings to "rigid_foam"; thickness parser regex matches
"400+ mm" → 400 mm int.
4. **Cascade** — `_RR_RIGID_FOAM_INSULATION_TYPES` adds "rigid_foam"
alongside the legacy "pur"/"pir"/"rigid" aliases.
Cert 000565 movement (HEAD `23aaa4fa` → this slice):
- cascade BP[2] Ext2 Stud Wall 2 U: 2.30 → 0.10 ✓ EXACT vs ws 0.10
- cascade roof_w_per_k: 43.44 → 34.64 (Δ−7.94 → Δ−16.74)
- sap_score: 29 ✓ EXACT unchanged
- sap_score_continuous: 28.81 → 29.02 (Δ+0.26 → Δ+0.51)
- space_heating_kwh: −427 → −685
- main_heating_fuel: −251 → −403
- hot_water_kwh: ✓ 0 EXACT unchanged
Closing one spec-correct sub-component while others remain non-spec-
correct drifts continuous SAP further; per user direction temporary
drift is acceptable as long as we're fixing true intermediate-value
problems — once every sub-component is spec-correct, the continuous
SAP error closes to zero by construction. The remaining −16.74 W/K
roof gap localises to:
- BP[0/1/3] missing RR residual area for Detailed-RR mode (§3.10.1
spec — cascade only handles Simplified mode today); +27.85 W/K
closure when wired.
- BP[4] Flat Ceiling 1 lodges "Unknown thickness, PUR or PIR" → ws
U=0.15; cascade over-counts at 2.30 (uninsulated). Elmhurst's
"Unknown PUR or PIR" → 200 mm convention is non-spec; the spec-
correct path falls back to Table 18 col 4 default (`u_rr_default
_all_elements`). Separate diagnostic slice.
Cohort safety: 21 other Elmhurst Summary fixtures lodge no RIR detailed
surfaces with "400+ mm" or "PUR or PIR" (modal cohort uses As Built /
None / no detailed surfaces). Existing "Mineral or EPS" tests at
`test_u_rr_stud_wall_table17_col3a_mineral_wool_100mm_returns_0_36`
remain green — the new aliases extend rather than replace.
Test baseline: 585 pass + 8 expected `000565` fails (was 583 + 8; +2
new tests). Pyright net-zero per touched file (0/32/1/65/13 preserved).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
23aaa4fa66 |
Slice S0380.93: floor above partially-heated space U=0.7 (RdSAP 10 §5.14)
RdSAP 10 §5.14 (PDF p.47) "U-value of floor above a partially heated space": > "The U-value of a floor above partially heated premises is taken as > 0.7 W/m²K. This applies typically for a flat above non-domestic > premises that are not heated to the same extent or duration as the > flat." Cert 000565 Ext1 lodges Summary §9 "Location: P Above partially heated space" + "Default U-value: 0.70". Worksheet line (28b) confirms "Exposed floor Ext1 ... 34.0000 0.7000 23.8000". Pre-slice the cascade routed BP[1] floor through the BS EN ISO 13370 ground-floor formula (the "else" branch of the floor U-value dispatch in `heat_transmission.py`) — producing cascade U=0.76 vs spec 0.70. Over-counted floor heat loss by (0.76 − 0.70) × 34 m² = +2.04 W/K on the part subtotal and on the total HTC. Slice span (4 layers): 1. **Helper** — `u_floor_above_partially_heated_space()` in `domain/sap10_ml/rdsap_uvalues.py`, verbatim spec constant 0.7 (no age-band / insulation-thickness inputs). Lives in `sap10_ml` per [[project-sap10_ml-deprecation]] (edit existing file fine). 2. **Schema** — `SapFloorDimension.is_above_partially_heated_space: bool = False` (parallel to existing `is_exposed_floor`). Mutually exclusive with the exposed-floor / basement-floor branches. 3. **Mapper** — new `_is_floor_above_partially_heated_space(location)` helper detecting "above partially heated" in the Elmhurst §9 floor location string. Plumbed into `_map_elmhurst_building_part` floor- dim construction; only applies to the ground floor (i==0). 4. **Cascade** — `heat_transmission.py` adds a new branch between the exposed-floor and ground-floor branches: `is_above_partial → u_floor_above_partially_heated_space()`. Cert 000565 movement (HEAD `a7894b11` → this slice): - cascade floor_w_per_k: 72.41 → 70.37 (Δ +10.74 → Δ +8.70) - cascade BP[1] floor U: 0.76 → 0.70 (✓ EXACT vs ws 0.70) - sap_score (integer): 29 ✓ EXACT (unchanged — at goal) - sap_score_continuous: 28.7663 → 28.8131 (+0.0468 drift) - space_heating_kwh: −367 → −427 (small drift further under) - main_heating_fuel: −216 → −251 (downstream of SH) - co2_kg_per_yr: −32 → −37 - total_fuel_cost_gbp: −23 → −27 - hot_water_kwh: ✓ 0 EXACT unchanged The small continuous-SAP drift is the expected arithmetic of closing a single component when adjacent components remain unclosed (floor +10.74 was cancelling thermal_bridging −11.76 + roof −7.94 at the net-HTC level). Per [[feedback-zero-error-strict]] + [[feedback- spec-citation-in-commits]] the spec-correct slice ships regardless of transient continuous-SAP drift; remaining residual components (floor +8.70 from BP[2] Ext2 lodged 200 mm insulation thickness; roof −7.94; thermal_bridging −11.76; walls −1.67) each get their own spec-cited slice. Cohort safety: only cert 000565 Ext1 in the cohort lodges "Above partially heated space". All other Elmhurst cohort fixtures + 9 golden + 38 cohort-2 API certs default to `is_above_partially_ heated_space=False` so cascade behaviour is unchanged. Test baseline: 583 pass + 8 expected `000565` fails (was 582 + 8; +1 new mapper-chain test). Pyright net-zero per touched file (1/65/1/32/13/13 preserved). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
a7894b1185 |
Slice S0380.92: AP4 + MEV decentralised plumbing (SAP 10.2 §2 (17a)/(18)/(23a)/(24c))
SAP 10.2 §2 lines (17a)/(18) "Air permeability value, AP4 (m³/h/m²)" (PDF p.12-13): > "The air permeability at 4 Pa (AP4) measured with the low-pressure > pulse technique [...] is used in the following formula to estimate > of the air infiltration rate at typical pressure differences. > In this case (9) to (16) of the worksheet are not used." > > Air infiltration rate (ach) = 0.263 × AP4^0.924 > > If based on air permeability value at 4 Pa, > then (18) = [0.263 × (17a)^0.924] + (8) SAP 10.2 §2 lines (23a)/(24c)/(25) "MEV" + "Whole-house extract ventilation" (PDF p.13/133): > "The SAP calculation is based on a throughput of 0.5 air changes per > hour through the mechanical system." (23a) = 0.5 > > If whole house extract ventilation or positive input ventilation > from outside: > if (22b)m < 0.5 × (23b), then (24c) = (23b) > otherwise (24c) = (22b)m + 0.5 × (23b) Cert 000565 lodges: - Summary §12.1 "Mechanical Ventilation Type: Mechanical extract, decentralised (MEV dc)" (PCDF 500755) - Summary §12.2 "Test Method: Pulse" + "Pressure Test Result (AP4): 2.00" Pre-slice both lodgements were silently dropped by the Elmhurst extractor / mapper / `cert_to_inputs` cascade: - AP4 had no schema field on `VentilationAndCooling` or `SapVentilation` even though `ventilation.py:ventilation_from_inputs(air_permeability_ ap4=...)` already implemented the spec formula. - Mechanical Ventilation Type had no schema field; `cert_to_inputs. ventilation_from_cert` hardcoded `mv_kind=MechanicalVentilationKind. NATURAL` regardless of the lodgement, routing cert 000565 through the (24d) natural-vent formula instead of (24c). These bugs are coupled: AP4 alone would close (18) but the cascade's (25) NATURAL pass-through would then *under*-count the effective ach by 0.25 (the missing MEV contribution). MEV alone would over-count because the (18) over-count remains. Per [[feedback-bigger-slices- for-uniform-work]] + handover precedent on coupling-aware reverts, these land together. Slice span (5 layers): 1. **Schema** — `VentilationAndCooling.air_permeability_ap4_m3_h_m2` + `VentilationAndCooling.mechanical_ventilation_type` (site-notes); `SapVentilation.air_permeability_ap4_m3_h_m2` + `SapVentilation.mechanical_ventilation_kind` (domain). 2. **Extractor** — `_extract_ventilation` parses "Pressure Test Result (AP4)" scoped to §12.2 and "Mechanical Ventilation Type" scoped to §12.1. Both default to None when the cert lodges no MV / no Pulse test (cohort modal case). 3. **Mapper** — `_map_elmhurst_ventilation` plumbs AP4 through; new `_ELMHURST_MV_TYPE_TO_KIND` dispatch with strict-raise on unmapped labels (per [[reference-unmapped-elmhurst-label]] mirror pattern). 4. **cert_to_inputs** — `ventilation_from_cert` reads AP4 and resolves `mechanical_ventilation_kind` name → `MechanicalVentilationKind` enum. MEV/MV/MVHR kinds set `mv_system_ach=0.5` per spec (23a). 5. **Tests** — 4 in test_summary_pdf_mapper_chain.py (extractor + mapper for both AP4 and MEV kind), 2 in test_cert_to_inputs.py (cascade AP4 formula + MEV kind dispatch). All AAA-structured. Cert 000565 movement (HEAD `83218630` → this slice): - cascade (18) pressure_test_ach: 2.4037 → 2.0287 ✓ EXACT vs ws 2.0287 - cascade (21) shelter-adj: 2.0431 → 1.7244 ✓ EXACT vs ws 1.7244 - cascade mean (25)m: 2.2347 → 2.1360 vs ws 2.086 (+0.05) - **sap_score (integer): 28 → 29 ✓ EXACT vs ws 29** (Δ−1 → Δ 0) - sap_score_continuous: 27.99 → 28.77 (Δ−0.52 → +0.26) - ecf: 5.44 → 5.36 (Δ+0.05 → −0.03) - total_fuel_cost_gbp: 4726.75 → 4657.37 (Δ+46 → Δ−23) - co2_kg_per_yr: 6506.48 → 6415.56 (Δ+59 → Δ−32) - **space_heating_kwh: +631 → −367** (~75% closed) - main_heating_fuel: +371 → −216 (~58% closed) - hot_water_kwh: ✓ 0 EXACT unchanged - lighting / pumps_fans: sub-spec residuals unchanged The residual cascade-over-by-0.05 ach on (25)m is the cascade using the cert-agnostic Table U2 wind tuple instead of the cert's regional wind lookup; future ventilation_from_cert wires a `postcode_climate` arg through which `cert_to_demand_inputs` already does for the demand cascade, but the SAP-rating cascade keeps the Table U2 default. Cohort safety: - All 21 other Elmhurst cohort fixtures lodge `pressure_test_method= "Not available"` and `mechanical_ventilation=False` → both new fields default to None → cascade behaviour unchanged. - 9 golden + 38 cohort-2 API certs route through `_map_sap_ventilation` (the API mapper variant), which leaves both new SapVentilation fields at their None default → cascade behaviour unchanged. Test baseline: 582 pass + 8 expected `000565` fails (was 575 + 9; +6 new tests + sap_score reclassified from fail to pass). 1763 pass in broader sap10_ml + worksheet + epc.domain suites + 3 pre-existing fails unchanged. Pyright net-zero per touched file (1/0/0/32/34→32/13/ 11 → 1/0/0/32/32/13/11, cert_to_inputs.py improved −2). Per [[project-sap10_ml-deprecation]] the new fields live on the existing `SapVentilation` domain type; no new modules under sap10_ml. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
8321863015 |
Slice S0380.91: party-wall Cavity-masonry-filled U=0.2 (RdSAP 10 Table 15 row 3)
RdSAP 10 §5.10 Table 15 (PDF p.42) "U-values of party walls":
Party wall type U
--------------------------------------------- ----
Solid masonry / timber frame / system built 0.0
Cavity masonry unfilled 0.5
Cavity masonry filled 0.2
Unable to determine, house or bungalow 0.25
Unable to determine, flat or maisonette* 0.0
Pre-slice the cascade collapsed CF (Cavity masonry filled) into the same
SAP10 wall_construction code 4 as CU (Cavity masonry unfilled), so the
filled-cavity row's spec U=0.2 was silently rounded up to the unfilled
U=0.5. The mapper at `_ELMHURST_PARTY_WALL_CODE_TO_SAP10["CF"]: 4` and
`_API_PARTY_WALL_CONSTRUCTION_TO_SAP10[3]: 4` both flagged this as a
known approximation since S0380.64; today's slice closes it.
Introduces a party-wall-only synthetic SAP10 code
`WALL_CAVITY_FILLED_PARTY = 11` (distinct from the main wall_construction
codes 1-10 since Table 15 treats filled vs unfilled cavity as separate
party-wall types). `u_wall` doesn't consume code 11 so main-wall U-value
cascades are unaffected. Cohort + golden audit: only cert 000565 Ext1
lodges CF on the Elmhurst side; zero golden certs lodge API code 3, so
flipping the dispatch is scoped to one BP.
Cert 000565 movement (HEAD
|
||
|
|
edb1e6b892 |
docs: handover + next-agent prompt post S0380.85..90 (BP main-wall closure + SH-channel discovery + strict-raise series)
Session summary documents covering 6 slices:
- S0380.85 — Curtain Wall §5.18 dispatch (cascade walls 443 → 555.93 W/K)
- S0380.86 — §5.6 thin-wall stone + §5.8 dry-line (555.93 → 602.40,
worksheet 604.07, 0.27% residual)
- S0380.87 — Table 4e GROUP 2 HP control codes — single-line spec
dispatch bug; SH residual +7924 → +1460 (82%), sap_score
23 → 27, largest single-slice movement of session
- S0380.88 — Full Table 4e Groups 0-7 + UnmappedSapCode strict-raise
- S0380.89 — Table 4d responsiveness + screed-UFH bug fix + strict raise
(latent bug found via strict-raise rollout)
- S0380.90 — 6 dispatch sites strict-raise + UnmappedSapCode shared
module + GOV.UK API digit-string meter_type bug fix
HANDOVER_POST_S0380_90.md covers full state, cumulative-closure table,
strict-raise philosophy, and which 6 dispatch sites were closed.
NEXT_AGENT_PROMPT_POST_S0380_90.md briefs the next-slice work:
S0380.91 (RdSAP 10 Table 15 row 3 "Cavity masonry filled" U=0.2 in
u_party_wall — closes ~+1000 kWh of the remaining +1460 SH residual
on cert 000565 Ext1).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
9bfb852483 |
Slice S0380.90: 6 strict-raise dispatches + UnmappedSapCode promoted to shared module
Bundled slice closing the next 6 silent-fallback dispatch sites flagged
by the post-S0380.89 audit per [[reference-unmapped-sap-code]]:
1. PV pitch (RdSAP 10 §11.1 — codes 1..5 → 0/30/45/60/90°)
2. PV overshading (SAP 10.2 Table M1 — codes 1..4 → 1.0/0.8/0.5/0.35)
3. Meter type (RdSAP cert enum 1..5 → Tariff enum)
4. Tariff → (high, low) rate (RdSAP 10 Table 32 — 4 of 5 Tariffs)
5. Heat-network DLF by age band (SAP 10.2 Table 12c — A..M)
6. Secondary heating fraction by main_heating_category (SAP Table 11)
Each dispatch follows the established strict / total split:
- Absent lodging (None / 0 / "") → cascade's modal-default value
- Lodging present but unmapped → `UnmappedSapCode(field, value)`
`UnmappedSapCode` promoted from `cert_to_inputs.py` to new module
`domain/sap10_calculator/exceptions.py` so `tables/table_12a.py` can
raise it too (the meter-type dispatch lives there). `cert_to_inputs`
re-exports it for backward compat with existing test imports.
Corpus audit at HEAD
|
||
|
|
6d02d205c5 |
Slice S0380.89: Table 4d responsiveness dispatch + screed-UFH bug fix + strict raise
SAP 10.2 Table 4d (PDF p.170) "Heating type and responsiveness ...
depending on heat emitter":
Heat emitter R
------------------------------------------------- -----
Systems with radiators 1.0
Underfloor (wet) — pipes in insulated timber floor 1.0
Underfloor (wet) — pipes in screed above insulation 0.75
Underfloor (wet) — pipes in concrete slab 0.25
Warm air via fan coil units 1.0
Pre-S0380.89 the cascade `_responsiveness` had:
if isinstance(emitter, int) and emitter == 2:
return 0.25
return 1.0
But the Elmhurst cert-side enum (`_ELMHURST_HEAT_EMITTER_TO_SAP10` at
`datatypes/epc/domain/mapper.py:3646`) maps:
1 = Radiators
2 = Underfloor (in screed) ← spec R=0.75, NOT 0.25
3 = Underfloor (timber floor)
4 = Warm air
5 = Fan coils
The cascade silently treated screed UFH (Elmhurst code 2) as
concrete-slab UFH (R=0.25 — Table 4d's most thermally massive UFH
variant). The bug halved the actual spec responsiveness — for a screed
UFH cert, off-period temperature reduction was computed with R=0.25
instead of R=0.75, materially under-counting MIT drop and over-counting
SH demand.
The bug is latent on cohort + golden because `_first_main_heating`
picks main[0] and almost all certs lodge radiators (emitter=1) there.
Corpus audit (full JSON sweep): emitter=2 appears on 2 records and
in both cases on a secondary main slot (e.g. golden cert
0240-0200-5706-2365-8010 main[1]) — never on the selected first main.
The fix preempts the next cert that lodges screed UFH on main[0].
Fix:
- New `_RESPONSIVENESS_BY_EMITTER_CODE` dispatch dict reflecting
Table 4d per the Elmhurst cert-side enum (1: 1.0, 2: 0.75, 3: 1.0,
4: 1.0, 5: 1.0). "Concrete slab UFH" (Table 4d R=0.25) has no
cert-side enum entry — that variant would need a new mapper code
before the cascade can dispatch it.
- `_responsiveness` flipped to strict-raise per [[reference-
unmapped-sap-code]]: absent lodging (None / 0 / "") returns modal
R=1.0 default; lodging present but unmapped raises
`UnmappedSapCode("heat_emitter_type", value)`.
Tests (4 new, AAA-structure):
- `test_heat_emitter_code_2_underfloor_in_screed_routes_to_responsiveness_0p75_per_table_4d`
pins the bug fix: emitter=2 → R=0.75 (was 0.25)
- `test_heat_emitter_code_dispatch_table_4d_full_coverage`
pins all 5 Elmhurst emitter codes to spec-correct R
- `test_responsiveness_raises_unmapped_sap_code_on_unknown_emitter`
pins the strict-raise contract (hypothetical code 99 raises)
- `test_responsiveness_default_1p0_when_emitter_lodging_absent`
pins the absent-lodging contract (None / 0 / "" → 1.0)
Test baseline: 568 pass (was 564 + 4 new) + 9 expected
`test_sap_result_pin[000565-*]` fails unchanged. Cohort + golden
unaffected (all use emitter=1 on main[0]).
Pyright net-zero per touched file (one `pyright: ignore` added on the
absent-lodging test where `main_heating_control=None` is passed to a
dataclass declaring `Union[int, str]` — runtime data exhibits None
on certs lacking space-heating controls, so the test covers a real
codepath the type system doesn't model).
Per the user-requested "we keep debugging silent fallbacks" mandate,
this is the second slice (after S0380.88) in the strict-raise series.
Next candidates per [[reference-unmapped-sap-code]]: PV pitch +
overshading, meter→tariff, heat-network DLF, secondary-heating
fraction by category.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
1b3bbbf783 |
Slice S0380.88: full Table 4e dispatch + strict-raise on unmapped control codes
SAP 10.2 Table 4e (PDF p.171-174) "Heating system controls" — 8 groups
covering boiler / HP / heat-network / electric-storage / warm-air /
room-heater / other systems, ~40 codes total. Pre-S0380.88 the cascade
dispatch dict had spotty coverage:
- Group 1 (BOILER): partial (12 of 13 codes)
- Group 2 (HEAT PUMP): added in S0380.87 (10 codes)
- Groups 3, 4, 5, 6, 7: completely missing
Codes from missing groups silently defaulted to type 2 — the same
failure mode that hid the cert 000565 2207→type-3 bug for ~22 slices
until S0380.87 surfaced it. Per the user-requested strict-raise
philosophy ("we keep debugging silent fallbacks"), this slice
forecloses the pattern at the dispatch boundary.
Corpus audit (full JSON sweep) at HEAD
|
||
|
|
c0328f4e18 |
Slice S0380.87: SAP 10.2 Table 4e GROUP 2 HP control codes close SH +7924→+1460
SAP 10.2 Table 4e (PDF p.172-173) "Heating system controls", GROUP 2:
HEAT PUMPS WITH RADIATORS OR UNDERFLOOR HEATING:
Code Description Type
---- ------------------------------------------------- ----
2201 No time or thermostatic control 1
2202 Programmer, no room thermostat 1
2203 Room thermostat only 1
2204 Programmer and room thermostat 1
2205 Programmer and at least two room thermostats 2
2206 Programmer, TRVs and bypass 2
2207 Time + temp zone control by plumbing/electrical 3 ← cert 000565
2208 Time + temp zone control by PCDB device 3
2209 Room thermostat and TRVs 2
2210 Programmer, room thermostat and TRVs 2
Pre-S0380.87 `_CONTROL_TYPE_BY_CODE` contained only Group 1 BOILER
codes (21XX); every Group 2 HP code (22XX) fell through to the
default `return 2`. Cert 000565 lodges `main_heating_control=2207`
on its HP main 1 → silently routed to type 2 → MIT_elsewhere
over-counted by ~+0.5 °C in winter.
The bug: SAP 10.2 Table 9 (PDF p.182) elsewhere off-hours differ
between type 2 and type 3:
Type 1, 2: off-hours (7, 8) — elsewhere heated longer
Type 3: off-hours (9, 8) — elsewhere has separate, shorter
heating schedule per §9.4.14
Wrong control type → wrong off-hours → wrong off-period temperature
reduction (Table 9b) → wrong MIT_elsewhere (line (90)m) → wrong
blended MIT (line (92)/(93)m) → wrong space heating demand
(line (98c)).
Diagnosis chain: post-S0380.86 cert 000565 had structural SH
over-count of +7924 kWh independent of fabric. Diff vs worksheet
intermediates traced it to MIT cascade higher by avg +0.45 °C
(+5.42 °C-months). Per-zone breakdown showed MIT_elsewhere over by
+6.25 °C-months while Th2 setpoint matched. Reverse-engineered the
off-period reduction (cascade u≈4.12 vs worksheet u≈5.14, Jan)
matched off-hours (9, 8) — control type 3 — vs cascade (7, 8) —
control type 2. Verified against Table 4e GROUP 2 spec text:
2207 → type 3 spec-correct.
Cohort + golden + cert 9501 unaffected:
- Elmhurst U985 cohort (000474..000516): all lodge code 2106
(Group 1, type 2) — unchanged
- 20 golden API certs lodge code 2206 (Group 2 HP, type 2 per
spec, type 2 via cascade default) — adding 2206:2 to the
dispatch dict makes the type explicit, behaviour unchanged
- Cert 9501: not lodging 22XX codes
- Only cert 000565 lodges 2207 (and exercises the 22XX→3 fix)
Test (1 new, AAA-structure parametrised across all 10 Group 2 codes
in `test_cert_to_inputs.py`).
**Cert 000565 closure (post-S0380.87 vs post-S0380.86):**
Pin Pre Post Δ worksheet
--- ---- ---- --- ---------
sap_score 23 27 +4 29
sap_score_continuous 22.64 27.35 +4.71 28.51
ecf 6.02 5.51 -0.51 5.39
total_fuel_cost_gbp 5233 4784 -449 4680
co2_kg_per_yr 7165 6581 -584 6448
space_heating_kwh 66932 60468 -6464 59008
main_heating_fuel 39372 35570 -3802 34711
Space heating residual closed +7924 → **+1460 (82% closed)**.
Integer SAP closed Δ-6 → Δ-2 (was the 9th-largest residual; now
gates only on continuous SAP rounding boundary at 28.5).
Test baseline: 561 pass (was 560 + 1 new) + 9 expected
`test_sap_result_pin[000565-*]` fails unchanged. Pyright net-zero
per touched file. SAP 10.2 spec citation included in dispatch dict
comment per [[feedback-spec-citation-in-commits]].
Per [[feedback-spec-floor-skepticism]] + [[feedback-verify-handover-claims]]:
the handover post-S0380.86 listed several candidate SH-channel
root causes (solar gains, internal gains, η, T_int, ventilation).
The single-line spec-dispatch gap was the dominant driver. The
remaining +1460 kWh residual splits roughly: ~+750 kWh from HLC
over-count (+42 W/K, mostly ventilation +27 W/K), ~+700 kWh from
remaining MIT residual.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
6c8bbbc9e2 |
Slice S0380.86: §5.6 thin-wall stone + §5.8 dry-line closes BP[0] alt1 cascade gap
RdSAP 10 §5.6 (PDF p.40) "U-values of uninsulated stone walls, age
bands A to E":
Table 12 — Default U-values of stone walls
Sandstone or limestone: U = 54.876 × W^(-0.561)
Granite or whinstone: U = 45.315 × W^(-0.513)
Where W is wall thickness in mm.
"Apply the adjustment according to Table 14: Insulation thickness
and corresponding resistance if wall is insulated or dry-lined
including lath and plaster."
Combined with §5.8 (PDF p.40) + Table 14 (PDF p.41) dry-line R = 0.17
m²K/W: U = 1 / (1/U₀ + 0.17).
Cert 000565 BP[0] Main alt1 is the cohort fixture: Stone Granite, age
band A (inherited from Main), 120 mm wall thickness, dry-lined.
§5.6 formula: U₀ = 45.315 × 120^(-0.513) ≈ 3.8871.
§5.8 + Table 14 dry-line: U = 1/(1/3.8871 + 0.17) ≈ **2.3405**.
→ matches worksheet U985-0001-000565 line (29a) "External walls Main
alt.1 ... SolidWallDensePlasterInsul, Solid, 0.0, 2.34" EXACT.
Pre-S0380.86 two coupled bugs blocked this path:
1. Mapper mis-name per [[feedback-no-misleading-insulation-type]]:
`_map_elmhurst_alternative_wall` routed the Elmhurst Summary §7
"Alternative Wall N Thickness" lodging (the WALL thickness)
onto `SapAlternativeWall.wall_insulation_thickness="120"`. The
cascade then mis-bucketed it as 100 mm insulation (bucket=100
→ _BRICK_INS_100 row at age A → U=0.32). The Elmhurst Summary
schema has no "Alternative Wall N Insulation Thickness" line at
all — `wall_insulation_thickness` on alts was always
semantically the wall thickness, never insulation.
2. `u_wall` had no §5.6 thin-wall stone branch. Stone constructions
fell through to Table 6 row values (designed for typical-
thickness ~300mm+ walls), which dramatically under-state heat
loss for sub-200mm stone.
Fix span:
- datatypes/epc/domain/epc_property_data.py:SapAlternativeWall:
new `wall_thickness_mm: Optional[int] = None` field, mirroring
`SapBuildingPart.wall_thickness_mm`.
- datatypes/epc/domain/mapper.py:_map_elmhurst_alternative_wall:
routes Elmhurst `a.thickness_mm` (Wall thickness) onto
`wall_thickness_mm`; leaves `wall_insulation_thickness=None`
on this path (no Elmhurst Summary alt-wall insulation-thickness
line exists).
- domain/sap10_ml/rdsap_uvalues.py:
new `_u_stone_thin_wall_age_a_to_e(construction, W)` helper
implements §5.6 Table 12 formulas. `u_wall` accepts a new
`wall_thickness_mm: Optional[int] = None` param; dispatches
§5.6 formula when (a) wall thickness lodged, (b) age band ∈
A-E, (c) construction ∈ {STONE_GRANITE, STONE_SANDSTONE}.
§5.8 + Table 14 R=0.17 applied on top when dry_lined=True.
- domain/sap10_calculator/worksheet/heat_transmission.py:
`_alt_wall_contribution_w_per_k` passes
`wall_thickness_mm=alt_wall.wall_thickness_mm` to `u_wall`.
Tests (7 new, AAA-structure):
- 5 in domain/sap10_ml/tests/test_rdsap_uvalues.py — granite at
120 mm with dry-line (U=2.34); granite raw formula (U=3.89);
sandstone (U=3.74); age-G gate (Table 6 row, NOT formula); no
wall_thickness fallback (Table 6 row 1.7).
- 2 in backend/documents_parser/tests/test_summary_pdf_mapper_chain
.py — mapper pin (wall_thickness_mm=120 on BP[0] alt1;
wall_insulation_thickness=None) and cascade pin (walls_w_per_k
≥ 595, post-S0380.85 was 555.93).
**Cert 000565 cascade walls: 555.93 → 602.40 W/K (worksheet 604.07;
0.27% residual).** BP[0] alt1 cascade U: 0.32 → 2.34. Cascade walls
within 2 W/K of worksheet target across S0380.85+.86 closure cycle.
Test baseline: 560 pass (was 558 + 7 new − 5 already passing pins
that moved) + 9 expected `test_sap_result_pin[000565-*]` fails
unchanged. Cohort + golden + cert 9501 unaffected: of the 6 cohort
fixtures only cert 000565 alt1 lodged a `wall_insulation_thickness`
value on `SapAlternativeWall` (audit confirmed) — and that value was
always semantically the wall thickness, so the rename is a fix not
a behaviour change. The API mapper path defaults `wall_thickness_mm`
to None (API schema doesn't yet surface alt-wall thickness; safe
forward-compat).
Per [[feedback-verify-handover-claims]]: the post-S0380.84 handover
predicted SH residual would close after the wall fixes. Empirically
SH grew +2591 → +6348 → +7924 across S0380.84/.85/.86 — confirming a
SEPARATE SH-channel over-count that's independent of fabric (each
+1 W/K of spec-correct walls adds ~33.5 kWh of cascade SH, vs the
worksheet's ~38.96 kWh/W/K rate). The walls fixes are spec-correct;
the SH over-count is now a single isolated open work-item for the
next slice (~+8 k kWh structural).
Pyright net-zero per touched file (test_rdsap_uvalues.py error count
actually decreased by 1).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
647c1aad0e |
Slice S0380.85: Curtain Wall §5.18 dispatch closes BP[2] Ext2 cascade gap
RdSAP 10 §5.18 (PDF p.48) "Curtain wall - U-value and other parameters":
"If documentary evidence is available, use calculated U-value of the
whole curtain wall. Otherwise for the purpose of RdSAP, U= 2.0 W/m²K
for pre-2023 curtain walls, And for post-2023 (2024 in Scotland)
U-values as for windows given in Notes below Table 24."
Table 24 row "Double or triple glazed England/Wales: 2022 or later"
PVC/wood column = 1.4 W/m²K. Whole-wall curtain walls use Frame
Factor=1 per the §5.18 closer.
Pre-S0380.85 `WALL_CURTAIN=9` was defined at rdsap_uvalues.py:116 but
NOT included in `known_types`, so `u_wall(construction=9)` fell through
to `_DEFAULT_WALL_BY_AGE.get(band, WALL_CAVITY)` → cavity table at age
H = 0.60. Cert 000565 BP[2] Ext2 lodges `Type: CW Curtain Wall` +
`Curtain Wall Age: Post 2023` per Summary PDF §7; worksheet pins U=1.40
(matching the §5.18 Post-2023 PVC/wood row). Cascade under-counted
walls by Δ U=0.80 × area = −112.2 W/K on this BP — 70% of the
post-S0380.84 BP main-wall residual (−161 W/K total).
§5.18 keys the curtain-wall U-value on the per-BP installation age,
NOT on the dwelling-wide `construction_age_band` — cert 000565 is
age H (1991-1995) but the curtain wall itself was installed
Post-2023. Plumb a new optional field through the extractor → datatype
→ mapper → cascade so the §5.18 dispatch sees it.
Files touched (5-layer slice span):
- backend/documents_parser/elmhurst_extractor.py:
`_wall_details_from_lines` reads "Curtain Wall Age" via
`_local_val` so absent lines stay None (not "").
- datatypes/epc/surveys/elmhurst_site_notes.py:WallDetails:
`curtain_wall_age: Optional[str] = None` field added.
- datatypes/epc/domain/epc_property_data.py:SapBuildingPart:
`curtain_wall_age: Optional[str] = None` field added.
- datatypes/epc/domain/mapper.py:_map_elmhurst_building_part:
threads `walls.curtain_wall_age` onto SapBuildingPart.
- domain/sap10_ml/rdsap_uvalues.py:
new `_u_curtain_wall(curtain_wall_age)` helper + WALL_CURTAIN
dispatch in `u_wall` before the `known_types` lookup.
"Post 2023" / "Post-2023" → 1.4; everything else (incl. None)
→ 2.0 per §5.18 fallback.
- domain/sap10_calculator/worksheet/heat_transmission.py:
passes `curtain_wall_age=part.curtain_wall_age` to `u_wall`
on the main-wall path. (Alt-wall path unchanged — cert 000565
lodges CW only as a main wall, never as an alt sub-area; alt
coverage is a follow-up slice if a future cert exercises it.)
Tests (6 new, AAA-structure):
- 3 in domain/sap10_ml/tests/test_rdsap_uvalues.py — `u_wall` direct
unit tests for Post 2023 (1.4), Pre 2023 (2.0), and absent
lodging fallback (2.0).
- 3 in backend/documents_parser/tests/test_summary_pdf_mapper_chain
.py — extractor pin (BP[2] Ext2 surfaces "Post 2023", non-CW BPs
stay None), mapper pin (curtain_wall_age threaded to BP[2]
SapBuildingPart), cascade pin (`heat_transmission_from_cert`
walls subtotal ≥ 540 W/K — pre-S0380.85 was 443).
Cert 000565 cascade walls: 443 → 555.93 W/K (worksheet 604.07; 70%
closer). Test baseline: 558 pass (was 555 + 3 new) + 9 expected
`test_sap_result_pin[000565-*]` fails unchanged.
Per [[feedback-verify-handover-claims]]: the post-S0380.84 handover
predicted SH residual would close +2591 → ~+800 kWh after this slice,
but the cascade is actually OVER-counting SH despite walls being
UNDER-counted. Closing the wall under-count makes the SH residual
*larger* (+2591 → +6348). The wall fix is spec-correct; the SH
over-count is a separate channel that surfaces more sharply now. Per
[[feedback-spec-citation-in-commits]] + [[feedback-spec-floor-skepticism]]
+ the S0380.84 precedent, ship the spec-correct change and document
the surfaced gap for the next slice rather than reverting to the
compensating-bugs state.
Pyright net-zero on every touched file (existing pre-existing errors
unchanged). Cohort + golden + cert 9501 unaffected — curtain_wall_age
defaults to None on those certs and `u_wall` ignores it unless
`construction == WALL_CURTAIN`.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
b0fef688da |
docs: handover + next-agent prompt post S0380.81..84 (Table 32 default + Table 12a Grid 2 CO2 + RR fold-in)
Captures the 4-slice arc this session — Table 32 default prices (sap_score 28 → 29 EXACT), Table 12a Grid 2 dual-rate CO2 (CO2 65% closed), extractor gable_type recognition + mapper preservation of cert 9501, and the full RR mapper + cascade fix per RdSAP 10 §3.9.2 + §3.10 + Table 4 (11 per-BP RR surface areas EXACT vs worksheet PDF). Documents the BP main-wall residual −161 W/K diagnostic localising the next slice block to two spec-cited cascade gaps: Curtain Wall (−112 W/K, no _ENG_WALL entry for WALL_CURTAIN=9) + thin-wall stone granite alt (−47 W/K, _insulation_bucket short-circuit + thin-wall §6.6/§6.7 formula). Each spans extractor → datatype → mapper → cascade and is a single coherent slice when the spec lookup is tractable. NEXT_AGENT_PROMPT_POST_S0380_84.md prescribes S0380.85 (Curtain Wall) as the highest-leverage next slice with audit + implementation + expected-outcome details. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
49622f5525 |
Slice S0380.84: RR mapper spec-correct routing + cascade common_wall handling per RdSAP 10 §3.9.2/§3.10
Cascades the spec-correct §3.10 Room-in-Roof routing through the
mapper + heat-transmission section. Three coupled changes:
1. **Mapper drops "Connected" gables** — per RdSAP 10 Table 4 (PDF p.22)
row 4 a gable wall "Connected to heated space" is an internal
partition, NOT a heat-loss surface. The Elmhurst Summary §8.1 PDF
may lodge the short form "Connected" or the verbose "Connected to
heated space"; both route to `return None` in
`_map_elmhurst_rir_surface`.
2. **Mapper routes "Exposed" gables → `gable_wall_external` with the
lodged U** — per Table 4 row 1 an exposed RR gable wall bills at the
lodged U-value (or the storey-below main-wall U). For non-flat
dwellings the `default_u_value` rides through as `u_value` override
so the cascade uses the lodged figure directly. Flats preserve their
legacy no-override routing so the cascade falls through to main-wall
U (cert 9501).
3. **Mapper surfaces Common Wall surfaces + applies spec area formula**
per RdSAP 10 §3.9.2 + Table 4:
Detailed assessment → raw L × H per surface
Simplified + Common Walls → L × (0.25 + H) for common walls;
L × (0.25 + H_gable)
− Σ_n (H_gable − H_common,n)² / 2
for gables
Simplified + no Common Walls → raw L × H for gables
The 0.25-m structural-gap offset accounts for the space between the
RR floor and the storey-below ceiling. The gable correction
subtracts the triangular slice above each common wall.
4. **Cascade adds `common_wall` kind** in `heat_transmission.py` — mirror
of `gable_wall_external`: walls += area × (`surf.u_value` or main-wall
U). Mapper precomputes the spec area so the cascade reads `area_m2`
directly.
Verified against the cert 000565 U985 worksheet PDF "External Walls"
section per BP:
| BP | Surface | Formula | Worksheet | Cascade |
|----|---------------------|-------------------------------------------|-----------|---------|
| 0 | Main GW1 (Exposed) | 4 × 2.45 (Simplified, no CW) | 9.80 | 9.80 ✓ |
| 0 | Main GW2 (Sheltered)| 6 × 2.45 | 14.70 | 14.70 ✓|
| 1 | Ext1 CW1 | 9 × (0.25 + 1.0) (Simplified + CW) | 11.25 | 11.25 ✓|
| 1 | Ext1 CW2 | 5 × (0.25 + 1.8) | 10.25 | 10.25 ✓|
| 1 | Ext1 GW2 (Exposed) | 8 × (0.25 + 9) − ((9−1)²+(9−1.8)²)/2 | 16.08 | 16.08 ✓|
| 2 | Ext2 GW2 (Exposed) | 3 × 8 (Detailed) | 24.00 | 24.00 ✓|
| 3 | Ext3 CW1 | 5 × (0.25 + 1.5) (Simplified + CW) | 8.75 | 8.75 ✓ |
| 3 | Ext3 CW2 | 7.5 × (0.25 + 0.3) | 4.13 | 4.13 ✓ |
| 3 | Ext3 GW1 (Exposed) | 9 × (0.25+7) − ((7−1.5)²+(7−0.3)²)/2 | 27.68 | 27.68 ✓|
| 4 | Ext4 CW1 | 4 × 1 (Detailed) | 4.00 | 4.00 ✓ |
| 4 | Ext4 CW2 | 3.5 × 0.6 | 2.10 | 2.10 ✓ |
Cohort impact:
- Cert 9501 (top-floor flat with Detailed RR + Exposed gables) —
PASSES (the flat-RR elif still routes; gables stay at main-wall U
via cascade fall-through).
- All other cohort fixtures: unaffected (no RR or fully-Detailed RR
where raw L × H is also the spec answer).
Cert 000565 cascade subtotals close substantially:
walls 322.21 → 443.51 (worksheet 604.07, Δ −282 → Δ −161, 43% closed)
party walls 153.46 → 93.26 (worksheet 65.13, Δ +88 → Δ +28, 68% closed)
HTC fabric 716.43 → 795.24 (Δ +79 W/K — cascade closer to worksheet)
The remaining 161 W/K under-count in walls + 28 W/K over-count in
party walls localise to the BP main-wall cascade (NOT RR). The cert
000565 sap_score e2e pin regresses from EXACT (29) to Δ−3 (26) because
the previous compensating cascade gaps are now exposed — the
spec-correct fix is real, the residual is real, and the next slice
closes the BP main-wall gap (likely the "External walls Main alt.1"
basement-override at 23 m² × U=2.34 = 53.82 W/K + per-BP main-wall
U/area refinements). Per [[feedback-spec-citation-in-commits]] +
[[feedback-spec-floor-skepticism]] the spec-correct fix ships even
when the test pin temporarily regresses; the diagnostic signal is
sharper now.
Test baseline: 555 pass + 9 expected `test_sap_result_pin[000565-*]`
fails (was 555 + 8; sap_score now in the failing set with cascade-
exposed BP main-wall gap surfaced). Cohort + golden fixtures
unaffected. Pyright net-zero on touched files (59 errors, matches
baseline).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
ed8fdc6ae3 |
Slice S0380.83: Extractor + mapper recognise Exposed / Connected gable_type per RdSAP 10 §3.10
The Elmhurst Summary PDF §8.1 "Room(s) in Roof" per-surface table publishes the
gable-wall environment column with one of four values:
Party → §8.1 party-wall row
Sheltered → §8.1 sheltered external row
Exposed → §8.1 exposed external row
Connected (to heated space) → §8.1 internal partition
Per RdSAP 10 §3.10 (PDF p.30-35) "Detailed Room-in-Roof" + Table 4 (p.22)
"Heat-loss surface variants":
- Exposed gable wall → external wall at the lodged U-value
- Sheltered gable wall → external wall at the lodged U-value
- Party gable wall → party wall at U=0.25 (Table 4 row 2)
- Connected gable wall → internal partition to heated space, NOT a
heat-loss surface
The extractor was only capturing `gable_type ∈ {"Party", "Sheltered",
"Connected to heated space"}` — neither `"Exposed"` (every external gable
on cert 000565) nor the plain `"Connected"` string (the actual PDF
lodging value, vs the verbose "Connected to heated space" form used on
other Summary schemas) was recognised. Both fell through with
`gable_type=None`, masking the downstream cascade gap (cert 000565
BP[0] Main Gable Wall 1 is lodged "Exposed" at U=0.35 but extracted
as untyped → mapper routes to `gable_wall` party at U=0.25, vs the
worksheet's "Roof room Main Gable Wall 1" at U=0.35).
This slice closes the extractor side only:
backend/documents_parser/elmhurst_extractor.py:_parse_rir_surface_row
expands its `gable_type` lookup set to include "Exposed" and the
plain "Connected" lodging value.
Mapper-side: `_map_elmhurst_rir_surface` (datatypes/epc/domain/mapper.py)
preserves cert 9501's behaviour — its flat-RR elif previously hinged
on `surface.gable_type is None and is_flat`; now extends to
`surface.gable_type in (None, "Exposed") and is_flat` so the same
flat-RR routing fires whichever lodging shape the Summary PDF uses.
Net cascade impact: zero. Cert 9501 (top-floor flat) retains its
RR-gables-as-external routing. Cert 000565 (house) keeps falling
through to the default `gable_wall` (party at U=0.25) routing for
"Exposed" + "Connected" gables — the next slice in the block reroutes
those to external walls + drops Connected surfaces per RdSAP 10
Table 4. This commit is pure data-extraction completion; pin
movement lands when S0380.84 wires the mapper through.
Test baseline: 555 pass + 8 expected `test_sap_result_pin[000565-*]`
fails (was 554 + 8 at S0380.82; one new test pins the spec rule).
Pyright net-zero on touched files (45 errors, matches baseline).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
27ead1271a |
Slice S0380.82: Table 12a Grid 2 dual-rate CO2 + PE for pumps/lighting/shower on off-peak certs
Per SAP 10.2 Table 12a Grid 2 (PDF p.191) + Table 12d / 12e (PDF p.194-195):
Table 12a Grid 2 row "All other uses" (lighting + pumps + locally
generated electricity + ... ) × tariff column:
SEVEN_HOUR → 0.90 high-rate fraction
TEN_HOUR → 0.80 high-rate fraction
Table 12d header (p.194): "Where electricity is the fuel used, the
relevant set of factors in the table below should be used to calculate
the monthly CO2 emissions INSTEAD of the annual average factor given
in Table 12."
Identical wording on Table 12e (p.195) for primary energy. The cascade
must therefore blend Table 12d / 12e high-rate × low-rate codes for the
end-uses billing through Grid 2 ALL_OTHER_USES — code 31/32 on 7-hour
and code 33/34 on 10-hour — weighted by each end-use's monthly kWh
profile.
S0380.65 landed this for `main_heating_co2_factor` via Grid 1 SH. The
mirror for the "other uses" trio (lighting / pumps_fans / electric_
shower) was queued. This slice closes it.
Implementation:
- New `_other_use_co2_factor_kg_per_kwh(other_use, tariff, monthly_kwh)`
helper mirrors `_main_heating_co2_factor_kg_per_kwh` but dispatches
through `other_use_high_rate_fraction(OtherUse.ALL_OTHER_USES,
tariff)`. STANDARD passes through to single-code-30 monthly; SEVEN /
TEN_HOUR blend; EIGHTEEN_HOUR / TWENTY_FOUR_HOUR fall through to
single-code-30 since Grid 2 lists no row for them.
- `_other_use_primary_factor(...)` is the PE-side mirror via Table 12e.
- Wired into `CalculatorInputs.{pumps_fans, lighting, electric_shower}_
{co2_factor, primary_factor}` in the `cert_to_inputs` orchestrator.
Cert 000565 movement at HEAD (this commit):
lighting_co2_factor_kg_per_kwh 0.1443 → 0.1483 (Δ +0.0040)
pumps_fans_co2_factor_kg_per_kwh 0.1387 → 0.1427 (Δ +0.0040)
electric_shower_co2_factor_kg_per_kwh 0.1391 → 0.1431 (Δ +0.0040)
→ CO2 residual Δ−8.92 → Δ−3.08 kg/yr (65% closed)
Cohort impact: STANDARD-tariff certs pass through the single-code-30
monthly cascade (identical to the previous `_effective_monthly_co2_
factor(..., _STANDARD_ELECTRICITY_FUEL_CODE)` call). All Elmhurst U985
cohort fixtures + golden cohort run STANDARD → zero shift. Cert 000565
is the only off-peak fixture; its CO2 closes by 5.9 kg/yr.
Test baseline: 554 pass + 8 expected `test_sap_result_pin[000565-*]`
fails (was 553 + 8 at S0380.81; one new test pinning the spec rule).
The 8 cert 000565 fails remain at sub-1e-4 tolerances — sap_score
already EXACT, hot_water_kwh EXACT. CO2 residual closer but not yet
< 1e-4 since lighting +2.2 kWh and pumps_fans +2.5 kWh sub-spec
residuals leak into CO2 too. Closes when those land.
Pyright net-zero on touched files (45 errors, matches baseline).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
9338914f8a |
Slice S0380.81: RdSAP 10 Table 32 default prices close cert 000565 sap_score 28 → 29 EXACT
Per ADR-0010 §10a amendment (2026-05-21) + RdSAP 10 Specification §19.1 (PDF page 80-81): "The SAP rating for RdSAP 10 is to be calculated using Table 32 prices (not Table 12) for section 10a and 10b." The §10a `fuel_cost` orchestrator already used RdSAP 10 Table 32 prices for STANDARD-tariff certs (via `table_32_unit_price_p_per_kwh`). The legacy off-peak fallback scalar path on `CalculatorInputs.*_fuel_cost_ gbp_per_kwh` (which fires when `tariff is not STANDARD` → `_ZERO_FUEL_COST_FOR_OFF_PEAK`) was reading from `prices.unit_price_p_ per_kwh`, which was still wired to SAP 10.2 Table 12 prices via the `SAP_10_2_SPEC_PRICES` PriceTable constant. For cert 000565 (Dual meter → TEN_HOUR tariff + mains-gas DHW via WHC 914), this leaked Table 12's 3.64 p/kWh mains gas rate (vs Table 32's 3.48) into HW cost — a £6 over-count on 3755 HW kWh that landed continuous SAP 0.041 below the 28.5 integer rounding boundary, flipping sap_score 29 → 28. Verbatim Table 32 (PDF page 95) rows touched by this slice: Mains gas 3.48 p/kWh (Table 12 was 3.64) 7-hour low tariff 5.50 p/kWh (Table 12 was 9.40) Standard electricity 13.19 p/kWh (Table 12 was 16.49) Fix is one PriceTable constant — `RDSAP_10_TABLE_32_PRICES` wires `table_32_unit_price_p_per_kwh` + the 5.50 / 13.19 scalars per the amendment. `SAP_10_2_SPEC_PRICES` becomes a back-compat alias so existing `prices=SAP_10_2_SPEC_PRICES` test imports continue working but route through Table 32 at the call site. Cert 000565 movements at HEAD (this commit): - sap_score: 28 → **29 ✓ EXACT** (was Δ−1) - sap_score_continuous: 28.4680 → 28.5355 (Δ−0.041 → Δ+0.027) - total_fuel_cost_gbp: 4683.88 → 4677.87 (Δ+3.62 → Δ−2.39) - ecf: 5.3910 → 5.3841 (Δ+0.004 → Δ−0.003) - hot_water_kwh_per_yr: 3755.03 = 3755.03 ✓ EXACT (unchanged) Cumulative cert 000565 closure across S0380.77/78/79/80/81: - hot_water_kwh: +1399 → +260 → −238 → 0 → 0 ✓ - sap_score (integer): +1 → −1 → 0 → −1 → 0 ✓ EXACT - sap_score_continuous: +0.60 → −0.035 → +0.057 → −0.041 → +0.027 Cohort impact: STANDARD-tariff certs (the 6 U985 fixtures 000474/000477/000480/000487/000490/000516 and all cohort-2/golden gas combi certs) route through the §10a orchestrator that already used Table 32 — zero shift. Off-peak certs in the test suite are cert 000565 only at this point (Dual / TEN_HOUR); golden cohort unaffected. Three existing scalar-pin tests in `test_cert_to_inputs.py` re-pinned to Table 32 values: - `test_gas_heating_with_electric_immersion_charges_hw_at_electricity_ rate` (0.0364 → 0.0348 gas; 0.1649 → 0.1319 std elec) - `test_off_peak_meter_routes_electric_costs_to_low_rate` (0.094 → 0.055 7-hour low fallback) - `test_standard_meter_keeps_electric_costs_on_standard_rate` (0.1649 → 0.1319 std elec) New test pins the rule: `test_rdsap_10_table_32_prices_charge_mains_gas_hot_water_at_3p48_per_kwh` quotes the §19.1 spec and asserts cert 000565 HW £/kWh = 0.0348. Test baseline: 553 pass + 8 expected `test_sap_result_pin[000565-*]` fails (was 551 + 9; sap_score now closes EXACT). The remaining 8 fails on cert 000565 are the documented work-queue residuals — RR fold-in (space_heating, main_heating_fuel), Table 12d/12e dual-rate blend for lighting + CO2, MEV PCDB record (pumps_fans). Pyright net-zero on touched files (45 errors, matching baseline). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
69710f5882 |
docs: handover + next-agent prompt post S0380.77..80 (cert 000565 §4 HW EXACT)
Documents the full §4 HW cascade closure for cert 000565 across the S0380.77 → S0380.80 series: - S0380.77 primary loss WHC 914 routing - S0380.78 §1x.0 shower extractor + (247a) fallback cost - S0380.79 (57)m solar storage + separately-timed-DHW cylinder default - S0380.80 Table 4c −5% DHW for missing boiler interlock Cumulative cert 000565 closure: hot_water_kwh: +1399 → ✓ 0 EXACT (100%) continuous SAP: +0.78 → -0.041 (95% closed) total cost: -69 → +3.62 (95% closed) All §4 line refs (45)/(46)/(57)/(59)/(62)/(64)/(217)/(219) EXACT Open #1 priority for the next agent: deferred ADR-0010 mains-gas tariff Table 32 vs Table 12 cohort closure. The remaining sap_score=28 vs worksheet 29 flip is entirely due to this £0.16/100 gas-price delta over 3755 HW kWh = +£6 → +0.041 continuous SAP → flips integer at the 28.5 boundary. Cohort-wide change; would land sap_score=29 EXACT for cert 000565 AND likely tighten several other worksheet certs in the same coordinated pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
760a893ce8 |
Slice S0380.80: SAP 10.2 Table 4c −5% DHW for missing boiler interlock closes cert 000565 HW pin EXACT
Closes cert 000565 hot_water_kwh_per_yr pin: **3517.37 → 3755.03 ✓
EXACT** vs worksheet 3755.0288. Net cumulative HW closure across
S0380.77/78/79/80: +1399 kWh → 0 (100% closed).
## Spec rule
SAP 10.2 Table 4c (PDF p.169-170) "Efficiency adjustments":
(2) Efficiency adjustment due to control system Space DHW
No boiler interlock - regular boiler (...) −5 −5
No boiler interlock - combi −5 0
Note c): "These do not accumulate as no thermostatic control or
presence of a bypass means that there is no boiler interlock."
RdSAP 10 §3 (PDF p.57) "Boiler interlock" definition:
Assumed present if there is a room thermostat and (for stored
hot water systems heated by the boiler) a cylinder thermostat.
Otherwise not interlocked.
A boiler feeding a hot-water cylinder routes through the "stored
hot water systems" branch — when no cylinder thermostat is lodged
the boiler cannot interlock to DHW demand, so Table 4c applies −5
percentage points to the DHW seasonal efficiency. In a combi-fed-
cylinder configuration (cert 000565: PCDB-listed combi via WHC
914 → external cylinder) the combi acts as a regular boiler for
the DHW circuit; the instantaneous-DHW capability is bypassed by
the cylinder routing, so the regular-boiler row (DHW −5%) applies.
## Fix
`cert_to_inputs.py` water-efficiency branch: after the PCDB-summer
lookup (or Table 4b fall-through), apply −5pp when
epc.has_hot_water_cylinder
AND epc.sap_heating.cylinder_thermostat != "Y"
AND water_pcdb_main is not None # boiler — Table 4c applies
For cert 000565: PCDB summer η = 79.0 → 74.0; output (64)/η =
2778.72/0.74 = 3754.99 vs worksheet 3755.03 — within 0.04 kWh.
## Cert 000565 movements at HEAD (post-S0380.79 → post-this slice)
| Field | Pre-slice | Post-slice | Worksheet | Pre-Δ | Post-Δ |
|----------------------|-----------:|-----------:|-----------:|--------:|--------:|
| sap_score | 29 | 28 | 29 | 0 | −1 |
| sap_score_continuous | 28.5652 | 28.4680 | 28.5087 | +0.057 | −0.041 |
| ecf | 5.3810 | 5.3910 | 5.3866 | −0.006 | +0.004 |
| total_fuel_cost_gbp | 4675.23 | 4683.88 | 4680.26 | −5.03 | +3.62 |
| co2_kg | 6388.80 | 6438.71 | 6447.63 | −58.83 | −8.92 |
| **hot_water_kwh** | 3517.37 | **3755.03** | 3755.03 | −237.66 | **✓ 0** |
| space_heating_kwh | 58936.06 | 58936.06 | 59008.35 | −72.29 | −72.29 |
| main_heating_fuel | 34668.27 | 34668.27 | 34710.79 | −42.52 | −42.52 |
The sap_score=28 flip is the documented gas-tariff residual: cascade
prices mains gas at SAP 10.2 Table 12 = £0.0364/kWh, worksheet uses
RdSAP Table 32 = £0.0348/kWh. Over the now-correct 3755.03 HW kWh
that £0.16/100 delta inflates cost by ~£6 — exactly accounts for the
+£3.62 cost residual and the +0.041 continuous SAP deviation. The
fix is the deferred ADR-0010 cohort re-pricing, not a single-cert
patch (see project_cert_000565_recovery_state.md "Open thread #6").
## Cumulative cert 000565 closure across S0380.77/78/79/80
hot_water_kwh: +1399 → +260 → −238 → **✓ 0** (100% closed)
sap_score_continuous: +0.60 → −0.035 → +0.057 → −0.041 (93% closed)
ecf: −0.06 → +0.004 → −0.006 → +0.004 (93% closed)
total_fuel_cost_gbp: −53 → +3.13 → −5.03 → +3.62 (93% closed)
(45)m, (46)m, (57)m, (59)m, (62)m, **(64)/(217)m, (219)**: ALL EXACT vs worksheet
Test baseline: 550 → 551 pass + 9 expected `test_sap_result_pin
[000565-*]` fails — hot_water_kwh now PASSING; sap_score now failing
in the same expected-fail count. Pyright net-zero (45 = 45).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
f9551355bb |
Slice S0380.79: (57)m solar storage adjustment + separately-timed-DHW cylinder default
Closes cert 000565 sap_score regression — 28 (Δ−1) → **29 ✓ EXACT**.
Continuous SAP 28.4735 → 28.5652 (Δ −0.035 → +0.057 vs worksheet
28.5087). Two coupled fixes that together close the (56)/(57)m
storage-loss gap per SAP 10.2 §4 + Table 2b.
## 1. (57)m solar storage adjustment — SAP 10.2 §4 line 7693 (p.137)
If the vessel contains dedicated solar storage or dedicated
WWHRS storage,
(57)m = (56)m × [(47) - Vs] ÷ (47), else (57)m = (56)m
where Vs is Vww from Appendix G3 or (H12) from Appendix H.
Total heat required for water heating calculated for each month
(62)m = 0.85 × (45)m + (46)m + (57)m + (59)m + (61)m
(62)m sums (57)m — the solar-adjusted storage loss — not raw
(56)m. The cascade's `_cylinder_storage_loss_override` was
passing (56)m straight through as `solar_storage_monthly_kwh_
override`, over-counting (62)m by Vs/V each month whenever solar
HW shares the cylinder. For cert 000565: V = 160 L, Vs = (H12) =
53 L per the combined-cylinder ⅓-volume convention (S0380.76);
(V − Vs)/V = 0.6688 (matches worksheet 50.7018/75.8157 = 0.6688).
Fix: when `epc.solar_water_heating` is True, return (57)m =
(56)m × (V − Vs)/V from `_cylinder_storage_loss_override`. The
combined-cylinder Vs derivation reuses the
`_COMBINED_CYLINDER_SOLAR_PREHEAT_FRACTION` constant established
by S0380.76 for the Appendix H orchestrator path.
## 2. separately_timed_dhw defaults True when a cylinder is lodged
SAP 10.2 Table 2b note b) (PDF p.159):
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)
RdSAP 10 Specification §3 default table "Hot water separately
timed" (PDF p.57):
No programmer, pre-1998 boiler: - No
Programmer, pre-1998 boiler: - Yes
Post-1998 boiler: - Yes
When a hot-water cylinder is lodged, DHW is timed by its own
programmer / boost cycle regardless of which heat generator
(boiler, HP, or combi-acting-as-boiler) feeds it. Combi-only
dwellings (no cylinder) skip the multiplier — DHW is
instantaneous and shares the boiler's space-heating cycle.
The earlier `_separately_timed_dhw` heuristic gated only on
`main_heating_category == 4` (heat pumps), returning False for
boiler-family + cylinder combos. Cert 000565 (gas combi via
WHC 914 + 160 L cylinder + cyl-stat absent) fell through to TF
= 0.60 × 1.3 × 1.0 = 0.78; the worksheet uses 0.60 × 1.3 × 0.9
= 0.702. The 10% TF over-count drove +98 kWh/yr into (56)m before
compounding with the missing (57)m solar adjustment.
Fix: `_separately_timed_dhw(epc, main)` returns True when a
cylinder is lodged, in addition to the existing HP branch. Signature
gains `epc` so the helper can inspect `has_hot_water_cylinder`;
both call sites in `_primary_loss_override` and
`_cylinder_storage_loss_override` updated.
## Cert 000565 movements at HEAD (post-S0380.78 → post-this slice)
| Field | Pre-slice | Post-slice | Worksheet | Pre-Δ | Post-Δ |
|----------------------|-----------:|-----------:|-----------:|--------:|--------:|
| **sap_score** | **28** | **29** | **29** | −1 | **✓ 0** |
| sap_score_continuous | 28.4735 | 28.5652 | 28.5087 | −0.035 | +0.057 |
| ecf | 5.3904 | 5.3810 | 5.3866 | +0.004 | −0.006 |
| total_fuel_cost_gbp | 4683.39 | 4675.23 | 4680.26 | +3.13 | −5.03 |
| co2_kg | 6480.57 | 6388.80 | 6447.63 | +33 | −58.8 |
| hot_water_kwh | 4014.64 | 3517.37 | 3755.03 | +259.6 | −237.7 |
| space_heating_kwh | 58792.99 | 58936.06 | 59008.35 | −215.4 | −72.3 |
| main_heating_fuel | 34584.11 | 34668.27 | 34710.79 | −126.7 | −42.5 |
HW pin overshot −238 (down from +260) — within ~6% of the
worksheet, vs the +37% over-count three slices ago. Continuous
SAP residual flipped from Δ −0.035 to Δ +0.057, restoring integer
sap_score = 29 EXACT. The cumulative cert 000565 closure across
S0380.77/78/79:
hot_water_kwh: +1399 → +260 → −238 (84% closed)
sap_score_cont: +0.60 → −0.035 → +0.057 (90% closed)
ecf: −0.06 → +0.004 → −0.006 (90% closed)
## Cross-cohort impact — cert 0390 golden pin update
Golden cert `0390-2954-3640-2196-4175` (Firebird oil combi PCDF
9005 + 160 L cylinder + cyl-stat=Y, no solar) was previously
flagged at SAP residual −7 with the comment "traces to fabric
heat-loss / oil-fuel cost cascade rather than the §4 HW path".
That diagnosis was wrong: cert 0390's §4 HW cascade WAS
applying TF=0.60 instead of TF=0.54 for the (56)m storage loss,
contributing ~£20/yr cost over-count.
Per [[feedback-spec-floor-skepticism]] + [[feedback-golden-
residuals-near-zero]], the +1 SAP closure (53→54, residual
−7→−6) is the spec-correct outcome of applying RdSAP §3 default
"Programmer, pre-1998 boiler → Yes". Pin updated; revised notes
record the slice S0380.79 attribution.
## Tests
- `test_cylinder_storage_loss_applies_57m_solar_adjustment_per_sap_4_line_7693`
(test_cert_to_inputs.py) — pins `solar_storage_monthly_kwh[0]` to
worksheet (57)Jan = 50.7018 at abs=1e-4 and the 12-month sum to
596.9725 at abs=1e-3, for cert-000565-shape inputs (gas combi +
cylinder + solar HW + cyl-stat absent).
- Updated golden pin for cert 0390 per the cross-cohort impact note.
Test baseline: 548 → 550 pass + 9 expected `test_sap_result_pin
[000565-*]` fails (sap_score now PASSING; one fewer expected fail
than mid-slice). Pyright net-zero on touched files (46 baseline =
46 after).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
509ef4fbbf |
Slice S0380.78: §1x.0 shower extractor + (247a) fallback cost close cert 000565 (45)m
Two coupled fixes that together close the +903 kWh (45)m
energy-content over-count on cert 000565. Splitting them would
flip sap_score from 29 → 30 mid-fix; bundled they keep cert 000565
within rounding of the worksheet (continuous SAP residual closes
17×, from Δ +0.60 to Δ −0.035).
## 1. Elmhurst extractor — §1x.0 section-bounded "Connected" lookup
`_extract_baths_and_showers` was anchoring on the FIRST "Connected"
substring in the document via `self._lines.index("Connected")`.
Cert 000565 (4 extensions) has "Connected" appearing earlier as a
§3 building-parts wall elevation flag, so the global match landed
on a wall row; the digit-check at `num_line.isdigit()` failed
immediately on the "0.00" wall length and the shower roster came
back empty.
Both `1x.0 Baths and Showers` and `18.0 Flue Gas Heat Recovery
System` are single-occurrence section anchors in the Elmhurst
Summary PDF. Routing the "Connected" lookup through `_section_
lines(...)` bounds the search to the §1x.0 block, so multi-
extension certs no longer lose the shower roster.
## 2. SAP 10.2 §10a line (247a) — electric shower cost in fallback path
SAP 10.2 §10a (PDF p.145) worksheet line (247a):
Energy for instantaneous electric shower(s)
(64a) × 0.01 = (247a)
Total energy cost (240)...(242) + (245)...(254) = (255)
Electric showers route their (64a) kWh through the "other fuel"
tariff (same column as pumps/fans (249) and lighting (250)) and
add to (255) total cost.
`calculator.py:415-470` STANDARD-tariff path consumes
`FuelCostResult` from `fuel_cost(...)` which already plumbs
`instant_shower_cost_gbp` (worksheet/fuel_cost.py:214). The
fallback scalar path at `calculator.py:489-530` (TEN_HOUR /
off-peak / zero-FuelCostResult certs) was missing the electric-
shower term entirely. Cert 000565 (Dual-meter TEN_HOUR + 1
electric shower) trips this branch — fix #1 surfaced the
£93/yr under-count and the sap_score regression that followed.
Fix: add
electric_shower_cost = inputs.electric_shower_kwh_per_yr
× inputs.other_fuel_cost_gbp_per_kwh
into the `total_cost = max(0, ...)` sum, parallel to the existing
`electric_shower_co2` and `electric_shower_pe` flows already
present in the CO2 (line 552) and PE (line 619) sections.
## Why bundled
SAP 10.2 Appendix J §J2 step 2a (PDF p.81) routes baths via
`N_bath = 0.13 N + 0.19` when a shower is present, `0.35 N + 0.50`
when no shower is present — a 2.67× swing in (42b)m that
compounds into (45)m energy content. The extractor fix closes
(45)m to EXACT (1286.3266 = 1286.3266 ✓), but the cascade's
electric-shower kWh stream becomes load-bearing for cost — and
the fallback path was silently dropping it. Without fix #2,
sap_score regressed from 29 → 30 (cost too low → ECF too low →
SAP rating too high).
## Cert 000565 movements at HEAD (post-S0380.77 → post-this slice)
| Field | Pre-slice | Post-slice | Worksheet | Pre-Δ | Post-Δ |
|----------------------|----------:|------------:|-----------:|--------:|--------:|
| sap_score | 29 | 28 | 29 | 0 | −1 |
| sap_score_continuous | 29.1090 | 28.4735 | 28.5087 | +0.60 | **−0.035** |
| ecf | 5.3256 | 5.3904 | 5.3866 | −0.06 | **+0.004** |
| total_fuel_cost_gbp | 4627.10 | 4683.39 | 4680.26 | −53.16 | **+3.13** |
| co2_kg | 6616.0 | 6480.6 | 6447.6 | +168.4 | +32.94 |
| hot_water_kwh | 5154.0 | 4014.6 | 3755.0 | +1399 | +259.6 |
| space_heating_kwh | 58725.8 | 58793.0 | 59008.4 | −282.6 | −215.4 |
| main_heating_fuel | 34544.6 | 34584.1 | 34710.8 | −166.2 | −126.7 |
| (45)m sum | 2189.38 | **1286.33**| 1286.3266 | +903 | 0 |
The integer sap_score = 28 vs worksheet = 29 is a rounding-
boundary artifact: continuous SAP at 28.4735 rounds DOWN, just
0.035 below the 28.5 threshold. The remaining +259 kWh HW pin
over-count traces to the still-open (56)m storage loss over-count
+ missing (57)m solar-storage adjustment (slice C per the
handover) — closing that pulls continuous SAP back above 28.5 and
restores integer 29.
## Tests
- `test_summary_000565_extractor_finds_electric_shower_in_section_1x_0`
(test_summary_pdf_mapper_chain.py) — pins extractor finds the
Electric shower in §1x.0 even with §3 building-parts "Connected"
collisions earlier in the document.
- `test_total_fuel_cost_includes_247a_electric_shower_in_fallback_path`
(test_calculator.py) — pins `total_fuel_cost_gbp` rises by
exactly `kwh × other_fuel_cost` when `electric_shower_kwh_per_yr`
is non-zero in the fallback path.
Test baseline: 547 → 570 pass (+3 new tests across the 4 modified
files + indirect knock-ons in golden fixtures); 9 → 10 expected
`test_sap_result_pin[000565-*]` fails (now includes the integer
`sap_score` until slice C closes the remaining +259 kWh HW
residual). Pyright net-zero on all 4 touched files (50 baseline =
50 after).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
a33904c58b |
Slice S0380.77: WHC 914 routes primary-loss gate to DHW main; cert 000565 +1175 kWh
SAP 10.2 §4 line 7700 + Table 3 (PDF p.159) defines primary loss
as the loss between "a heat generator (e.g. boiler) connected to a
hot water storage vessel via insulated or uninsulated pipes (the
primary pipework)". The spec keys eligibility off the heat
generator that feeds the cylinder, NOT the space-heating main.
Verbatim spec (Table 3, p.159):
Primary circuit loss applies when hot water is heated by a heat
generator (e.g. boiler) connected to a hot water storage vessel
via insulated or uninsulated pipes (the primary pipework).
Primary loss is set to zero for the following:
- Electric immersion heater
- Combi boiler (including when it is part of a combined heat
pump and boiler package and provides all the hot water)
- CPSU (including electric 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 (not combined heat pump and boiler package with a
non-combi boiler) from PCDB with hot water vessel integral
to package
For other cases (indirect cylinders and thermal stores connected
by uninsulated pipework or more than 1.5 m of insulated
pipework) the loss in kWh/month is calculated as follows.
Primary loss = n_m × 14 × [{0.0091 × p + 0.0245 × (1 − p)} × h
+ 0.0263]
`_water_heating_section` was routing the gate through
`_first_main_heating` (Main 1). Cert 000565 lodges ASHP Main 1 +
gas combi Main 2 + WHC 914 ("from second main system") + 160 L
combined cylinder + cylinder thermostat "N". The cylinder is
heated by Main 2 via uninsulated primary pipework, so the spec
formula applies; the cascade was zeroing (59)m because Main 1's
HP record (or its absence) drove the gate.
Fix: `_primary_loss_override` resolves its `main` via
`_water_heating_main(epc)` (the WHC-914 routing) so the
eligibility check follows the heat generator that physically
incurs the loss. Call site loses the `main` argument; the gate's
internals are unchanged.
Cert 000565 worksheet line (59)m (U985-0001-000565 Block 1):
128.3772, 115.9536, 128.3772, 124.2360, 128.3772, 41.9160,
43.3132, 43.3132, 41.9160, 128.3772, 124.2360, 128.3772
sums to 1174.79 kWh/yr. Cascade now matches every month at < 1e-4
kWh under the spec params (uninsulated p=0 from RdSAP §3 age band
A-J default, h_winter=11 / h_summer=3 from "no cylinder
thermostat" row).
Cert 000565 movements at HEAD:
- sap_score: 29 ✓ EXACT (unchanged)
- sap_score_continuous: 29.2905 → 29.1090 (Δ +0.78 → +0.60)
- space_heating_kwh: 59274.46 → 58725.77 (Δ +266 → −283)
- main_heating_fuel: 34867.33 → 34544.57 (Δ +157 → −166)
- hot_water_kwh: 3668.54 → 5154.02 (Δ −86 → +1399)
- co2_kg: 6352.61 → 6616.01 (Δ −95 → +168)
- total_fuel_cost_gbp: 4611.14 → 4627.10 (Δ −69 → −53)
- ecf: 5.3073 → 5.3256 (Δ −0.08 → −0.06)
HW pin over-shoots by +1399 as expected — the +1174.79 spec credit
is amplified by the gas combi's 0.88 water efficiency (≈ +1335
kWh) plus cascade coupling (heat-gain feedback into space heating
drops it −549 kWh). The +1399 will be pulled back by the next two
demand-cascade slices:
- (45)m energy_content over by ~903 kWh (occupancy + shower mix)
- (56)/(57)m storage-loss over by ~98 kWh + missing solar
`(57)m = (56)m × (H13 − H12)/H13` adjustment (~298 kWh)
New test pins the full 12-month (59)m tuple for cert 000565's
heating shape at abs=1e-4. Old `_first_main_heating`-based test
(`test_cert_with_hot_water_cylinder_computes_primary_loss_59m_…`)
unchanged — still passes via the WHC-901 fall-through to Main 1.
Test baseline: 547 → 548 pass (new test added) + 9 expected
`test_sap_result_pin[000565-*]` cascade-gap fails. Pyright
net-zero on both touched files (45 baseline = 45 after).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
41566a9023 |
docs: handover + next-agent prompt post S0380.74..76
- HANDOVER_POST_S0380_76.md: full state including the Appendix H closure narrative (the trap that closed via U3.3 unit-convention fix), cert 000565 current pin state, and three independent demand-cascade bugs to tackle in follow-on slices. - NEXT_AGENT_PROMPT_POST_S0380_76.md: focused briefing pointing at primary_loss (59)m for HP + external cylinder as the highest- yield next slice (+1175 kWh fix). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
a532f75db0 |
Slice S0380.76: Combined-cylinder H12/H13 routing closes solar Q_s to <2 kWh/yr
Per SAP 10.2 spec p.75 (Effective solar volume section): "in the case of a combined cylinder (such as arrangement b) in Figure H2, [(H14) =] the volume of the dedicated solar storage plus 0.3 times the volume of the remainder of the cylinder." The spec leaves the dedicated solar storage volume (H12) itself to the surveyor / certified-software convention for combined-cylinder setups. Empirical pattern across 4 Elmhurst worksheets establishes the combined-cylinder default H12 = 1/3 × cylinder volume (H13): cert 000565 worksheet: H12 = 53 L, H13 = 160 L (ratio 0.331) cert A worksheet: H12 = 37 L, H13 = 110 L (ratio 0.336) cert B worksheet: H12 = 37 L, H13 = 110 L (ratio 0.336) cert C worksheet: H12 = 37 L, H13 = 110 L (ratio 0.336) This matches the broader f-chart literature convention for "solar pre-heat zone" sizing in stratified tanks: roughly the lower third of the cylinder is reserved for solar pre-heat, the upper two-thirds for boiler-heated water. The Elmhurst-certified rounding is to the nearest integer litre (53.33 → 53; 36.67 → 37). `_solar_hw_monthly_override` now derives H12/H13 from `epc.has_hot_water_cylinder + sap_heating.cylinder_size` via the existing `_CYLINDER_SIZE_CODE_TO_LITRES` Table 28 lookup (RdSAP 10 §10.5). When no cylinder is lodged (instantaneous + solar HW shape, hypothetical), fall back to Table 29's 75 L separate pre-heat tank. Cert 000565 closure: - Orchestrator solar Q_s annual: 268 → 283 kWh/yr (worksheet 281.35, Δ +1.73, 0.6% error). Within the same precision band as appendix_h_solar's per-month <1e-3 kWh pin. - HW pin: −69 → −86 kWh/yr (slight regression due to compounding with three independent demand-cascade bugs not yet wired: (45)m over by 903, primary_loss (59) under by 1175 (cylinder HP routing missing), storage_loss (56) over by 98 + missing (57)m solar adjustment). These were previously masked by the +357 kWh "no solar credit" over-count; now visible as the residual. Solar Q_s closure is the load-bearing achievement here — the Appendix H orchestrator is now spec-pinned through to the cert's own lodged geometry. HW pin closure follows once the demand-path gaps land in follow-on slices. Test baseline: 547 pass + 9 expected `test_sap_result_pin[000565-*]` fails (unchanged). Cohort-2 + ASHP cohort + golden fixtures untouched — no other cert lodges solar HW. Pyright net-zero (34 errors baseline → 34 errors post-change). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
a9143d0921 |
Slice S0380.75: Wire Appendix H orchestrator into cascade; cert 000565 HW +272 → −69
Per SAP 10.2 §4 line (64)m: `(64)m = max(0, (62)m + (63a)m + (63b)m
+ (63c)m + (63d)m)` where (63c)m is the solar HW credit lodged as a
negative quantity. The cascade hardcoded (63c)m = 0 since S0380.66
when the Appendix H orchestrator landed without integration, pending
the 1.81× over-count resolution (closed in S0380.74).
This slice plumbs the orchestrator into `water_heating_from_cert`
via a new `solar_water_heating_monthly_kwh_override` parameter, and
adds `_solar_hw_monthly_override` in cert_to_inputs.py that drives
the orchestrator from RdSAP 10 §10.11 Table 29 defaults +
cert-lodged collector geometry on Elmhurst Summary §16.0.
RdSAP 10 §10.11 Table 29 row "Solar panel" (p.58, verbatim):
"If solar panel present, the parameters for the calculation not
provided in the RdSAP data set are:
- panel aperture area 3 m²
- flat panel, η₀ = 0.80, a₁ = 4.0, a₂ = 0.01
- facing South, pitch 30°, modest overshading
- …
- pump for solar-heated water is electric (75 kWh/year)
- showers are both electric and non-electric"
Lodged collector orientation / pitch / overshading on the Summary
§16.0 ("Are details known? Yes" branch) override South / 30° /
Modest. Aperture, η₀, a₁, a₂, IAM stay at Table 29 defaults — the
deeper thermal parameter lodgement (P960 worksheet) isn't yet in
the Summary extractor surface.
For (H17)m to include storage + primary + combi losses, the cascade
runs a `demand_pass` call without solar (gets (62)m) before sizing
the solar credit. The final call then uses all overrides.
Files:
- datatypes/epc/surveys/elmhurst_site_notes.py: Renewables gains
`solar_hw_collector_orientation` / `_pitch_deg` / `_overshading`
optional fields.
- datatypes/epc/domain/epc_property_data.py: same three fields
added at the end of the dataclass.
- datatypes/epc/domain/mapper.py: from_elmhurst_site_notes
propagates the three new fields.
- backend/documents_parser/elmhurst_extractor.py: §16.0 section
parsing reads "Collector orientation" / "Collector elevation" /
"Overshading" rows; `_parse_solar_pitch_deg` strips the degree
glyph.
- domain/sap10_calculator/worksheet/water_heating.py: new
`solar_water_heating_monthly_kwh_override` param on
`water_heating_from_cert`; threaded into `output_from_water_
heater_monthly_kwh(solar_monthly_kwh=...)`.
- domain/sap10_calculator/rdsap/cert_to_inputs.py: Table 29
constants + `_solar_hw_monthly_override` helper +
`_orientation_from_summary_string` mapper. Added the demand_pass
intermediate call so (H17)m sees the full (62)m. Negates the
orchestrator output at the boundary (spec convention: heat
displaced from boiler is negative on line (63c)m).
Cert 000565 cascade pin shifts:
- hot_water_kwh_per_yr: +271.84 → −68.96 (4× closer)
- sap_score_continuous: +0.6334 → +0.7732 (drift downstream of HW)
- ecf: −0.0643 → −0.0784 (drift)
- total_fuel_cost: −56.08 → −68.36 (drift)
- co2: −19.77 → −22.66 (drift)
- sap_score (int): 29 EXACT (unchanged)
- space_heating / main_heating_fuel / lighting / pumps_fans:
unchanged
The remaining −69 kWh HW residual is the gap between Table 29
defaults (H12 = 75 L separate tank) and cert 000565's lodged H12 =
53 L + combined cylinder 160 L. Closing this requires extracting
solar storage volume + combined-cylinder routing from the cert (P960
worksheet block lodges these explicitly; Summary doesn't). That's
the follow-on slice.
Test baseline: 547 pass + 9 expected `test_sap_result_pin[000565-*]`
fails preserved. Cohort-2 + ASHP cohort + all golden fixtures
untouched (no certs other than 000565 lodge `solar_water_heating =
True`).
Pyright net-zero on touched files (68 errors at baseline = 68 errors
post-change).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
3bf728ce2f |
Slice S0380.74: Appendix H (H7) U3.3 monthly-integrated convention closes 1.81× over-count
Root cause: SAP 10.2 has an internal unit-convention ambiguity for (H7)m between page 75 (Equation H1 implies W/m² 24-hour-average flux) and page 76 (verbatim "Monthly solar radiation per m² from U3.3 in Appendix U", i.e. kWh/m²/month monthly integrated). Page 77 (H23) formula's `× hours / 1000` term double-converts when (H7) is W/m². The cascade's `surface_solar_flux_w_per_m2` returns the §U3.2 24h-avg flux in W/m² (verified bit-exact vs Elmhurst worksheet line 295: SE 90° Jan region 0 = 36.7938 W/m²). The (H9) helper was using this directly without applying the U3.3 conversion that page 76's "from U3.3" cross-reference calls for. Elmhurst-certified software follows the U3.3 reading. SAP 10.2 spec p.76 line (H7): "Monthly solar radiation per m² from U3.3 in Appendix U". Appendix U §U3.3 (p.130) defines the conversion S_monthly = 0.024 × n_m × S(orient,p,m), where S(orient,p,m) is the §U3.2 24-hour-average flux in W/m². Therefore: (H7)m_U3.3 [kWh/m²/month] = flux_U3.2 [W/m²] × hours / 1000 Option A fix (per ChatGPT-mediated research): apply the U3.3 conversion inside the (H9) helper, so (H9) is in kWh/month rather than W. Spec p.77 (H23) formula then carries the conversion's dimensional residue correctly without double-counting. Diagnostic that closed the trap: back-solving poly(X_cas, Y_eff) = ws_H24/H17 at fixed X across 24 worksheet-positive observations from 4 cert fixtures (000565 + new A/B/C at sap worksheets/Solar PV tests/) revealed Y_eff/Y_cascade took ONLY two distinct values: - 0.7200 (exact) for every 30-day month observation - 0.7440 (exact) for every 31-day month observation i.e. exactly days × 24 / 1000. No utilizability function, no missing constant — a per-month unit-conversion factor that the polynomial non-linearity had been masking. Closure metrics (HEAD post-fix): - 000565 (W-30, modest): annual Δ −0.0000 kWh (every month exact) - A-baseline (S-30, modest): annual Δ +0.0001 kWh - B-highY (S-30, none): annual Δ −0.0000 kWh (incl Oct 10.5905) - C-lowY (N-60, signif): annual Δ −4.36 kWh (polynomial zero-clamp boundary; worksheet poly = 0.0024 → 0.41 kWh, cascade poly = −0.04 → 0) 47/48 month-observations pin at <1e-4 kWh. Test baseline: 547 pass + 9 expected `test_sap_result_pin[000565-*]` cascade-gap fails (unchanged — orchestrator still NOT integrated into water_heating.py:943; that's the follow-on slice that closes cert 000565's HW pin +272 → ~0). Pyright net-zero on both touched files. Files: - domain/sap10_calculator/worksheet/appendix_h_solar.py: rename `monthly_solar_energy_available_h9_w` → `_h9_kwh_per_month`, add `hours_in_month` param, apply U3.3 conversion. Y23 param renamed accordingly. Orchestrator updated. - domain/sap10_calculator/worksheet/tests/test_appendix_h_solar.py: add cert 000565 (H24)m monthly magnitude pin at abs < 1e-3 kWh; update H9 + Y23 unit tests for new kWh/month units. - BRIEF_APPENDIX_H_EN_15316_RESEARCH.md: new "Closure" section with the days-in-month diagnostic, root cause, and lessons. - HANDOVER_POST_4_CERT_EMPIRICAL.md: NEW — closure handover. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
6f024e3937 |
docs: handover + research brief + next-agent prompt for cert 000565 Appendix H
Session-end handover docs for the cert 000565 wacky-stress-test investigation. Three documents covering: - **HANDOVER_POST_S0380_73_APPENDIX_H_BLOCKED.md** — full state of the cohort closure work (S0380.70-.73) plus the Appendix H Solar HW investigation findings. Cumulative ASHP cluster compression −3.10 → −0.06 PE kWh/m² over 4 slices. Cert 000565 HW pin blocked at +272 kWh/yr on a 1.81× formula over-count. - **BRIEF_APPENDIX_H_EN_15316_RESEARCH.md** — self-contained brief for a research agent or human looking up BS EN 15316-4-3 Method 2 to identify the missing clamp / useful-gain rule / validity envelope behind the over-count. Includes the cert 000565 diagnostic (per-month ratio 1.5-1.7× summer, 3-4× shoulder), seven specific questions ranked by hypothesis likelihood, and the 36-data-point empirical-fit setup. - **NEXT_AGENT_PROMPT_POST_S0380_73.md** — directive for the next agent. Awaits 3 user-generated solar-HW cert worksheets (A baseline / B high-Y / C low-Y) to empirically test whether the 1.81× ratio is systematic or cert-specific. Decision point: ship an empirical correction (if 36-point fit closes all 3 certs + cert 000565) or hold for the EN standard. Also resolves the long-standing H3=4.0 / H4=0.01 default mystery: sub-agent located the source in RdSAP 10 Specification §10.11 Table 29 row "Solar panel" page 58. RdSAP overrides the input set; the calculator is still SAP 10.2 Appendix H. So the defaults aren't the source of the over-count. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
c63d674082 |
Slice S0380.73: Appendix M1 §3a D_PV cooking uses L20 electricity, not L18 heat gain (ASHP cohort tail closure)
The Appendix M1 §3a PV-eligible-demand cascade `_pv_eligible_demand_ monthly_kwh` assembled its `cooking_monthly_kwh` argument from `internal_gains_result.cooking_monthly_w × 24 × n_m / 1000`. That field is the SAP 10.2 Appendix L18 cooking HEAT GAIN — not the L20 ELECTRICITY consumption that Appendix M1 §3a requires. Per SAP 10.2 Appendix L (p.91): L18: G_C = 35 + 7 × N (heat gain in watts, used by §5) L20: E_cook = 138 + 28 × N (electricity in kWh/yr, used by M1) L21: E_cook,m = E_cook × n_m / 365 (monthly electricity) The two formulas differ by ~2.2× because not all cooking electricity stays as internal heat — extraction fans, heat absorbed into food, etc. The §5 internal-gains accounting for (98c)m space-heating still wants the L18 gain (left untouched). Only the M1 §3a path needs the L20 electricity figure. Magnitude on cert 0380 (cohort-2 ASHP+5kWh battery, TFA 60.43): - Pre-fix cascade cooking annual (L18 watt-hours): 428.6 kWh - Spec L20 cooking annual: 193.7 kWh (2.21× over-count) - Pre-fix cascade D_PV summer Jul: 311.6 kWh - Worksheet-implied D_PV Jul (β-inverse on (233a/b)m): ~292.6 kWh - Pre-fix cascade (233a) annual: 1925.55 vs worksheet 1899.73 (+25.82) The cohort-2 ASHP STANDARD-tariff cluster (20 certs, all using PV + battery, all winter-peaked HP load + summer PV surplus): - Pre-S0380.71: mean PE residual -3.10 kWh/m² - Post-S0380.71 (main heat monthly Table 12d/12e): -0.66 - Post-S0380.72 (HW monthly Table 12d/12e): -0.36 - Post-S0380.73 (Appendix L20 cooking electricity): -0.06 Cumulative S0380.71-.73: 48× compression of the cluster. Also affects 12 gas-combi PV certs (cohort-1 cert 2130 + 11 cohort-2) which shifted ~+0.5 PE — those carry a separate unrelated bug in the gas-fuel PE cascade where the cooking-fix moved them further from zero. Re-pinned at their new (still-positive) residuals; an investigation slice for the gas-combi PV PE over-count is the natural next thread. Changes: - NEW module-level constants `_COOKING_ELECTRICITY_BASE_KWH_L20 = 138` + `_COOKING_ELECTRICITY_PER_OCCUPANT_KWH_L20 = 28` (Appendix L20). - `cert_to_inputs` cooking_monthly_kwh computation (Appendix M1 §3a D_PV path only): replaced L18 watts × hours/1000 with L20 + L21 `(138 + 28 × N) × n_m / 365` using `wh_result.occupancy` for N. - The §5 internal-gains use of `cooking_monthly_w` (L18 heat gain) is untouched — still feeds (98c)m correctly. Tests: - `test_appendix_m1_d_pv_cooking_constants_pin_to_spec_l20_not_l18_ gains` — pins L20 constants 138 / 28 directly so a future "let's reuse L18 here" refactor fires immediately. - `test_golden_fixtures.py`: 20 ASHP cluster + 12 gas-combi PV cert pins re-pinned at the post-S0380.73 residuals. Baseline: 547 pass + 9 expected `test_sap_result_pin[000565-*]` cascade-gap fails. Pyright net-zero on every touched file. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
b0c4c6e0c4 |
Slice S0380.72: HW PE/CO2 via Table 12d/12e monthly cascade (ASHP cohort follow-on)
S0380.71 closed STANDARD-tariff electric MAIN heating PE/CO2 via the monthly Table 12d/12e cascade. The same spec rule applies to HOT WATER but the cascade still hardcoded the annual flat factors at `cert_to_inputs.py:3697-3699` (CO2) and `:3826-3828` (PE), plus the §12 / §13a section helpers. This slice extends the spec-citation fix to HW. Per SAP 10.2 Table 12d (p.195) and Table 12e (p.196) headers: "Where electricity is the fuel used, the relevant set of factors in the table below should be used to calculate the monthly [CO2 emissions / primary energy] instead the annual average factor given in Table 12." The rule applies to ALL electric end-uses regardless of tariff, including the HW path. For electric HW (`water_heating_fuel=29` API standard electricity → Table 12 code 30) the monthly cascade weighted by `wh_result.output_monthly_kwh` (HW demand monthly proxy) lands at ~0.140 CO2 / ~1.517 PE vs annual flat 0.136 / 1.501 on a HW demand profile. Cohort closure (20 STANDARD-tariff ASHP certs): Mean PE residual: −0.66 → −0.36 kWh/m² (≈+0.30 closure per cert) Worst cert 9796: −1.36 → −1.08 PE / −0.005 → −0.002 CO2 t/yr Cumulative S0380.71 + S0380.72 closure for the ASHP cohort: Mean PE residual: −3.10 → −0.36 kWh/m² (8.6× compression) Changes: - NEW `_hot_water_co2_factor_kg_per_kwh(epc, hw_monthly_kwh)` helper — electric HW fuel → monthly Table 12d cascade; non-electric HW fuel → annual Table 12 factor. - NEW `_hot_water_primary_factor(epc, hw_monthly_kwh)` helper — PE mirror per Table 12e. - `cert_to_inputs` `hot_water_co2_factor_kg_per_kwh` / `hot_water_primary_factor` fields routed through the new helpers (was annual flat `co2_factor_kg_per_kwh` / `primary_energy_factor`). - `environmental_section_from_cert` (§12) + `primary_energy_section_ from_cert` (§13a) section helpers updated to read the cert_to_inputs HW factor fields rather than recomputing annual flat — keeps the worksheet line refs in sync with the cascade. - Imports: add `PRIMARY_ENERGY_FACTOR`, `_DEFAULT_CO2_KG_PER_KWH`, `_DEFAULT_PEF` from `table_12` for the helpers' degenerate paths. Tests: - `test_electric_water_heating_co2_and_pe_factors_apply_monthly_ table_12d_12e` — pins electric HW > annual flat by the winter- weighting margin. - `test_gas_water_heating_co2_and_pe_factors_pass_through_annual_ table_12` — pins mains-gas HW at 0.210 / 1.130 (Table 12 code 1 annual factors). - `test_golden_fixtures.py`: 20 ASHP cluster cert pins updated to the post-S0380.72 residuals; other certs unchanged. Baseline: 546 pass + 9 expected `test_sap_result_pin[000565-*]` cascade-gap fails. Pyright net-zero on every touched file. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
3d6cf5ea17 |
Slice S0380.71: Monthly Table 12d/12e cascade for STANDARD-tariff electric mains (ASHP cohort closure)
S0380.65 added the dual-rate Table 12a Grid 1 + Table 12d blend for the electric main_heating CO2 factor, but the STANDARD-tariff branch fell back to the annual Table 12 flat (code 30 = 0.136). Same gap existed on the PE side (no helper at all — line 3756 hardcoded `primary_energy_factor(main_fuel)` = 1.501 annual flat). For the 20-cert STANDARD-tariff ASHP cohort this hid a systematic +2.7 kWh/m² PE under-count on every cert. Per SAP 10.2 Table 12d header (p.195) and Table 12e header (p.196): "Where electricity is the fuel used, the relevant set of factors in the table below should be used to calculate the monthly [CO2 emissions / primary energy] instead the annual average factor given in Table 12." The spec rule applies regardless of tariff — only the high/low split is tariff-dependent. STANDARD-tariff electric mains still require the monthly cascade, just with single-code (30) factors. Cohort closure (PE residual vs lodged EPC PEUI): 9796: -4.18 → -1.36 4800: -3.83 → -0.59 2536: -3.48 → -1.02 ... 20 cluster certs mean: -3.10 → -0.66 Changes: - `_main_heating_co2_factor_kg_per_kwh` — drop STANDARD-tariff fallback; instead apply `_effective_monthly_co2_factor` with `_STANDARD_ELECTRICITY_FUEL_CODE` (30) for STANDARD-tariff electric mains. Dual-rate path unchanged. - NEW `_main_heating_primary_factor(main, tariff, monthly_kwh)` — PE-side mirror covering both STANDARD (single-code 30 monthly cascade) and dual-rate (Table 12a Grid 1 high/low blend over Table 12e high/low codes) paths. - `cert_to_inputs` `space_heating_primary_factor` field — routed through the new helper (was annual `primary_energy_factor`). Tests: - Updated `test_standard_meter_ashp_main_heating_co2_factor_…` (renamed `…_falls_back_to_annual_table_12` → `…_applies_monthly_table_12d_code_30`) to assert the monthly cascade > annual flat by the winter-weighting margin. - Added `test_standard_meter_ashp_main_heating_primary_factor_…` pinning the PE Table 12e analog. - Added `test_dual_meter_ashp_main_heating_primary_factor_…` pinning the dual-rate Table 12a Grid 1 PE blend. - `test_golden_fixtures.py`: 20 ASHP cluster cert pins updated to the post-S0380.71 residuals (mean PE residual -3.10 → -0.66 kWh/m²). Other certs unchanged. Baseline: 544 pass + 9 expected `test_sap_result_pin[000565-*]` cascade-gap fails. Pyright net-zero on every touched file. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
fc68fb21f6 |
Slice S0380.70: Secondary heating CO2/PE via lodged fuel type (cohort cert 2102 closure)
Cohort-2 cert 2102 (House coal secondary) and cohort-1 cert 0300-2747 (mains-gas secondary) both exposed the same bug: cert_to_inputs hardcoded `_STANDARD_ELECTRICITY_FUEL_CODE` for the secondary CO2 and PE factors, ignoring the cert's lodged `secondary_fuel_type`. The cost-side helper `_secondary_fuel_cost_gbp_per_kwh` already routes through the lodged code; this slice mirrors it on the CO2 and PE side. Per SAP 10.2 Table 12d (p.195) and Table 12e (p.196) header text: "Where electricity is the fuel used, the relevant set of factors in the table below should be used to calculate the monthly [CO2 emissions / primary energy] instead the annual average factor given in Table 12." → electricity end-uses use the monthly Table 12d/12e cascade; non-electric fuels (House coal, mains gas, wood logs, etc.) pass through the annual Table 12 factor. Per Appendix M Table 4a + the API mapper's `_api_secondary_fuel_type` spec-fuel override (S0380.43), cert 2102's lodged API code 33 (electricity off-peak) is rewritten to Table 32 code 11 (House coal) because `secondary_heating_type=631` "Open fire in grate" is physically incompatible with an electric secondary fuel. The new `_secondary_fuel_code` helper preserves Table 12 codes (House coal 11 stays 11) and translates raw gov-API codes via API_FUEL_TO_TABLE_12 (e.g. lodged API 29 → Table 12 30 "standard electricity") so the Table 12d/12e monthly lookups resolve consistently across both mapper output regimes. Cert 2102 DEMAND-path residuals (vs lodged): PE +20.36 → +0.20 kWh/m² (lodged 228 integer-rounded) CO2 -0.79 → +0.005 t/yr (lodged 4.1 integer-rounded) Cert 0300-2747 DEMAND-path residuals (mains-gas secondary, fuel 26): PE +8.28 → +0.93 kWh/m² CO2 -0.25 → +0.25 t/yr Other 23 golden certs all use the electricity default and stay pin- exact via the API→Table 12 translation in `_secondary_fuel_code`. New helpers in cert_to_inputs.py: - `_secondary_fuel_code(epc)` — resolves the cert's secondary fuel code through the dual API/Table-12 fallback that `co2_factor_kg_per_kwh` already uses. - `_secondary_heating_co2_factor_kg_per_kwh(epc, secondary_monthly_kwh)` — Table 12d monthly for electric, Table 12 annual for non-electric. - `_secondary_heating_primary_factor(epc, secondary_monthly_kwh)` — Table 12e monthly for electric, Table 12 annual for non-electric. Four call sites replaced: - `cert_to_inputs` `secondary_heating_co2_factor_kg_per_kwh` field (line ~3552) - `cert_to_inputs` `secondary_heating_primary_factor` field (line ~3625) - `environmental_section_from_cert` secondary CO2 §12 (line ~1863) - `primary_energy_section_from_cert` secondary PE §13a (line ~1967) Tests: - `test_house_coal_secondary_routes_to_annual_table_12_co2_and_pe_factors` pins 0.395 / 1.064 (Table 12 code 11). - `test_secondary_heating_with_lodged_type_but_no_fuel_defaults_to_electricity` pins monthly-weighted electricity factors > annual 0.136 / 1.501 (§A.2.2 default still applies). - `test_golden_fixtures.py`: cert 2102 + 0300-2747 pins updated to the new residuals; 57 other golden certs untouched. Baseline: 542 pass + 9 expected `test_sap_result_pin[000565-*]` cascade-gap fails. Pyright net-zero on every touched file. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
a2a4396e25 |
docs: handover + next-agent prompt for S0380.64..69
New `HANDOVER_POST_S0380_69.md` covers the 6 slices shipped this session — cert 000565 closure to sap_score=29 EXACT + main_heating_ co2_factor=0.1533 EXACT, Appendix H pure math module + orchestrator (magnitude calibration deferred on SAP 10.2 spec ambiguity), and the cohort-2 (38 cert) PE/CO2 golden-coverage addition. Includes residuals table, open work breakdown with reasons (Appendix H spec ambiguity, RR fold-in geometry, MEV PCDB external blocker, House coal secondary cascade), spec-source quick-reference, and key-file map. Predecessor `HANDOVER_CERT_000565_COST_CASCADE.md` (S0380.52..63) gets a "superseded by" note at the top so the chain is navigable. `NEXT_AGENT_PROMPT_POST_S0380_69.md` is a self-contained prompt for a new agent picking this up cold — references the memories to load, ranks 5 well-scoped next-slice options (cert 2102 House coal / Appendix H magnitude calibration / RR fold-in / PE cluster / MEV coupled set), and includes the standard probe commands. Reinforces `feedback_sap_10_2_only_never_10_3` as a critical-load constraint. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
c4b2782931 |
Slice S0380.69: Add 38 cohort-2 certs to test_golden_fixtures.py with PE/CO2 pins
Per [[project-golden-coverage-state]] memory + docs/HANDOVER_GOLDEN_COVERAGE.md:
cohort-2 (38 certs) was chain-tested at 1e-4 SAP via
`test_summary_pdf_mapper_chain.py::test_api_cohort_2_full_chain_sap_
matches_worksheet_at_1e_minus_4` but NOT in golden — PE/CO2 cascade
output for 38 worksheet-backed certs had zero regression guards.
Largest invisible drift: cert 2102-3018-0205-7886-5204 at PE +20.36
kWh/m² / CO2 −0.79 t/yr. Was the +20.4 PE outlier called out in the
handover doc. Now pinned and visible to any future cascade refactor.
Cohort-2 SAP residuals all close to 0 at integer (1e-4 continuous-SAP
convergence rounds to exact match). PE / CO2 baseline residuals
captured at HEAD:
- PE residual range: −4.18 to +20.36 kWh/m². Median ≈ −0.1.
- CO2 residual range: −0.79 to +0.05 t/yr. Median ≈ −0.02.
- 14 certs cluster at PE ≈ −2.7 to −4.2 kWh/m² (gas combi PCDB
+ boiler PE under-count pattern, shared with cohort-1
cert 2130 and ASHP cohort certs).
Pinned per the existing `_GoldenExpectation` shape — SAP at abs=0
(integer), PE at abs=0.01 kWh/m², CO2 at abs=0.001 t/yr. Notes kept
short for each cohort-2 cert because the pinned residual itself is
the signal; per-cert slice history lives in the chain test's
`_COHORT_2_API_CLOSED` list and `sap worksheets/Additional data with
api/<cert>/dr87-*.pdf` worksheets.
Test suite: 317 pass (was 279) + 9 expected 000565 cascade-gap fails
(unchanged). Pyright: 1 error baseline preserved on the
`pytest.approx` import line (per [[feedback-prefer-abs-diff-over-
pytest-approx]] this is the legacy `test_api_to_domain_mapper_
preserves_main_heating_index_number` line that pre-dates the AAA
convention; not touched by this slice).
Next-investigation target: cert 2102 PE +20.36 / CO2 −0.79 closure.
Likely sits in the secondary-heating House coal PE/CO2 cascade
(S0380.43 closed SAP via spec-fuel routing but didn't address the
PE/CO2 paths). Visible as a fired golden test on any cascade refactor
of that surface.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
f0ab7446b2 |
Slice S0380.68: Appendix H (H7)m flux helper + top-level orchestrator
Builds on S0380.66 (Appendix H pure helpers) + S0380.67 (W·h → kWh
unit fix) to assemble the spec-ordered H7 → H9 → … → H24 cascade
into a single entry point. Cert 000565's complete Appendix H input
set now flows through one call:
h24 = solar_water_heating_input_monthly_kwh(
collector_orientation=Orientation.W, collector_pitch_deg=30.0,
region=0, # UK average per rating
aperture_area_m2=3.0, # (H1)
zero_loss_efficiency=0.8, # (H2)
linear_heat_loss_a1=4.0, # (H3)
second_order_heat_loss_a2=0.01, # (H4)
loop_efficiency=0.9, # (H5)
incidence_angle_modifier=0.94, # (H6)
overshading_factor=0.8, # (H8) Table H2 "Modest"
overall_heat_loss_coefficient_from_test=6.5, # (H10) override
dedicated_solar_storage_volume_l=53.0, # (H12)
combined_cylinder_total_volume_l=160.0, # (H13)
hot_water_demand_monthly_kwh=..., # (62)m
wwhrs_monthly_kwh=(0,)*12, # (63a)m
cold_water_temperatures_monthly_c=TABLE_J1_TCOLD_FROM_MAINS_C,
external_temperatures_monthly_c=..., # (96)m
solar_hot_water_only=True,
)
New module surface:
- `monthly_collector_solar_flux_w_per_m2` — thin 12-month wrapper over
the existing `surface_solar_flux_w_per_m2` (Appendix U §U3.2
orientation + tilt polynomial). Cert 000565 collector: W, 30° pitch,
Thames Valley.
- `solar_water_heating_input_monthly_kwh` — chains all line-ref
helpers in spec order; returns (H24)m as a 12-tuple.
Tests:
- `test_monthly_collector_solar_flux_h7_returns_twelve_values_
matching_appendix_u` — smoke test pinning Jan < May < Jun shape
for the W-facing 30°-pitched collector.
- `test_solar_water_heating_input_monthly_kwh_returns_winter_zero_
summer_peak_shape` — orchestrator shape pin: 12-month tuple, all
non-negative, winter clamp to zero (Jan/Feb/Nov/Dec via Equation
H1's negative-X dominance), monotone Mar < May, Sep < Jun.
Magnitude pin against worksheet line 415 (Σ(H24)1..12 = 281.3478)
is DEFERRED to the next slice: current orchestrator output is
~510 kWh annual (1.8× the worksheet), traced to a spec ambiguity
between the top-level Equation H1 Y formula
(Y = Px · Aap · IAM · η0 · ηloop · Im · Hm / (1000 · Dm) — excludes
overshading H8) and the line-ref (H23) formulation
(Y = [(H18) · (H6) · (H5) · (H9) · ((41) · 24)] / [1000 · (H17)],
where (H9) = (H1) · (H2) · (H7) · (H8) includes H8). Both are
present in SAP 10.2 spec page 75-76 and differ by a factor of H8
(0.8 for cert 000565). Picking the spec-correct branch requires
either a worksheet trace of one cert's (H22)/(H23) intermediates or
a confirmed errata; the next slice runs that down and pins the
magnitude.
Test suite: 279 pass + 9 expected 000565 cascade-gap fails (unchanged
— orchestrator is not yet wired into `water_heating_from_cert`).
Pyright net-zero on both touched files.
Spec source: SAP 10.2 specification (14-03-2025) Appendix H pp.74-78
+ Appendix U §U3.2 page 127.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
2795e2569d |
Slice S0380.67: Appendix H (H9) helper + fix (H23) W·h → kWh units
S0380.66 landed (H23) with an incorrect unit treatment: the spec
formula on SAP 10.2 p.76 is
Y_HW = [(H18)m × (H6) × (H5) × (H9)m × (41)m × 24] ÷ [1000 × (H17)m]
and Appendix U (per `domain/sap10_calculator/climate/appendix_u.
horizontal_solar_irradiance_w_per_m2`) returns (H7)m as a monthly-
average flux in W/m². That makes (H9)m = (H1) × (H2) × (H7)m × (H8)
an instantaneous power in W — the `× hours × 24 / 1000` factor in
the (H23) formula is what time-integrates W·h → kWh so the Y_HW
ratio lands dimensionless against (H17)m (kWh/month).
S0380.66's (H23) elided the time integration by absorbing it into
the input parameter (a kWh/m²/month name) — that broke unit
consistency with the downstream Appendix U integration this module
will consume in the next slice.
Changes:
- New `monthly_solar_energy_available_h9_w` — pure (H9)m calculator
taking aperture, η₀, (H7)m flux tuple, and overshading. Returns W.
- `hot_water_factor_y_monthly_h23`: parameter renamed
`monthly_solar_energy_available_h9_w` (was `..._kwh_per_m2`); new
`hours_in_month` parameter; formula now includes the spec's
`× hours / 1000` time integration explicitly.
Tests:
- `test_monthly_solar_energy_available_h9_applies_spec_formula` —
cert 000565 H1/H2/H8 with flat 100 W/m² flux → 192 W (the spec
multiplicand 3 × 0.8 × 100 × 0.8).
- `test_hot_water_factor_y_h23_applies_w_to_kwh_time_integration` —
unit-consistency pin: H9=1000 W, hours=744, H17=744 kWh → Y=1.0.
- `test_hot_water_factor_y_h23_clamps_lower_bound_at_zero` updated
to the new parameter name and supplies `hours_in_month`.
Test suite: 277 pass + 9 expected 000565 cascade-gap fails. Pyright
net-zero on both touched files.
Spec source: SAP 10.2 specification (14-03-2025) Appendix H p.76.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
db4f1b3167 |
Slice S0380.66: SAP 10.2 Appendix H solar HW pure math module (HW path)
New module `domain/sap10_calculator/worksheet/appendix_h_solar.py`
implements line refs (H10), (H11), (H14)..(H16), (H17)..(H24) for the
hot-water solar contribution path. The space-heating path (H25)..(H29)
is deferred until a fixture exercises it — cert 000565 lodges solar
HW only (worksheet line 414 H29=0 across all months).
Algorithm per SAP 10.2 specification §Appendix H pages 74-78
(14-03-2025 revision). The monthly procedure follows EN 15316-4-3:2017:
the collector + cylinder + demand parameters feed dimensionless X and
Y ratios into Equation H1 with Table H3 (p.78) correlation factors:
Q_s,w = (Ca·Y + Cb·X + Cc·Y² + Cd·X² + Ce·Y³ + Cf·X³) × (H17)m
clamped to [0, (H17)m] per spec p.76. Cert 000565 worksheet line 415
shows total Q_s,w = 281.3478 kWh/year delivered to HW from a 3 m²
flat-plate collector + 53 L dedicated solar storage in a 160 L
combined cylinder, W orientation, 30° pitch, Thames Valley region.
Helpers implemented:
- `overall_heat_loss_coefficient_h10` — 5 + 0.5×(H1) or test-cert
- `loop_heat_loss_coefficient_h11` — (H3) + 40·(H4) + (H10)/(H1)
- `effective_solar_volume_h14` — separate / combined cylinder
- `reference_volume_h15` — 75 × (H1)
- `storage_tank_correction_coefficient_h16` — [(H15)/(H14)]^0.25
- `hot_water_demand_monthly_h17_kwh` — (62)m − (63a)m
- `proportion_solar_to_hot_water_monthly_h18` — HW-only/SH-only/blend
- `hot_water_reference_temperature_h20_c` — 55 + 3.86·Tcold − 1.32·Te
- `hot_water_reference_temperature_difference_h21_c` — (H20) − (96)
- `hot_water_factor_x_monthly_h22` — clamp [0, 18]
- `hot_water_factor_y_monthly_h23` — clamp ≥ 0
- `heat_delivered_to_hot_water_monthly_h24_kwh` — Equation H1 + clamp
[0, (H17)m]
18 unit tests cover:
- Spec-default vs test-certificate (H10)
- Cert 000565 worksheet pinned (H11) ≈ 6.5667 (line 407)
- Cert 000565 worksheet pinned (H14) = 85.1 (line 410)
- Cert 000565 worksheet pinned (H15) = 225 (line 411)
- Cert 000565 worksheet pinned (H16) ≈ 1.2752 (line 412)
- Separate-tank vs combined-cylinder branches of (H14)
- All three branches of (H18) (HW-only, SH-only, blend formula)
- (H20)/(H21) spec formulas verbatim
- (H22) zero-demand short-circuit + upper clamp at 18
- (H23) negative-input lower clamp at 0
- (H24) Equation H1 polynomial with Table H3 factors
- (H24) demand-cap clamp when Y dominates positive
- (H24) zero-floor clamp when X dominates negative
Scope EXCLUDES (deferred to follow-on slices):
- Appendix U §U3.3 monthly solar radiation lookup for the collector's
orientation/tilt → (H7)m → (H9)m
- Solar SH path (H25-H29)
- Appendix H §H2 primary-loss reduction Table H4
- EpcPropertyData solar collector field schema additions
- Elmhurst + API extractor / mapper updates
- Cascade integration via `water_heating_from_cert.solar_monthly_kwh`
Pyright: 0 errors on both touched files. 275 pass + 9 expected 000565
cascade-gap fails on the handover test suite (unchanged from S0380.65).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
7855a71560 |
Slice S0380.65: SAP 10.2 Table 12d + Table 12a Grid 1 dual-rate CO2 factor for electric mains on off-peak
Pre-S0380.65 the cascade applied the annual-flat SAP 10.2 Table 12
code-30 CO2 factor (0.136 kg/kWh) to all electric main heating fuel,
ignoring both the Table 12d monthly variation AND the Table 12a
Grid 1 (SH) high-rate fraction. For winter-peaked heat-pump loads on
an off-peak tariff this hid ~600 kg/yr of CO2 — cert 000565 worksheet
line 261:
Main 1 (high-rate cost) 20826.4764 kWh × 0.1581 = 3293.6388 kg
Main 1 (low-rate cost) 13884.3176 kWh × 0.1460 = 2027.2261 kg
───────────── ────────────
blended 34710.7941 × 0.1533 = 5320.87 kg
Cascade impact on cert 000565:
- co2_kg_per_yr 5823.16 → 6427.86 (Δ −624 → Δ −20)
- main_heating_co2_factor 0.1360 → 0.1533 (matches spec line 261)
- sap_score 29 EXACT (unchanged — CO2 doesn't enter
the energy-rating cost factor)
Spec basis:
- SAP 10.2 Table 12a Grid 1 (p.191): SH high-rate fraction by
`Table12aSystem` × tariff. ASHP_OTHER + TEN_HOUR = 0.6 high
(Slice S0380.61 already exercises this on the cost side).
- SAP 10.2 Table 12d (p.194): monthly CO2 factors by electricity
fuel code. 7-hour split = codes 32 (high) / 31 (low); 10-hour
split = codes 34 (high) / 33 (low). 18-hour (38/40) and 24-hour
(35) fall through to code-30 monthly factors in the table —
no dual-rate split applies for those tariffs.
- RdSAP 10 §12 page 62 (Slice S0380.60): meter_type=Dual + HP
without PCDB record → Rule 1 → TEN_HOUR tariff.
Implementation:
- New `_TARIFF_HIGH_LOW_FUEL_CODES_TABLE_12` dict — mirror of the
existing `_TARIFF_HIGH_LOW_RATES_P_PER_KWH` cost-rates dict.
Only SEVEN_HOUR + TEN_HOUR have a Table 12d split entry.
- New `_main_heating_co2_factor_kg_per_kwh(main, tariff, monthly_kwh)`
helper — mirror of `_space_heating_fuel_cost_gbp_per_kwh` on the
CO2 side. Falls back to the annual `_co2_factor_kg_per_kwh` for
non-electric mains, STANDARD tariff, mains without a Table 12a
Grid 1 row yet (storage / direct-acting electric — TODO matches
cost-helper coverage gap), tariffs without a Table 12d split,
and zero-fuel degenerate cases.
- Wire helper into `CalculatorInputs.main_heating_co2_factor_kg_per_kwh`
using the `energy_requirements_result.main_1_fuel_monthly_kwh`
profile already precomputed in `cert_to_inputs`.
Tests:
- `test_dual_meter_ashp_main_heating_co2_factor_applies_table_12a
_grid_1_split` — minimal ASHP code 224 + meter_type=1 cert
asserts the effective factor exceeds the pre-S0380.65 annual
flat (0.136 + 0.005) per spec.
- `test_standard_meter_ashp_main_heating_co2_factor_falls_back_
to_annual_table_12` — meter_type=2 (Standard) pass-through pin
at 0.136. Locks in non-regression for non-dual-meter certs.
Test suite: 480 pass + 9 expected 000565 cascade-gap fails (was
478/9 pre-S0380.65). Pyright net-zero on both touched files
(cert_to_inputs.py 34/34; test_cert_to_inputs.py 11/11).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
6b02bad018 |
Slice S0380.64: Elmhurst per-extension wall_construction mappings + strict-raise
Pre-S0380.64 the mapper silently fell through to wall_construction=None
on three Elmhurst code lodgements that the cohort PDFs use:
- "SG Stone: granite or whinstone" (cert 000565 Ext1)
- "B Basement wall" (cert 000565 Ext3 + Ext4)
- "CF Cavity masonry filled" party wall (cert 000565 Ext1)
Cascade impact on cert 000565 (vs U985-0001-000565.pdf worksheet):
- sap_score 30 → 29 EXACT (was Δ +1)
- sap_score_continuous 30.23 → 29.14 (Δ +1.72 → +0.63)
- space_heating_kwh_per_yr 57909 → 59274 (Δ −1100 → +266)
- HTC 1281 → 1321 W/K (was 234 W/K short
of worksheet line 39 monthly avg 1515.38)
Spec basis:
- SG → 1 (WALL_STONE_GRANITE per domain.sap10_ml.rdsap_uvalues)
is the granite-specific Elmhurst variant of "ST Stone"; same
SAP10 enum, no cascade behaviour change for stone walls.
- B → 6 (BASEMENT_WALL_CONSTRUCTION_CODE per
datatypes/epc/domain/epc_property_data.py:361) routes the
cascade through `part.main_wall_is_basement` →
`u_basement_wall(age_band)` per RdSAP 10 §5.17 / Table 23
(heat_transmission.py:640). Empirically established from a
2026 50k-bulk GOV.UK API sweep (88% co-occurrence with
walls[].description = "Basement wall").
- CF → 4 (Cavity, RdSAP 10 Table 15 row 3 spec U=0.20). The
cascade's `u_party_wall` returns 0.0 / 0.5 / 0.25 for code 4
today, so CF conservatively rounds up to the cavity-unfilled
U=0.5 — matches the pre-existing
`_API_PARTY_WALL_CONSTRUCTION_TO_SAP10[3]` approximation
until `u_party_wall` gains a filled-cavity branch (TODO).
Strict-coverage gate per [[reference-unmapped-api-code]] mirror:
`_elmhurst_wall_construction_int` and
`_elmhurst_party_wall_construction_int` now raise
`UnmappedElmhurstLabel` on a non-empty Elmhurst code that isn't in
the lookup dict, rather than silently returning None. Empty
lodgings (absent fields) continue to return None — the cascade's
own defaults apply. The silent-None failure mode is what hid cert
000565's ~300 W/K cascade fabric-loss gap from the audit chain
until the S0380.64 space-heating residual probe surfaced it.
Cohort coverage swept: every Summary PDF in the test fixtures
folder lodges only {SO, CA, CW, SG, B} wall types and
{'', S, U, CU, CF} party-wall types — the new dict entries cover
all observed codes, so strict-raise does not regress any cohort
fixture (478 pass, 9 expected 000565 cascade-gap fails; was 427
pass + 10 fails per HANDOVER_CERT_000565_COST_CASCADE.md).
Pyright net-zero on touched files (mapper.py 32 → 32 errors;
test_summary_pdf_mapper_chain.py 13 → 13 errors — all pre-existing
in unrelated sections).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
a21195ff25 |
Slice S0380.63: SAP 10.2 Table 4f additive pumps_fans components — Main 2 flue + solar HW
Cert 000565 has two Table 4f line items the existing
`_PUMPS_FANS_KWH_BY_MAIN_CATEGORY` lookup misses:
- (230e) Main 2 gas-combi flue fan = 45 kWh (Main 1 is the HP, so
Main 1's category doesn't carry the gas flue fan — the 2-main
cert has its flue fan on Main 2)
- (230g) Solar HW pump = 80 kWh (= [25 + 5×H1] × 2 per Table 4f
with H1 = 3 m² collector aperture default)
New `_table_4f_additive_components(epc)` sums these on top of the
Main 1 category base. Per SAP 10.2 Table 4f page 174:
- Gas boiler flue fan (fan-assisted): 45 kWh
- Solar thermal system pump (electrically powered):
[25 + 5×H1] × (2000 ÷ 1000) kWh, where H1 is the solar
collector aperture area in m²
H1 currently defaults to 3.0 m² (cert 000565 lodging — flat-panel
3 m² is the most common UK domestic solar HW spec). TODO: extend
the Elmhurst schema + extractor to lodge `solar_collector_aperture_
area_m2` from Summary §16 so the cascade reads the actual value.
Cert 000565 cascade impact:
- pumps_fans_kwh_per_yr: 130 → 255 (Δ −122.52 → +2.48)
The remaining +2.48 surplus is the (230a) MEV component
miscounted in the 130 default base — Main 1 HP should give base
= 0 per Table 4f ("circulation pump in COP"), but mapper-side
HP-category derivation is its own deferred slice. With MEV
properly wired and HP category = 4, the cert closes exactly to
the 252.52 worksheet pin.
- total_fuel_cost_gbp: Δ −167.49 → −150.93 (the +125 kWh delta
bills at the ALL_OTHER_USES blended rate £0.1324 → +£16.55)
- sap_score_continuous: Δ +1.91 → +1.72
Deferred (out of slice scope):
- (230a) MEV / MVHR — needs PCDB MEV lookup table + IUF derivation.
For cert 000565 worksheet shows MEV = 127.5 kWh = IUF × SFP ×
1.22 × V (V = 641.59 m³, PCDF 500755 SFP = 0.1274, IUF ≈ 1.278
derived from worksheet). Next slice.
- HP SAP code (224, 211-227, 521-524) → main_heating_category=4
in mapper — would change pumps_fans Main 1 base from 130 default
to 0 (correct per Table 4f HP row). Currently blocked on MEV
cascade landing — without MEV, dropping base from 130 to 0
worsens the residual.
Cohort regression check: 427 pass + 10 expected 000565 fails. The
14 Elmhurst Summary fixtures + JSON fixtures + cohort ASHP all
either: (a) have no Main 2 lodged → no flue contribution, or
(b) have no solar HW lodged → no pump contribution. The additive
helper returns 0 for those certs.
Spec source: SAP 10.2 §10a Table 4f page 174 (verified verbatim).
RdSAP 10 references Table 4f via §19.1.
Pyright net-zero (34 / 34).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
e19145aca0 |
Slice S0380.62: wire Table 32 standing charges into the off-peak cost fallback
The cascade's `additional_standing_charges_gbp(main_fuel_code, water_heating_fuel_code, tariff)` function (table_32.py:178) was already producing the right values — for cert 000565 it returns £143 (£120 mains gas standing + £23 10-hour high-rate electricity standing per Table 32 page 95). But the value only landed in `FuelCostResult.additional_standing_charges_gbp` inside `_fuel_cost`, which returns `_ZERO_FUEL_COST_FOR_OFF_PEAK` for non-STANDARD tariff. The calculator then falls back to the inline cost math (scalar fuel-cost × kWh) which had no standing-charge component → £143 was silently dropped from the off-peak cost cascade. New `CalculatorInputs.standing_charges_gbp: float = 0.0` field carries the standing-charge total into the fallback path. The inline cost summation adds it before max-clamp + PV credit. STANDARD-tariff certs route via `fuel_cost.additional_standing_ charges_gbp` (set inside `_fuel_cost`) and the calculator ignores this scalar on that path — no double-count. `cert_to_inputs` populates the new field unconditionally; the value is just zero on standard-tariff certs (Table 12 note (a) gates standing-charge inclusion regardless). Cert 000565 cascade impact: - standing_charges_gbp = £143.00 ✓ (exact match to worksheet line 251) - total_fuel_cost_gbp: Δ −310 → −167 (46% reduction) - sap_score_continuous: Δ +3.61 → +1.91 (47% reduction) - co2_kg_per_yr: Δ unchanged (standing charges don't bill CO2) Cohort regression check: 427 pass + 10 expected 000565 fails. The 14 existing Elmhurst fixtures + JSON fixtures all have meter_type= None → STANDARD → standing routes via FuelCostResult unchanged. Spec source: RdSAP 10 Table 32 page 95 standing-charge column; SAP 10.2 Table 12 note (a) inclusion gating. Pyright net-zero on both files (0 / 34). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
b732ceac83 |
Slice S0380.61: wire RdSAP §12 dispatch + Table 12a high-rate fractions into cost scalars
Builds on S0380.60. The three scalar fuel-cost helpers (`_space_heating_fuel_cost_gbp_per_kwh`, `_hot_water_fuel_cost_gbp_ per_kwh`, `_other_fuel_cost_gbp_per_kwh`) now consume a `tariff: Tariff` argument computed once at the call site via `_rdsap_tariff(epc)` — replacing the previous binary all-low / all-high override that biased HP-on-Dual-meter cost by £±1k on cert 000565. Three pieces wired: 1. `_rdsap_tariff(epc)` — applies §12 dispatch consulting BOTH main heating systems (per "the main system or either main system if there are two") + PCDB Table 362 "or database" branch. Replaces `tariff_from_meter_type(meter_type)` at the three cost-helper call sites. 2. `_TARIFF_HIGH_LOW_RATES_P_PER_KWH` — RdSAP 10 Table 32 page 95 (high, low) p/kWh tuples per Tariff enum. Codes 31/32 (E7), 33/34 (E10), 38/40 (E18), 35 (24-hour single rate). 3. `_table_12a_system_for_main(main)` — maps a Table 4a SAP code (211-217, 221-227, 521-524) to the Grid 1 SH row: `ASHP_APP_N` (when PCDB Table 362 record) or `ASHP_OTHER` (default). Other electric carriers (storage 401-409, underfloor 421-422, electric boilers 191-196, CPSU 192) return None until a fixture surfaces them — those mains fall back to the pre-Table-12a `e7_low_rate_p_per_kwh` scalar. Cost helpers now: - `_space_heating_fuel_cost_gbp_per_kwh(main, tariff, prices)`: Off-peak + electric main + `Table12aSystem` recognised → blended rate = high_frac × high_rate + (1-high_frac) × low_rate. STANDARD or unknown Table12aSystem → preserve legacy fallback. - `_other_fuel_cost_gbp_per_kwh(tariff, prices)`: Off-peak → blended via Grid 2 `ALL_OTHER_USES` row (0.90 high on 7-hour, 0.80 high on 10-hour). - `_hot_water_fuel_cost_gbp_per_kwh(water_fuel, main, tariff, prices)`: signature swap (meter_type → tariff) for consistency. Behavioural change deferred (HW Grid 1 WH-row split is its own slice). Cert 000565 cascade impact (HP code 224 + Dual → §12 Rule 3 → TEN_HOUR + Table 12a ASHP_OTHER SH 0.60 high, ALL_OTHER_USES 0.80 high): - space_heating tariff: 0.094 → 0.11808 ✓ matches worksheet - other_fuel tariff: 0.165 → 0.13244 ✓ matches worksheet - hot_water tariff: gas 0.0364 (Table 12 mains gas) — vs worksheet 0.0348 (Table 32 mains gas; price-table divergence is a separate concern outside this slice) - total_fuel_cost_gbp: Δ −1,081 → −310 (71% reduction) - sap_score_continuous: Δ +13.81 → +3.61 Cohort regression check: 427 pass + 10 expected 000565 fails. Test `test_off_peak_meter_routes_electric_costs_to_low_rate` updated to expect the spec-correct Table 12a-blended 0.14311 (was 0.1649 under the pre-S0380.61 "empirical" override). Spec source: SAP 10.2 Table 12a Grid 1 (SH) + Grid 2 (other uses) page 191. RdSAP 10 §12 page 62 dispatch (verified Slice S0380.60). RdSAP 10 Table 32 page 95 prices. Pyright net-zero on both touched files (34 / 11; baseline 34 / 11). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
488492a927 |
Slice S0380.60: RdSAP 10 §12 page 62 — Dual-meter tariff dispatch (Rules 1-4)
Cert 000565 surfaced the spec gap. Worksheet shows "Electricity
Tariff: 10 Hour Off Peak" while the Summary PDF only lodges
"Electricity meter type: Dual" — no separate tariff-hour field is
exported. Elmhurst SAP picks 10-hour because RdSAP 10 §12 page 62
contains a published inference algorithm:
> If the meter is dual 18-hour/24-hour it is 18-hour/24-hour tariff.
> Otherwise the choice between 7-hour and 10-hour is determined as
> follows.
> 1. If the main heating system (or main system if there are two)
> is an electric CPSU (192) it is 10-hour tariff.
> 2. Otherwise, if … electric storage heaters (401 to 409), or
> electric dry core or water storage boiler (193 or 195), or
> electric underfloor heating (421 or 422) — it is 7-hour tariff.
> 3. If that has not resolved it then if … direct-acting electric
> boiler (191), or heat pump (211 to 224, 521 to 524, or
> database), or electric room heaters — it is 10-hour tariff.
> 4. If none of the above applies it is 7-hour tariff.
Cert 000565 Main 1 SAP code 224 (ASHP) + Dual meter → Rule 3 →
10-hour. Matches the worksheet exactly.
New `rdsap_tariff_for_cert(meter_type, main_1_sap_code=...,
main_2_sap_code=..., main_1_is_heat_pump_database=...,
main_2_is_heat_pump_database=...)` implements the dispatch.
"or database" branch covers PCDB Table 362 heat-pump lodgements per
the spec's "or database" wording. Callers compute the boolean via
`heat_pump_record(main_heating_index_number) is not None`.
The pre-existing `tariff_from_meter_type(meter_type)` keeps its
contract for legacy call sites — returns SEVEN_HOUR as the Dual
default (the §12 Rule 4 fallback). Docstring updated to point at the
new helper for callers that need spec-correct dispatch.
Code sets (verbatim §12 page 62):
- `_RULE_1_CPSU_CODES` = {192}
- `_RULE_2_STORAGE_CODES` = {401..409, 193, 195, 421, 422} (NOT 423/424/425)
- `_RULE_3_TEN_HOUR_CODES` = {191, 211..224, 521..524}
- electric room heater codes (Table 4a 6xx) deferred with TODO until a
fixture surfaces them — Rule 4 fallback is correct in the interim
(electric room heater certs would currently get 7-hour, biasing
their cost residual; not on the active fixture front).
This commit is the FOUNDATIONAL change — no cost helpers are wired
to the new dispatch yet, so cohort/golden tests are unchanged
(354 pass + 10 expected 000565 fails). The next slice wires
`_space_heating_fuel_cost_gbp_per_kwh` / `_hot_water_fuel_cost_gbp_
per_kwh` / `_other_fuel_cost_gbp_per_kwh` to use the new dispatch +
Table 12a high-rate fractions for off-peak certs.
Spec source: `domain/sap10_calculator/docs/specs/RdSAP 10
Specification 10-06-2025.pdf` §12 page 62. Verified verbatim per
[[feedback-verify-handover-claims]] before implementing.
Pyright net-zero (0 / 0).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
1ce1a6974b |
docs: flag deferred HP-on-E7 Table 12a + Table 4f pumps_fans cascade gap
Cert 000565 reveals a coupling between two SAP 10.2 cascade gaps
that prevents an isolated fix to either:
1. `_space_heating_fuel_cost_gbp_per_kwh` applies the E7 low-rate
override to any electric main on a Dual meter. Per Table 12a,
heat pumps on E7 use a ~33% high / 67% low split (cert 000565
empirically) — NOT 100% low. The current binary all-low/all-high
biases space-heating cost £-1.1k / £+1.3k respectively.
2. `_PUMPS_FANS_KWH_BY_MAIN_CATEGORY[4] = 0` for HPs (Table 4f says
the circulation pump is in the COP). But certs with MEV / flue
fans / solar HW pumps have those components added on top — cert
000565's worksheet pin = 127.5 MEV + 45 flue + 80 solar = 252.5
kWh, none of which the cascade currently sums.
Probed a fix that derives `main_heating_category=4` from
`sap_main_heating_code in {211-227, 521-527}` (the Table 4a HP
rows) and exempts category=4 from the off-peak override. The
mapper change is architecturally correct but coupling to (1) +
(2) leaves residuals worse at HEAD than at the prior commit — so
both edits are reverted and the spec rationale is folded into
TODO docstrings on the two helpers:
- `_elmhurst_main_heating_category` (mapper) — flags the deferred
HP SAP code route + the two cascade prerequisites
- `_space_heating_fuel_cost_gbp_per_kwh` (cascade) — flags the
Table 12a high/low split as a future cascade slice
Cohort regression check: 192 pass + 10 expected 000565 fails —
identical baseline to S0380.59. Docs-only, pyright net-zero.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
98384999be |
Slice S0380.59: cascade WHC 914 routing extended to _hot_water_fuel_cost_gbp_per_kwh
Final routing site missed in Slice S0380.56 — the `_hot_water_fuel_cost_gbp_per_kwh` argument at the input-builder call site was still passing `epc.sap_heating.water_heating_fuel` and `main` (= Main 1) directly, bypassing the WHC 914 helpers. For cert 000565 (WHC 914 + HP Main 1 + gas combi Main 2 with empty `epc.sap_heating.water_heating_fuel`): - Before: helper received `water_heating_fuel=None, main=Main 1`, fell through to `_fuel_cost_gbp_per_kwh(Main 1, prices)` = electric tariff (£0.165/kWh) — HW kWh × £0.165 over-counted vs the actual gas-combi DHW route. - After: helper receives `water_heating_fuel=26 (mains gas), main=Main 2`. Tariff resolves to Table 32 mains-gas rate £0.0364/kWh. Cert 000565 cascade impact: - hot_water_fuel_cost_gbp_per_kwh: 0.1649 → 0.0364 (correct gas tariff) - total_fuel_cost_gbp: 4,116.21 → 3,598.75 (HW component dropped by 4026 × (0.165 - 0.036) ≈ £518; the cascade was over-billing HW at electric rates). - Δ vs expected: −564 → −1,081 (cost is now further from the worksheet because the surplus HW electric-charge was masking Main 1's HP-on-E7 tariff bug — the cascade applies the `e7_low_rate` rate to HP electricity, which is wrong; HPs run on demand, not overnight. Next slice will exempt category=4 heat pumps from the off-peak override.) Single-main certs: behavioural identical — `_water_heating_fuel_ code(epc)` falls back to the explicit `epc.sap_heating.water_ heating_fuel`, and `_water_heating_main(epc)` returns Main 1. Cohort regression check: 249 pass + 10 expected 000565 fails — no regression. Pyright net-zero (34 / 34). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
3e05881042 |
Slice S0380.58: Elmhurst per-extension Room(s) in Roof extraction + TFA fix
Cert 000565 surfaced a per-extension Room(s) in Roof coverage gap.
§4 Dimensions lodges an RR floor area for every BP (Main + each
extension) and §8.1 lodges full construction details per BP. The
old extractor parsed RR from §4 + §8.1 for Main only — the 4
extensions' RR areas (34 + 5 + 32 + 2 = 73 m²) were silently
dropped, leaving TFA at 246.91 m² vs the worksheet's 319.91 m²
(23% deficit).
Schema:
- `ExtensionPart.room_in_roof: Optional[RoomInRoof] = None` field.
None for single-storey extensions (no RR lodged); populated for
every extension that lodges a §4 RR floor area > 0.
Extractor:
- `_room_in_roof_from_bodies(dim_body, rir_body, age_band)`
parameterises the previously Main-only `_extract_room_in_roof`
so the same parsing applies to each extension.
- `_extract_extensions` now slices §8.1 by BP (alongside the
existing §4/§7/§8/§9 slicing) and reads each extension's RR age
band from §3's "<N>th Ext. Room(s) in Roof <band>" line via a
new regex.
- A new defensive "§4 lodges RR area but §8.1 has no construction
details" branch returns a partial `RoomInRoof` with empty surfaces
so the cascade still attributes the floor area to TFA. (Not
triggered on 000565 — all 5 BPs lodge construction details — but
needed for older Elmhurst variants per the existing extractor
comment style.)
Mapper:
- `_map_elmhurst_building_parts` now passes each extension's
`room_in_roof` through `_map_elmhurst_room_in_roof` to the
extension's `SapBuildingPart.sap_room_in_roof`. Previously the
loop hardcoded the field as None.
- `total_floor_area_m2` derivation now also sums each extension's
`room_in_roof.floor_area_m2`. Without this, the per-BP RR floor
area is lodged on the BP but the cert's top-level TFA stays at
the pre-fix value.
Cert 000565 cascade impact:
- TFA: 246.91 → 319.91 ✓ (matches U985-0001-000565.pdf Block 1)
- space_heating_kwh_per_yr: Δ −9,107.71 → −1,099.50 (88% reduction)
- main_heating_fuel_kwh_per_yr: Δ −5,357.47 → −646.76 (88% reduction;
space_heating × 1/HP COP — main_heating tracks space_heating)
- lighting_kwh_per_yr: Δ −236.19 → +2.18 (essentially closed —
RdSAP §12-1 lighting is TFA-proportional)
- hot_water_kwh_per_yr: Δ +214.50 → +271.84
- co2_kg_per_yr: Δ −1,438.16 → −751.06
- total_fuel_cost_gbp: Δ −1,055.62 → −564.05
- sap_score_continuous: Δ +1.70 → +6.75 (cost/TFA dropped because
cost rose ~14% but TFA rose ~30% — the remaining −564 cost gap
has to close before SAP catches up)
Single-storey-extension certs: `room_in_roof=None` for each extension
(no §4 RR lodgement), no behavioural change. Cohort regression check:
415 pass + 10 expected 000565 fails — no regression on the 14 Summary
fixtures + JSON fixtures that don't carry per-extension RR.
Pyright net-zero on all 3 touched files (32 / 0 / 0).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
3b61ca8cec |
Slice S0380.57: Elmhurst mapper infers electricity fuel for electric SAP main heating codes
Elmhurst §14.0 leaves "Fuel Type" empty for electric main heating
systems (heat pumps, electric boilers, storage heaters, electric
underfloor, warm-air HPs) — the SAP code identifies the carrier
directly. The mapper was reading the empty string via
`_elmhurst_main_fuel_int(mh.fuel_type)` → None, and downstream
`_main_fuel_code` returned None, so Table 32 unit-price lookups
defaulted to mains gas. Cert 000565 (HP Main 1, SAP code 224) was
being charged 29,353 kWh/yr of electricity at the gas tariff —
£0.0364/kWh instead of £0.165/kWh.
New `_ELECTRIC_SAP_MAIN_HEATING_CODES` frozen set covers the Table
4a electric carrier rows:
191-196 Electric boilers
211-217, 221-227 Heat pumps (224 = ASHP 2013+, 1.70 COP)
401-409 Electric storage heaters
421-425 Electric underfloor heating
521-527 Warm-air heat pumps
Inference fires in both Main 1 (`_map_elmhurst_sap_heating`) and
Main 2 (`_map_elmhurst_main_heating_2`) construction paths — when
`_elmhurst_main_fuel_int(fuel_type)` returns None AND the SAP code
is in the electric set, fall back to `_STANDARD_ELECTRICITY_FUEL_
CODE = 30` (Table 12 row "Electricity, standard tariff").
Cert 000565 cascade impact (compounding with S0380.56):
- sap_score: 71 → 30 (target 29 → Δ +1.7; was Δ +44)
- sap_score_continuous: 71.42 → 30.21 (target 28.51 → Δ +1.70; was Δ +42.91)
- ecf: 2.05 → 5.22 (target 5.39 → Δ −0.17; was Δ −3.34)
- total_fuel_cost_gbp: 1,423.80 → 3,624.64 (target 4,680.26 → Δ −1,055.62; was Δ −3,256.46)
- co2_kg_per_yr: 7,181.62 → 5,009.47 (target 6,447.63 → Δ −1,438.16; was Δ +733.99)
(now undershooting — independent cascade gap
around Table 12d monthly electric CO2 factor
interpolation; separate slice)
Single-main non-HP certs: no behavioural change (`fuel_type` lodged
explicitly for gas/oil boilers → `_elmhurst_main_fuel_int` returns
non-None → inference branch not entered). Cohort regression check:
472 pass + 10 expected 000565 fails — no regression.
Spec source: SAP 10.2 Table 4a main heating SAP codes + Table 12 fuel
codes (electricity, standard tariff = 30). Heat-pump cohort efficiency
values cross-referenced in `domain/sap10_ml/sap_efficiencies.py:42-44`.
Pyright net-zero on mapper.py (32 / 32).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
e0bca4c3cd |
Slice S0380.56: cascade WHC 914 routing extended to fuel cost / CO2 / PE
Slice S0380.55 routed water-heating EFFICIENCY to Main 2 for WHC 914. This slice extends the routing to water-heating FUEL — the cascade's CO2 factor, PE factor, and Table 32 fuel-cost lookups were still pinned to Main 1's fuel code via the legacy `epc.sap_heating.water_heating_fuel or main_fuel` pattern. For cert 000565 (WHC 914 + HP Main 1 + gas combi Main 2): - `epc.sap_heating.water_heating_fuel` is None (Elmhurst §15 doesn't lodge a separate water-heating fuel type) - `_main_fuel_code(Main 1)` is None (HP, no fuel_type lodged in §14.0 — Elmhurst convention for heat pump certs) - Old pattern: water_fuel = None → `co2_factor_kg_per_kwh(None) = 0` → HW CO2 silently 0 (off by ~833 kg/yr vs gas combi truth) New helper `_water_heating_fuel_code(epc)` mirrors `_water_heating_ main(epc)`: prefers the explicitly-lodged `water_heating_fuel`, otherwise falls back to `_main_fuel_code` of whichever main system services DHW per WHC. Wired into 5 cascade sites (CO2 / PE / cost × hot-water + per-end-use CO2 + per-end-use PE factors). Cert 000565 cascade impact: - hot_water_co2 (kg/yr): factor=0 → 0.21 (gas) — now correctly attributes ~833 kg HW CO2 to gas combustion - hot_water_primary_factor: 0 → gas Table 12e value - hot_water_high_rate_gbp_per_kwh: previously fell through to Main 1 fuel-code which is also None → gas tariff sentinel; now derives explicitly from Main 2's mains-gas fuel (Table 32 code 26) - co2_kg_per_yr pin: +287 → +734 (got "worse" because HW gas CO2 is now correctly counted; remaining surplus is from an INDEPENDENT Main 1 fuel-inference bug — `_main_fuel_code` returns None for HP Main 1 because Elmhurst §14.0 leaves `Fuel Type` empty for heat pumps, so the cost/CO2 cascade defaults Main 1 to the gas tariff) The Main-1 HP fuel-inference bug is the next slice. For single-main non-HP certs the helper resolves identically to the prior pattern (water_heating_fuel explicit, or Main 1 fuel) — no behavioural change for the existing fixture corpus. Pyright net-zero (34 / 34). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
1eff5cf4a1 |
Slice S0380.55: cascade WHC 914 → Main 2 water-heating efficiency routing
Closes the second half of the cert 000565 Main 2 work. After Slice
S0380.54 lodged Main 2 on the EpcPropertyData, the water-heating
cascade still derived efficiency from Main 1 (the heat pump) instead
of Main 2 (the gas combi that actually services DHW).
Per the Elmhurst RdSAP convention, `Water Heating SapCode 914` =
"from second main system" — DHW is generated by Main 2, not Main 1.
The §4 / Appendix D2.1 summer-efficiency lookup must therefore key
off Main 2's PCDB Table 105 record (cert 000565: PCDB 15100 Vaillant
Ecotec plus 415, summer η = 88%) rather than Main 1's HP COP.
Implementation:
- New `_water_heating_main(epc)` helper — returns Main 2 when WHC
is in `_WATER_FROM_SECOND_MAIN_CODES = {914}` AND a second main is
lodged; otherwise returns Main 1 (matches prior behaviour for
single-main certs + WHC 901/902 "from main system")
- The water-eff branch at the §4 cascade now reads `water_pcdb_main
= gas_oil_boiler_record(water_main.main_heating_index_number)`
+ `_water_efficiency_with_category_inherit(water_main.sap_main_
heating_code, water_main.main_heating_category, _main_fuel_code(
water_main))` — same logic as before but parametrised by the
water-heating main rather than hard-coded to Main 1
Cert 000565 cascade impact on hot_water_kwh_per_yr pin:
- Before: actual 1,844.66 kWh/yr (= HW heat / HP COP 1.70 — wrong)
Δ −1,910.36 vs U985-0001-000565.pdf expected 3,755.03
After Slice S0380.54 (Main 2 lodged but cascade still using Main 1):
actual 3,919.91 kWh/yr, Δ +164.88 (regression from the no-cascade
baseline because Main 2 PCDB was lodged but water_eff still came
from Main 1's HP-vs-default fallthrough)
- After this slice: actual 3,969.53 kWh/yr (= HW heat / 0.88)
Δ +214.50 — 89% reduction vs the original Main-1 WHC 914 routing,
remaining gap is fine-grained (FGHRS / solar HW / Table 3a no-keep-
hot territory — separate slice)
For single-main certs (the 14 existing Summary fixtures + 8 ASHP
cohort certs): `_water_heating_main` returns Main 1, identical to
the prior `main` reference. Cohort regression check: 472 pass + 10
expected 000565 fails — no broader regression.
Spec source: SAP 10.2 §4 water-heating cascade + Appendix D2.1 (D1
equation) summer-efficiency override; Elmhurst RdSAP water-heating
code 914 ("from second main system").
Pyright net-zero on cert_to_inputs.py (34 errors before, 34 after).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|