mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
docs: handover for §10a Fuel costs + SPEC_COVERAGE PCDB-followup updates
HANDOVER_NEXT.md rewritten for §10a Fuel costs (xlsx rows ~614-740, spec lines 8044-8084). Covers (240)..(255) line refs — refactor calculator.py inline cost arithmetic into a worksheet-shape `FuelCostResult` orchestrator following the §9a precedent. Three-slice plan: orchestrator + dataclass + synthetic, atomic CalculatorInputs/cert_to_inputs wiring, Table 12a off-peak split. SPEC_COVERAGE PCDB slice progress table picks up the two fixture-lodgement commits + the mapper-chain regression test; updated narrative confirms no domain-model changes were needed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
15d6b78149
commit
6d6767ce62
2 changed files with 174 additions and 154 deletions
|
|
@ -1,207 +1,225 @@
|
|||
# Handover — §8c Space cooling + 000490 SAP-score diagnostic
|
||||
# Handover — §10a Fuel costs (xlsx ~614–740)
|
||||
|
||||
**For the agent picking up the next chunk of work.** Read this BEFORE invoking `/grill-me`. Read all of it. Caveman mode is the house style — terse, technical, no filler.
|
||||
|
||||
Owner: `khalim@domna.homes`. Branch: `ara-backend-design-prd`.
|
||||
|
||||
This handover covers **two tickets in order**:
|
||||
This handover covers **the §10a Fuel costs worksheet block** — xlsx rows ~614–740, spec lines 8044–8084. Line refs (240)..(255). Mirrors the §9a slice you've just shipped: refactor existing `calculator.py` inline cost arithmetic into a worksheet-shape orchestrator with named line-ref outputs.
|
||||
|
||||
1. **Ticket A — Diagnose + (best-effort) close the 000490 e2e SAP-score gap.** §8 wiring exposed a +3 SAP overshoot. The previous agent claimed "the easy win is to wire `water_heating_from_cert` into `cert_to_inputs`" — that claim was **wrong** (it's already wired, see §A.2 below). The real gap is in upstream precision and is multi-source. Ticket A is to investigate and chip away, with explicit user check-ins before changing pinned expected values.
|
||||
2. **Ticket B — Implement §8c Space cooling.** xlsx rows 435–466, worksheet line refs (100)..(108). All 6 Elmhurst fixtures = 0 cooling. Should be a small slice.
|
||||
|
||||
Hard rules (same as previous handovers):
|
||||
Hard rules (unchanged):
|
||||
- **Caveman mode** house style.
|
||||
- **Tolerance**: don't loosen test tolerances to make them pass. If the orchestrator can't hit the locked tolerance, pause and ask the user.
|
||||
- **Spec PDFs**: don't scan more than ~50 lines without checking with the user.
|
||||
- **Fixture `build_epc()`**: don't modify — handover §11 of the original §6 handover is still in force. Local `_build_section_*_epc(fixture)` wrappers if you need to override windows.
|
||||
- **Commit per slice**, one slice = one commit, AAA test convention (`# Arrange / # Act / # Assert`), Co-Authored-By trailer.
|
||||
- **Commit per slice**, one slice = one commit, AAA test convention (`# Arrange / # Act / # Assert`), `Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>` trailer.
|
||||
|
||||
---
|
||||
|
||||
## §A — Ticket A: 000490 SAP-score gap diagnostic
|
||||
## §A — Current state (what just shipped)
|
||||
|
||||
### A.1 Current state
|
||||
After §8 wiring (commit `f6ab7626`) and `_currently_within_3_points` test update (in `f6ab7626` too), 000490 cert-driven calculator output is:
|
||||
Last commits on this branch:
|
||||
|
||||
| Metric | Calculator | PDF | Delta |
|
||||
|---|---|---|---|
|
||||
| SAP integer | 60 | 57 | +3 |
|
||||
| Continuous SAP | 60.22 | 57.40 | +2.82 |
|
||||
| Annual space heating kWh | 11467.18 | 11183.275 | +2.5% |
|
||||
| Annual HW fuel kWh | 3090.47 | 2850.57 | +8.4% |
|
||||
| Total fuel cost £ | 756.99 | 807.54 | −6.3% |
|
||||
| ECF | 2.4538 | 3.0539 | −20% |
|
||||
|
||||
### A.2 What's already wired (do NOT redo)
|
||||
- ✅ `water_heating_from_cert` IS the active path in `cert_to_inputs._hw_kwh_and_gains_from_cert` (file `cert_to_inputs.py`, line ~738). Legacy `predicted_hot_water_kwh` is a fallback only when `epc.total_floor_area_m2 is None`. For Elmhurst fixtures TFA is always present.
|
||||
- ✅ `internal_gains_from_cert` (§5), `solar_gains_from_cert` (§6), `mean_internal_temperature_monthly` (§7), `space_heating_monthly_kwh` (§8) all wired.
|
||||
- ✅ Per-fixture orchestrator-level conformance: all 6 Elmhurst fixtures pass §3–§8 at line-ref level (see `test_*.py::test_*_matches_elmhurst_worksheet_all_fixtures` tests).
|
||||
|
||||
### A.3 Where the gap actually is
|
||||
|
||||
The drift is in **scalar derivations the cert→inputs adapter computes from cert codes** — not in the per-month physics. Verified:
|
||||
|
||||
```python
|
||||
inputs = cert_to_inputs(_w000490.build_epc())
|
||||
inputs.main_heating_efficiency # → 0.8000
|
||||
# PDF Vaillant Ecotec Pro Manufacturer-declared: 0.882
|
||||
```
|
||||
15d6b781 pcdb followup: e2e mapper-chain regression test
|
||||
7d4f3d78 pcdb followup: 000474 fixture lodges main_heating_index_number=16839; e2e ceiling 7 → 2
|
||||
1b43c95c pcdb followup: 000490 fixture lodges main_heating_index_number=10328 (Vaillant Ecotec Pro)
|
||||
e63516cb docs: SPEC_COVERAGE PCDB integration row + slice progress + gap-list update
|
||||
a104dd55 pcdb slice 3: cert_to_inputs precedence cascade — Table 105 overrides Table 4a/4b
|
||||
23678228 pcdb slice 2: runtime gas_oil_boiler_record lookup via Table 105 NDJSON
|
||||
fe04cd3a pcdb slice 1: pcdb10.dat ETL → 8 per-table NDJSON files + parser + 8 tests
|
||||
53c393bf docs: SPEC_COVERAGE §9a row + slice progress table + PCDB gap-list update
|
||||
380b6781 §9a slice 2: CalculatorInputs.energy_requirements + cert_to_inputs wiring (atomic)
|
||||
2b5fc6a5 §9a slice 1: space_heating_fuel_monthly_kwh orchestrator + EnergyRequirementsResult
|
||||
05d9dc73 docs: SPEC_COVERAGE §8f row + slice progress table
|
||||
43cc16bc §8f slice 1: fabric_energy_efficiency_kwh_per_m2_yr + 6-fixture conformance
|
||||
c9f15a2e docs: SPEC_COVERAGE §8c row + slice progress table
|
||||
f3797066 §8c slice 3: CalculatorInputs + MonthlyEntry + SapResult + cert_to_inputs wiring (atomic)
|
||||
3b9fa936 §8c slice 2: 6-fixture ALL_FIXTURES conformance with shared template constants
|
||||
cf28eec4 §8c slice 1: space_cooling_monthly_kwh orchestrator + utilisation_factor_loss leaf
|
||||
```
|
||||
|
||||
Drivers (ordered by likely SAP impact):
|
||||
347 tests passing. 0 new pyright errors on touched modules. Sections §1, §3, §5, §6, §7, §8, §8c, §8f, §9a (single-main), §13 Full. PCDB Table 105 Gas/Oil Boilers wired into the efficiency cascade end-to-end (mapper → domain → calculator), 4 PCDB-listed corpus golden certs flowing through PCDB precedence.
|
||||
|
||||
1. **Water-heating efficiency** (`cert_to_inputs._legacy_water_heating_efficiency`)
|
||||
- Currently selects ~0.78-0.80 from a static SAP10 efficiency table by `sap_main_heating_code`.
|
||||
- PDF uses the **Manufacturer-declared** value (Vaillant Ecotec Pro 88.2%) from PCDB / cert efficiency lodgement.
|
||||
- Look for: cert field carrying the manufacturer efficiency directly, OR the PCDB lookup that's currently stubbed (per ADR-0009 grill: `NoOpPcdbLookup`).
|
||||
- Fixing this should drop HW fuel from 3090 → ~2750 kWh, closing ~340 kWh on annual fuel.
|
||||
**000490 + 000474 post-PCDB residuals (live, after this branch):**
|
||||
|
||||
2. **Space heating delivered = q_heat / main_heating_efficiency**
|
||||
- Same efficiency cascade. PDF (210) Jan = 88.2% throughout. Ours = 80%.
|
||||
- q_heat itself is correct (we just nailed §8 to 1e-1 kWh).
|
||||
- Fixing efficiency drops `main_heating_fuel_kwh_per_yr` from 14334 → ~13000 kWh.
|
||||
| metric | 000490 actual | 000490 PDF | 000474 actual | 000474 PDF |
|
||||
|---|---|---|---|---|
|
||||
| SAP score | 63 | 57 (+6) | 63 | 62 (+1) |
|
||||
| main_heating_fuel kWh | 13001.3 | 13003.85 (-0.02%) | 12304.8 | ~11968 (+2.8%) |
|
||||
| total_fuel_cost £ | 706.23 | 807.54 (-12.5%) | 651.85 | 655.69 (-0.6%) |
|
||||
| e2e SAP ceiling | 6 | — | 2 | — |
|
||||
|
||||
3. **§3 transmission HLC precision** (~+2.5% on annual space heating)
|
||||
- Calculator's `inputs.heat_transmission.total_w_per_k` = 236.6211 (matches PDF LINE_37).
|
||||
- But cert_to_inputs derives `total_w_per_k` from windows + walls + roof + floor + thermal_bridging. If any of those is off, HLC drifts → §7 MIT drifts → §8 space heating drifts.
|
||||
- This is much smaller than the efficiency gap.
|
||||
|
||||
### A.4 Recommended approach for Ticket A
|
||||
|
||||
**Do not** attempt to fix the entire efficiency cascade in one go — it touches §4 + §9 + §12 simultaneously and is a multi-day rewrite.
|
||||
|
||||
**Do** spend ~30 minutes investigating and either:
|
||||
|
||||
a) **Find a cert field that carries Manufacturer efficiency directly** that we're not reading (most likely: `epc.sap_heating.main_heating_details[0].efficiency_declared_pct` or similar). If present, wire it through `cert_to_inputs._main_heating_efficiency_pct` as a precedence-over-table override. Single-slice fix. **Surface findings to the user before implementing.**
|
||||
|
||||
b) **If no such cert field exists**, write a one-paragraph finding into `SPEC_COVERAGE.md` "Prioritised gap list" under "Boiler efficiency Manufacturer override" + ship as a known gap. Skip to Ticket B.
|
||||
|
||||
The user explicitly said in the previous turn: *"this is silly"* about the 000490 overshoot — meaning they want progress on it, but understand it might not be a one-line fix. Bias toward (a) if cheap; flag and defer if expensive.
|
||||
|
||||
### A.5 Don't update e2e tolerances during Ticket A
|
||||
The `_currently_within_3_points` ceiling for 000490 should stay 3 until Ticket A actually moves it. Tightening to 2 / 1 only after physics improves; loosening is a regression.
|
||||
000474 is near-closure. 000490 widened on cost (not kWh) because the cert pre-dates the 14-Mar-2025 SAP10.2 amendment — pre-amendment gas unit prices were ~13% higher than what `domain.sap.tables.table_12.SAP_10_2_SPEC_PRICES` carries. ADR-0010 §3 Validation Cohort discipline says this is *known* spec-version drift, not a calculator regression. The fuel-kWh closure is the spec-faithful direction.
|
||||
|
||||
---
|
||||
|
||||
## §B — Ticket B: §8c Space cooling
|
||||
## §B — Ticket: §10a Fuel costs
|
||||
|
||||
### B.1 Mission
|
||||
Implement §8c (xlsx rows 435–466) — produce `cooling_monthly_kwh` for all 12 months. All 6 Elmhurst fixtures = 0 (no air conditioning lodged). Should be the smallest section yet.
|
||||
|
||||
Worksheet lines you must close:
|
||||
- (100)m heat loss rate L_m (cooling) — recomputed via §7 chain at cooling Ti (≈ same as heating MIT for our fixtures)
|
||||
- (101)m utilisation factor for loss η_loss (cooling Table 10a, formula differs from Table 9a)
|
||||
- (102)m useful loss = η × L
|
||||
- (103)m gains (excluding pumps/fans Table 5a per spec — see §6 of the SAP10.2 PDF)
|
||||
- (104)m cooling requirement Q_cool
|
||||
- ∑(104) over Jun-Aug only (cooling inclusion rule — June to August, disregarding September to May)
|
||||
- (105) cooled-area fraction f_C (almost always 0 in RdSAP — set 0 for all 6 fixtures)
|
||||
- (106)m × intermittency factor (Table 10b)
|
||||
- (107)m space cooling requirement after f_C
|
||||
- (108) cooling per m² = (107) / TFA
|
||||
Implement §10a (xlsx rows ~614–740) — produce per-end-use cost lines (240)..(255) via a new `worksheet/fuel_cost.py` orchestrator. Refactor `calculator.py`'s inline cost arithmetic to delegate to the orchestrator; expose worksheet-line-shape `FuelCostResult` on `CalculatorInputs`. Six Elmhurst fixtures already cover this implicitly via `total_fuel_cost_gbp` — just need to surface the per-line refs.
|
||||
|
||||
### B.2 SAP10.2 spec anchors (already extracted)
|
||||
### B.2 SAP10.2 spec anchors (lines 8044–8084)
|
||||
|
||||
From the spec at `/tmp/sap102.txt`:
|
||||
- Line 10320: `Qcool = 0.024 × (Gm - mLm) × nm × fcool × fintermittent` (note the SIGN — gains − losses, opposite of heating)
|
||||
- Line 10321: `Set Qcool to zero if negative or less than 1 kWh.`
|
||||
- Line 10325: `Include the cooling requirements for each month from June to August (disregarding September to May).`
|
||||
| Line ref | Description |
|
||||
|---|---|
|
||||
| (240a)-(240e) | Space heating main 1 cost. Off-peak: (240a) Table 12a high-rate fraction, (240b) low-rate = 1-(240a), (240c) high-rate cost = (211)×(240a)×price×0.01, (240d) low-rate cost, (240e) collapses to other-fuel single-rate cost. |
|
||||
| (241a)-(241e) | Space heating main 2 cost — zero-branch in scope A (no two-main fixtures). |
|
||||
| (242a)-(242e) | Secondary heating cost. |
|
||||
| (243)-(247) | Water heating cost. (243) high-rate fraction (Table 12 — note: not Table 12a; HW uses a different row). (244) = 1-(243). (245)/(246) high/low cost. (247) other-fuel single-rate. |
|
||||
| (247a) | Instant electric shower (64a) × price. Zero in all 6 fixtures. |
|
||||
| (248) | Space cooling cost (221) × price. Zero in all 6 fixtures (§8c f_C=0). |
|
||||
| (249) | Pumps/fans/keep-hot cost (231) × price. Spec note: if off-peak, list each (230a)..(230g) separately by Table 12a. |
|
||||
| (250) | Lighting cost (232) × price. |
|
||||
| (251) | Additional standing charges (Table 12 note (a) — currently NOT applied for rating purposes for standard electricity tariff). |
|
||||
| (252) | Energy saving/generation Appendix M/N items (PV credit, micro-CHP). Currently `pv_credit` is computed inline at calculator.py:282-283. |
|
||||
| (253)/(254) | Appendix Q items (saved/used). Zero in our corpus. |
|
||||
| (255) | Total = sum of all (240).. through (254). Currently `total_cost = max(0.0, main_heating_cost + ... − pv_credit)` at calculator.py:296. |
|
||||
|
||||
Table 10a (utilisation factor for cooling) is a separate formula from Table 9a heating η — page 186 of SAP10.2 spec.
|
||||
Table 10b (intermittency factor) — page 187.
|
||||
Table 10c (SEER for cooling system fuel calc) — page 187.
|
||||
### B.3 Current state in code
|
||||
|
||||
### B.3 Existing state
|
||||
- No `space_cooling.py` module exists.
|
||||
- Calculator has no cooling fields.
|
||||
- All 6 Elmhurst fixtures have `has_fixed_air_conditioning=False` → no cooling system → cooling fuel = 0.
|
||||
|
||||
### B.4 Likely shape (mirror §8 pattern)
|
||||
Calculator does the cost arithmetic inline in `calculate_sap_from_inputs` (calculator.py:295-305):
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True)
|
||||
class SpaceCoolingResult:
|
||||
heat_loss_rate_monthly_w: tuple[float, ...] # (100)
|
||||
utilisation_factor_loss_monthly: tuple[float, ...] # (101) — Table 10a
|
||||
useful_loss_monthly_w: tuple[float, ...] # (102)
|
||||
cooling_gains_monthly_w: tuple[float, ...] # (103)
|
||||
cooling_requirement_monthly_kwh: tuple[float, ...] # (104)
|
||||
intermittency_factor_monthly: tuple[float, ...] # (106)
|
||||
space_cooling_monthly_kwh: tuple[float, ...] # (107) ← what the calculator consumes
|
||||
space_cooling_kwh_per_yr: float
|
||||
space_cooling_per_m2_kwh: float # (108)
|
||||
|
||||
|
||||
def space_cooling_monthly_kwh(
|
||||
*,
|
||||
monthly_heat_transfer_coefficient_w_per_k: tuple[float, ...],
|
||||
monthly_internal_temperature_c: tuple[float, ...],
|
||||
monthly_external_temperature_c: tuple[float, ...],
|
||||
monthly_total_gains_w: tuple[float, ...],
|
||||
total_floor_area_m2: float,
|
||||
cooled_area_fraction: float = 0.0, # f_C — almost always 0 for RdSAP
|
||||
# Table 10b intermittency factor by control type, default to 0.25 (intermittent)
|
||||
intermittency_factor: float = 0.25,
|
||||
) -> SpaceCoolingResult: ...
|
||||
main_heating_cost = main_fuel_kwh * inputs.space_heating_fuel_cost_gbp_per_kwh
|
||||
secondary_heating_cost = secondary_fuel_kwh * inputs.secondary_heating_fuel_cost_gbp_per_kwh
|
||||
hot_water_cost = inputs.hot_water_kwh_per_yr * inputs.hot_water_fuel_cost_gbp_per_kwh
|
||||
pumps_fans_cost = inputs.pumps_fans_kwh_per_yr * inputs.other_fuel_cost_gbp_per_kwh
|
||||
lighting_cost = inputs.lighting_kwh_per_yr * inputs.other_fuel_cost_gbp_per_kwh
|
||||
total_cost = max(0.0, main_heating_cost + secondary_heating_cost + hot_water_cost + pumps_fans_cost + lighting_cost - pv_credit)
|
||||
```
|
||||
|
||||
### B.5 Conformance budget
|
||||
**All 6 fixtures = 0 cooling** for every line. So conformance is trivial: assert every per-line tuple is zero. The interesting test is the **synthetic positive case** — pin a non-zero cooled-area fraction + warm summer month + verify Q_cool emerges, then verify with `f_C = 0` it collapses to zero.
|
||||
Missing relative to spec:
|
||||
1. **Table 12a high-rate fractions** for off-peak tariffs (electric storage / heat pumps) — currently `space_heating_fuel_cost_gbp_per_kwh` is a single scalar; for off-peak certs the spec splits into high-rate × fraction + low-rate × (1-fraction). The cert_to_inputs `_off_peak_main_heating_cost(...)` path collapses both into the effective per-kWh cost.
|
||||
2. **(251) Standing charges** — Table 12 note (a) excludes standing charges from the rating ECF in the standard tariff case but includes them when a non-standard tariff is selected. Currently not applied at all.
|
||||
3. **(252) Per-row Appendix M/N** — currently single `pv_credit` scalar. Spec wants a row per (233a)-(235d) (PV / wind / hydro / micro-CHP).
|
||||
4. **Worksheet-line surfacing** — none of (240a)..(254) are exposed individually. Only the (255) total bubbles up via `total_fuel_cost_gbp`.
|
||||
|
||||
### B.4 Likely shape (mirror §9a pattern — composite slot on CalculatorInputs)
|
||||
|
||||
```python
|
||||
# packages/domain/src/domain/sap/worksheet/fuel_cost.py
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FuelCostResult:
|
||||
"""SAP 10.2 §10a worksheet line refs (240)..(255)."""
|
||||
# Main system 1 (off-peak split)
|
||||
main_1_high_rate_fraction: float # (240a)
|
||||
main_1_low_rate_fraction: float # (240b)
|
||||
main_1_high_rate_cost_gbp: float # (240c)
|
||||
main_1_low_rate_cost_gbp: float # (240d)
|
||||
main_1_other_fuel_cost_gbp: float # (240e)
|
||||
main_1_total_cost_gbp: float # (240) — sum
|
||||
# Main system 2 — zero-branch placeholders
|
||||
main_2_high_rate_fraction: float # (241a)
|
||||
main_2_low_rate_fraction: float # (241b)
|
||||
main_2_high_rate_cost_gbp: float # (241c)
|
||||
main_2_low_rate_cost_gbp: float # (241d)
|
||||
main_2_other_fuel_cost_gbp: float # (241e)
|
||||
main_2_total_cost_gbp: float # (241)
|
||||
# Secondary
|
||||
secondary_high_rate_fraction: float # (242a)
|
||||
... # (242b)..(242e)
|
||||
secondary_total_cost_gbp: float # (242)
|
||||
# Water heating
|
||||
water_high_rate_fraction: float # (243)
|
||||
water_low_rate_fraction: float # (244)
|
||||
water_high_rate_cost_gbp: float # (245)
|
||||
water_low_rate_cost_gbp: float # (246)
|
||||
water_other_fuel_cost_gbp: float # (247)
|
||||
water_total_cost_gbp: float # sum
|
||||
# Other end-use rows
|
||||
instant_shower_cost_gbp: float # (247a)
|
||||
space_cooling_cost_gbp: float # (248)
|
||||
pumps_fans_cost_gbp: float # (249)
|
||||
lighting_cost_gbp: float # (250)
|
||||
standing_charges_gbp: float # (251) — zero in standard tariff
|
||||
pv_credit_gbp: float # (252) — keep negative
|
||||
appendix_q_saved_gbp: float # (253) — zero in corpus
|
||||
appendix_q_used_gbp: float # (254) — zero in corpus
|
||||
total_cost_gbp: float # (255) — Σ minus PV
|
||||
```
|
||||
|
||||
Slice plan suggestion (mirror §9a 3-slice cadence):
|
||||
|
||||
```
|
||||
Slice 1 — FuelCostResult + fuel_cost_from_energy_requirements orchestrator (no off-peak split:
|
||||
assume single rate; all 28 fields populated). Synthetic test pins (255) total matches the
|
||||
calculator's current inline math.
|
||||
Slice 2 — CalculatorInputs.fuel_cost composite slot + cert_to_inputs wiring + calculator delegation
|
||||
(calculator.py:295-305 becomes `total_cost = inputs.fuel_cost.total_cost_gbp`). Round-trip
|
||||
test asserts SapResult.total_fuel_cost_gbp unchanged from pre-refactor.
|
||||
Slice 3 — Table 12a high-rate fraction lookup + off-peak split for main heating / HW / pumps/fans.
|
||||
Touches the 4 cert paths that lodge off-peak meter_type. Spec (251) standing charges
|
||||
deferred (Table 12 note (a) — needs separate slice gated on the standard-tariff rule).
|
||||
Slice 4 — docs: SPEC_COVERAGE §10a row + slice progress table.
|
||||
```
|
||||
|
||||
### B.5 What to defer (name as remaining-work in slice 4)
|
||||
|
||||
- **(251) Additional standing charges** — Table 12 note (a) rule. Currently zero. Needs separate slice covering when standing charges enter the rating ECF.
|
||||
- **(252) Per-Appendix-M/N row split** — keep `pv_credit_gbp` as a single scalar in scope A; expand to per-line (233a)..(235d) when wind / hydro / micro-CHP fixtures land.
|
||||
- **(253)/(254) Appendix Q** — zero placeholders; populate when a Q-item cert lands.
|
||||
- **(247a) Instant electric shower** — line (64a) currently always 0 from `_hot_water_fuel_kwh_per_yr`. Wire when electric-shower cert lodges.
|
||||
- **Off-peak per-row (230a)..(230g) split** — spec line 8076 says "if off-peak tariff, list each of (230a) to (230g) separately and apply fuel price according to Table 12a". Out of scope; requires the §9a (230a)-(230h) Table 4f pumps/fans breakdown first.
|
||||
|
||||
### B.6 Calculator coupling
|
||||
After orchestrator: add `CalculatorInputs.space_cooling_monthly_kwh: tuple[float, ...]` field (defaults `(0.0,) * 12` for backwards-compat). The calculator's `MonthlyEntry` already has `space_heat_requirement_kwh`; either add `space_cool_requirement_kwh` or just expose the annual total on `SapResult`. Mirror §8 pattern.
|
||||
|
||||
### B.7 Slice plan (suggested)
|
||||
Mirror §9a path (i) — cert_to_inputs precompute. `CalculatorInputs.fuel_cost: FuelCostResult` composite slot. `calculate_sap_from_inputs` collapses to:
|
||||
|
||||
```
|
||||
Slice 1 — SpaceCoolingResult + space_cooling_monthly_kwh orchestrator
|
||||
(Table 10a η_loss + Jun-Aug inclusion rule); synthetic positive
|
||||
test (non-zero f_C, hot July) + synthetic zero test (f_C=0)
|
||||
Slice 2 — Per-fixture SECTION_8C_COOLED_AREA_FRACTION=0 constants +
|
||||
ALL_FIXTURES e2e (every line tuple = 0 across all 6 fixtures)
|
||||
Slice 3 — CalculatorInputs.space_cooling_monthly_kwh + cert_to_inputs
|
||||
wiring (atomic) + drop legacy if any
|
||||
Slice 4 — SPEC_COVERAGE §8c row + slice progress table
|
||||
```python
|
||||
fuel_cost = inputs.fuel_cost
|
||||
total_cost = fuel_cost.total_cost_gbp
|
||||
# (existing co2 + primary energy paths consume `main_fuel_kwh` etc. as before — they're orthogonal)
|
||||
```
|
||||
|
||||
Smaller than §8 because there's no upstream-drift surprise to chase (all fixtures = 0).
|
||||
Existing scalar CalculatorInputs fields (`space_heating_fuel_cost_gbp_per_kwh`, etc.) stay as inputs to the orchestrator. Calculator stops reading them — symmetric with how §9a left `main_heating_efficiency` in place but stopped using it after `energy_requirements` landed.
|
||||
|
||||
### B.7 Tests
|
||||
|
||||
- **Synthetic** unit tests on the orchestrator: single-rate cost = kWh × £/kWh × 1.0; off-peak split honours Table 12a fractions; PV credit subtracts at the spec-correct precedence; zero-branch lines stay zero.
|
||||
- **6-fixture ALL_FIXTURES** at orchestrator level — call with fixture LINE_211 / LINE_215 / LINE_219 / lighting / pumps_fans pins + SAP_10_2_SPEC_PRICES, assert (255) total matches PDF cost. **Will partially fail for 000490** (spec-version drift). Recommend pinning per-fixture LINE_255_TOTAL_FUEL_COST_GBP at the **current state** (after PCDB) so future improvements show as tightening; document the 000490 spec-version delta in the docstring as already done for the e2e SAP score test.
|
||||
- **Cert-round-trip** in `test_cert_to_inputs.py`: build a typical cert, run cert_to_inputs → calculator, assert `inputs.fuel_cost.total_cost_gbp == result.total_fuel_cost_gbp` to float equality (pre-refactor parity).
|
||||
|
||||
### B.8 Don't list
|
||||
|
||||
- Do **not** touch the existing fuel-cost-per-kWh wiring in `cert_to_inputs._space_heating_fuel_cost_gbp_per_kwh / _hot_water_fuel_cost_gbp_per_kwh / _other_fuel_cost_gbp_per_kwh`. These are the orchestrator's *inputs*; keep them.
|
||||
- Do **not** modify SAP_10_2_SPEC_PRICES — the Table 12 prices are spec-current. The 000490 cost gap is spec-version drift, not a price-table bug.
|
||||
- Do **not** loosen e2e tolerance ceilings on 000490 / 000474 unless the refactor breaks parity. The current ceilings (6 / 2) reflect post-PCDB state and should hold across a pure refactor.
|
||||
|
||||
---
|
||||
|
||||
## §C — Codebase pointers
|
||||
|
||||
- §8 orchestrator (close template): [packages/domain/src/domain/sap/worksheet/space_heating.py](../../packages/domain/src/domain/sap/worksheet/space_heating.py)
|
||||
- §7 orchestrator (per-zone η pattern): [packages/domain/src/domain/sap/worksheet/mean_internal_temperature.py](../../packages/domain/src/domain/sap/worksheet/mean_internal_temperature.py)
|
||||
- cert→inputs adapter: [packages/domain/src/domain/sap/rdsap/cert_to_inputs.py](../../packages/domain/src/domain/sap/rdsap/cert_to_inputs.py) (look for the chain at lines 870-960 for how §5/§6/§7/§8 stack)
|
||||
- E2e SAP-score test: [packages/domain/src/domain/sap/worksheet/tests/test_e2e_elmhurst_sap_score.py](../../packages/domain/src/domain/sap/worksheet/tests/test_e2e_elmhurst_sap_score.py)
|
||||
- §8 fixture pins (template): `_elmhurst_worksheet_000490.py` `SECTION_7_*` + `LINE_85..LINE_99` constants
|
||||
- §8 ALL_FIXTURES test: [test_space_heating.py](../../packages/domain/src/domain/sap/worksheet/tests/test_space_heating.py)
|
||||
- Extract script template: `/tmp/extract_section8.py` (rewrite for §8c — only ~30 lines)
|
||||
- §9a orchestrator template: [packages/domain/src/domain/sap/worksheet/energy_requirements.py](../../packages/domain/src/domain/sap/worksheet/energy_requirements.py) — 16-field dataclass + free function. Mirror this shape for `FuelCostResult`.
|
||||
- Calculator inline cost code (target of refactor): [packages/domain/src/domain/sap/calculator.py:280-305](../../packages/domain/src/domain/sap/calculator.py#L280-L305).
|
||||
- cert_to_inputs cost-per-kWh resolution: [packages/domain/src/domain/sap/rdsap/cert_to_inputs.py](../../packages/domain/src/domain/sap/rdsap/cert_to_inputs.py) — search for `_space_heating_fuel_cost_gbp_per_kwh` etc.
|
||||
- Table 12 lookups: [packages/domain/src/domain/sap/tables/table_12.py](../../packages/domain/src/domain/sap/tables/table_12.py).
|
||||
- Existing PCDB integration (recent precedent for new ETL/lookup pattern): [packages/domain/src/domain/sap/tables/pcdb/](../../packages/domain/src/domain/sap/tables/pcdb/).
|
||||
- §9a tests: [packages/domain/src/domain/sap/worksheet/tests/test_energy_requirements.py](../../packages/domain/src/domain/sap/worksheet/tests/test_energy_requirements.py).
|
||||
- e2e SAP score tests: [packages/domain/src/domain/sap/worksheet/tests/test_e2e_elmhurst_sap_score.py](../../packages/domain/src/domain/sap/worksheet/tests/test_e2e_elmhurst_sap_score.py).
|
||||
- SPEC_COVERAGE: [docs/sap-spec/SPEC_COVERAGE.md](SPEC_COVERAGE.md).
|
||||
- ADR-0009 / ADR-0010: [docs/adr/0009-deterministic-sap-calculator.md](../adr/0009-deterministic-sap-calculator.md), [docs/adr/0010-sap10-calculator-spec-target-and-validation.md](../adr/0010-sap10-calculator-spec-target-and-validation.md).
|
||||
|
||||
## §D — Commit chain to inspect
|
||||
---
|
||||
|
||||
```bash
|
||||
git log --oneline --grep '§8 slice'
|
||||
# 9113f30a §8 slice 1 — orchestrator + summer clamp
|
||||
# 1f078af7 §8 slice 2 — 6-fixture conformance
|
||||
# f6ab7626 §8 slice 3 — calculator + cert_to_inputs wiring
|
||||
# bb827803 docs — SPEC_COVERAGE §8 + slice progress
|
||||
```
|
||||
|
||||
## §E — Skills
|
||||
## §D — Skills
|
||||
|
||||
The dev container ships `/grill-me`, `/tdd`, `/caveman`. Default flow:
|
||||
|
||||
```
|
||||
/grill-me → walk down design tree
|
||||
/tdd implement §8c Space cooling → one test → one impl → repeat
|
||||
/grill-me → walk down design tree, recommend scope cuts
|
||||
/tdd implement §10a Fuel costs → one test → one impl → repeat
|
||||
```
|
||||
|
||||
User invokes `/grill-me` first when ready. Audit + surface unknowns first; wait for `/grill-me`.
|
||||
User invokes `/grill-me` first when ready. Audit B.5 / B.7 / B.8 + surface unknowns first; wait for `/grill-me`.
|
||||
|
||||
## §F — Definitely do NOT
|
||||
---
|
||||
|
||||
- Do **not** claim "wiring water_heating_from_cert is the easy win" — that ship has sailed (see §A.2).
|
||||
- Do **not** update the 000490 `_currently_within_3_points` ceiling unless Ticket A actually moves the SAP score.
|
||||
## §E — Definitely do NOT
|
||||
|
||||
- Do **not** touch the §10a-adjacent §11a (256-258) rating chain — already Full.
|
||||
- Do **not** scan more than ~50 lines of spec PDF without asking the user for the specific table/page.
|
||||
- Do **not** modify existing fixture `build_epc()` to change boiler / PCDB lodgement — only 000490 + 000474 carry PCDB pointers per real PDF lodgement. Other 4 fixtures don't lodge PCDB and stay on Table 4a defaults until the user provides a PCDB code.
|
||||
- Do **not** loosen test tolerances to make tests pass — ask the user.
|
||||
- Do **not** scan more than ~50 lines of a spec PDF before asking the user for the specific table/page.
|
||||
- Do **not** modify existing fixture `build_epc()` functions.
|
||||
- Do **not** invoke `/ultrareview` yourself.
|
||||
- Do **not** widen the e2e SAP-score ceiling on 000474 (currently 2). It's tight on purpose post-PCDB.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
Tracks which sections of the SAP 10.2 specification are implemented in `packages/domain/src/domain/sap/`. Per ADR-0009 the calculator is built from the spec, not reverse-engineered from cert data. This doc is the worksheet-driven roadmap for what remains.
|
||||
|
||||
Updated 2026-05-21 after §8c (slices `cf28eec4`…`f3797066`), §8f (`43cc16bc`), §9a single-main slices (`2b5fc6a5`…`380b6781`), and PCDB Table 105 integration (`fe04cd3a`…`a104dd55`).
|
||||
Updated 2026-05-21 after §8c (slices `cf28eec4`…`f3797066`), §8f (`43cc16bc`), §9a single-main slices (`2b5fc6a5`…`380b6781`), PCDB Table 105 integration (`fe04cd3a`…`a104dd55`), and PCDB fixture lodgement (`1b43c95c`…`15d6b781`).
|
||||
|
||||
The canonical SAP10.2 algorithm lives in [`2026-05-19-17-18 RdSap10Worksheet.xlsx`](../../2026-05-19-17-18%20RdSap10Worksheet.xlsx) at the repo root — each line ref `(1)..(486)` maps to a cell. The worksheet sub-modules under `packages/domain/src/domain/sap/worksheet/` implement those line refs directly; Elmhurst worksheets validate end-to-end via `tests/_elmhurst_worksheet_*.py`.
|
||||
|
||||
|
|
@ -302,7 +302,9 @@ Status now: 100-cert MAE 4.49, 300-cert MAE 5.45, bias near zero (±0.2). Worksh
|
|||
| ETL parser + 8 tests | `domain.sap.tables.pcdb.parser`: typed `GasOilBoilerRecord` + `RawPcdbRecord`. Ground-truth verified against ncm-pcdb.org.uk for Baxi 000098 / Potterton 000619 / Saunier 000732. Handles latin-1 encoding (degree-sign in addresses), `'obsolete'` status string, `'>70kW'` range indicator. | ✅ | `fe04cd3a` |
|
||||
| ETL `run_etl` writes 8 JSONL files | One newline-delimited JSON file per table (105 typed; 122/143/313/353/362/391/506 raw). 17MB total. Runnable via `PYTHONPATH=packages/domain/src python -m domain.sap.tables.pcdb.etl`. Idempotent; commit JSONL alongside source `pcdb10.dat`. | ✅ | `fe04cd3a` |
|
||||
| Runtime lookup `gas_oil_boiler_record(pcdb_id)` | `domain.sap.tables.pcdb` loads Table 105 NDJSON at import; ~50ms one-off, O(1) lookups thereafter. Returns None for unknown PCDB IDs → caller falls back to Table 4a/4b cascade. | ✅ | `23678228` |
|
||||
| cert_to_inputs precedence (Table 105 only) | Appendix D2.1: PCDB winter overrides `main_heating_efficiency`; PCDB summer overrides `water_efficiency` scalar. Heat-network DLF override still wins where applicable. None of the 6 Elmhurst fixtures lodge a PCDB pointer; corpus golden certs that do see real efficiency changes (golden tolerance widened ±5 → ±7). | ✅ | `a104dd55` |
|
||||
| cert_to_inputs precedence (Table 105 only) | Appendix D2.1: PCDB winter overrides `main_heating_efficiency`; PCDB summer overrides `water_efficiency` scalar. Heat-network DLF override still wins where applicable. None of the 6 Elmhurst fixtures lodge a PCDB pointer initially; corpus golden certs that do see real efficiency changes (golden tolerance widened ±5 → ±7). | ✅ | `a104dd55` |
|
||||
| Elmhurst fixture PCDB lodgement: 000490 + 000474 | `_elmhurst_worksheet_000490.build_epc()` lodges `main_heating_index_number=10328` (Vaillant Ecotec Pro 28, winter 88.2%); `_elmhurst_worksheet_000474.build_epc()` lodges `16839` (Vaillant ecoTEC pro 28 VUW GB 286/5-3, winter 88.7%). 000474 e2e ceiling tightens 7 → 2 SAP points; 000490 widens 3 → 6 (spec-version drift on fuel cost — pre-amendment cert). `make_main_heating_detail` extended with `main_heating_index_number` / `main_heating_data_source` / `sap_main_heating_code` kwargs. | ✅ | `1b43c95c`, `7d4f3d78` |
|
||||
| API → domain mapper-chain regression test | `test_api_to_domain_mapper_preserves_main_heating_index_number` parametrised over the 4 PCDB-listed golden corpus certs. Pins that `EpcPropertyDataMapper.from_api_response` → `cert_to_inputs` chain surfaces the PCDB pointer + applies PCDB winter efficiency. Confirms no domain-model changes were needed — `MainHeatingDetail.main_heating_index_number` has existed since schema 17_1 and all mapper paths from 17_1+ pass it through verbatim. | ✅ | `15d6b781` |
|
||||
| Heat pump Appendix N cascade via Table 362 | Apply Appendix N in-use factor (×0.95), MCS installation factor (×1.39 for GSHP MCS-installed), design flow temperature adjustment. Replace SCOP 2.30 Table 4a fallback for `main_category=4`. | ⏸ deferred (typed Table 362 parser + Appendix N cascade) | — |
|
||||
| Equation D1 monthly water heating cascade | Spec D2.1 (2): η_water_monthly = (Q_space + Q_water) / (Q_space/winter + Q_water/summer). Promotes water_eff scalar → 12-tuple. Refactor of `_hot_water_fuel_kwh_per_yr`. | ⏸ deferred (single-digit-% HW kWh precision for combi boilers) | — |
|
||||
| Solid fuel boiler precedence via Table 122 | PCDB override for `main_category=3` (solid fuel) — typed parser + wiring. | ⏸ deferred | — |
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue