Commit graph

4952 commits

Author SHA1 Message Date
Khalim Conn-Kowlessar
fe04cd3a35 pcdb slice 1: pcdb10.dat ETL → 8 per-table NDJSON files + parser + 8 tests
Parser/ETL for BRE PCDB pcdb10.dat (April 2026 revision). domain.sap.tables.pcdb.parser exposes parse_table_105 (typed GasOilBoilerRecord with brand/model/winter+summer+comparative-HW efficiency/output kW/final year) plus parse_table_raw for generic positional ingestion (pcdb_id + raw row only). etl.py runs the full ETL: reads pcdb10.dat as latin-1, writes per-table .jsonl files under docs/sap-spec/. Idempotent; runnable via PYTHONPATH=packages/domain/src python -m domain.sap.tables.pcdb.etl.

Per Q1=D grilling: all 8 tables of interest ingested — 105 (Gas/Oil Boilers, typed) plus 122/143/313/353/362/391/506 (raw). Per-table typed refinement deferred to the follow-up slices that wire each table's cert-side cascade. Per Q3=B: typed fields decode against ncm-pcdb.org.uk ground-truth records (Baxi 000098 + Potterton 000619 + Saunier Duval 000732 verified by user); full raw row preserved on every record for forensics. Per Q2 user choice: NDJSON .jsonl format chosen over indented JSON to keep diff-friendliness while halving file size (17MB total vs 31MB pretty-printed).

Edge cases handled: latin-1 encoding (manufacturer addresses carry the degree sign), `'obsolete'` status string where a year would otherwise live, `'>70kW'` range indicator on output-power fields — non-numeric values fall to None with the raw string preserved on `raw`.

Slice 2 lands the domain.sap.tables.pcdb runtime lookup module (per-table by-pcdb-id dicts loaded at import time). Slice 3 wires Table 105 into cert_to_inputs.main_heating_efficiency / water_efficiency precedence cascades per Q5=B (space heating + water heating scalar override; equation D1 monthly + Appendix N HP factor + FGHRS/WWHRS/HIU deferred).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 09:43:41 +00:00
Khalim Conn-Kowlessar
53c393bfba docs: SPEC_COVERAGE §9a row + slice progress table + PCDB gap-list update
Adds §9a as a first-class row (consistent with §8c/§8f sub-section precedent). The §9 row updates from "Partial — single main only, no Table 11 secondary" to "Full (single-main + Table 11 secondary)" with a deferred list naming the four remaining slices: two-main system, cooling SEER, Table 4f pumps/fans breakdown, Appendix Q.

The PCDB gap-list entry (item 1) updates to flag §9a ALL_FIXTURES PDF-derived LINE_206/(211)/(215) pinning as blocked. The 88.2% figure that surfaced from a previous agent's notes cannot be verified without PCDB — corrected the narrative accordingly.

Per-§9a slice progress table mirrors §8c/§8f structure with line refs (201)..(238), commit shorthands, and a Remaining work list naming six follow-ups (PCDB integration, two-main, cooling SEER, Table 4f, Appendix Q, (238) on SapResult).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 08:41:23 +00:00
Khalim Conn-Kowlessar
380b6781e8 §9a slice 2: CalculatorInputs.energy_requirements + cert_to_inputs wiring + SapResult fields + _solve_month refactor (atomic)
Path (i) — cert_to_inputs precompute. cert_to_inputs calls space_heating_fuel_monthly_kwh from local SpaceHeatingResult + Table 11 secondary fraction + per-system efficiencies; stashes the EnergyRequirementsResult on new `CalculatorInputs.energy_requirements` composite slot (default = _ZERO_ENERGY_REQUIREMENTS_RESULT).

_solve_month stops doing q/η inline — reads precomputed (211)m / (215)m fuel tuples directly via `inputs.energy_requirements.{main_1,secondary}_fuel_monthly_kwh[m-1]`. Existing `CalculatorInputs.main_heating_efficiency` / `.secondary_heating_efficiency` / `.secondary_heating_fraction` stay on the dataclass as inputs to the orchestrator (now redundant for the calculator's read path; kept for audit + backwards compat).

SapResult gains flat `main_2_heating_fuel_kwh_per_yr` and `space_cooling_fuel_kwh_per_yr` scalars — both zero in scope A, populated by future two-main + Table 10c SEER slices.

Round-trip test pins `inputs.energy_requirements.main_1_fuel_kwh_per_yr == result.main_heating_fuel_kwh_per_yr` to float equality (no rounding from the cert→inputs hop) and asserts scope-A scalars stay zero. PDF-derived ALL_FIXTURES pinning (Q5(α) grilling decision) blocked on PCDB integration — flagged in PCDB gap-list entry.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 08:39:26 +00:00
Khalim Conn-Kowlessar
2b5fc6a575 §9a slice 1: space_heating_fuel_monthly_kwh orchestrator + EnergyRequirementsResult + 4 synthetic tests
Spec lines 7909-7953 (worksheet block §9a). Composes per-system fuel kWh from (98c)m, Table 11 secondary fraction (201), and per-system efficiencies (206)/(207)/(208). Formula: (211)m = (98c)m × (204) × 100 / (206) where (204) collapses to (202) = 1 − (201) in scope A's single-main case.

EnergyRequirementsResult dataclass mirrors the full §9a worksheet shape with 16 fields including (203)/(205)/(207)/(209)/(213)/(221) zero-branch placeholders — worksheet-shape-fidelity precedent (§8c Q4/Q7/Q9, §8f Q3 grilling). First multi-main / fixed-AC / PCDB cert triggers the slices that populate them.

Synthetic tests: (a) single-main no-secondary 80% efficiency Σ(211)=Σ(98c)/0.8, (b) Table 11 secondary fraction split (201)=0.1 produces (211)+(215) at correct ratios, (c) summer-clamp zeros from §8 (98c)m propagate through linearly to (211)m/(215)m, (d) scope-A two-main + cooling-fuel fields remain zero regardless of inputs.

Calculator + cert_to_inputs wiring lands in slice 3. PDF-derived ALL_FIXTURES pins for slice 2 deferred until PCDB integration grounds LINE_206 (Manufacturer-declared boiler efficiency); flagged in the SPEC_COVERAGE PCDB gap-list entry.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 08:33:22 +00:00
Khalim Conn-Kowlessar
05d9dc73f8 docs: SPEC_COVERAGE §8f row + slice progress table
Adds §8f as a first-class row in the Sections §§1–13 table (consistent with §8c precedent for §-letter sub-sections). The §11 row updates from "Not implemented" to Partial: the (109) formula function now exists in `worksheet/fabric_energy_efficiency.py`, but the §11 compliance-conditions worksheet rerun (different ventilation / HW / lighting / gains column per spec lines 2152-2164) is deferred.

Per-§8f slice progress table mirrors §8c's: line ref (109), commit shorthand, and a Remaining work list naming the two follow-ups (§11 compliance conditions + Σ(98a) ≠ Σ(98c) regression coverage when Appendix H solar space heating lands).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 08:13:38 +00:00
Khalim Conn-Kowlessar
43cc16bc65 §8f slice 1: fabric_energy_efficiency_kwh_per_m2_yr + 6-fixture conformance + atomic wiring
Spec line 7898: (109) = (98a) ÷ (4) + (108). New `worksheet/fabric_energy_efficiency.py` exposes a free function (no dataclass — single scalar output); `SpaceHeatingResult.space_heating_requirement_kwh_per_yr` (Σ(98a)) added so the spec literal — pre Appendix H solar offset — is the FEE input, not Σ(98c).

cert_to_inputs computes FEE from local SpaceHeatingResult + SpaceCoolingResult and passes via new `CalculatorInputs.fabric_energy_efficiency_kwh_per_m2_yr` (default 0.0 for backwards compat); calculator pass-through to `SapResult.fabric_energy_efficiency_kwh_per_m2_yr`. MonthlyEntry untouched — FEE has no per-month physics, only an annual scalar.

Six Elmhurst fixtures all (98b)=0 + (108)=0 → LINE_109 = LINE_99 exactly; ALL_FIXTURES asserts within 5e-3 tolerance (display-rounding floor inherited from LINE_98C_ANNUAL_KWH pins). Round-trip test asserts SapResult.fee equals space_heating_kwh_per_yr / TFA for the SAP10 minimal cert.

§11 compliance conditions (different ventilation / HW / lighting / gains column) are deferred — the FEE here is computed off rating-conditions inputs as a transparency output. Future §11 slice invokes the same function with §11-conditions upstream values.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 08:12:45 +00:00
Khalim Conn-Kowlessar
c9f15a2e0e docs: SPEC_COVERAGE §8c row + slice progress table
Adds §8c as a first-class row in the Sections §§1–13 table per Q13 grilling (sub-sections are first-class — §8c, §8f). The §10 spec heading collapses into a pointer at §8c since they describe the same xlsx block.

Per-§8c slice progress table mirrors §8's: line refs (100)..(108), commit shorthands, and a Remaining work list naming the three follow-up slices the first cooling-enabled cert triggers (Table 5a exclusion in cooling gains, RdSAP cooled-area defaulting, Table 10c SEER fuel/cost cascade).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 08:00:01 +00:00
Khalim Conn-Kowlessar
f37970666e §8c slice 3: CalculatorInputs + MonthlyEntry + SapResult + cert_to_inputs wiring (atomic)
Full §8 mirror per Q9 grilling: CalculatorInputs.space_cooling_monthly_kwh (default (0,)*12), MonthlyEntry.space_cool_requirement_kwh, SapResult.space_cooling_kwh_per_yr. _solve_month indexes into the cooling tuple and calculate_sap_from_inputs sums the per-month entries.

cert_to_inputs calls space_cooling_monthly_kwh with f_C=0 and cooling_gains=(0,)*12 — RdSAP convention since the cert never lodges cooled-area data and every `has_fixed_air_conditioning=False` cert collapses (107) to zero. The first cooling-enabled fixture needs a cooling_gains_from_cert helper + RdSAP cooled-area defaulting rule (deferred — SPEC_COVERAGE §8c row).

Round-trip test pins inputs.space_cooling_monthly_kwh = (0,)*12, result.space_cooling_kwh_per_yr = 0.0, and every MonthlyEntry.space_cool_requirement_kwh = 0.0 for a typical SAP10 minimal cert.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 07:58:34 +00:00
Khalim Conn-Kowlessar
3b9fa936f0 §8c slice 2: 6-fixture ALL_FIXTURES conformance (all-zero) with shared template constants
Shared SECTION_8C_ALL_ZERO_MONTHLY / SECTION_8C_ETA_LOSS_ALL_ONE / SECTION_8C_INTERMITTENCY_MONTHLY constants live in _elmhurst_fixtures.py; each of the 6 fixtures references them via plain attributes plus SECTION_8C_COOLED_AREA_FRACTION = 0.0 and the per-line LINE_103/106/107/108 + LINE_107_ANNUAL_KWH pins.

(100), (102), (104) values depend on H × (24−T_e) per fixture and are not pinned here — the algebra is exercised by the synthetic-positive leaf/orchestrator tests in slice 1. First cooling-enabled cert will need a fixture pinning those lines; deferred per Q10 grilling decision.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 07:54:55 +00:00
Khalim Conn-Kowlessar
cf28eec44d §8c slice 1: space_cooling_monthly_kwh orchestrator + utilisation_factor_loss leaf + 7 tests
Tables 10a (η_loss with γ rounding to 8 dp + L=0 sentinel) and 10b (Q_cool with Jun-Aug inclusion mask + post-f_C × f_intermittent 1-kWh clamp per spec line 10321). Internal temperature hardcoded at 24 °C per Table 10a; intermittency factor scalar in / worksheet-shape tuple out.

Synthetic positive test (γ=1 closed-form branch) hand-computes the Jul-only 4.65 kWh end-to-end; synthetic zero test pins f_C=0 collapse. Leaf tested across all three γ-branches plus the rounding boundary and the L=0 sentinel.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 07:49:00 +00:00
Khalim Conn-Kowlessar
a4dfb7a021 docs: gap-list entry for boiler/HP Manufacturer efficiency (PCDB) — 000490 +3 SAP driver
Surfaces the documented driver behind the 000490 e2e overshoot (inputs.main_heating_efficiency = 0.80 vs PDF Vaillant Ecotec Pro 0.882) as item #1 in the Prioritised gap list. Per ADR-0010 §4 this is a prerequisite — not a section-sweep slice — so closing the 000490 SAP gap waits for the PCDB seam.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 07:09:23 +00:00
Khalim Conn-Kowlessar
67af2e9b43 docs: handover for §8c Space cooling + 000490 SAP-score diagnostic
Two tickets in order for the next agent:

1. Ticket A — Investigate the 000490 +3 SAP overshoot. Corrects the
   previous agent's claim that "wiring water_heating_from_cert is the
   easy win"; that's already done. Real driver is the boiler efficiency
   cascade selecting 0.80 instead of the PDF Manufacturer-declared
   0.882 (Vaillant Ecotec Pro). Time-boxed diagnostic; flag and defer
   if expensive.

2. Ticket B — §8c Space cooling (xlsx rows 435-466, lines (100)..(108)).
   All 6 Elmhurst fixtures = 0 cooling. Small slice; mirror §8 pattern.

Includes spec anchors (Qcool formula sign, Jun-Aug inclusion rule),
codebase pointers, slice plan, and the standard "do not" list.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 23:03:15 +00:00
Khalim Conn-Kowlessar
bb827803ac docs: SPEC_COVERAGE §8 row flip to Full + slice progress table
§8 Space heating requirement: Partial → Full. Six Elmhurst fixtures
conform end-to-end on (95)..(99) at 5e-2..1e-1 kWh per month; tolerances
reflect 4-d.p. fixture pin propagation, not physics drift. Spec
inclusion rule (Jun..Sep summer clamp) now applied; 000490 SAP-score
gap to PDF=57 documented (currently 60 — closes incrementally as §3 /
§4 / §5 upstream precision tightens).

Also renumbers the §9 row to "Energy requirements per heating system"
(its SAP10.2 worksheet title) — the previous "§9 Space heating" entry
conflated §8 and §9.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 22:55:17 +00:00
Khalim Conn-Kowlessar
f6ab76269a §8 slice 3: calculator + cert_to_inputs wired to §8 orchestrator (atomic)
Adds CalculatorInputs.space_heating_monthly_kwh (98c)m. _solve_month
indexes the field directly instead of calling monthly_heat_requirement_kwh
inline — q_heat now flows from the §8 orchestrator (including the
Table 9c step 10 summer clamp).

cert_to_inputs reuses the per-month HTC + total-gains tuples already
computed for §7 plus the MIT result, and calls space_heating_monthly_kwh
to populate the new field. Single codepath; mirrors §5/§6/§7 wiring.

Synthetic test fixtures (_baseline_inputs, _baseline_dwelling) compose
§7 → §8 in sequence so the BRE worked-example trace + calculator
sanity tests stay consistent with the spec-correct chain. Tests that
override calculator inputs at runtime (`test_zero_HTC`, `test_colder_
climate`) now recompute the upstream tuples instead of trusting a
calculator-internal recompute that no longer exists.

E2e SAP-score impact (000490): SAP shifted 57 → 60. The pre-§8 match
was fortuitous compensation — missing summer clamp's +1575 kWh/yr over-
prediction cancelled small under-predictions in §3/§5. Post-§8 the
residual upstream-precision gap surfaces (+2.5% space heating, +8.4% HW
fuel, −6.3% total cost, +3 SAP integer). Test updated to "within 3
points" with full delta breakdown documented — same pattern as the
000474 "within 7 points" test. Target stays SAP=57.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 22:53:23 +00:00
Khalim Conn-Kowlessar
1f078af7db §8 slice 2: 6 Elmhurst fixtures conform on (95)..(99)
Adds LINE_95_M_USEFUL_GAINS_W, LINE_97_M_HEAT_LOSS_RATE_W,
LINE_98A_M_SPACE_HEATING_KWH, LINE_98C_M_TOTAL_SPACE_HEATING_KWH,
LINE_98C_ANNUAL_KWH, LINE_99_PER_M2_KWH to each
_elmhurst_worksheet_*.py fixture, plus an ALL_FIXTURES-parametrised
end-to-end test.

Tolerances vary by line ref per §5's per-line precedent:
  - (95) η × G          → 5e-2 W per month
  - (97) H × ΔT         → 5e-2 W per month
  - (98a)/(98c)         → 1e-1 kWh per month
  - ∑(98c) annual       → 1e-1 kWh
  - (99) per-m²         → 5e-3 kWh

Looser than §6/§7's flat 5e-3 W budget because §8 inputs (LINE_93,
LINE_94, LINE_84) carry 4-d.p. display rounding from upstream worksheets,
and §8's 0.024·31·(L−ηG) amplifies that rounding into the per-month kWh
band. The orchestrator computes in full precision; tolerances reflect
the fixture-pin precision floor, not physics error.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 22:35:12 +00:00
Khalim Conn-Kowlessar
9113f30aa8 §8 slice 1: space_heating_monthly_kwh orchestrator + summer clamp + SpaceHeatingResult
Adds the §8 orchestrator producing (95)..(99) line refs for all 12 months.
Composes the existing monthly_heat_requirement_kwh leaf with the spec
inclusion rule (Table 9c step 10 final clause):

  "Include the heating requirement for each month from October to May
   (disregarding June to September)"

Jun..Sep are zeroed regardless of computed value, on top of the per-month
value clamp (< 1 kWh / negative).

SpaceHeatingResult exposes (95) useful gains, (97) heat loss rate, (98a)
space heating requirement, (98b) solar space heating (always 0 — Appendix
H deferred), (98c) total, Σ(98c) annual + (99) per-m². All length-12
tuples + 2 scalars.

Driven by Elmhurst 000490 (98c) annual = 11183.2752 kWh to abs=5e-3 kWh.
Without the summer clamp the current calculator over-predicts annual by
+1575 kWh (+14%) on this fixture; the clamp closes the gap to spec.

Slice 3 wires CalculatorInputs.space_heating_monthly_kwh + cert_to_inputs;
calculator stops calling monthly_heat_requirement_kwh inline.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 22:11:22 +00:00
Khalim Conn-Kowlessar
eec8fb6f4f docs: SPEC_COVERAGE §7 row flip to Full + slice progress table
§7 Mean internal temperature: Partial → Full. Six Elmhurst fixtures
conform end-to-end on (85)..(94) to ≤5e-3 °C / unitless on every per-zone
line ref every month (588 monthly assertions GREEN). Slice progress
table records the chain from per-zone η fix through legacy deletion.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 21:48:29 +00:00
Khalim Conn-Kowlessar
a7f39685a0 §7 slice 5: delete legacy mean_internal_temperature_c + unused imports
Removes:
  - mean_internal_temperature_c (legacy single-η whole-dwelling fn)
  - _zone_mean_temperature_c (only used by the deleted fn)
  - calculator.py imports of mean_internal_temperature_c + utilisation_factor
    (both unused since slice 4 removed the η-iteration loop)
  - 2 obsolete tests asserting legacy single-η behaviour (coverage
    subsumed by the §7 ALL_FIXTURES parametrised e2e at slice 3)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 21:47:11 +00:00
Khalim Conn-Kowlessar
8ec9da4742 §7 slice 4: calculator + cert_to_inputs wired to §7 orchestrator (atomic)
Adds CalculatorInputs.mean_internal_temp_monthly_c (93)m and
CalculatorInputs.utilisation_factor_monthly (94)m. _solve_month indexes
directly into both — the 2-pass η fixed-point loop is gone (SAP10.2 §7
Table 9c is sequential, not iterative).

cert_to_inputs computes per-month HTC = transmission HLC + 0.33·V·(25)m,
sums (73)m + (83)m for total gains, and calls
mean_internal_temperature_monthly to populate both new fields. Single
codepath for all callers.

Synthetic test fixtures (_baseline_inputs, _baseline_dwelling) compute
their MIT + η via the §7 orchestrator too — preserves consistency with
the cert path while keeping the BRE worked-example trace asserting the
new spec-correct per-zone η values.

Atomic with cert_to_inputs (originally planned as slice 4 + slice 5):
introducing the calculator fields without populating them in cert_to_inputs
would break every cert-driven test. e2e SAP-score tests (000490 within 1
point, 000474 within 7 points) still pass with the new sequential η path.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 21:43:55 +00:00
Khalim Conn-Kowlessar
ff5d8c70c1 §7 slice 3: 6 Elmhurst fixtures conform on (85)..(94) to ≤5e-3
Adds SECTION_7_LIVING_AREA_FRACTION, SECTION_7_CONTROL_TYPE,
SECTION_7_RESPONSIVENESS, SECTION_7_THERMAL_MASS_PARAMETER_KJ_PER_M2_K
plus LINE_85..LINE_94 expected outputs across all 6 _elmhurst_worksheet_*
fixtures, and an ALL_FIXTURES-parametrised end-to-end test.

The test sources its inputs from §1-§6 fixture pins:
  (84) monthly total gains = LINE_73 + LINE_83
  (39) monthly HTC         = LINE_37 + 0.33·V·LINE_25_M
  external temp = Appendix U Table U1 region 0 (UK-avg, SAP rating pass)

Asserts every per-zone line ref to abs=5e-3 °C / unitless:
  (85) T_h1                    × 6 = 6
  (86) η_living monthly        × 12 × 6 = 72
  (87) MIT living monthly      × 12 × 6 = 72
  (88) T_h2 monthly            × 12 × 6 = 72
  (89) η_elsewhere monthly     × 12 × 6 = 72
  (90) MIT elsewhere monthly   × 12 × 6 = 72
  (91) f_LA                    × 6 = 6
  (92) blended MIT monthly     × 12 × 6 = 72
  (93) adjusted MIT monthly    × 12 × 6 = 72
  (94) η_whole monthly         × 12 × 6 = 72
                                 total = 588 GREEN assertions

All 6 fixtures land at default scalars (control_type=2 gas combi w/
programmer+RT, R=1.0 Table 4d gas radiators, TMP=250 SAP mass-medium
default, Table 4e adj=0). Per-fixture f_LA reflects habitable_rooms_count.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 21:37:58 +00:00
Khalim Conn-Kowlessar
13c2c6514f §7 slice 2: two-main case 1 weighted-R per Table 9b
Adds secondary_fraction (203) + secondary_responsiveness orchestrator
params. When both main systems heat the whole house (Table 9c case 1),
the u-formula consumes a weighted responsiveness:
  R_eff = (1 - (203)) × R_primary + (203) × R_secondary

Synthetic equivalence test pins the contract: any (frac, R_primary,
R_secondary) call lands the same MIT as a single-main call with the
weighted R. No fixture exercises case 1 (all 6 Elmhurst = single combi),
so secondary_fraction defaults to 0 → identity behaviour.

Case 2 (different parts heated separately) deferred — needs (203) >
1-(91) branch + conditional T_2 averaging + per-system Table 4e
adjustment. No fixture data to drive.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 21:30:37 +00:00
Khalim Conn-Kowlessar
fa49d7b946 §7 slice 1: mean_internal_temperature_monthly orchestrator with per-zone η
Adds MeanInternalTemperatureResult + mean_internal_temperature_monthly,
implementing SAP10.2 §7 Table 9c steps 1-9 sequentially:
  - (86) η_living  = f(Ti = T_h1 = 21°C)
  - (89) η_elsewhere = f(Ti = T_h2 from Table 9)
  - (94) η_whole = f(Ti = (93)m adjusted MIT)

Three distinct η values per month, each computed from its own zone's Ti
via the existing utilisation_factor leaf. Closes the 6.6e-3 °C drift on
000490 (92)m Jan that the prior single-η implementation produced.

Driven by 000490 Jan worksheet (92)m = 15.1899 to abs=5e-3 °C. Other 11
months + per-zone line refs are exercised by the ALL_FIXTURES e2e test
in slice 3.

Legacy `mean_internal_temperature_c` retained (still used by calculator
_solve_month iteration); slice 4 deletes both when calculator wires the
new orchestrator's (93)m + (94)m fields.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 21:28:32 +00:00
Khalim Conn-Kowlessar
34f4fa8bef docs: SPEC_COVERAGE §6 row flip to Full + slice progress table
§6 Solar gains: Partial → Full. Six Elmhurst fixtures conform end-to-end
on (83) total solar gains and (84) total gains to ≤5e-3 W on every month
(144 monthly assertions GREEN). Slice progress table records the chain
from tracer Z-solar lookup through legacy deletion.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 21:03:06 +00:00
Khalim Conn-Kowlessar
a0ce45c98c §6 slice 7: delete legacy _solar_gains_w + WindowInput + _window_inputs
Removes:
  - calculator.WindowInput dataclass
  - calculator.CalculatorInputs.windows field
  - calculator._solar_gains_w function
  - cert_to_inputs._window_inputs / _g_perpendicular / _frame_factor
  - cert_to_inputs._G_PERPENDICULAR_BY_GLAZING_TYPE / _FRAME_FACTOR_BY_MATERIAL
    / _ORIENTATION_BY_CODE lookup tables (duplicated, spec-correct versions
    live in solar_gains.py)
  - 3 obsolete tests in test_cert_to_inputs.py that probed deleted internals;
    one asserted the spec-incorrect Metal frame factor 0.83 (Table 6c spec
    value is 0.8).

Test fixtures in test_calculator.py + test_bre_worked_examples.py pin the
prior synthetic solar 12-tuple verbatim so heat-balance numerics stay
identical pre/post §6 wiring.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 21:01:32 +00:00
Khalim Conn-Kowlessar
cd2bd9cedc §6 slice 6: cert_to_inputs swaps legacy _solar_gains_w → solar_gains_from_cert
CalculatorInputs.solar_gains_monthly_w now flows from the §6 orchestrator
instead of the legacy per-month leaf. Roof windows + rooflights pass empty
because cert summaries (incl. Elmhurst) don't lodge them distinctly; the
§6 conformance test in test_solar_gains.py exercises the roof glazing path
via SECTION_6_ROOF_WINDOWS fixture overrides.

Behavioural delta vs legacy path: orchestrator's Table 6b uses 0.76 for
glazing codes 2 + 3 (spec-correct: "Double glazed, air or argon filled")
where _window_inputs hardcodes 0.72. Golden cert fixtures remain within
their ±5-SAP tolerance.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 20:53:51 +00:00
Khalim Conn-Kowlessar
376cdb6bc3 §6 slice 5: CalculatorInputs.solar_gains_monthly_w + per-month index lookup
Adds the §6 (83)m output as a required 12-tuple field on CalculatorInputs;
_solve_month indexes into it directly instead of recomputing solar each
month via _solar_gains_w(windows, region, month).

Test (test_calculator_consumes_solar_gains_monthly_w_field_for_per_month_solar)
pins the read path: an explicit non-zero monthly tuple flows through
calculate_sap_from_inputs unchanged.

cert_to_inputs preserves identical behaviour during the migration by
computing the new field via the legacy _solar_gains_w leaf per month.
Slice 6 swaps that for solar_gains_from_cert; slice 7 deletes the legacy
leaf + WindowInput + windows field.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 20:51:49 +00:00
Khalim Conn-Kowlessar
377caea20a §6 slice 4: 6 Elmhurst fixtures conform on (83) + (84) to ≤5e-3 W
Adds SECTION_6_VERTICAL_WINDOWS, SECTION_6_ROOF_WINDOWS,
SECTION_6_ROOFLIGHTS, LINE_83_M_TOTAL_SOLAR_W, LINE_84_M_TOTAL_GAINS_W
to each of the 6 _elmhurst_worksheet_*.py fixtures, plus an
ALL_FIXTURES-parametrised end-to-end test in test_solar_gains.py.

144 assertions GREEN (12 months × 2 lines × 6 fixtures) at abs=5e-3 W:
  - (83) total solar gains via solar_gains_from_cert
  - (84) = §5 LINE_73_M_TOTAL_INTERNAL_GAINS_W + (83) — cross-checks
    §5 conformance and §6 orchestrator in one go.

000516 exercises the roof window path (1.18 m² NE at 45° pitch, Z=1.0).
000474/000477/000487 carry mixed glazing types (g⊥=0.72 + g⊥=0.76 within
the same fixture) — verifies _g_perpendicular respects per-window
manufacturer-declared values.

`_build_section_6_epc(fixture)` is local to the test (handover §11):
fixture build_epc()s stay untouched. make_window gains a convenience
`solar_transmittance` shortcut so fixture literals stay readable.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 20:42:59 +00:00
Khalim Conn-Kowlessar
d56fef4d62 §6 slice 3: _g_perpendicular honours SapWindow.window_transmission_details
Elmhurst lodges per-window g⊥ via Manufacturer-source
window_transmission_details.solar_transmittance on every window — the
Table 6b code lookup is the cascade fallback, not the primary path.
Without this the orchestrator picks Table 6b defaults that don't match
the worksheet (e.g. glazing_type=4 defaults to low-E soft 0.63, but the
manufacturer-declared value 0.76 is what the §6 row uses).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 20:34:05 +00:00
Khalim Conn-Kowlessar
4b83e7023f §6 slice 2: solar_gains_from_cert orchestrator (000490 line (83) ≤5e-3 W)
Adds RoofWindowInput + RooflightInput + SolarGainsResult dataclasses and
the solar_gains_from_cert orchestrator. Aggregates per-orientation sums
from epc.sap_windows (Table 6b/6c/6d lookups internal); roof windows take
explicit pitch (RdSAP10 Table 24 default 45°, Z=1.0) and rooflights are
horizontal per SAP10.2 §U3.2 p128 (pitch=0°, Z=1.0).

Driven by U985-0001-000490 worksheet (83) total solar gains 12-tuple to
abs=5e-3 W (audit reconciled the underlying flux + window-gain leaves to
≤5e-5 W; the 5e-3 W budget is the conformance ceiling for §6).

Table 6b g⊥ values are corrected vs cert_to_inputs._window_inputs (which
ships 0.72 for codes 2&3 — the spec is 0.76 for "Double glazed (air or
argon filled)"). The legacy lookup dies in slice 8 when _window_inputs
is deleted.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 20:28:46 +00:00
Khalim Conn-Kowlessar
da5909de3d §6 slice 1: Z-solar Table 6d lookup (winter solar access factor)
z_solar_for_overshading() returns Table 6d first column (0.3/0.54/0.77/1.0).
Tracer for §6 — mirrors §5's _Z_L_BY_OVERSHADING pattern. Distinct from the
lighting Z_L (third column) used by §5 and the cooling Z (second column,
out of scope for SAP heating rating).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 20:07:33 +00:00
Khalim Conn-Kowlessar
29feee7869 docs: handover for §6 Solar gains agent
Captures the §5 implementation pattern (slice-per-test/impl/commit,
ALL_FIXTURES e2e conformance, frozen Result dataclass, calculator.py
wiring) and the SAP10.2 / Table 6d gotchas that cost time during §5
(Z_solar vs Z_L columns, rooflight Z=1.0, existing modules untrusted).

Hard constraints documented for the next agent:
  - 6-fixture conformance ≤5e-3 W on every line (do not loosen tests).
  - Stop and ask the user after ~15 min of unsuccessful reconciliation
    or before scanning more than ~50 lines of spec PDF.
  - Don't touch the untracked `sap worksheets/` folder.

Surfaces the pre-grilling unknowns the §6 agent should propose
recommended answers for during `/grill-me`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 19:29:30 +00:00
Khalim Conn-Kowlessar
52a11f5e74 docs: SPEC_COVERAGE — rooflight Z_L=1.0 closed, §5 to ≤5e-3 W everywhere
Slice 13 (380115e2) closed the only remaining §5 conformance bias.
Promote that item from "remaining" → "done" in the §5 slice progress
table, tighten the conformance summary to "every line ≤5e-3 W", and
shift "rooflight derivation from cert" up as a forward-looking item
(orchestrator accepts the arg but cert_to_inputs always passes 0).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 19:26:28 +00:00
Khalim Conn-Kowlessar
380115e244 §5 slice 13: rooflight Z_L=1.0 closes 000516 to ≤5e-3 W on every line
Table 6d note 2: roof windows / rooflights use Z_L = 1.0 regardless of
the overshading bucket applied to the rest of the dwelling's glazing.

Before this slice the orchestrator approximated rooflights as average
overshading (Z_L=0.83), driving 000516's (67) lighting 0.18 W (0.54%)
high. All wall windows in our 6-fixture corpus were correctly handled;
000516 is the only fixture with a lodged rooflight (the 1.18 m² NE
"window" showing Z=1.0 in the worksheet §6).

  fixture | (67) max |err| before | after
  --------+----------------------+--------
  000516  | 0.1823 W (0.54%)     | <0.005 W (<0.02%)
  others  | <0.0003 W            | <0.0003 W

Changes:
  - internal_gains_from_cert gains rooflight_total_area_m2 (default 0).
    Rooflights summed at g_L=0.80 (Table 6b DG) × FF=0.7 (Table 6c PVC)
    × Z_L=1.0 alongside wall windows (which still use the dwelling's
    overshading-derived Z_L).
  - SECTION_5_ROOFLIGHT_AREAS_M2 added to every fixture (empty tuple
    except 000516 which carries (1.18,)).
  - Tolerances on the §5 parametrised e2e test tightened from 2e-1 W
    on (67) and 3e-1 W on (73) to 5e-3 W on both — every fixture now
    closes to display rounding.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 19:25:53 +00:00
Khalim Conn-Kowlessar
2d4fa24de9 docs: §5 close — SPEC_COVERAGE flip from "Full" stub to actual full
The pre-§5-rebuild SPEC_COVERAGE row optimistically marked §5 as Full
when only 4 of 8 worksheet lines were implemented and the lighting path
used the L5b/L8c fallback (≈22 W/month bias for typical cert lodgings).

Updates the §5 row with the actual coverage post-rebuild:
worksheet-driven (66)..(73), Table 5 Column A throughout, Table 5a
9-row dispatch with heating-season mask, Appendix L L1-L12 lighting
including RdSAP §12-1 per-lamp-type defaults + Table 6d Z_L light
access factor, and orchestrator wired into cert_to_inputs + calculator.

Adds a §5 slice progress table mirroring §4's format, with the
12-slice commit chain and the remaining work (rooflight Z_L=1.0,
cert-driven fan/PIV/HIU dispatch, frame/glazing string parsing, Column
B reduced-gain forms for new-build).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 19:15:43 +00:00
Khalim Conn-Kowlessar
bf6a7e04b3 §5 slice 11: wire calculator.py to internal_gains_from_cert + drop legacy
Removes the legacy SAP-10.3-flavoured scalar internal_gains_w API (plus
its InternalGainsBreakdown dataclass, _default_occupancy_sap_j, and the
L5b/L8c fallback constants used only by the legacy path). Calculator
now indexes a CalculatorInputs.internal_gains_monthly_w 12-tuple per
month instead of recomputing inline.

cert_to_inputs:
  - _hot_water_fuel_kwh_per_yr now also returns the §4 (65)m
    heat_gains_monthly_kwh tuple (was discarded). Plumbed forward into
    internal_gains_from_cert via water_heating_gains bridge.
  - Calls §5 orchestrator with EpcPropertyData + dwelling_volume_m3 +
    (65)m + AVERAGE overshading (Table 6d default per note 1).
  - Falls back to (0.0,) * 12 internal gains when TFA missing.

CalculatorInputs gains a new required field `internal_gains_monthly_w`.
Synthetic-input tests (test_calculator, test_bre_worked_examples)
updated to pass a 450 W constant tuple.

All 283 §1-§7 tests pass. E2e SAP-score regression unaffected for
000490 (still within 1 point) and 000474 (still within 7) because the
legacy fixture build_epc()s don't carry §5-specific sap_windows /
bulbs / heating-details, so the orchestrator returns the L5b lighting
fallback + zero (65)m — matches the legacy scalar's behaviour.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 19:14:33 +00:00
Khalim Conn-Kowlessar
99e5c2cd44 §5 slice 10: extract LINE_66..LINE_73 + ALL_FIXTURES e2e conformance
Adds SECTION_5_BULB_COUNT_LEL, SECTION_5_WINDOW_AREAS_M2,
SECTION_5_PUMP_AGE_STR and LINE_66..LINE_73 expected outputs to every
Elmhurst fixture (000474, 000477, 000480, 000487, 000490, 000516).
Constants extracted from the U985-0001-NNNNNN worksheets supplied
2026-05-20. All six fixtures share the same shape: all-LEL bulb
lighting, gas combi pump with unknown install date, average overshading.

Adds an ALL_FIXTURES-parametrized test in test_internal_gains.py that
composes a §5 EPC from the fixture's constants and drives
internal_gains_from_cert. Tolerances: ≤1e-3 W on the linear-in-N rows
(66/69/71), ≤2e-1 W on (67) lighting (worksheet-rounded N + rooflight
Z_L=1.0 approximated by AVERAGE Z_L=0.83), ≤5e-2 W on (68) appliances,
≤3e-1 W on (73) sum. Result: 26 tests pass; six fixtures conform to
≤0.6% lighting bias end-to-end.

The fixture's base build_epc() is unchanged — §5 EPC composition lives
in a test helper so the existing e2e SAP-score regression (000490, 000474)
remains pinned for the upcoming calc.py wiring slice.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 19:06:34 +00:00
Khalim Conn-Kowlessar
f81e744b02 §5 slice 9: internal_gains_from_cert orchestrator + lookalike tracer test
Wires all §5 leaf functions into a single from_cert orchestrator that
chains (66) → (67) → (68) → (69) → (70) → (71) → (72) → (73) and
returns an InternalGainsResult. The caller provides §4 (65)m heat
gains (the only non-cert input) and overshading defaults to AVERAGE.

Cert derivations:
  - Occupancy via Appendix J Table 1b from TFA
  - Lighting: RdSAP §12-1 per-lamp-type bulb defaults aggregated to
    C_L,fixed + ε_fixed; C_daylight via L2a from sap_windows × Z_L
    from Table 6d. L5b + L8c fallbacks when no bulb/window data lodged.
  - Pumps/fans: maps central_heating_pump_age_str on the first
    MainHeatingDetail to PumpDateCategory. Liquid-fuel / warm-air / PIV
    / MV / HIU branches deferred (reachable via leaf fns; currently
    return 0 in the orchestrator for the combi-gas-natural-vent
    population that covers all 6 Elmhurst fixtures).

Slice 9 tracer test hand-builds a 000490-lookalike EPC rather than
mutating `_elmhurst_worksheet_000490.build_epc()` — keeps the existing
e2e SAP-score regression test pinned. Slice 10 will extend the fixture
proper and parametrize over ALL_FIXTURES.

Also: extends make_minimal_sap10_epc with low_energy_fixed_lighting_bulbs_count
since the existing builder only exposed CFL/LED/incandescent separately.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 18:50:40 +00:00
Khalim Conn-Kowlessar
53aba1332e §5 slice 8: (73) total_internal_gains_monthly_w + InternalGainsResult
Closes the §5 leaf-function surface:
  - total_internal_gains_monthly_w sums (66) + (67) + (68) + (69)
    + (70) + (71) + (72) element-wise. (71) carries negative sign so the
    losses term subtracts.
  - InternalGainsResult frozen dataclass bundles all 7 line refs plus the
    total as 12-tuples — the typed payload returned by the orchestrator.

Verified against Elmhurst U985-0001-000490 (73)m to ≤1e-2 W/month.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 18:38:46 +00:00
Khalim Conn-Kowlessar
f77229e4b4 §5 slice 7: (70) pumps_fans_monthly_w — Table 5a 9-row dispatch
Implements Table 5a row-by-row leaf functions:
  central_heating_pump_w     pump install-date bucket (3/7/10 W)
  liquid_fuel_boiler_pump_w  10 W when oil-fuel pump inside dwelling
  liquid_fuel_warm_air_pump_w  10 W for liquid-fuel warm-air systems
  warm_air_heating_fan_w     SFP × 0.04 × V (heating-season)
  piv_fan_w                  IUF × SFP × 0.12 × V (year-round)
  balanced_mv_no_hr_fan_w    IUF × SFP × 0.06 × V (year-round)
  heat_interface_unit_w      PCDB kWh/day × 1000 / 24 (year-round)

Plus pumps_fans_monthly_w(heating_season_w, year_round_w) which applies
the Table 5a footnote-a seasonal mask (Jun-Sep = 0 W heating-season
contribution per Elmhurst worksheet convention).

PumpDateCategory enum maps from EpcPropertyData.central_heating_pump_age_str
("Pre 2013" / "Post 2013" / "Unknown" / etc.) at the orchestrator layer.

MVHR and MEV systems intentionally have no leaf fn — gains are zero per
Table 5a notes (MVHR effect is in MVHR efficiency; MEV simply omitted).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 18:37:14 +00:00
Khalim Conn-Kowlessar
50fd940ab9 §5 slice 6: (67) lighting_monthly_w — full Appendix L L1-L12 cascade
Implements the full SAP10.2 Appendix L lighting calculation: Λ_B (L1)
→ Λ_req (L3) → Λ_prov (L6) → Λ_topup (L7) → E_L,fixed/topup/portable
(L9a-d) → monthly cosine modulation (L10) → 0.85 × 1000 / (24 × n_m)
heat-gain bridge (L12).

Critical detail uncovered while reconciling against the 000490
worksheet: C_daylight uses Z_L from Table 6d's **third column** (light
access factor), NOT the 0.77 first column used for §6 solar gains. For
"Average" overshading Z_L = 0.83. Conflating the two columns gives a
~2% lighting-energy bias.

Verified against Elmhurst U985-0001-000490 (67)m to ≤5e-3 W/month
(0.14% on E_L) using worksheet bulb table (8 LEL × 80 lm/W × 15 W)
and Table 6b/6c/6d defaults for the window inputs.

The orchestrator slice will derive C_L,fixed + ε_fixed from RdSAP §12-1
per-lamp-type defaults (LED 100 lm/W, CFL 55 lm/W, LEL 80 lm/W,
incandescent 11.2 lm/W) and C_daylight from the cert's window data.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 18:33:43 +00:00
Khalim Conn-Kowlessar
0bc9eac34c §5 slice 5: (68) appliances_monthly_w — Appendix L13/L14/L16a
E_A = 207.8 × (TFA × N)^0.4714 (L13) chained through monthly factor
1 + 0.157 × cos(2π × (m - 1.78) / 12) (L14) then watts via × 1000 /
(24 × n_m) (L16a). Column A typical-gain form — 1.0× conversion. L16's
0.67× reduced form deferred (new-build DPER/TPER use).

Verified against Elmhurst U985-0001-000490 (68)m row to ≤5e-2 W
(display rounding from the (TFA × N)^0.4714 term).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 18:07:34 +00:00
Khalim Conn-Kowlessar
a4d6321f21 §5 slice 4: (72) water_heating_gains_monthly_w — bridge from §4 (65)m
Pure unit conversion: G_WH,m = 1000 × (65)m / (n_m × 24). The §4
heat_gains_from_water_heating_monthly_kwh output already encodes the
25%/80% spec-recovery factors for delivered-heat vs pipe-side losses;
this bridge just lands the kWh/month into watts for the §5 sum.

Verified against Elmhurst U985-0001-000490 (72)m row — exact to 4 d.p.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 18:04:15 +00:00
Khalim Conn-Kowlessar
984a5b18d6 §5 slice 3: (69) cooking_monthly_w — 35 + 7N const-tuple
SAP10.2 Table 5 Column A row "Cooking": G_C = 35 + 7 × N watts,
year-round. Fuel-agnostic (gas/electric same gain — fuel matters only
for §12 cost). Verified against Elmhurst U985-0001-000490 worksheet
(69)m row.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 18:02:15 +00:00
Khalim Conn-Kowlessar
021f43ba67 §5 slice 2: (71) losses_monthly_w — -40×N const-tuple
SAP10.2 Table 5 "Losses" row: -40 × N watts year-round. Captures
cold-water inflow + evaporation heat sinks. Verified against the
Elmhurst U985-0001-000490 worksheet (71)m row.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 18:00:15 +00:00
Khalim Conn-Kowlessar
3ec56216b0 §5 slice 1: (66) metabolic_monthly_w — 60×N const-tuple
Tracer bullet for §5 internal-gains rebuild. New 12-tuple monthly API
lands alongside the legacy scalar internal_gains_w stub; calculator.py
keeps building until the §5 wiring slice. SAP10.2 Table 5 Column A is
the rating + cooling default — Column B (new-build DPER/TPER) deferred.

Deletes the legacy SAP-10.3-flavoured test_internal_gains.py per the
rebuild plan; new tests will accrete slice-by-slice.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 17:59:00 +00:00
Khalim Conn-Kowlessar
74b2c1131f §4 conformance: extend Elmhurst fixtures to 6/6 across (42)..(65)
Populates §4 LINE_42..LINE_65 + per-fixture HW inputs (HAS_BATH,
MIXER_SHOWER_FLOW_RATES_L_PER_MIN, COLD_WATER_TEMPS_C, LOW_WATER_USE,
COMBI_LOSS_OVERRIDE, ELECTRIC_SHOWER_OVERRIDE) in 000477, 000480,
000487, 000516 — values extracted from the Elmhurst U985 worksheets
supplied 2026-05-20. 000474 + 000490 get the same input constants for
uniform parametrization.

Adds electric_shower_monthly_kwh_override to water_heating_from_cert
to unlock 000487 (instantaneous electric shower, no mixer). The
orchestrator's has_shower flag now also accounts for the electric path.

Extends 6 parametrized §4 tests from (000474, 000490) to ALL_FIXTURES
and adds a new ALL_FIXTURES-parametrized e2e test exercising the
orchestrator end-to-end through (42)..(65) for every Elmhurst fixture.
Tolerance on (43)/(44) loosened to 5e-3 to absorb Elmhurst's 4-d.p.
display rounding.

Result: 150/150 tests pass; §1-§4 conform at ≤1e-2 kWh / 5e-3 L for
every fixture. Deferred branches surfaced via overrides:
- PCDB Table 3b combi loss (000474, 000477, 000516)
- Non-time-clock Table 3a combi loss rows (000480, 000487)
- Electric-shower (64a)m derivation from cert codes (000487)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 17:29:10 +00:00
Khalim Conn-Kowlessar
3c2f975c6d cert_to_inputs: wire §4 worksheet orchestrator into HW kWh derivation
Replaces the legacy `predicted_hot_water_kwh` cascade with a call into
`water_heating_from_cert` for the modal combi-gas-mains population. The
new helper `_hot_water_fuel_kwh_per_yr` chains the §4 cascade end-to-end
(occupancy → daily hot water → energy content → distribution + combi
loss → (62)m total → (64)m output) then divides by water-heater
efficiency to land annual fuel kWh — the slot CalculatorInputs expects.

Section-by-section validation across all 6 Elmhurst fixtures shows:
  §1 dimensions   exact (≤ 1e-4) on all 6
  §2 ventilation  exact (≤ 1e-4) on all 6
  §3 heat trans   exact on non-RR (000474, 000490) within 0.04 W/K
                  (display-rounding); RR fixtures under-count per the
                  formal SapRoomInRoof sub-area deferral.
  §4 hot water    exact on the 2 fixtures with LINE_42/LINE_64 lodged
                  (000474 PCDB override + 000490 cascade-default); 4 RR
                  fixtures emit plausible orchestrator values.

End-to-end SAP impact (legacy → new):
  000490  57=57 (cont 56.72 → 56.92, closer to worksheet 57.40)
  000474  55→56 (cont 55.39 → 55.59, expected 62, still 6pt under)

Caveats / future slices:
  - Cold water source defaults to mains (no domain-model field yet).
  - Shower flow rate defaults to 7 L/min vented (no shower_outlet_type
    plumbing yet); both fixtures actually lodge this so no false drift.
  - Cylinder + solar + WWHRS / PV / FGHRS branches default to zero.
  - PCDB Table 3b combi loss not implemented; orchestrator accepts a
    `combi_loss_monthly_kwh_override` for now but cert_to_inputs always
    falls to Table 3a row "time-clock keep-hot".
  - water_efficiency variable misnamed "pct" — it's a decimal (0.0-1.0).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 16:35:53 +00:00
Khalim Conn-Kowlessar
171cb97c6e e2e: SAP-score regression test against both Elmhurst worksheets
First end-to-end test running EpcPropertyData → cert_to_inputs →
calculate_sap_from_inputs → SapResult and comparing against the
Elmhurst worksheet's headline SAP rating (line 258).

Current state:
  000490 mid-terrace gas combi, time-clock keep-hot
    SAP rating:    57 = 57  ✓ exact integer match
    Continuous:    56.72 vs 57.40  → 0.7 points off (rounding noise)

  000474 end-terrace gas combi, PCDB Vaillant ecoTEC pro
    SAP rating:    55 vs 62  → 7 points UNDER
    Space heating: 12299.6 vs 10612.9  (+16%)
    Hot water:     3020.0  vs 2291.8   (+32%)

The 000474 gap localises to (a) the legacy hot-water cascade not
knowing about PCDB Table 3b combi loss (over-estimates HW by 32%) and
(b) likely a downstream space-heating-efficiency consequence. Both will
shrink once the §4 worksheet orchestrator + Table 3b are wired into
cert_to_inputs.

Tolerances set at the CURRENT gap so subsequent improvements show up
as tightening, not silent drift. The 000474 ceiling drops to ≤2 SAP
points once the worksheet §4 path lands in the mapper.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 16:27:04 +00:00
Khalim Conn-Kowlessar
d6e2c99f5b §4 orchestrator: water_heating_from_cert + WaterHeatingResult
Chains every leaf function landed in slices 1-9 into a single call that
takes an EpcPropertyData + the few site-notes inputs that aren't on the
domain object yet (shower flow rates, has_bath, cold-water source, low-
water-use flag). Mirrors heat_transmission_from_cert's shape from §3.

WaterHeatingResult exposes the line refs (42), (43), (44)m, (45)m, (46)m,
(61)m, (62)m, (64)m, (65)m plus the annual sum of (64)m as
`output_kwh_per_yr` — that's the slot calculator.py's CalculatorInputs
expects for `hot_water_kwh_per_yr` (modulo division by water heater
efficiency, handled by the caller).

`combi_loss_monthly_kwh_override` accepts a (61)m array for PCDB-tested
boilers (Table 3b/3c) since those need r1+F1 parameters we haven't
implemented. Defaulting to Table 3a row "time-clock keep-hot" suits the
modal non-PCDB combi lodging.

Validated end-to-end against both Elmhurst non-RR fixtures:
  - 000490: cascade-default combi loss, output matches annual to 0.01 kWh
  - 000474: PCDB-derived (61)m injected, output matches to 0.01 kWh

Cylinder + solar + WWHRS/PV/FGHRS + electric-shower branches default to
zero — extension slices land them when needed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 16:23:56 +00:00
Khalim Conn-Kowlessar
6a3552a50d docs: §4 slice progress — happy path closes both non-RR fixtures
Updates SPEC_COVERAGE.md with the 9 §4 slices landed since the last doc
sweep, and lays out the remaining work in priority order:
  1. §4 orchestrator (water_heating_from_cert)
  2. Wire calculator.py to the new worksheet module
  3. End-to-end SAP score validation against Elmhurst worksheets
  4. Cylinder + solar + renewables branches (population coverage)
  5. PCDB-backed Table 3b/3c combi loss (000474 sits here)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 16:15:18 +00:00