Commit graph

4941 commits

Author SHA1 Message Date
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
Khalim Conn-Kowlessar
43da3ea064 §4 slice 9: line (65)m heat gains from water heating
(65)m = 0.25 × [0.85 × (45)m + (61)m + (64a)m]
        + 0.80 × [(46)m + (57)m + (59)m]

First bracket recovers 25% of delivered-heat losses (hot water at the
tap + combi cycling + electric-shower waste heat); second bracket
recovers 80% of pipe-side losses (distribution + solar storage +
primary circuit) since pipework typically sits inside the heated
envelope. Per spec footnote on xlsx row 302, callers should zero (57)m
when the hot water store is OUTSIDE the heated space (e.g. communal
heat networks).

Validated against both Elmhurst fixtures to <1e-3 kWh:
  000490 Jan: 0.25×(0.85×187.86 + 50.96 + 0) + 0.80×(28.18 + 0 + 0)
            = 0.25×210.64 + 0.80×28.18 = 52.66 + 22.54 = 75.20 ✓
  000474 Jan: 0.25×(0.85×174.40 + 28.72 + 0) + 0.80×(26.16 + 0 + 0)
            = 0.25×176.96 + 0.80×26.16 = 44.24 + 20.93 = 65.17 ✓

LINE_64A_M and LINE_65_M lodged on both fixtures.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 16:12:53 +00:00
Khalim Conn-Kowlessar
feef819814 §4 slice 8: line (64)m output from water heater
(64)m = max(0, (62)m + (63a)m + (63b)m + (63c)m + (63d)m)

The four (63 a-d) inputs are WWHRS, PV-diverter, solar HW and FGHRS
contributions — entered as negative quantities so the formula uses +,
not −. The max-clamp guards "if (64)m < 0 then set to 0" per the spec
worksheet text: a renewable-heavy summer can't show negative delivered
heat.

Both Elmhurst non-RR fixtures lodge zero for all four (no WWHRS, no PV
diverter, no solar, no FGHRS), so (64)m = (62)m for every month.
Validated end-to-end on both with abs=1e-3 kWh tolerance.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 16:08:18 +00:00
Khalim Conn-Kowlessar
bfba610b70 §4 slice 7: combi loss (Table 3a time clock) + (62)m total demand
Two new public functions:

  combi_loss_monthly_kwh_table_3a_keep_hot_time_clock()
    Table 3a row "Instantaneous, with keep-hot facility controlled by
    time clock" → 600 × n_m / 365 kWh/month (flat 600 kWh/year prorated
    by month length, no fu adjustment).

  total_water_heating_demand_monthly_kwh(...)
    Spec formula (62)m = 0.85 × (45)m + (46)m + (57)m + (59)m + (61)m.
    (56)m storage loss is intentionally absent — folded into storage-
    system efficiency at the (64)m stage. (46)m distribution loss
    appears here AND in (65)m heat gains (weight 0.8), per spec.

000490 close end-to-end through (62)m: combi with time-clock keep-hot,
no storage, no solar, no primary loss → Jan = 0.85×187.86 + 28.18 + 0 +
0 + 50.96 = 238.82 matching the worksheet to 1e-3.

000474 deferred: its PCDF-listed Vaillant boiler uses Table 3b (tested
to EN 13203-2) which needs PCDB-backed r1 + F1 parameters. The (61)m
implementation for that branch lands in a future slice along with the
PCDB stub plumbing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 16:06:05 +00:00
Khalim Conn-Kowlessar
a3c687f1b0 §4 slice 6: lines (45)m energy content + (46)m distribution loss
(45)m = 4.18 × V_d,m × n_m × (52 − Tcold[m]) / 3600    [kWh/month]
                                          Appendix J equation J14
  (46)m = 0.15 × (45)m                    spec §4 step 7 (normal systems)
        = 0                                (instantaneous at point of use,
                                            hot water codes 907 / 909)

4.18 J/(g·K) is the specific heat of water; / 3600 converts to kWh. The
J14 transform converts daily L of hot water at delivery temperature into
the monthly sensible-heat requirement.

Both Elmhurst non-RR fixtures use a combi boiler from a central system
(neither 907 nor 909), so distribution loss is the full 15 % of (45)m.
Lodged LINE_45_M and LINE_46_M arrays on both fixtures for forward use.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 15:58:40 +00:00
Khalim Conn-Kowlessar
702b1c6ce6 §4 slice 5: lines (43) annual avg + (44)m monthly total
Two thin wrappers landing the aggregation step:

  (44)m = (42a)m + (42b)m + (42c)m        Appendix J equation J13
  (43)  = V_d,shower,ave + V_d,bath,ave + V_d,other,ave   J12

A subtle spec point caught here: (43) is the SUM OF THE COMPONENT
ANNUAL AVERAGES (per the J12 text), not the days-weighted mean of (44)m.
The two are arithmetically different because Table J2's days-weighted
mean is 0.99973 rather than 1.0 — the "other uses" term contributes its
unmodulated baseline (9.8N+14), and only the showers + baths terms get
the days-weighted reduction. Spec-following the J12 wording matches the
Elmhurst (43) values to 1e-3 L/day on both fixtures.

  annual_average_hot_water_other_uses_l_per_day  exposes V_d,other,ave
  annual_average_hot_water_l_per_day              composes the J12 sum
  total_hot_water_monthly_l_per_day               J13 (44)m sum

LINE_43 + LINE_44_M lodged on 000474 and 000490 fixtures.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 15:56:23 +00:00
Khalim Conn-Kowlessar
1dcbdb28e6 §4 slice 4: hot_water_mixer_showers_monthly_l_per_day (line (42a)m)
Appendix J equations J1–J3. Per-day hot water draw for mixer showers
combines the per-day shower count (rising with N, depressed slightly
when a bath is also present) with each outlet's flow × 6 min × Table J5
behavioural factor, then multiplied by the cold-water-dependent hot
fraction (41 °C delivery vs 52 °C hot supply, Tcold from J1).

Multi-outlet handling: N_shower is split across outlets so a dwelling
with two identical mixers produces the same (42a)m total as a single
outlet — the count only matters when outlets have different flow rates.
Instantaneous electric showers belong in (64a)m and must be excluded
from the input.

Validated against the Elmhurst non-RR fixtures (both 1 vented mixer at
7 L/min, mains Tcold):
  - 000490 N=2.1468 → Jan V_d,hot = 52.6878
  - 000474 N=1.8896 → Jan V_d,hot = 48.9139

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 15:51:14 +00:00
Khalim Conn-Kowlessar
dad7fbf31f §4 slice 3: hot_water_baths_monthly_l_per_day (line (42b)m)
Appendix J equations J6, J7, J8. Daily hot water for bath fills depends
on N, presence of bath and/or shower, and monthly Tcold:

  N_bath  = 0                if no bath but a shower exists
          = 0.13×N + 0.19    if bath + shower
          = 0.35×N + 0.50    otherwise
  V_d,bath[m] = N_bath × 73 × J5_fbeh[m] × (42−Tcold[m])/(52−Tcold[m])

Tables J1 (mains + header tank Tcold) and J5 (behavioural factor) are
exported as module constants for reuse by (42a)m showers next.

Validated against the Elmhurst non-RR fixtures, both with bath + shower
and "Cold Water Source: From mains":
  - 000490 N=2.1468 → Jan V_d,bath = 27.3868
  - 000474 N=1.8896 → Jan V_d,bath = 25.4345

Also covers the zero-bath branch and the 5% low-water-use reduction.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 15:48:26 +00:00
Khalim Conn-Kowlessar
5cc68ab3fd §4 slice 2: hot_water_other_uses_monthly_l_per_day (line (42c)m)
Appendix J equation J11 — daily hot water use for non-shower / non-bath
purposes (sinks, dishwashers, etc.) is annual-avg V_d,other,ave = 9.8 ×
N + 14, modulated month-by-month by the Table J2 monthly factors and
reduced by 5% when the dwelling meets the 125 L/person/day water-use
target.

Validated against both Elmhurst non-RR fixtures to better than 1e-3 L:
  - 000490 N=2.1468 → V_d,other,ave ≈ 35.04, Jan = 38.5426
  - 000474 N=1.8896 → V_d,other,ave ≈ 32.52, Jan = 35.7697

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 15:43:56 +00:00
Khalim Conn-Kowlessar
aff678e8eb §4 slice 1: assumed_occupancy (worksheet line (42), Appendix J)
First slice of the §4 worksheet-driven rewrite (xlsx rows 207-304).
New module `domain/sap/worksheet/water_heating.py` lands the line-ref
mapped functions; subsequent slices append below.

`assumed_occupancy(tfa)` implements the SAP10.2 Appendix J Table 1b
piecewise formula. Validated against:
  - canonical xlsx worked example  (TFA Q23 → N U209)
  - Elmhurst U985-0001-000474       (TFA 56.79 → N 1.8896)
  - Elmhurst U985-0001-000490       (TFA 66.06 → N 2.1468)
  - boundary case TFA ≤ 13.9        (N=1 floor)

The legacy `domain.ml.demand._default_occupants_sap_j` mirror stays in
place until the §4 worksheet rewrite is complete; both sources will be
reconciled in a later slice once dependent callers move over.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 15:27:03 +00:00
Khalim Conn-Kowlessar
d90827446a docs: sweep stale handover, mark §3 Full, scaffold §4 slice plan
§3 close (LINE_31/33/36/37 exact for both non-RR Elmhurst worksheets) is
now landed across slices 344a9c9d..cf244762. HANDOVER_S3_CLOSE.md was
written as a mid-stream working brief; with §3 done it now creates doc
rot, so it's removed in favour of SPEC_COVERAGE.md as the single source
of truth.

SPEC_COVERAGE.md updates:
  - §3 marked Full (non-RR); RR sub-area deferral noted
  - §4 carries the ordered slice plan for the worksheet-driven rewrite
    (xlsx rows 207–304, line refs (42)..(65))
  - Hierarchy callout: the canonical SAP10.2 algorithm lives in the
    repo-root xlsx, not in any handover doc

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 15:18:46 +00:00
Khalim Conn-Kowlessar
cf244762d5 Elmhurst 000474: §3 LINE_33 + LINE_37 close exactly
Closes the second non-RR Elmhurst worksheet (mid-terrace, 3 parts).
LINE_33 (209.1084) and LINE_37 (232.1169) reproduce to 0.1 W/K.

Cert inputs lodged on the fixture:
  - Ext1 SapFloorDimension(is_exposed_floor=True) — Table 20 route
  - Ext2 ground floor (tiny 1.35 m², P=3.30) stays on Table 19 fn 1
    suspended-timber default for age B (cascade → U≈1.25, worksheet 1.25)
  - door_count=2 → 3.70 m² total door area
  - WINDOW_TOTAL_AREA_M2=11.72 split across two glazing types
    (Type 1: 6.22 m² post-2002 raw U=2.0, Type 2: 5.50 m² pre-2002 raw
    U=2.8). Area-weighted aggregate raw U=2.37 reproduces the worksheet's
    25.37 W/K through the curtain-resistance transform.

Non-RR §3 scope closed:
  - LINE_31  exact (existing test)
  - LINE_33  exact ← this slice + the 000490 slice
  - LINE_36  exact (existing test, y × LINE_31)
  - LINE_37  exact ← this slice + the 000490 slice

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