Commit graph

5 commits

Author SHA1 Message Date
Khalim Conn-Kowlessar
fb3a84ce94 Slice 102b: cylinder storage loss via SAP 10.2 Tables 2/2a/2b
SAP 10.2 §4 line 7690 (full spec PDF p.136) defines the cylinder storage
loss cascade for any cert with a hot water cylinder lodged:
  (54) = V × L × VF × TF           (Table 2 absence-of-declared-loss branch)
  (55) = (54)                       (no manufacturer's declared loss)
  (56)m = (55) × n_m                (per spec, n_m = days in month)
where
  L  = Table 2 (PDF p.158) Note 1 formula for the lodged insulation type
       (factory-insulated cylinders: 0.005 + 0.55/(t+4.0); loose jacket:
       0.005 + 1.76/(t+12.8))
  VF = Table 2a (PDF p.158) Note 2 closed form (120/V)^(1/3)
  TF = Table 2b (PDF p.159) base 0.60 for indirect / electric-immersion
       cylinders, × 1.3 if no thermostat, × 0.9 if DHW separately timed

Prior, `water_heating_from_cert` hard-coded `solar_storage_monthly_kwh
= zero12` and `_water_heating_worksheet_and_gains` had no path to
populate it. The new `cylinder_storage_loss_monthly_kwh` helper in
`worksheet/water_heating.py` exposes Tables 2 / 2a / 2b as small typed
functions plus a composite; the cert-side orchestrator in
`rdsap/cert_to_inputs.py::_cylinder_storage_loss_override` resolves
the lodged cylinder fields and injects the override.

Code → litres mapping ground-truthed against worksheet (47) line refs
in /sap worksheets/Additional data with api/<cert>/dr87-*.pdf for the
7-cert ASHP cohort: code 3 → 160 L (Medium, 6 certs) and code 4 →
210 L (Large, cert 9418). Codes 2 / 5 / 6 (Normal / Inaccessible /
Exact) absent from the cohort and not yet mapped.

Cylinder insulation type code → "factory_insulated" mapping
(_CYLINDER_INSULATION_TYPE_FACTORY = 1) ground-truthed against all 7
ASHP cohort worksheets ("Foam" lodgement → SAP 10.2 Table 2 Note 2
"factory-insulated cylinder where the insulation is applied in the
course of manufacture irrespective of the insulation material used").

RdSAP §3 default table (PDF p.57) — "Hot water separately timed:
Post-1998 boiler: Yes" — applied to heat-pump main heating systems
(cat 4) per the cohort worksheet evidence.

Cert 0380 (Mitsubishi ASHP, 160 L factory 50 mm, thermostat + separately
timed) lands the spec formula at worksheet (56) Jan = 36.9530 kWh/month
(test pinned at 1e-4); HW kWh/yr 242.21 → 431.38, recovering ~189 kWh/yr
of cylinder loss the cascade was previously dropping.

Cohort regression: cert 0390-2954 (oil boiler + 160 L cylinder) tightens
PE residual -28.6783 → -27.5026 kWh/m² and CO2 residual -2.7640 →
-2.6570 t/yr — both move closer to the lodged values (improvement).
Re-pinned with a slice-102b note.

Closed boiler chain tests (001479, 0330, 9501) unaffected: those certs
lodge has_hot_water_cylinder=false so the override stays None and the
existing zero-storage-loss default fires.
2026-06-01 16:28:45 +00:00
Khalim Conn-Kowlessar
a736db3f4a Slice 101b: HP cert 0380 — cavity+EWI wall U + Table 11 cat-4 secondary
Two HP-specific cascade gaps blocking cert 0380:

(a) Cavity wall + filled cavity + external insulation:
    Cert 0380's `walls[0].description="Cavity wall, filled cavity and
    external insulation"` with `wall_insulation_type=6` +
    `wall_insulation_thickness="100mm"`. RdSAP 10 §4-4 (page 73) lists
    "cavity plus external" as a distinct insulation type code (6 in
    the API schema; 7 is "cavity plus internal"). The U-value is the
    composite U = 1 / (1/U_filled + R_ins) per §5.8 page 40 + Table 14
    R-value lookup, with the cascade-2-d.p. round matching the dr87
    worksheet's column display.

    For cert 0380: U_filled (age D)=0.7 + R_ins (100mm @ λ=0.04)=2.5
    → U_unrounded=0.2545 → rounded 0.25 (worksheet exact). Walls HLC
    14.87 → 11.6150 (= worksheet 11.6150). (37) total fabric heat
    loss 99.34 → **96.0889** (= worksheet 96.0889 EXACT).

    Added `WALL_INSULATION_CAVITY_PLUS_EXTERNAL: Final[int] = 6` and
    `WALL_INSULATION_CAVITY_PLUS_INTERNAL: Final[int] = 7` constants
    + `_WALL_INSULATION_LAMBDA_W_PER_MK = 0.04` default thermal
    conductivity. New `u_wall` branch fires when cavity + composite
    insulation type + non-zero thickness.

(b) SAP 10.2 Table 11 secondary fraction — missing cat-4 entry:
    The dict `_SECONDARY_HEATING_FRACTION_BY_CATEGORY` had entries
    for cats 1/2/3/5/6/7/10 but DID NOT include cat 4 (heat pump),
    despite the inline comment explicitly noting "Cat 4 (heat pump):
    0.00 (HP eff includes any secondary)". Cert 0380 lodges
    `secondary_heating_type=691` + `main_heating_category=4` (HP,
    PCDB idx 104568), so the cascade fell through to the DEFAULT
    fraction 0.10 — billing 547 kWh × 13.19 p/kWh = £72 as
    "secondary heating" that the worksheet correctly shows as £0.

    Added `4: 0.00` to the dict.

Effect on cert 0380 API path:
- walls HLC 14.87 → 11.62 (worksheet exact)
- (37) total HLC 99.34 → 96.09 (worksheet exact)
- main_heating_cost £282 → £314 (worksheet £316)
- secondary_heating £72 → £0 (worksheet £0)
- sap_continuous 87.62 → 90.48 (Δ -0.89 → +1.97 — over-correcting
  because hot-water cascade is still cascade-£66 vs worksheet £204
  including electric shower; HP HW-COP + electric-shower cost are
  the next slices).

No golden cert residual shifts (cohort certs don't lodge HP cat 4
or composite cavity+EWI walls).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 16:28:45 +00:00
Khalim Conn-Kowlessar
485a74028e Slice 96: flat-roof U-value defaults — RdSAP 10 §5.11 Table 18 col (3)
Cert 0330 (mid-terrace boiler, Summary_000897.pdf) Summary path was at
Δ +0.4667 SAP vs worksheet 61.5993 because Ext1's flat roof fell through
`_ROOF_BY_AGE` (Table 18 column (1), pitched-roof "between joists"
defaults) to 0.40 W/m²K for age D — the spec value is 2.30 W/m²K from
column (3) "Flat roof" (RdSAP 10 spec page 45).

RdSAP 10 §5.11 Table 18 column (3) verbatim:
  Age A,B,C,D → 2.30; E → 1.50; F → 0.68; G → 0.40; H,I → 0.35;
  J,K → 0.25; L → 0.18; M → 0.15.

Footnote (a): "If the roof insulation is 'none' use U = 2.3 (all roof
types, except for thatched roofs)" — confirms the col-3 entries for
old ages are the uninsulated row, applied because cert 0330's Ext1
lodges "Flat" construction with no measured insulation thickness.

Changes:
- `_FLAT_ROOF_BY_AGE` added in rdsap_uvalues.py
- `u_roof` gains `is_flat_roof: bool = False` parameter
- `heat_transmission_from_cert` detects flat roofs from
  `part.roof_construction_type` ("flat" substring) and routes through
  the new column.

Effect on baseline:
- cert 0330 Summary chain test: RED Δ+0.4667 → GREEN at 1e-4 (worksheet
  total fabric heat loss 237.7549 W/K matches cascade to 4 d.p.)
- cert 001479 Layer 4 chain test: unchanged (Main pitched, no flat
  components)
- cohort certs 000477/000516: unchanged (no flat roofs)
- golden cert 0300-2747-7640-2526-2135: SAP residual +1 → 0 (improved),
  Ext1 is genuinely flat; pe/co2 residuals re-pinned. The dwelling has
  the same Main-pitched + Ext1-flat shape as cert 0330; same fix.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 16:28:45 +00:00
Khalim Conn-Kowlessar
a7b08a4e8f refactor: move docs/sap-spec/ contents into domain/sap10_calculator/
Locality of reference — SAP-specific docs, specs, and runtime data
now live alongside the calculator that consumes them, mirroring the
prior packages→domain layout moves.

Move targets:

- Narrative MDs → domain/sap10_calculator/docs/
    NEXT_AGENT_PROMPT.md, HANDOVER_NEXT.md, SAP_CALCULATOR.md
- Spec PDFs → domain/sap10_calculator/docs/specs/
    RdSAP 10 Specification 10-06-2025.pdf
    PCDF_Spec_Rev-06b_12_May_2021.pdf
    sap-10-2-full-specification-2025-03-14.pdf
    sap-10-3-full-specification-2026-01-13.pdf
- PCDB runtime data → domain/sap10_calculator/tables/pcdb/data/
    pcdb10.dat (8.3MB) + 7× pcdb_table_*.jsonl (18MB total)

Path code rewrites (load-bearing):

- tables/pcdb/__init__.py: replaced parents[4]/'docs'/'sap-spec' with
  Path(__file__).resolve().parent/'data' for Table 105 JSONL loading.
- tables/pcdb/postcode_weather.py: same rebase for the pcdb10.dat path
  read by _postcode_climate_table().
- tables/pcdb/etl.py __main__: same rebase for the manual ETL invocation
  (source + output_dir both now point inside the package).
- tests/test_pcdb_etl.py: _PCDB_DAT_PATH now derives from
  parents[1]/'tables'/'pcdb'/'data' (was parents[3]/'docs'/'sap-spec').

Citation rewrites:

- 12 .py docstrings and 4 .md docs (ADRs + READMEs + narrative docs)
  had `docs/sap-spec/<file>` strings rewritten to their new locations.
- Two cases where the catch-all sed misfired (an ADR-0009 line about a
  PCDB extract; the pcdb __init__.py docstring about ETL output) were
  hand-corrected to point at tables/pcdb/data/ rather than docs/specs/.

docs/sap-spec/ is now empty (will be removed in a follow-up sweep or
left as a vestigial empty dir for future repurposing). ADRs 0009 and
0010 remain at docs/adr/ — they're part of the chronological
cross-cutting decision log, not calculator-specific narrative.

Verified:

- Calculator's 1e-4 production gate
  (test_api_001479_full_chain_sap_matches_worksheet_pdf_exactly) GREEN.
- Wider sweep (domain/sap10_calculator/ + domain/sap10_ml/): 1654
  passed / 20 failed — exact pre-move baseline. All 20 failures
  pre-existing (10 hand-built skeleton + 4 cohort chain + 6 cohort
  diff).
- Pyright net-zero on the 4 touched runtime/test files (0 errors)
  and unchanged on heat_transmission.py (13) / cert_to_inputs.py (35) /
  mapper.py (33).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 13:17:18 +00:00
Khalim Conn-Kowlessar
68401c517a refactor: lift-and-shift packages/domain/src/domain/ml → domain/sap10_ml
Sibling migration to the sap10_calculator move — `domain.ml` now lives
at the root-level layout (`domain/sap10_ml/`) matching the pattern
already used by `domain.addresses`, `domain.tasks`, `domain.postcode`,
and `domain.sap10_calculator`.

Changes:

- `git mv packages/domain/src/domain/ml → domain/sap10_ml` (19 files;
  history preserved).
- Subpackage rename: `domain.ml` → `domain.sap10_ml`. 32 references
  rewritten across .py and .md files: 11 internal + 21 external
  (datatypes/epc/domain/mapper.py, 14 files in domain/sap10_calculator,
  2 backend tests, 2 ADRs, 1 README, 1 design doc).
- Path-string updates: `pytest.ini` testpath
  `packages/domain/src/domain/ml/tests` → `domain/sap10_ml/tests` so
  ML tests stay in the default auto-discovered sweep. `CONTEXT.md`
  also updated.

`packages/domain/src/domain/` is now empty — the workspace `domain/`
tree has been fully migrated. Together with the `domain/__init__.py`
deletions from the sap10_calculator commit (29ac35cc), `domain` is
now a single root-level namespace package with subpackages
{addresses, sap10_calculator, sap10_ml, tasks} + the standalone
`postcode.py` module.

Verified:

- Focused sweep (backend mapper-chain + sap10_calculator worksheet
  e2e + golden fixtures): 99 passed / 19 failed — identical baseline.
- Wider sweep (all sap10_calculator + sap10_ml): 1654 passed / 20
  failed (same pre-existing failures).
- domain/sap10_ml/tests: 210/210 PASSED at new path.
- Pyright net-zero: heat_transmission.py 13, cert_to_inputs.py 35,
  mapper.py 33, rdsap_uvalues.py 1 (all unchanged from baseline).

Note: `packages/domain/pyproject.toml` still declares
`packages = ["src/domain"]` for the hatchling wheel — that target
directory is now empty and the wheel build is effectively a no-op.
Retiring the workspace package or repointing the wheel is a follow-up.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 13:01:35 +00:00