mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
5348 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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>
|
||
|
|
353303168d |
Slice S0380.54: Elmhurst §14.1 Main Heating2 extraction + 2nd MainHeatingDetail
Cert 000565 lodges §14.1 Main Heating2 as PCDB 15100 (Vaillant Ecotec
plus 415, 88%, mains gas, 0% space heat) — this is the system that
services DHW via `Water Heating SapCode 914` ("from second main
system"). The previous extractor / mapper shape supported only ONE
main heating system, dropping Main 2 entirely.
New shape:
- `MainHeating2` dataclass (slim §14.1-shaped: PCDB ref, fuel type,
flue type, fan_assisted_flue, percentage_of_heat, SAP code)
- `MainHeating.main_heating_2: Optional[MainHeating2]` — None when
§14.1 is absent OR lodges only placeholder zeros (the PCDB-only
convention; the two JSON fixtures + 14 existing Summary fixtures
all lodge "0 / 0" for an absent Main 2)
- `_extract_main_heating_2` parses §14.1; returns None when neither
PCDB ref nor SAP code identifies Main 2
- `_map_elmhurst_main_heating_2` builds `MainHeatingDetail` from the
Main 2 lodgement with `main_heating_number=2` and `main_heating_
fraction=percentage_of_heat`; strict-raises `UnmappedElmhurstLabel`
(mirroring Slice S0380.53's Main 1 raise) when Main 2 has neither
identifier — surfaces coverage gaps at extraction time
Per RdSAP convention "0%" is lodged without a space (vs Main 1's
"100 %" with a space) — robust percentage parse via `rstrip("%")` so
both forms thread through.
Cohort impact:
- 14 existing Summary PDF fixtures + 2 JSON fixtures: Main 2 returns
None (placeholder zeros) → no 2nd MainHeatingDetail produced → no
cascade behaviour change (regression-tested: 415 pass + 10 expected
000565 fails, identical to S0380.53 baseline)
- Cert 000565: 2nd MainHeatingDetail now lodged with sap_code=None,
pcdb=15100 (Table 105 gas-boiler 88% efficiency), category=2,
fuel=26 (mains gas), fraction=0
Cascade still uses Main 1 for water-heating efficiency in the WHC
914 branch — that routing fix is the next slice. This commit is
the plumbing-only half; the SAP-result pin residuals are unchanged
at HEAD because the cascade hasn't been wired to read Main 2 yet.
Pyright net-zero on all 3 touched files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
bb9097e1a5 |
Slice S0380.53: Elmhurst §14.0 "Main Heating SAP Code" extraction + strict-raise
Cert 000565 surfaced an Elmhurst extractor schema gap. §14.0 lodges
"Main Heating SAP Code 224" identifying Main 1 as an Air Source Heat
Pump (SAP 10.2 Table 4a row 224: "Air source heat pump, 2013 or
later") — but the extractor was dropping the line. The mapper
therefore produced a `MainHeatingDetail` with `sap_main_heating_code
= None` AND `main_heating_index_number = None` (because `PCDF boiler
Reference = 0` for HP certs), leaving the cascade to fall back to
the 0.80 gas-boiler default efficiency.
Cascade impact on cert 000565 main_heating_fuel_kwh_per_yr pin:
- Before: actual 62,375.80 kWh/yr (= 59,008 / 0.80 wrong default)
Δ +27,665.01 vs U985-0001-000565.pdf expected 34,710.79
- After: actual 29,353.32 kWh/yr (= 59,008 / 1.70 HP COP via §A4.1)
Δ −5,357.47 (remaining gap is on the space_heating side, not
heating efficiency)
The strict-raise mirrors [[unmapped-api-code]] (Slice S0380.51) and
[[unmapped-elmhurst-label]] (cylinder size / glazing type) — when
neither the §14.0 SAP code nor the PCDB boiler reference identifies
Main 1, the mapper raises `UnmappedElmhurstLabel("main_heating",
...)` so the coverage gap surfaces at extraction time instead of as
an opaque downstream SAP delta. Per user end-of-S0380.52 directive:
"if we're missing mapping on EpcPropertyDataMapper - let's raise an
exception".
Spec source: SAP 10.2 §A4 Appendix A "Heat pump cascade", Table 4a
row 224 (Air source heat pump, 2013 or later) — `seasonal_efficiency`
reads the SAP code when no PCDB Table 105/362 record overrides.
Touched:
- datatypes/epc/surveys/elmhurst_site_notes.py: `MainHeating.
main_heating_sap_code: Optional[int]` field added (treat 0 as None
per Elmhurst convention — PCDB-listed boilers lodge §14.0 SAP code
as 0 and identify themselves via the PCDB index instead)
- backend/documents_parser/elmhurst_extractor.py:
`_extract_main_heating` reads §14.0 "Main Heating SAP Code" via the
existing `_local_val` slice helper; 0/absent → None
- datatypes/epc/domain/mapper.py: `_map_elmhurst_sap_heating` passes
`sap_main_heating_code=mh.main_heating_sap_code` to
`MainHeatingDetail`, and raises `UnmappedElmhurstLabel` when
neither identifier resolves
Cohort regression check: 415 pass + 10 expected 000565 failures
(unchanged from S0380.52 — same pins, different residuals). Pyright
net-zero on all 3 touched files.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
e51fcb74ca |
Slice S0380.52: cert 000565 Elmhurst-only mapper-driven cascade pin + glazing-label coverage
User pivot at end of prior session: don't hand-build EpcPropertyData
fixtures — route Summary PDFs through `EpcPropertyDataMapper.from_
elmhurst_site_notes` so the pin grid exercises extractor + mapper +
calculator, and each new Elmhurst doc grows mapper coverage instead
of bespoke fixture code.
New fixture cert 000565 is a stress-test cert (5 building parts, age
mix A→J, conservatory with heaters, curtain wall, basement walls,
mixed party-wall constructions) that surfaces many uncommon cascade
paths absent from the cohort-2 + ASHP corpus.
Mapper coverage extended for 3 Elmhurst §11 glazing labels surfaced
on this cert (per RdSAP-Schema-21.0.1, `datatypes/epc/domain/
epc_codes.csv` glazed_type rows):
"Triple between 2002 and 2021": 9 (RdSAP-21 schema row 9 — triple
glazing, installed 2002-2022 in EAW; `_G_PERPENDICULAR_BY_
GLAZING_TYPE[9] = 0.68`, `_G_LIGHT_BY_GLAZING_CODE[9] = 0.70`)
"Single glazing": 1 (alias of bare "Single"; cascade
g_L = 0.90, g⊥ = 0.85 per SAP 10.2 Table 6b)
"Double glazing, known data": 3 (Elmhurst lodgement of RdSAP-21
schema row 7 "double, known data"; manufacturer U-value and
g-value lodged via WindowTransmissionDetails override the
cascade's defaults — grouped under code 3 with other unknown-
date DG variants for cascade-equivalence on g_L/g⊥)
Per [[feedback-e2e-validation-philosophy]] + [[feedback-zero-error-
strict]]: pin tolerances are abs=1e-4 against U985-0001-000565.pdf
Block 1 line refs (pinned: SAP int + SAP continuous + ECF + total
fuel cost + CO2 + space heating + main 1 fuel + secondary fuel +
hot water + lighting + pumps/fans).
Outcome: 1/11 pin green (`secondary_heating_fuel_kwh_per_yr = 0`);
10 pins are now named calculator-gap residuals to fix in subsequent
slices:
main_heating_fuel_kwh_per_yr +27,665.01 kWh/yr (heat-pump SAP code
224 + gas combi via WHC 914 "from second main"; cascade probably
runs ASHP for DHW instead of routing through gas combi)
hot_water_kwh_per_yr +164.88 kWh/yr (FGHRS / solar HW /
Table 3a no-keep-hot for the gas combi DHW path)
lighting_kwh_per_yr -236.19 kWh/yr (RdSAP §12-1 bulb-
count cascade; 27 total / 7 low-energy / 20 incandescent lodged)
pumps_fans_kwh_per_yr -122.52 kWh/yr (cascade defaults
to 130; expected 252.52 = MEV PCDF 500755 + flue + solar pump)
Cohort regression check: 472 pass + 10 expected 000565 failures.
Pyright net-zero (32 errors before, 32 after).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
fc84c6d49a |
docs: extend handover with Elmhurst-only path + 000565 extended test case
User clarified end-of-session: mapper is a thin enum-and-shape translation; when residuals remain after closing mapper coverage gaps, the gap is in the **calculator cascade**. This unlocks an Elmhurst-only fixture path that doesn't need API JSON at all. The fixture shape mirrors the 6 historical Elmhurst U985 fixtures (000474, 000477, 000480, 000487, 000490, 000516) at `domain/sap10_calculator/worksheet/tests/_elmhurst_worksheet_NNNNNN.py` + `test_e2e_elmhurst_sap_score.py`: build_epc() → cert_to_inputs → calculate_sap_from_inputs ↳ every SapResult field pinned at abs=1e-4 against U985 line refs Any failing pin is definitionally a calculator bug. The user generates certs in Elmhurst SAP and exports Summary + worksheet ZIPs — no gov.uk EPB lodgement required. Extended test case (000565) ready at `sap worksheets/extended test case/`: - Summary_000565.pdf (input) - U985-0001-000565.pdf (worksheet ground-truth) Cert 000565 is a wacky stress-test that exercises 3-4 zero-coverage cascade paths in one cert: Main + 4 extensions, age mix A through J, RR on every part with mixed ages, conservatory with fixed heaters, curtain-wall Ext2 post-2023, mixed wall types (solid brick + stone + curtain wall), mixed party walls (CU + CF + Unable to determine). After this cert lands, the user has agreed to generate single-feature certs (oil only, LPG only, solid fuel only, electric direct only, multi-main-heating, basement) to surface single-cause calculator gaps. Handover doc now has implementation outline (mirror _elmhurst_worksheet_000474.py shape) and a coverage-paths table showing which targets each fuel-type/config exercises. |
||
|
|
88bf1ef472 |
docs: handover after S0380.47..S0380.51 — golden coverage state
Captures the per-cert validation state at HEAD
|
||
|
|
b7fbbcca96 |
Slice S0380.51: strict-raise UnmappedApiCode on API integer enums
Mirrors the Elmhurst `UnmappedElmhurstLabel` coverage gate on the GOV.UK API path. The same failure mode (silently routing an unknown enum to a default / None hides cascade gaps until a downstream SAP- delta investigation surfaces them) was hitting the API mapper: existing helpers like `_api_floor_construction_str` returned None on unrecognised codes per the comment "Only the values observed across the 10 golden fixtures (1, 2) are mapped; unrecognised codes fall through to None." Adds `UnmappedApiCode(ValueError)` at the API mapper boundary and threads it through five strict helpers: - `_api_party_wall_construction_int` (RdSAP10 Table 15) - `_api_floor_construction_str` (Slice 88 floor signal) - `_api_floor_type_str` (RdSAP10 §5 rule (12)) - `_api_roof_construction_str` (Slice 89 cos(30°) factor) - `_api_sheltered_sides` (SAP10.2 §S5) Each helper distinguishes: - "lodging absent" → return None (unchanged behaviour) - "lodging present and mapped" → translate (unchanged behaviour) - "lodging present but unrecognised" → raise UnmappedApiCode (NEW) Two coverage gaps surfaced immediately at strict-run, both fixed in the same slice with the worksheet-backed lodged-floor descriptions: 1. `floor_heat_loss=2` — cert 7536 Main lodges this (floors[] description "To unheated space, insulated"); also lodged on cert 2031 / etc. Added mapping → "To unheated space". 2. `floor_heat_loss=3` — cert 7536 Ext2 lodges this with the same floors[] description as Main code 2 — same cascade signal. 3. `floor_heat_loss=6` — cert 9501 + cert 9390 (top-floor flats) lodge this with floors[] description "(another dwelling below)". The cascade routes party-floor handling via property_type=Flat + cert.floors[] description independently of this string, so the explicit None entry preserves the cascade match (cert 9501 stays at exact 1e-4 SAP vs worksheet 68.5252) while distinguishing "decided no string" from "unknown". Six new tests document the contract: - Five unit tests inject an out-of-range integer (99) into a real cohort cert JSON and assert UnmappedApiCode raises with the right `field` and `value`. - One coverage forcing function (`test_all_golden_fixtures_extract _via_api_without_unmapped_code_raise`) loops every JSON under `fixtures/golden/` through `from_api_response` and asserts no raise — future fixtures with unmapped enums fail this test until a dict entry is added. 763 → 769 pass + 0 fail (5 unit + 1 cohort-coverage test added). Pyright net-zero (32 → 32 baseline preserved). The pattern is ready to extend to other silently-falling-through helpers — e.g., `_api_glazing_transmission` (codes 4-12, 15+ noted in the existing comment as "not yet mapped — incremental coverage as new fixtures surface them"), `_api_cascade_glazing_type` (pass- through is intentional, so probably leave alone). Each addition is its own slice. |
||
|
|
3d1e6f103a |
Slice S0380.50: §4 seasonal monthly HW fuel for PV β cascade
The PV β-factor cascade was prorating the annual hot-water fuel kWh uniformly by days when feeding D_PV,m per Appendix M1 footnote 32. The worksheet uses §4 (219)m = (62)m / efficiency monthly — which is seasonal (peaks in Jan when cold-mains-inlet drives energy content, troughs in Jul/Aug). For cert 0380: worksheet Jul (219) = 68.30 kWh vs cascade days-prorated 74.60 kWh — over-counted summer D_PV by ~6 kWh/month. Per Appendix M1 footnote 32: "D_PV,m = ... + E_water,m" where "E_water,m = (219)_m if water heating fuel code applied in Section 10a of the SAP worksheet is 30". (219)_m is the §4 fuel kWh per month, not annual / 12. Fix: scale `wh_result.output_monthly_kwh` to sum to the annual fuel `hw_kwh` (equivalent to dividing each month by the annual-average efficiency — exact for single-COP HP water heaters; close enough for PCDB combi winter/summer-split efficiencies because the annual total already accounts for the seasonal-efficiency mix). None fall- back to the legacy days-proration when wh_result is absent (TFA-missing certs). Cohort PE residual closure (kWh/m²): | Cert | Post-S0380.49 | Post-S0380.50 | |---|---:|---:| | 0350 | -2.96 | **-2.90** | | 0380 | -3.06 | **-2.96** | | 2225 | -3.73 | **-3.54** | | 2636 | -3.44 | **-3.28** | | 3800 | -3.25 | **-3.16** | | 9285 | -2.81 | **-2.74** | | 9418 | -3.01 | **-2.89** | Modest but real cohort closure (~0.1 kWh/m² each). The remaining ~3 kWh/m² traces to a small cascade β over-count (0.751 vs worksheet 0.739) — likely Appendix L monthly-weighting details for appliances/ cooking/electric-shower in D_PV; deferred to a follow-up slice. Cert 9501 (PV no battery) unchanged at +0.65 PE. CO2 cohort: <0.11 t/yr (within tolerance, re-pinned in same slice). SAP scores all exact. 763 pass + 0 fail. Pyright net-zero. |
||
|
|
33edff136a |
docs: PV β-split phase COMPLETE handover (6/6 slices)
Finalises the handover doc after S0380.49 ships the effective-monthly Table 12e PE factor for the PV split. Full cohort residual trajectory table across all four milestones (pre-44 / post-45 / post-48 / post-49), final cross-cascade architecture diagram, and the punch-list of open work (β fine-tuning, HP electricity demand, monthly E_PV distribution) — none in the β-split phase scope, each a candidate follow-up slice. Cluster PE residual closed by ~50% magnitude over the phase: -7..-14 → -2.8..-3.7 kWh/m². CO2 all <0.11 t/yr; SAP all exact. |
||
|
|
e75198ce5d |
Slice S0380.49: effective-monthly Table 12e PE factor for PV split per SAP 10.2 Appendix M1 §8
The PE cascade was crediting the PV split at annual Table 12 factors (IMPORT 1.501 / EXPORT 0.501) instead of the spec-correct effective monthly Table 12e factors. Per Appendix M1 §8 (p.94): "For calculation of primary energy, for electricity used within the dwelling apply the normal import PE factors for the relevant tariff from Table 12e. For the electricity exported, apply the factors for 'electricity sold to grid, PV', also from table 12e." Cert 0380 worksheet (page 5) lodges 1.4960 / 0.4268 — the effective monthly values weighted by E_PV,dw,m / E_PV,ex,m. The cascade now computes the same via `_effective_monthly_pe_factor` (the helper already in place for secondary heating, pumps+fans, lighting, electric showers). Two new Optional fields on `CalculatorInputs`: - `pv_dwelling_primary_factor` — falls back to `other_primary_factor` - `pv_exported_primary_factor` — falls back to `pv_export_primary_factor` Both populated in `cert_to_inputs.py` via `_effective_monthly_pe_ factor(pv_split.epv_*_monthly_kwh, fuel_code)` — code 30 (standard electricity) for dwelling, code 60 (electricity sold to grid, PV) for exported. Mirrors the existing CO2 cascade shape exactly. Cohort PE residual closure (kWh/m²): | Cert | Post-S0380.48 | Post-S0380.49 | |---|---:|---:| | 0350 | -3.58 | **-2.96** | | 0380 | -4.01 | **-3.06** | | 2225 | -4.50 | **-3.73** | | 2636 | -4.14 | **-3.44** | | 3800 | -4.01 | **-3.25** | | 9285 | -3.46 | **-2.81** | | 9418 | -3.76 | **-3.01** | | 2130 (PV gas) | -9.70 | **-8.22** | 7-cert ASHP+battery cluster closed by 0.6-0.8 kWh/m² each (matches the +0.074 differential between annual 0.501 and worksheet 0.4268 applied to E_PV,ex ≈ 640 kWh/yr / TFA 60.43 = 0.78 kWh/m²). The remaining -3 kWh/m² residual is β fine-tuning (cascade 0.751 vs worksheet 0.7426 — small monthly D_PV distribution detail). Cert 9501 (PV no battery) drifted +0.25 → +0.65 PE — known shape change from the factor correction; β=0.498 matches worksheet exactly so the drift uncovers a different small gap previously masked by the wrong factors. Still well within tolerance. CO2 + SAP unchanged. Pyright net-zero on touched files (34 errors before, 34 after — all pre-existing). |
||
|
|
6788c99087 |
docs: refresh HANDOVER_PV_BETA_SPLIT after S0380.44..S0380.48 (5/6 shipped)
Updates the PV β-split handover doc after the three new slices land:
- S0380.47 cost cascade wiring (zero cohort impact via Table 32 collapse)
- S0380.48 real-API battery_capacity schema gap (cohort PE +2.7..+8.1
→ -3.5..-4.5)
- Restates the open slice (S0380.49) as wiring effective-monthly
Table 12e PE factor into the PV cascade — the remaining ~4 kWh/m²
PE delta is structural (currently uses annual factors instead of
monthly-weighted).
Key narrative correction: the prior handover's "E_PV magnitude bug"
hypothesis ("cascade thinks 2570 kWh/yr vs worksheet 831") was wrong.
Reading the cert 0380 worksheet PDF directly (dr87-0001-000899.pdf
page 3 line 233) shows -2563.3692 kWh/yr — matching our cascade
exactly. The real bug was the schema dropping flat-shape
battery_capacity, fixed in S0380.48. Lesson captured in the doc:
verify handover-cited numerics against the source PDF before
implementing the prescribed fix (same discipline as spec-floor
skepticism applied to handover claims).
Includes the full PE residual cohort table across all three milestones
(pre-44 / post-45 / post-48) and the Slice 6 implementation outline.
|
||
|
|
bf99b1c7df |
Slice S0380.48: surface real-API pv_batteries[].battery_capacity (5 kWh)
The 7-cert ASHP+battery PE cluster was overshooting by +2.7..+8.1 kWh/m²
after the PE β-split landed in S0380.45. The handover hypothesised an
E_PV magnitude bug ("cascade thinks 2570 kWh/yr vs worksheet 831"). The
worksheet PDF for cert 0380 (dr87-0001-000899.pdf line 233) was
verified to show **-2563.3692** kWh/yr — matching our cascade. The
real bug was different: the **5-kWh battery wasn't reaching the
cascade**, so β-coefficients used the no-battery branch (C1=1.61,
β≈0.36) instead of the 5-kWh branch (C1=1.12, β≈0.75).
Per SAP 10.2 Appendix M1 §3c-d (p.94): "C_bat is the usable capacity
of the battery in kWh, limited to a maximum value of 15 kWh. C_bat=0
if no battery present." Cert 0380 lodges `pv_battery_count: 1` and
`pv_batteries: [{"battery_capacity": 5}]` — but the schema's
`PvBatteries` dataclass had only `pv_battery: Optional[PvBattery]`,
matching the older synthetic fixture shape (nested
`{"pv_battery": {"battery_capacity": 5}}`). The real-API payload's
flat `battery_capacity: 5` was silently dropped during `from_dict`.
Two surgical changes:
- `datatypes/epc/schema/rdsap_schema_21_0_1.py`: add
`battery_capacity: Optional[float] = None` as a sibling to
`pv_battery` on `PvBatteries`. Synthetic-shape certs continue to
populate the nested form; real-API certs now populate the flat form.
- `datatypes/epc/domain/mapper.py:_first_pv_battery`: prefer nested
when present, fall back to the flat lifted field. Domain still
exposes a single uniform `PvBatteries(pv_battery=PvBattery(...))`
shape downstream.
Cohort impact (PE residual kWh/m² vs worksheet):
| Cert | Pre-S0380.48 | Post-S0380.48 |
|---|---:|---:|
| 0350 | +2.73 | -3.58 |
| 0380 | +8.09 | -4.01 |
| 2225 | +4.48 | -4.50 |
| 2636 | +3.42 | -4.14 |
| 3800 | +3.58 | -4.01 |
| 9285 | +3.20 | -3.46 |
| 9418 | +4.67 | -3.76 |
Cluster magnitude dropped from +2.7..+8.1 to -3.5..-4.5 — the cascade
now over-credits PV by ~4 kWh/m² (vs previously under-crediting by
~5 kWh/m²). The residual flipped sign because cascade β=0.75-0.81
slightly exceeds worksheet β=0.74 (read from page-3 line 233a/233b
ratio 1903.39/2563.37 = 0.7426). The remaining ~4 kWh/m² under-shoot
traces to two structural factors deferred until a fresh closure
slice ships:
1. The synthetic-default `pv_export_primary_factor = 0.501` is the
annual Table 12 code-60 value. The worksheet uses the effective
monthly Table 12e factor weighted by E_PV,ex,m (cert 0380: 0.4268
= -0.074 differential). The cascade's `_effective_monthly_pe_
factor` already computes the same weighting for PV — but the
calculator's PV PE credit reads `inputs.other_primary_factor`
(=1.501) and `inputs.pv_export_primary_factor` (=0.501) directly,
bypassing the per-end-use effective-monthly cascade.
2. Cascade β slightly higher than worksheet (0.751 vs 0.7426 on
cert 0380) — likely a monthly-distribution detail in D_PV.
SAP scores remain exact across the cohort (residual +0 every cert).
CO2 residuals all <0.11 t/yr (well within the 0.001-tolerance pin
range after re-pin). 9501 (PV no battery) preserved at +0.255 PE /
-0.047 CO2 — no regression. Re-pins all 7 golden fixtures in the
same slice per [[feedback-commit-per-slice]].
Pyright net-zero on touched files (32 errors before, 32 after).
|
||
|
|
42ed38f77d |
Slice S0380.47: wire β-split into cost cascade per SAP 10.2 Appendix M1 §6
SAP 10.2 Appendix M1 §6 (p.94): "When calculating the fuel cost benefits ... apply the normal import electricity price to PV energy used within the dwelling and the 'electricity sold to grid, PV' price from Table 12 to the energy exported." Adds the third leg of the β-factor split (PE was S0380.45, CO2 was S0380.46). Now uniform across all three cascades: PE → IMPORT PEF × E_dw + EXPORT PEF × E_ex CO2 → IMPORT CO2 × E_dw + EXPORT CO2 × E_ex Cost → IMPORT £ × E_dw + EXPORT £ × E_ex Mechanism: - `worksheet/fuel_cost.py`: optional `pv_dwelling_kwh_per_yr` + `pv_exported_kwh_per_yr` + `pv_dwelling_import_price_gbp_per_kwh` keyword args; when all three are set, split the credit; otherwise fall back to legacy single-rate-EXPORT (preserves synthetic test constructions). - `rdsap/cert_to_inputs.py`: new `_pv_dwelling_import_price_gbp_per_kwh` helper that pulls Table 32 code 30 (standard electricity = 13.19 p/kWh) for standard tariff; off-peak branch uses `prices.e7_low_rate_p_per_kwh` as the natural extension point when the first off-peak PV cert lands (currently short-circuited by the `Tariff != STANDARD` guard at line 2710). - `calculator.py`: new `pv_dwelling_import_price_gbp_per_kwh` field on `CalculatorInputs` with synthetic-fallback split logic mirroring the precomputed-fuel_cost path. Maintains the cross-cascade architecture documented in the prior handover. Cohort impact: **none**. Per ADR-0010 RdSAP10 amendment, Table 32 collapses code 30 (standard electricity import) and code 60 (electricity sold to grid, PV) to the SAME 13.19 p/kWh rate. So the β-split's E_dw × 13.19 + E_ex × 13.19 == E_total × 13.19, matching the legacy single-rate credit at 1e-4 — 763 pass + 0 fail across the full chain test suite (Elmhurst U985, cohort-1 ASHP, cohort-2 38-cert sweep, 15-cert golden fixtures). The β-split shape is now in place for the off-peak case (where weighted Table 12a high/low rates would diverge) and any future amendment that splits import/export prices. Pyright net-zero on touched files (34 errors before, 34 after — all pre-existing). |
||
|
|
cbc6d5dbc8 |
docs: refresh NEXT_AGENT_PROMPT for PV β-split slices 4-6
Replaces stale legacy content (cert-mapper-validation workflow, dated to a 9-triple staging slice) with the current handoff: branch state, 3 shipped slices (S0380.44 → S0380.46), and concrete directives for the 3 remaining slices (cost cascade wiring, E_PV magnitude audit, final fixture re-pin). Companion to docs/HANDOVER_PV_BETA_SPLIT.md. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
beb0db9522 |
docs: handover after S0380.44..S0380.46 — PV β-split 3/6 wiring slices shipped
Documents progress on the SAP 10.2 Appendix M1 β-factor split for the PE / CO2 / cost cascades + golden-fixture residual closure. Shipped: - Slice 1 (S0380.44): pure β-factor calculator module + 13 unit tests - Slice 2 (S0380.45): wire β into PE cascade - Slice 3 (S0380.46): wire β into CO2 cascade Cert 9501 (PV no battery): PE Δ -8.28 → +0.25, CO2 Δ +0.20 → -0.05 — clean spec validation. The 7-cert ASHP+battery cohort overshoots PE by +2.7..+8.1 because the cascade's E_PV is ~3× the worksheet's value (cert 0380 cascade 2570 kWh vs worksheet 831 kWh). E_PV magnitude audit deferred to Slice 5. Open: - Slice 4 (S0380.47, next): wire β into cost cascade - Slice 5 (S0380.48): E_PV magnitude audit - Slice 6 (S0380.49): re-pin fixtures + verify chain tests <1e-4 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
5b269f23b6 |
Slice S0380.46: wire β-split into CO2 cascade per SAP 10.2 Appendix M1 §7
The CO2 cascade in calculator.py had no PV credit at all
(environmental_section_from_cert had a stale `pv_credit = 0.0` with
the comment "no PV in any Elmhurst fixture", but that helper isn't
called by `calculate_sap_from_inputs` anyway). The full ASHP+PV
cluster therefore over-counted CO2 by +0.16..+0.28 t/yr — the entire
PV CO2 offset was missing.
Wiring (calculator.py):
- New fields: `pv_dwelling_co2_factor_kg_per_kwh: Optional[float]`,
`pv_exported_co2_factor_kg_per_kwh: Optional[float]`.
- CO2 cascade now subtracts:
pv_co2_credit = E_PV,dw × dwelling_CO2_factor
+ E_PV,ex × exported_CO2_factor
when the split + factors are set. None preserves the legacy
zero-credit behaviour for synthetic CalculatorInputs constructions.
Wiring (cert_to_inputs.py):
- New constant: `_PV_EXPORT_FUEL_CODE_TABLE_12 = 60` (SAP 10.2
Table 12 code 60, "electricity sold to grid, PV") — the EXPORT
factor key per Appendix M1 §6/§7/§8.
- The dwelling CO2 factor is the effective monthly Table 12d Σ
weighted by E_PV,dw,m at code 30 (Standard electricity); the
exported CO2 factor is the same Σ weighted by E_PV,ex,m at
code 60 ("Electricity sold to grid, PV"). Both reuse the
existing `_effective_monthly_co2_factor` helper.
Test impact (CO2 residual cluster, re-pinned in this slice):
Pre-Slice 46 → Post-Slice 46:
- 0330 (no PV): -0.034 → -0.034 (unchanged ✓)
- 0350 (PV + 5 kWh battery): +0.171 → -0.084
- 0380 (PV + 5 kWh battery): +0.279 → -0.054
- 2130 (PV + gas combi): +0.299 → -0.046
- 2225 (PV + 5 kWh battery): +0.263 → -0.071
- 2636 (PV + 5 kWh battery): +0.219 → -0.058
- 3800 (PV + 5 kWh battery): +0.261 → -0.014
- 9285 (PV + 5 kWh battery): +0.157 → -0.098
- 9418 (PV + 5 kWh battery): +0.232 → -0.046
- 9501 (PV, no battery): +0.202 → -0.047
Cluster magnitude dropped 3-5× — over-count flipped to slight
under-count (-0.01..-0.10 vs +0.16..+0.28). The remaining negative
residual is largely the same E_PV-magnitude bug from Slice 45 (PV
is over-credited because the cascade thinks E_PV ≈ 3× the worksheet
value for the 5-kWh-battery cohort). Slice 47 (cost cascade) + Slice
S0380.48 (E_PV magnitude audit) will close the cluster further.
Chain tests still <1e-4 — CO2 cascade isn't gated by the chain
tests' SAP-rating-vs-worksheet assertions.
Test suite: 763 pass + 0 fail. Pyright net-zero per touched file
(calculator.py 0/0; cert_to_inputs.py 34/34; test_golden_fixtures.py 1/1).
Spec citations:
- SAP 10.2 specification Appendix M1 §7 (p.94) — PV CO2 credit split.
- SAP 10.2 Table 12d (p.194) code 60 — monthly CO2 factor for
"electricity sold to grid, PV" (already in `tables/table_12.py`).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
49de18e83a |
Slice S0380.45: wire β-split into PE cascade per SAP 10.2 Appendix M1 §8
The PE cascade in calculator.py was crediting ALL PV generation at the
IMPORT PEF (Table 12 ~1.501) instead of splitting per Appendix M1
§4/§8 — onsite-consumed E_PV,dw at the IMPORT PEF and exported E_PV,ex
at the EXPORT PEF (Table 12 code 60 = 0.501). The over-credit on the
exported portion was the primary driver of the ASHP-cohort PE Δ -7..-15
kWh/m² under-count.
Wiring (cert_to_inputs.py):
- `_pv_array_monthly_generation_kwh(array, climate)` — per-array E_PV,m
via Appendix M1 §2 (p.92) apportioning: 0.8 × kWp × ZPV × monthly
solar radiation. Reuses ORIENTATION/PITCH/Z lookups already in
`_pv_array_generation_kwh_per_yr`. Annual sum equals the existing
helper to float precision.
- `_pv_monthly_generation_kwh(epc, climate)` — sums per-array monthlies;
falls back to the same §11.1 b) percent-roof-area synthesis as the
annual helper for certs without per-array detail.
- `_pv_battery_capacity_kwh(epc)` — total usable battery capacity =
per-battery capacity × pv_battery_count. The 15 kWh cap per §3c is
applied inside `pv_beta_coefficients` and not duplicated here.
- `_pv_eligible_demand_monthly_kwh(...)` — assembles D_PV,m per §3a
p.93: lighting + appliances + cooking + electric showers + pumps
& fans, plus E_space,m when main fuel is Table-12 {30, 32, 34, 35,
38} (electricity not at off-peak) and E_water,m when water heating
fuel is Table-12 30 (standard electricity). Off-peak immersion ×
(243) and the Appendix G4 PV-diverter branch are deferred —
current cohort fixtures don't exercise them.
- In `cert_to_inputs`: assemble monthly EPV + DPV + battery, call
`pv_split_monthly`, pass `pv_dwelling_kwh_per_yr` +
`pv_exported_kwh_per_yr` through to CalculatorInputs.
Wiring (calculator.py):
- New fields: `pv_dwelling_kwh_per_yr: Optional[float]`,
`pv_exported_kwh_per_yr: Optional[float]`,
`pv_export_primary_factor: float = 0.501` (Table 12 code 60).
- PE cascade now does:
pv_offset = E_PV,dw × IMPORT_PEF + E_PV,ex × EXPORT_PEF
when both split fields are set. Legacy fall-through to all-IMPORT
when either is None (preserves synthetic CalculatorInputs
constructions in unit tests).
Test impact (golden-fixture residual shifts — all expected, re-pinned):
Pre-Slice 45 → Post-Slice 45:
- 0330 (no PV): +0.44 → +0.44 (unchanged ✓)
- 0350 (PV + 5 kWh battery): -7.78 → +2.73
- 0380 (PV + 5 kWh battery): -14.60 → +8.09
- 2130 (PV + gas combi): -38.63 → -9.70 (also SAP +1 shift)
- 2225 (PV + 5 kWh battery): -11.77 → +4.48
- 2636 (PV + 5 kWh battery): -9.65 → +3.42
- 3800 (PV + 5 kWh battery): -9.61 → +3.58
- 9285 (PV + 5 kWh battery): -7.96 → +3.20
- 9418 (PV + 5 kWh battery): -7.30 → +4.67
- 9501 (PV, no battery): -8.28 → +0.25 (CLOSED ✓)
Cert 9501 closing to +0.25 with the β-split alone confirms the
implementation is spec-correct. The 7-cert 5-kWh-battery cohort
now over-shoots in the positive direction because the cascade's
E_PV magnitude is ~3× the worksheet's (cert 0380 cascade 2570 kWh/yr
vs worksheet 831 kWh/yr — peak_power=3 interpreted as 3 kWp while
worksheet uses ~1 kWp). With E_PV overestimated, R_PV = E_PV / D_PV
is too high → β_m from §3d formula too low → not enough credit
shifts to the IMPORT factor. Slice S0380.46 audits the cascade's
E_PV magnitude (kWp interpretation, S lookup, or ZPV mapping).
Chain tests (cohort-1 + cohort-2 SAP-rating-vs-worksheet) all stay
<1e-4 — Slice 45 only touches the PE cascade; SAP rating uses the
cost cascade which is still on the old all-export path.
Test suite: 763 pass + 0 fail. Pyright net-zero on touched files.
Spec citations:
- SAP 10.2 specification Appendix M1 §3a (p.93) — D_PV,m assembly.
- SAP 10.2 specification Appendix M1 §3c-d (p.94) — β formula.
- SAP 10.2 specification Appendix M1 §4 (p.94) — E_PV,dw / E_PV,ex.
- SAP 10.2 specification Appendix M1 §8 (p.94) — PE factor split.
- SAP 10.2 Table 12 code 60 — EXPORT PEF = 0.501.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
5344bc8920 |
Slice S0380.44: SAP 10.2 Appendix M1 §3-4 PV β-factor calculator (no wiring)
Pure-function module + 13 unit tests for the photovoltaic onsite/export
split. No cascade wiring yet — Slices S0380.45..47 will wire β into the
PE / CO2 / cost cascades respectively (which currently all over-credit
the exported PV portion at the IMPORT factor).
Module: `domain/sap10_calculator/worksheet/photovoltaic.py`
- `PhotovoltaicSplit` frozen dataclass — monthly β + (E_PV,dw,m,
E_PV,ex,m) with annual-sum properties matching worksheet line
refs (233a) and (233b).
- `pv_beta_coefficients(Cbat)` — three coefficients keyed on battery
capacity (kWh), capped at 15 per §3c:
CPV1 = 1.610 - 0.0973 × Cbat
CPV2 = 0.415 - 0.00776 × Cbat
CPV3 = 0.511 + 0.0866 × Cbat
- `pv_split_monthly(epv, dpv, battery_kwh)` — per §3d-4:
R_PV,m = E_PV,m / D_PV,m
β_m = min(exp(-CPV1 × (R_PV,m × CPV2)^CPV3), D_PV,m / E_PV,m)
E_PV,dw,m = E_PV,m × β_m; E_PV,ex,m = E_PV,m × (1 - β_m)
Edge cases (not in spec but implied by physics):
- E_PV,m = 0 → β = 0; both onsite and exported = 0
- D_PV,m = 0 → cap forces β = 0; all PV exports
Unit-test coverage (13 tests, AAA convention, `abs(diff) <= tol`):
- β coefficient constants at Cbat=0, 5 (ASHP cohort), 15 (cap)
- Cbat>15 clamps to 15; Cbat<0 clamps to 0 (defensive)
- Hand-computed β worked example (no battery): β≈0.4864 at E_PV=100,
D_PV=200 — pinned at 1e-7 against precomputed value AND at 1e-9
against the live formula recomputation (load-bearing math pin)
- Edge cases: E_PV=0 → no split; D_PV=0 → full export
- Battery monotonicity: β increases with Cbat for fixed (E_PV, D_PV)
- Energy conservation: E_PV,dw + E_PV,ex = E_PV per month + annually
- Tuple length validation (raises on != 12 months)
- Return shape pinned to `PhotovoltaicSplit` dataclass contract
Test suite: 750 → 763 pass + 0 fail. Pyright net-zero on new files.
Spec citation: SAP 10.2 specification Appendix M1 §3-4 (p.93-94).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
29c4b029e3 |
docs: handover after S0380.39..S0380.43 — cohort-2 API path 38/38 closed
Session shipped 5 slices that closed the entire cohort-2 API-path
cluster (S0380.39 bulk-fetch, S0380.40 parametrized test, S0380.41
RdSAP 21 → SAP 10.2 glazing alias, S0380.42 Decimal HALF_UP per-window
areas, S0380.43 SAP 631 → spec fuel).
Documents:
- Cross-mapper parity at cascade established for all 38 cohort-2
certs (and 9 cohort-1 ASHP); both paths < 1e-4 vs worksheet.
- Tolerance tightening deferred — 1e-4 is the realistic floor at
HEAD (worst residual 4.91e-5 on cert 2102).
- Lessons learned: GOV.UK RdSAP 21 enum != cascade enum (codes
needing remap are incremental as fixtures surface them);
Decimal HALF_UP per-window areas extends the S0380.34/35
pattern; SAP heating-type → spec fuel dispatch is the new
forcing-function pattern for cert-lodgement inconsistencies.
- Open front: golden-residuals → ~0 on PE/CO2. ASHP cluster
(-7..-15 kWh/m² PE / +0.16..+0.28 t/yr CO2 across 7 certs with
the same PCDB heat pump) is the highest-value single thread —
likely SAP 10.2 Appendix L1 / Table 12 PE-factor or CO2-factor
cascade gap. Three concrete diagnostic probes proposed.
Test baseline at HEAD: 750 pass + 0 fail.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
6dccb15b03 |
Slice S0380.43: SAP 631 open-fire → House coal spec fuel — closes cert 2102
Cert 2102 lodges `secondary_heating_type=631` ("Open fire in grate"
per SAP 10.2 Appendix M Table 4a, BS EN 13229:2001 inset-appliance
class — solid fuel) but `secondary_fuel_type=33` (electricity, Table 32
off-peak 7hr) — physically incompatible (an open fire grate doesn't
run on electricity). The Elmhurst Summary path independently resolves
to Coal (Table 32 code 11) via the §15 "Secondary Fuel: Coal" lodgement
(see `test_summary_2102_secondary_heating_routes_house_coal_for_open_fire`).
API mapper now applies the same spec-derived default via the new
`_api_secondary_fuel_type` helper:
- When `secondary_heating_type` is in the
`_API_SECONDARY_HEATING_SPEC_FUEL` dispatch (currently {631: 11}),
AND the lodged `secondary_fuel_type` is electric (codes 30-40),
substitute the spec default (House coal).
- Legitimate non-default solid-fuel lodgement (e.g. SAP 631 with
lodged fuel_type=15 Wood logs) passes through unchanged.
The override is keyed on the heating-type → spec-fuel dispatch dict
(extend as new fixtures surface analogous inconsistencies), not a
blanket per-code rewrite — keeps the lodged data trusted by default
while spec-correcting the narrow class of inconsistent lodgements.
Applied at all 6 API schema-version mapping sites in `from_api_response`
via replace_all (lines 637/767/922/1080/1278/1544). Worksheet target
for cert 2102: line (242) "Space heating - secondary 3585.24 × 3.6700
= 131.58" confirms 3.67 p/kWh = Table 32 fuel code 11 (House coal).
Test impact:
- Cohort-2 cert 2102 API path: -6.30 → +4.9e-5 (<1e-4 ✓).
Moves from `_COHORT_2_API_OPEN` to `_COHORT_2_API_CLOSED`.
- `_COHORT_2_API_OPEN` is now empty — the residual-pin test
`test_api_cohort_2_open_cert_residual_matches_current_pin` is
deleted (cohort fully closed; re-add if future cert surfaces).
- Cohort-2 API path: **38/38 < 1e-4** matching Summary path 38/38.
Cross-mapper parity at the cascade is fully established for
cohort-2 per [[feedback-cross-mapper-parity-via-cascade]].
- Cohort-1 ASHP 9/9 unchanged.
Test suite: 750 pass + 0 fail. Pyright net-zero on touched files
(mapper.py 32/32 baseline; chain test 0/0).
Spec citations:
- SAP 10.2 Appendix M Table 4a code 631 "Open fire in grate"
(Category C, Room heaters, eff 37/32%, solid fuel via BS EN
13229:2001 inset-appliance class — see spec p.156).
- SAP 10.2 Table 32 code 11 "House coal" 3.67 p/kWh.
- Cert 2102 worksheet line (242) reproduces 131.58 = 35.84 × 3.67
confirming house-coal pricing for the secondary cascade.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
e1b7b30c40 |
Slice S0380.42: Decimal HALF_UP per-window areas per RdSAP10 §15 — closes cert 1536
Cert 1536 lodged window dimensions including (0.65 × 0.70) × 3
windows. In float arithmetic 0.65 × 0.70 = 0.45499999999999996,
which the `_round_half_up(float, dp)` helper snaps to 0.45 vs the
spec answer 0.46 (Decimal: 0.65 × 0.70 = 0.4550 exact, HALF_UP at
2 d.p. = 0.46). The shortfall of 0.01 m² × 3 windows = 0.03 m²
under-counted as ~0.073 W/K of conduction loss vs the worksheet's
windows_w_per_k = 25.6354 — closing the cert 1536 residual at
+0.00152 to <2e-6.
Same class of bug as the S0380.34/35 living-area / gross-wall /
party-wall closures (Decimal HALF_UP at the 0.005 boundary that
float drops). RdSAP10 §15 (p.66) lists "all element areas (gross)
including window areas: 2 d.p." — Decimal is the only arithmetic
that matches that boundary deterministically.
Three cascade sites now use Decimal HALF_UP for per-window areas:
- heat_transmission.py: `_decimal_round_half_up_product(W, H, 2)`
replaces `_round_half_up(W × H, 2)` at the windows_w_per_k cascade
AND at the per-bp window-area accumulation (the wall-net deduction
branch must agree with the conduction branch for cascade-internal
consistency, per the existing comment at line 575-583).
- internal_gains.py: `_decimal_window_area_2dp(W, H)` replaces the
inline `_round_area_2dp(W × H)` in the daylight factor `g_l`
sum so §5 (66)..(67) sees the same per-window areas as §3 (27).
- solar_gains.py: same Decimal helper replaces `_round_area_2dp` in
`_wall_window_solar_gain_monthly_w` so §6 (74)..(81) area = (27).
The `_round_area_2dp` helpers were inlined per-module in pre-S0380.42
work; this slice deletes them since the Decimal-aware product
replaces all call sites. `_round_half_up` stays in heat_transmission
for non-product per-element area calls (single-value rounds).
Test impact:
- Cohort-2 cert 1536 API path: +0.00152 → -1e-6 (<1e-4 ✓).
Moves from _COHORT_2_API_OPEN to _COHORT_2_API_CLOSED. Cohort
distribution: 37/38 exact (was 34/38 at start of session);
only cert 2102 (-6.30 secondary-heating routing) remains open.
- Cohort-2 cert 0300/9380 unchanged (already <1e-4 after S0380.41).
- Cohort-1 ASHP 9/9 unchanged: <1e-4 on both paths.
- Elmhurst 6-cert worksheet sweep: unchanged (lodges
`window_width=area, window_height=1.0` per the Elmhurst lodging
convention — Decimal(area) × Decimal(1.0) = Decimal(area), no
rounding shift).
Test suite: 750 pass + 0 fail. Pyright net-zero per touched file
(heat_transmission 13/13; internal_gains 4/4 pre-existing; solar_gains
0/0; chain test 0/0).
Spec citation: RdSAP 10 Specification §15 "Rounding of data" p.66 —
"All element areas (gross) including window areas and conservatory
wall area: 2 d.p." Decimal is the float-precision-stable arithmetic
that matches this rule at the .005 boundary.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
a96e6765ab |
Slice S0380.41: GOV.UK RdSAP 21 glazing-type code 1 → DG pre-2002 cascade
Closes the cohort-2 API-path +0.42..+0.44 cluster (certs 0300/9380
closed to <1e-4; cert 1536 partially closed +0.4445 → +0.0015 — a
sub-2e-3 secondary tail remains for Slice S0380.42).
Root cause: per `datatypes/epc/domain/epc_codes.csv` the GOV.UK API
schema RdSAP-Schema-21.0.0 defines `glazed_type=1` as "double glazing
installed before 2002 in EAW, 2003 in SCT, 2006 NI". Three cohort-2
certs (0300/1536/9380) lodge this code with `glazing_gap=16+` and
description "Fully double glazed" — but the API mapper passed the
raw code straight through to SapWindow.glazing_type, and:
1. `_api_glazing_transmission` had no (1, "16+") entry, so the
U-value lookup returned None and the cascade defaulted to U=2.5
instead of the spec-correct U=2.7 (RdSAP 10 Table 24 row 2,
PVC/wooden frame, 16+ gap = 2.7).
2. The cascade's `_G_LIGHT_BY_GLAZING_CODE` table is keyed on the
SAP 10.2 Table 6b enum (the Elmhurst extractor produces this
enum via `_ELMHURST_GLAZING_LABEL_TO_SAP10`), where code 1 means
"single glazed" (g_L=0.90). Passing RdSAP 21 code 1 straight
through gave the cascade the wrong g_L for the daylight factor
calculation, off by 0.90 vs spec 0.80.
Both gaps closed in one slice because they're the same misinterpretation:
- `_API_GLAZING_TYPE_TO_TRANSMISSION` + `_API_GLAZING_TYPE_GAP_TO_
TRANSMISSION` now alias code 1 as a schema sibling of code 3 — both
resolve to RdSAP 10 Table 24 row 2 ("DG pre-2002 / unknown install
date"). Per-gap entries cover the full 6mm=3.1 / 12mm=2.8 / 16+=2.7
row; type-only fallback uses the 12mm default U=2.8.
- New `_API_TO_SAP10_CASCADE_GLAZING_CODE = {1: 2}` remap is applied
in `_api_sap_window` AFTER the U-value lookup, so SapWindow.glazing_
type carries the SAP 10.2 cascade enum (code 2 = DG pre-2002 air-
filled, g_L=0.80) while the U lookup stays keyed on the raw GOV.UK
API code. The cohort-1 codes 2/3/13/14 already coincide with the
cascade table's intended SAP 10.2 g_L values, so no remap entry
required for them; only divergent codes get a remap.
Test impact:
- Cohort-2 API path: 34/38 → 36/38 at 1e-4 (0300 +4.8e-5; 9380 -5e-6
both move from _COHORT_2_API_OPEN to _COHORT_2_API_CLOSED).
- Cert 1536 pin updated from 66.337334 to 65.894324; ws Δ now +0.0015
(was +0.4445) — same root-cause fix dominated, residual tail is
distinct-cause work for the next slice.
- Cert 2102 unchanged (-6.30 residual, secondary-heating routing gap).
- Cohort-1 (9 ASHP certs) unaffected: 9/9 still < 1e-4 on both paths.
Test suite: 750 pass + 0 fail. Pyright net-zero per touched file.
Spec citations:
- RdSAP-Schema-21.0.0 glazed_type=1 → datatypes/epc/domain/epc_codes.csv
- RdSAP 10 Specification §8.2 Table 24 (p.49) row 2 "Double glazed:
Installed England/Wales before 2002 / Scotland before 2003 /
N. Ireland before 2006" — U=2.7 (PVC/wooden, 16+ gap).
- SAP 10.2 Table 6b: DG air-filled g_L=0.80 (vs single 0.90).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
ff25746f44 |
Slice S0380.40: parametrized API-path chain sweep for cohort-2 (34/38 at 1e-4)
Mirror of the cohort-2 Summary-path sweep that closed across
S0380.30..38: for each of the 38 cohort-2 certs whose API JSON was
fetched in S0380.39, drive the full API chain (`from_api_response`
→ `cert_to_inputs` → `calculate_sap_from_inputs`) and assert
`sap_score_continuous` vs the worksheet's lodged SAP at abs <= 1e-4.
Per cross-mapper parity ([[feedback-cross-mapper-parity-via-cascade]]):
the SAP cascade is the load-bearing equivalence check between
EpcPropertyData produced by from_api_response and from_elmhurst_site_notes.
If both paths hit the worksheet at 1e-4, they're cascade-output-
equivalent for load-bearing fields — strictly stronger than a noisy
structural EpcPropertyData diff.
Two parametrized tests, both green at HEAD:
- test_api_cohort_2_full_chain_sap_matches_worksheet_at_1e_minus_4:
34 certs that hit the worksheet at 1e-4 on the API path immediately
(the cascade can't tell which mapper produced the EPC).
- test_api_cohort_2_open_cert_residual_matches_current_pin:
4 certs that don't yet hit 1e-4 — pinned at their current cascade
output as forcing functions per [[project-api-to-sap-residual-test]].
When a follow-up slice closes the underlying mapper/spec gap, the
cascade output moves and the pin fires, forcing the cert to migrate
from _COHORT_2_API_OPEN to _COHORT_2_API_CLOSED.
Open cohort residuals (handover to Slice C+):
- 0300/1536/9380: tight +0.42..+0.44 band — likely a single shared
cascade-spec gap (API-mapper-specific, since Summary path hits 1e-4)
- 2102: -6.30 — Summary test (test_summary_2102_secondary_heating_
routes_house_coal_for_open_fire) shows the cert lodges house-coal
open-fire secondary heating; API mapper likely routes secondary
fuel differently. Probe `secondary_heating` block first.
Test suite: 712 → 750 pass (0 fails). Pyright net-zero on touched file.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
22ae6f4d77 |
Slice S0380.39: bulk-fetch 38 cohort-2 EPC API JSONs for cross-mapper parity
Adds scripts/fetch_cohort2_api_jsons.py (throwaway one-off) plus 38 golden fixtures under domain/sap10_calculator/rdsap/tests/fixtures/golden/ covering every cert in "sap worksheets/additional with api 2/". Each JSON is the inner `data` payload from the gov.uk EPB /api/certificate endpoint — the same shape EpcPropertyDataMapper .from_api_response consumes today. Required prerequisite for Slice B (parametrized API-path chain test that mirrors the cohort-2 Summary-path sweep at 1e-4 vs worksheet). Per the cross-mapper-parity primitive: API EPC and Elmhurst EPC must produce SAP within 1e-4 of each other and of the worksheet — the SAP cascade is the load-bearing equivalence check. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
f992824824 |
docs: handover after S0380.31..S0380.38 — cohort-2 Summary path COMPLETE, thread 4 next
State at HEAD
|
||
|
|
883d66ac65 |
Slice S0380.38: loosen FEE round-trip tolerance 1e-9 -> 1e-6
test_no_ac_cert_round_trips_fee_equals_space_heating_per_m2 encodes a real SAP 10.2 invariant: when (108) = 0 (no fixed AC) and Appendix H solar is absent (every cohort cert), (109) FEE must equal space_heating_kwh / TFA. The 1e-9 tolerance was too tight. The cascade computes: - FEE: sum_round_per_month(annual_98a) / TFA - space_heating_kwh: sum(monthly_98a_kwh) summed in calculator The two paths sum the same 12 monthlies in different rounding orders and disagree at ~8e-8 (cascade FEE = 95.39072333333334; SH/TFA = 95.39072341347577). 1e-6 is two orders of magnitude tighter than any meaningful path divergence (a stray 4-d.p. rounding step or unintended AC contribution would blow past instantly) and ~12.5x looser than the observed float-arithmetic drift, so the invariant still fires. Also swaps pytest.approx for `abs(a - b) <= tol` per [[feedback-abs-diff-over-pytest-approx]] (strict-pyright flags pytest.approx as partially-unknown; nets -1 error on the file). Test baseline: 712 pass + 0 fails (was 712 + 1). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
1cea73df7c |
Slice S0380.37: drop cert 001479 hand-built fixture — covered by passing production-path chain tests
Cert 001479 was added in
|
||
|
|
b0919e8d6f |
Slice S0380.36: tighten _ASHP_COHORT_CHAIN_TOLERANCE 0.04 -> 1e-4 after S0380.31 closes cohort
Cohort-1 ASHP cohort residuals at HEAD
|
||
|
|
d61a27e0ff |
Slice S0380.35: round gross-wall and party-wall areas in Decimal arithmetic per RdSAP10 §15 — closes cohort-2 cert 2800 / 4800 +0.0007 SAP residuals
RdSAP10 §15 p.66 (Rounding of data):
"All element areas (gross) including window areas and
conservatory wall area: 2 d.p."
Certs 2800 and 4800 lodge heat_loss_perimeter = 21.25 m and
room_height = 2.30 m. The exact-decimal products
21.25 * 2.30 = 48.8750 (gross wall area)
6.25 * 2.30 = 14.3750 (party wall area)
sit ON the HALF_UP rounding boundary and must round to 48.88
and 14.38 m^2. Float representation drops them BELOW the
boundary:
21.25 (float) * 2.30 (float) ~= 48.87499...
HALF_UP 2 d.p. = 48.87
6.25 (float) * 2.30 (float) ~= 14.37499...
HALF_UP 2 d.p. = 14.37
The 0.01 m^2 area shortfall feeds into (29a) net wall area and
(32) party wall area, and into (31) total external area for
(36) thermal bridging — propagating a +0.0007 SAP residual via
the U-weighted heat-loss sums.
Adds `_decimal_round_half_up_sum` helper and routes both
gross-wall and party-wall sums through it, mirroring the
S0380.34 fix on `_living_area_fraction`. Certs that sit off
the .005 boundary (i.e. nearly all) are unaffected; certs
that land on it close from +0.0007 → <5e-5.
Cohort-2 distribution after S0380.31..S0380.35:
38 exact (was 36 exact + 2 <=0.07).
Cohort-1 ASHP cohort: 9/9 <1e-4 (unchanged).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
a92a33a8d8 |
Slice S0380.34: round living area in Decimal arithmetic per RdSAP10 §15 — closes cert 2536 +0.0007 SAP residual
RdSAP10 §15 p.66 (Rounding of data):
"All internal floor areas and living area: 2 d.p."
Cert 2536 (3 habitable rooms → Table 27 fraction 0.30,
TFA 45.65 m^2) sits ON the HALF_UP rounding boundary:
0.30 (exact) * 45.65 = 13.6950
HALF_UP 2 d.p. = 13.70
(worksheet fLA = 13.70 / 45.65 = 0.3001)
Float arithmetic drops the spec product BELOW the boundary:
0.30 (binary) ~= 0.2999999...
product ~= 13.69499...
HALF_UP 2 d.p. = 13.69
(cascade fLA = 13.69 / 45.65 = 0.29989)
The 0.00021 fLA shortfall feeds straight into the worksheet
(91) -> (92) MIT blend, undershoots MIT by ~0.001 C, and
shaves 0.29 kWh off (98c) useful space heating — a +0.0007
SAP residual via the (211) main heating fuel x p/kWh.
Compute the product in Decimal so HALF_UP lands on the exact
.005 decimal boundary the spec defines. Certs that sit off the
boundary (e.g. 2800/4800: 0.30 x 46.87 = 14.0610 -> 14.06 in
both Decimal and float) are unaffected.
Cohort-2 distribution after S0380.31..S0380.34:
36 exact + 2 <=0.07 (was 35 exact + 3 <=0.07).
Cert 2536: +0.000715 -> -9.2e-8.
The remaining 2800 / 4800 +0.0007 residuals come from a
different cause (off the HALF_UP boundary) — defer to a
separate slice.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|