refactor: lift-and-shift packages/domain/src/domain/sap → domain/sap10_calculator

Migration of the SAP 10.2 calculator package from the uv-workspace
src-layout (`packages/domain/src/domain/sap`) to the root-level layout
(`domain/sap10_calculator`), matching the pattern already used by
`domain.addresses` / `domain.tasks` / `domain.postcode`.

Changes:

- `git mv packages/domain/src/domain/sap → domain/sap10_calculator`
  (92 files; git auto-detected all as renames so blame/history is
  preserved).
- Subpackage rename: `domain.sap` → `domain.sap10_calculator`. 48
  Python files rewritten (`from domain.sap.X` → `from domain.sap10_
  calculator.X`); zero remaining `domain.sap` refs after the sed pass.
- Path-string updates: 3 .py files (test fixtures + xlsx loader) +
  6 markdown docs (CONTEXT.md, 2 ADRs, 3 sap-spec docs, sap10_
  calculator/README.md) had hard-coded `packages/domain/src/domain/
  sap/...` paths rewritten to `domain/sap10_calculator/...`.
- `Path(__file__).parents[N]` rebasing: the old tree was 3 levels
  deeper than the new one (`packages/domain/src/`), so 4× `parents[7]`
  became `parents[4]` and 1× `parents[6]` became `parents[3]` across
  `tables/pcdb/{__init__.py, postcode_weather.py, etl.py}`,
  `worksheet/tests/_xlsx_loader.py`, and `tests/test_pcdb_etl.py`.
- PEP 420 namespace package: deleted both `domain/__init__.py`
  (root + workspace, both load-bearing only as empty/docstring) so
  Python combines `domain.sap10_calculator` (root) and `domain.ml`
  (workspace) into one namespace package. Confirmed via
  `domain.__path__ == ['/workspaces/model/domain',
  '/workspaces/model/packages/domain/src/domain']`. Without this,
  the root `domain/__init__.py` shadowed the workspace one and
  `domain.ml` was unreachable.

Verified:

- Full sweep (`backend/documents_parser/tests/test_summary_pdf_
  mapper_chain.py + domain/sap10_calculator/worksheet/tests/test_
  e2e_elmhurst_sap_score.py + domain/sap10_calculator/rdsap/tests/
  test_golden_fixtures.py`): 99 passed / 19 failed — exact same
  counts as pre-refactor. All 19 failures pre-existing (9 hand-built
  001479 + 6 cohort diff + 4 cohort chain non-spec).
- Wider sweep (all sap10_calculator + domain.ml): 1654 passed /
  20 failed (the +1 vs the focused sweep is the pre-existing
  `test_roof_insulated_assumed_with_ni_thickness_uses_50mm_per_
  section_5_11_4` which was already failing on the previous baseline).
- Pyright net-zero on the three load-bearing baselines:
  `heat_transmission.py` 13, `cert_to_inputs.py` 35, `mapper.py` 33.

Lift-and-shift only — no semantic renames (`Sap10Calculator` stays
`Sap10Calculator`), no testpaths edits in pytest.ini (sap tests
continue to be invoked by explicit pytest paths).

Note: `domain.ml` still lives at `packages/domain/src/domain/ml/`.
Migrating it would close out the dual-`domain/` layout but is
out of scope for this commit.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-05-26 12:19:27 +00:00
parent 87b6045c97
commit 29ac35ccbe
98 changed files with 200 additions and 204 deletions

View file

@ -1,6 +1,6 @@
"""End-to-end validation for the Elmhurst Summary→EpcPropertyData chain. """End-to-end validation for the Elmhurst Summary→EpcPropertyData chain.
The 6 Elmhurst worksheet fixtures in `domain.sap.worksheet.tests` The 6 Elmhurst worksheet fixtures in `domain.sap10_calculator.worksheet.tests`
build their `EpcPropertyData` synthetically they validate the build their `EpcPropertyData` synthetically they validate the
calculator + cascade in isolation from the mapper. This file pins calculator + cascade in isolation from the mapper. This file pins
the OTHER half of the chain: `from_elmhurst_site_notes` must produce the OTHER half of the chain: `from_elmhurst_site_notes` must produce
@ -37,9 +37,9 @@ from typing import cast
from backend.documents_parser.elmhurst_extractor import ElmhurstSiteNotesExtractor from backend.documents_parser.elmhurst_extractor import ElmhurstSiteNotesExtractor
from datatypes.epc.domain.mapper import EpcPropertyDataMapper from datatypes.epc.domain.mapper import EpcPropertyDataMapper
from domain.sap.calculator import calculate_sap_from_inputs from domain.sap10_calculator.calculator import calculate_sap_from_inputs
from domain.sap.rdsap.cert_to_inputs import SAP_10_2_SPEC_PRICES, cert_to_inputs from domain.sap10_calculator.rdsap.cert_to_inputs import SAP_10_2_SPEC_PRICES, cert_to_inputs
from domain.sap.worksheet.tests import ( from domain.sap10_calculator.worksheet.tests import (
_elmhurst_worksheet_000474 as _w000474, _elmhurst_worksheet_000474 as _w000474,
_elmhurst_worksheet_000477 as _w000477, _elmhurst_worksheet_000477 as _w000477,
_elmhurst_worksheet_000480 as _w000480, _elmhurst_worksheet_000480 as _w000480,
@ -63,7 +63,7 @@ _SUMMARY_001479_PDF = _FIXTURES / "Summary_001479.pdf"
# matches worksheet continuous SAP at 1e-4". # matches worksheet continuous SAP at 1e-4".
_API_001479_JSON = ( _API_001479_JSON = (
Path(__file__).parents[3] Path(__file__).parents[3]
/ "packages/domain/src/domain/sap/rdsap/tests/fixtures/golden" / "domain/sap10_calculator/rdsap/tests/fixtures/golden"
/ "0535-9020-6509-0821-6222.json" / "0535-9020-6509-0821-6222.json"
) )
@ -108,7 +108,7 @@ def _summary_pdf_to_textract_style_pages(pdf_path: Path) -> list[str]:
def test_summary_000474_mapper_produces_three_building_parts() -> None: def test_summary_000474_mapper_produces_three_building_parts() -> None:
# Arrange — cert U985-0001-000474 is a mid-terrace with 3 building # Arrange — cert U985-0001-000474 is a mid-terrace with 3 building
# parts (Main + 2 extensions) per the hand-built worksheet fixture # parts (Main + 2 extensions) per the hand-built worksheet fixture
# at packages/domain/src/domain/sap/worksheet/tests/ # at domain/sap10_calculator/worksheet/tests/
# _elmhurst_worksheet_000474.py. Routing the Summary PDF through # _elmhurst_worksheet_000474.py. Routing the Summary PDF through
# extractor + mapper must yield the same count. # extractor + mapper must yield the same count.
pages = _summary_pdf_to_textract_style_pages(_SUMMARY_000474_PDF) pages = _summary_pdf_to_textract_style_pages(_SUMMARY_000474_PDF)

View file

@ -6,7 +6,7 @@ Two boundary factories convert raw inputs to canonical members:
- `BuildingPartIdentifier.extension(n)` (site-notes / construction id) - `BuildingPartIdentifier.extension(n)` (site-notes / construction id)
P6.1 starts P6 (strict-type EpcPropertyData) from the documented pain P6.1 starts P6 (strict-type EpcPropertyData) from the documented pain
point in packages/domain/src/domain/sap/worksheet/dimensions.py:74-82. point in domain/sap10_calculator/worksheet/dimensions.py:74-82.
""" """
from __future__ import annotations from __future__ import annotations

View file

@ -51,7 +51,7 @@ The ML model is **not deprecated**. It is repurposed as a **residual learner** a
A full SAP 10.3 worksheet plus the data-extraction rules from RdSAP 10 Appendix S. Module organisation: A full SAP 10.3 worksheet plus the data-extraction rules from RdSAP 10 Appendix S. Module organisation:
``` ```
packages/domain/src/domain/sap/ domain/sap10_calculator/
__init__.py # Sap10Calculator entry point + SapResult dataclass __init__.py # Sap10Calculator entry point + SapResult dataclass
worksheet/ worksheet/
dimensions.py # §1 dimensions.py # §1
@ -79,7 +79,7 @@ packages/domain/src/domain/sap/
cascade_defaults.py # the RdSAP10 "assume-typical" rules (currently in rdsap_uvalues.py) cascade_defaults.py # the RdSAP10 "assume-typical" rules (currently in rdsap_uvalues.py)
``` ```
The existing `domain.ml.*` modules stay where they are during Session A; they continue serving the live ML pipeline. Session B promotes them into `domain.sap.*` once parity is reached. The existing `domain.ml.*` modules stay where they are during Session A; they continue serving the live ML pipeline. Session B promotes them into `domain.sap10_calculator.*` once parity is reached.
## Sap10Calculator interface ## Sap10Calculator interface
@ -118,7 +118,7 @@ We do **not** retire the existing ML pipeline until both validations pass.
- **The six ML targets remain those from ADR-0007.** The residual head predicts deltas against the same six quantities. - **The six ML targets remain those from ADR-0007.** The residual head predicts deltas against the same six quantities.
- **ADR-0008's physics-as-feature pattern stays valid for the ML residual head.** The residual head probably needs fewer features, but the cascade U-value defaults and SAP efficiency lookups remain useful as feature builders if the calculator subset alone underfits. - **ADR-0008's physics-as-feature pattern stays valid for the ML residual head.** The residual head probably needs fewer features, but the cascade U-value defaults and SAP efficiency lookups remain useful as feature builders if the calculator subset alone underfits.
- **`energy_rating_current` remains excluded from features.** Same leakage rule. - **`energy_rating_current` remains excluded from features.** Same leakage rule.
- **RdSAP 10 cert-extraction rules are now first-class in the codebase.** Rules that were ad-hoc in `transform.py` move into `domain.sap.rdsap.appendix_s`. - **RdSAP 10 cert-extraction rules are now first-class in the codebase.** Rules that were ad-hoc in `transform.py` move into `domain.sap10_calculator.rdsap.appendix_s`.
- **The training parquet schema continues at v2.x.** A new column `calculator_sap_score` lands as a non-breaking addition once Session A reaches parity. The schema version bumps to v3.0.0 only when the residual targets replace the raw targets — a coordinated AutoGluon-repo deploy, per ADR-0008's cutover discipline. - **The training parquet schema continues at v2.x.** A new column `calculator_sap_score` lands as a non-breaking addition once Session A reaches parity. The schema version bumps to v3.0.0 only when the residual targets replace the raw targets — a coordinated AutoGluon-repo deploy, per ADR-0008's cutover discipline.
## SAP 10.2 → SAP 10.3 implications ## SAP 10.2 → SAP 10.3 implications
@ -147,7 +147,7 @@ Re-derivation work is bounded — a few hundred numbers across tables — and th
## Consequences ## Consequences
- A new top-level domain area `domain.sap.*` is introduced; over Sessions B/C it absorbs `domain.ml.{envelope,demand,ecf,rdsap_uvalues,sap_efficiencies,ventilation}.py`. The ML transform stops shipping those as standalone features once the residual head takes over. - A new top-level domain area `domain.sap10_calculator.*` is introduced; over Sessions B/C it absorbs `domain.ml.{envelope,demand,ecf,rdsap_uvalues,sap_efficiencies,ventilation}.py`. The ML transform stops shipping those as standalone features once the residual head takes over.
- The codebase carries two SAP outputs: cert-reported `sap_score` (ground truth at training time) and calculator-emitted `sap_score` (ground truth at inference time for any RdSAP cert input). The product layer chooses; for "score this hypothetical post-retrofit state", calculator wins. - The codebase carries two SAP outputs: cert-reported `sap_score` (ground truth at training time) and calculator-emitted `sap_score` (ground truth at inference time for any RdSAP cert input). The product layer chooses; for "score this hypothetical post-retrofit state", calculator wins.
- The deterministic calculator is **version-bound to SAP 10.3.** A future SAP 10.4 is a calculator MAJOR bump and an ADR. The ML residual head is SAP-version-agnostic only insofar as the residual distribution it learns stays stationary; in practice a spec bump retrains the residual head. - The deterministic calculator is **version-bound to SAP 10.3.** A future SAP 10.4 is a calculator MAJOR bump and an ADR. The ML residual head is SAP-version-agnostic only insofar as the residual distribution it learns stays stationary; in practice a spec bump retrains the residual head.
- Spec PDFs live in `docs/sap-spec/` (this repo). The repo now carries the canonical reference for what the calculator computes. License: SAP 10.3 © Crown copyright 2026; RdSAP 10 © BRE — both are public-interest references for SAP-compliant software, included for traceability. - Spec PDFs live in `docs/sap-spec/` (this repo). The repo now carries the canonical reference for what the calculator computes. License: SAP 10.3 © Crown copyright 2026; RdSAP 10 © BRE — both are public-interest references for SAP-compliant software, included for traceability.

View file

@ -4,7 +4,7 @@
## Why this ADR exists ## Why this ADR exists
ADR-0009 was written before a second-order problem in the validation corpus was visible: the 250k-cert training parquet spans **multiple SAP spec versions** (SAP 10.1 from 2019, SAP 10.2 pre- and post-14-March-2025 amendment), each of which was the active table when its certs were lodged. The prior session's `domain.sap.tables.table_12_cert_calibration` layer was implicitly absorbing this version mixture into a single "best fit" price set ~1025 % lower than the SAP 10.2 (14-03-2025) spec — closer to the SAP 10.1 era prices. Every spec-correctness slice that touched a downstream component (HW cylinder zero-loss, gas standing charges, Table 12a fractional blending) registered as a regression on the parity probe because the cert-cal layer had been numerically calibrated against the buggy state of every other component. ADR-0009 was written before a second-order problem in the validation corpus was visible: the 250k-cert training parquet spans **multiple SAP spec versions** (SAP 10.1 from 2019, SAP 10.2 pre- and post-14-March-2025 amendment), each of which was the active table when its certs were lodged. The prior session's `domain.sap10_calculator.tables.table_12_cert_calibration` layer was implicitly absorbing this version mixture into a single "best fit" price set ~1025 % lower than the SAP 10.2 (14-03-2025) spec — closer to the SAP 10.1 era prices. Every spec-correctness slice that touched a downstream component (HW cylinder zero-loss, gas standing charges, Table 12a fractional blending) registered as a regression on the parity probe because the cert-cal layer had been numerically calibrated against the buggy state of every other component.
This ADR resolves four entangled decisions at once. They are coupled — none of them is the right call in isolation. This ADR resolves four entangled decisions at once. They are coupled — none of them is the right call in isolation.
@ -14,7 +14,7 @@ This ADR resolves four entangled decisions at once. They are coupled — none of
ADR-0009 named SAP 10.3 (13-01-2026) as the calculator's target. No SAP-10.3-lodged certs exist in the corpus; assessor software has not migrated. Targeting SAP 10.3 produces a calculator whose output is verifiable against no cert. The active target is SAP 10.2 (14-03-2025 amendment) — both the document RdSAP 10 (10-06-2025) cross-references for heating-system identification, and the amendment that current assessor software is on. ADR-0009 named SAP 10.3 (13-01-2026) as the calculator's target. No SAP-10.3-lodged certs exist in the corpus; assessor software has not migrated. Targeting SAP 10.3 produces a calculator whose output is verifiable against no cert. The active target is SAP 10.2 (14-03-2025 amendment) — both the document RdSAP 10 (10-06-2025) cross-references for heating-system identification, and the amendment that current assessor software is on.
`packages/domain/src/domain/sap/tables/table_12.py` is re-labelled as SAP 10.2 (14-03-2025). Its CO2 factors are corrected to spec (0.210 kg/kWh mains gas, 0.136 kg/kWh standard electricity — the file currently has SAP 10.3 values 0.214 and 0.086). Prices already match SAP 10.2 (3.64 p mains gas, 16.49 p standard electricity, etc.) — the misleading "+25 % shift from SAP 10.2 to 10.3" comment is removed; the 13.19 p figure is from SAP 10.1, not SAP 10.2. `domain/sap10_calculator/tables/table_12.py` is re-labelled as SAP 10.2 (14-03-2025). Its CO2 factors are corrected to spec (0.210 kg/kWh mains gas, 0.136 kg/kWh standard electricity — the file currently has SAP 10.3 values 0.214 and 0.086). Prices already match SAP 10.2 (3.64 p mains gas, 16.49 p standard electricity, etc.) — the misleading "+25 % shift from SAP 10.2 to 10.3" comment is removed; the 13.19 p figure is from SAP 10.1, not SAP 10.2.
A future ADR retargets to SAP 10.3 once the cert corpus migrates (expected late 2026 or 2027 once BRE updates RdSAP to reference SAP 10.3). A future ADR retargets to SAP 10.3 once the cert corpus migrates (expected late 2026 or 2027 once BRE updates RdSAP to reference SAP 10.3).
@ -24,7 +24,7 @@ The cert-calibration table is bug-masking. Its prices are pre-March-2025 SAP val
This includes the `cert_calibration_e7_codes` extension that routes codes 191196 (direct-electric) and 691696 (room heaters) to off-peak rates — Table 12a is explicit that "other direct-acting electric heating" bills 100 % at the high rate on a 7-hour tariff. The S-B14 finding that motivated this hack is in §8 of the handover as a documented dead-end. This includes the `cert_calibration_e7_codes` extension that routes codes 191196 (direct-electric) and 691696 (room heaters) to off-peak rates — Table 12a is explicit that "other direct-acting electric heating" bills 100 % at the high rate on a 7-hour tariff. The S-B14 finding that motivated this hack is in §8 of the handover as a documented dead-end.
`domain.sap.tables.table_12.unit_price_p_per_kwh` becomes the only price API. Parity probes are updated to use it. `domain.sap10_calculator.tables.table_12.unit_price_p_per_kwh` becomes the only price API. Parity probes are updated to use it.
### 3. Validation Cohort is filtered to a single spec-version window ### 3. Validation Cohort is filtered to a single spec-version window
@ -84,9 +84,9 @@ The 000490 Elmhurst fixture had a recorded -12.5% cost gap (£706 vs £807 PDF)
### Consequences ### Consequences
- **`packages/domain/src/domain/sap/tables/table_32.py`** ships the RdSAP10 unit prices + standing charges + Table 12 note (a) gating function. Table 12 keeps the CO2 + PEF columns. - **`domain/sap10_calculator/tables/table_32.py`** ships the RdSAP10 unit prices + standing charges + Table 12 note (a) gating function. Table 12 keeps the CO2 + PEF columns.
- **`packages/domain/src/domain/sap/tables/table_12a.py`** ships the high-rate-fraction lookups for off-peak split (Table 12a in SAP 10.2 PDF page 191 — RdSAP10 §19.1 cross-references this table directly). `Tariff.TEN_HOUR` carried for spec completeness even though RdSAP cert `meter_type` enum (1..5) has no 10-hour code. - **`domain/sap10_calculator/tables/table_12a.py`** ships the high-rate-fraction lookups for off-peak split (Table 12a in SAP 10.2 PDF page 191 — RdSAP10 §19.1 cross-references this table directly). `Tariff.TEN_HOUR` carried for spec completeness even though RdSAP cert `meter_type` enum (1..5) has no 10-hour code.
- **`packages/domain/src/domain/sap/worksheet/fuel_cost.py`** ships the §10a orchestrator producing `FuelCostResult` (32 fields, line refs (240)..(255)). `cert_to_inputs._fuel_cost` precompute wires it from cert state. - **`domain/sap10_calculator/worksheet/fuel_cost.py`** ships the §10a orchestrator producing `FuelCostResult` (32 fields, line refs (240)..(255)). `cert_to_inputs._fuel_cost` precompute wires it from cert state.
- The 000474 Elmhurst fixture cost residual widened from -0.6% to +10.7% (SAP rating ceiling loosened 2 → 4) because the pre-amendment wrong-table-but-cancels-kWh accidentally compensated for upstream §4 HW kWh + Appendix L lighting overestimates. **§4 HW worksheet tightening is the next ticket** — see project memory `project_section_4_hw_next_ticket`. Ceiling drops back to 2 (or below) when that lands. - The 000474 Elmhurst fixture cost residual widened from -0.6% to +10.7% (SAP rating ceiling loosened 2 → 4) because the pre-amendment wrong-table-but-cancels-kWh accidentally compensated for upstream §4 HW kWh + Appendix L lighting overestimates. **§4 HW worksheet tightening is the next ticket** — see project memory `project_section_4_hw_next_ticket`. Ceiling drops back to 2 (or below) when that lands.
- Golden corpus SAP tolerance widened ±7 → ±11 per the Validation Cohort discipline (oil unit price +55% from Table 12 → Table 32 moves oil-heated golden certs whose lodged SAP scores pre-date Table 32). - Golden corpus SAP tolerance widened ±7 → ±11 per the Validation Cohort discipline (oil unit price +55% from Table 12 → Table 32 moves oil-heated golden certs whose lodged SAP scores pre-date Table 32).

View file

@ -41,14 +41,14 @@ plumbing fails loudly.
### Public API (the only thing you need from the SAP module) ### Public API (the only thing you need from the SAP module)
```python ```python
from domain.sap.rdsap.cert_to_inputs import ( from domain.sap10_calculator.rdsap.cert_to_inputs import (
cert_to_inputs, # Rating cascade cert_to_inputs, # Rating cascade
cert_to_demand_inputs, # Demand cascade cert_to_demand_inputs, # Demand cascade
local_climate_for_cert, local_climate_for_cert,
environmental_section_from_cert, environmental_section_from_cert,
primary_energy_section_from_cert, primary_energy_section_from_cert,
) )
from domain.sap.calculator import calculate_sap_from_inputs, SapResult from domain.sap10_calculator.calculator import calculate_sap_from_inputs, SapResult
``` ```
See `SAP_CALCULATOR.md` §2 for the recommended `dwelling_outputs(epc)` See `SAP_CALCULATOR.md` §2 for the recommended `dwelling_outputs(epc)`
@ -101,8 +101,8 @@ side is what you need to map out:
schema versions (SAP-Schema-18/19, RdSAP-Schema-18) to schema versions (SAP-Schema-18/19, RdSAP-Schema-18) to
`EpcPropertyData` lives there. `EpcPropertyData` lives there.
3. **Is SAP scoring already wired to the API?** Search the backend for 3. **Is SAP scoring already wired to the API?** Search the backend for
imports of `domain.sap.rdsap.cert_to_inputs` or imports of `domain.sap10_calculator.rdsap.cert_to_inputs` or
`domain.sap.calculator`. If it's not yet wired, the integration test `domain.sap10_calculator.calculator`. If it's not yet wired, the integration test
is a forcing function for wiring it. is a forcing function for wiring it.
4. **What's the response shape?** The 4 outputs above are what the EPC 4. **What's the response shape?** The 4 outputs above are what the EPC
publishes; the API may already expose them, or may expose a wider publishes; the API may already expose them, or may expose a wider
@ -132,10 +132,10 @@ expanding.
| File | Why | | File | Why |
|---|---| |---|---|
| [`docs/sap-spec/SAP_CALCULATOR.md`](./SAP_CALCULATOR.md) | Module API + architecture (you're heading there) | | [`docs/sap-spec/SAP_CALCULATOR.md`](./SAP_CALCULATOR.md) | Module API + architecture (you're heading there) |
| [`packages/domain/src/domain/sap/calculator.py`](../../packages/domain/src/domain/sap/calculator.py) | `SapResult` fields you'll assert against | | [`domain/sap10_calculator/calculator.py`](../../domain/sap10_calculator/calculator.py) | `SapResult` fields you'll assert against |
| [`packages/domain/src/domain/sap/rdsap/cert_to_inputs.py`](../../packages/domain/src/domain/sap/rdsap/cert_to_inputs.py) | The 3 public entry points + the section helpers | | [`domain/sap10_calculator/rdsap/cert_to_inputs.py`](../../domain/sap10_calculator/rdsap/cert_to_inputs.py) | The 3 public entry points + the section helpers |
| [`packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000474.py`](../../packages/domain/src/domain/sap/worksheet/tests/_elmhurst_worksheet_000474.py) | A reference fixture — `build_epc()` shows the EpcPropertyData shape | | [`domain/sap10_calculator/worksheet/tests/_elmhurst_worksheet_000474.py`](../../domain/sap10_calculator/worksheet/tests/_elmhurst_worksheet_000474.py) | A reference fixture — `build_epc()` shows the EpcPropertyData shape |
| [`packages/domain/src/domain/sap/worksheet/tests/test_e2e_elmhurst_sap_score.py`](../../packages/domain/src/domain/sap/worksheet/tests/test_e2e_elmhurst_sap_score.py) | The current e2e test pattern — model your integration test on this | | [`domain/sap10_calculator/worksheet/tests/test_e2e_elmhurst_sap_score.py`](../../domain/sap10_calculator/worksheet/tests/test_e2e_elmhurst_sap_score.py) | The current e2e test pattern — model your integration test on this |
| `backend/` (explore) | API entry points | | `backend/` (explore) | API entry points |
| [`datatypes/epc/domain/mapper.py`](../../datatypes/epc/domain/mapper.py) | Schema → EpcPropertyData mappers | | [`datatypes/epc/domain/mapper.py`](../../datatypes/epc/domain/mapper.py) | Schema → EpcPropertyData mappers |
@ -146,18 +146,18 @@ expanding.
```bash ```bash
# Confirm SAP calculator is still 930/930 green # Confirm SAP calculator is still 930/930 green
python -m pytest \ python -m pytest \
packages/domain/src/domain/sap/worksheet/tests/test_section_cascade_pins.py \ domain/sap10_calculator/worksheet/tests/test_section_cascade_pins.py \
packages/domain/src/domain/sap/worksheet/tests/test_e2e_elmhurst_sap_score.py \ domain/sap10_calculator/worksheet/tests/test_e2e_elmhurst_sap_score.py \
--no-cov --no-header --tb=no -q --no-cov --no-header --tb=no -q
# Show the 4 EPC outputs for fixture 000474 # Show the 4 EPC outputs for fixture 000474
cd packages/domain/src && python -c " cd packages/domain/src && python -c "
from domain.sap.rdsap.cert_to_inputs import ( from domain.sap10_calculator.rdsap.cert_to_inputs import (
cert_to_inputs, local_climate_for_cert, cert_to_inputs, local_climate_for_cert,
environmental_section_from_cert, primary_energy_section_from_cert, environmental_section_from_cert, primary_energy_section_from_cert,
) )
from domain.sap.calculator import calculate_sap_from_inputs from domain.sap10_calculator.calculator import calculate_sap_from_inputs
from domain.sap.worksheet.tests import _elmhurst_worksheet_000474 as w from domain.sap10_calculator.worksheet.tests import _elmhurst_worksheet_000474 as w
epc = w.build_epc() epc = w.build_epc()
pc = local_climate_for_cert(epc) pc = local_climate_for_cert(epc)
rating = calculate_sap_from_inputs(cert_to_inputs(epc)) rating = calculate_sap_from_inputs(cert_to_inputs(epc))
@ -183,7 +183,7 @@ postcode-conditions EI value, not what the EPC publishes.
- **Extending the SAP calculator.** It's closed at the EPC-output layer. - **Extending the SAP calculator.** It's closed at the EPC-output layer.
If you find an additional cert-shape variation that breaks the If you find an additional cert-shape variation that breaks the
calculator, capture it as a new conformance fixture (see calculator, capture it as a new conformance fixture (see
`packages/domain/src/domain/sap/README.md`) — don't paper over it in `domain/sap10_calculator/README.md`) — don't paper over it in
the integration test. the integration test.
- **BEDF fuel pricing.** The Fuel Bill on the EPC uses postcode-specific - **BEDF fuel pricing.** The Fuel Bill on the EPC uses postcode-specific
BEDF prices (PCDB Table 200), which are deferred. The 4 outputs above BEDF prices (PCDB Table 200), which are deferred. The 4 outputs above

View file

@ -111,7 +111,7 @@ from datatypes.epc.domain.mapper import EpcPropertyDataMapper
import json, dataclasses import json, dataclasses
from pathlib import Path from pathlib import Path
api = json.loads(Path('/workspaces/model/packages/domain/src/domain/sap/rdsap/tests/fixtures/golden/0535-9020-6509-0821-6222.json').read_text()) api = json.loads(Path('/workspaces/model/domain/sap10_calculator/rdsap/tests/fixtures/golden/0535-9020-6509-0821-6222.json').read_text())
api_mapped = EpcPropertyDataMapper.from_api_response(api) api_mapped = EpcPropertyDataMapper.from_api_response(api)
pages = _summary_pdf_to_textract_style_pages(Path('/workspaces/model/backend/documents_parser/tests/fixtures/Summary_001479.pdf')) pages = _summary_pdf_to_textract_style_pages(Path('/workspaces/model/backend/documents_parser/tests/fixtures/Summary_001479.pdf'))
sn = ElmhurstSiteNotesExtractor(pages).extract() sn = ElmhurstSiteNotesExtractor(pages).extract()
@ -234,7 +234,7 @@ override field, (c) wait for more cert pairs to confirm pattern.
## Cached artefacts ## Cached artefacts
- `packages/domain/src/domain/sap/rdsap/tests/fixtures/golden/0535- - `domain/sap10_calculator/rdsap/tests/fixtures/golden/0535-
9020-6509-0821-6222.json` — API JSON for cert 001479 (RdSAP-Schema- 9020-6509-0821-6222.json` — API JSON for cert 001479 (RdSAP-Schema-
21.0.1). 21.0.1).
- `backend/documents_parser/tests/fixtures/Summary_001479.pdf` - `backend/documents_parser/tests/fixtures/Summary_001479.pdf`
@ -273,8 +273,8 @@ before this rewrite).
```bash ```bash
PYTHONPATH=/workspaces/model:/workspaces/model/packages/domain/src \ PYTHONPATH=/workspaces/model:/workspaces/model/packages/domain/src \
python -m pytest backend/documents_parser/tests/test_summary_pdf_mapper_chain.py \ python -m pytest backend/documents_parser/tests/test_summary_pdf_mapper_chain.py \
packages/domain/src/domain/sap/worksheet/tests/test_e2e_elmhurst_sap_score.py \ domain/sap10_calculator/worksheet/tests/test_e2e_elmhurst_sap_score.py \
packages/domain/src/domain/sap/rdsap/tests/test_golden_fixtures.py \ domain/sap10_calculator/rdsap/tests/test_golden_fixtures.py \
--no-cov -q --no-cov -q
``` ```
Expect **99 passed / 19 failed**. All 19 failures pre-existing: Expect **99 passed / 19 failed**. All 19 failures pre-existing:

View file

@ -10,21 +10,21 @@ Current Carbon, Current Primary Energy, and Fuel Bill).
**Current state: 930/930 pins green** (768 rating + 90 demand + 72 e2e). **Current state: 930/930 pins green** (768 rating + 90 demand + 72 e2e).
This document is the public API + architecture reference. For fixture This document is the public API + architecture reference. For fixture
authoring see [`packages/domain/src/domain/sap/README.md`](../../packages/domain/src/domain/sap/README.md). authoring see [`domain/sap10_calculator/README.md`](../../domain/sap10_calculator/README.md).
--- ---
## 1. Public API ## 1. Public API
Three entry points, all in `domain.sap.rdsap.cert_to_inputs`: Three entry points, all in `domain.sap10_calculator.rdsap.cert_to_inputs`:
```python ```python
from domain.sap.rdsap.cert_to_inputs import ( from domain.sap10_calculator.rdsap.cert_to_inputs import (
cert_to_inputs, # SAP rating + EI rating (UK-avg climate) cert_to_inputs, # SAP rating + EI rating (UK-avg climate)
cert_to_demand_inputs, # Current Carbon + Current PE (postcode climate) cert_to_demand_inputs, # Current Carbon + Current PE (postcode climate)
local_climate_for_cert, # postcode → PostcodeClimate (None on miss) local_climate_for_cert, # postcode → PostcodeClimate (None on miss)
) )
from domain.sap.calculator import calculate_sap_from_inputs, SapResult from domain.sap10_calculator.calculator import calculate_sap_from_inputs, SapResult
``` ```
### 1.1 Rating cascade — `cert_to_inputs(epc)` ### 1.1 Rating cascade — `cert_to_inputs(epc)`
@ -93,11 +93,11 @@ upgrade wall insulation), re-run, observe the delta. The shape:
```python ```python
import dataclasses import dataclasses
from domain.sap.rdsap.cert_to_inputs import ( from domain.sap10_calculator.rdsap.cert_to_inputs import (
cert_to_inputs, local_climate_for_cert, cert_to_inputs, local_climate_for_cert,
environmental_section_from_cert, primary_energy_section_from_cert, environmental_section_from_cert, primary_energy_section_from_cert,
) )
from domain.sap.calculator import calculate_sap_from_inputs from domain.sap10_calculator.calculator import calculate_sap_from_inputs
def dwelling_outputs(epc): def dwelling_outputs(epc):
"""The 4 EPC-facing outputs for any cert. """The 4 EPC-facing outputs for any cert.
@ -165,7 +165,7 @@ Two cascades stacked on a shared physics core:
Climate is the only difference between the two cascades. Internally, the Climate is the only difference between the two cascades. Internally, the
climate is plumbed through as either an `int` region index (0..21) or a climate is plumbed through as either an `int` region index (0..21) or a
`PostcodeClimate` instance (PCDB Table 172). Four functions in `PostcodeClimate` instance (PCDB Table 172). Four functions in
`domain.sap.climate.appendix_u` dispatch on `isinstance`: `domain.sap10_calculator.climate.appendix_u` dispatch on `isinstance`:
`external_temperature_c`, `wind_speed_m_per_s`, `external_temperature_c`, `wind_speed_m_per_s`,
`horizontal_solar_irradiance_w_per_m2`, plus `_latitude_deg` in `horizontal_solar_irradiance_w_per_m2`, plus `_latitude_deg` in
`worksheet/solar_gains.py`. `worksheet/solar_gains.py`.
@ -192,7 +192,7 @@ on `CalculatorInputs`.
## 4. File map ## 4. File map
``` ```
packages/domain/src/domain/sap/ domain/sap10_calculator/
├── calculator.py # Top-level orchestrator (CalculatorInputs → SapResult) ├── calculator.py # Top-level orchestrator (CalculatorInputs → SapResult)
├── README.md # Fixture authoring cookbook ├── README.md # Fixture authoring cookbook
├── rdsap/ ├── rdsap/
@ -288,12 +288,12 @@ monthly_infiltration_ach 6/6
```bash ```bash
# Full SAP calculator suite (cascade pins + e2e + helpers) # Full SAP calculator suite (cascade pins + e2e + helpers)
python -m pytest packages/domain/src/domain/sap/ --no-cov python -m pytest domain/sap10_calculator/ --no-cov
# Cascade pins only (the conformance suite) # Cascade pins only (the conformance suite)
python -m pytest \ python -m pytest \
packages/domain/src/domain/sap/worksheet/tests/test_section_cascade_pins.py \ domain/sap10_calculator/worksheet/tests/test_section_cascade_pins.py \
packages/domain/src/domain/sap/worksheet/tests/test_e2e_elmhurst_sap_score.py \ domain/sap10_calculator/worksheet/tests/test_e2e_elmhurst_sap_score.py \
--no-cov --no-header --tb=no -q --no-cov --no-header --tb=no -q
``` ```
@ -316,7 +316,7 @@ These are non-negotiable per `[[feedback-zero-error-strict]]` /
## 6. Adding a new conformance fixture ## 6. Adding a new conformance fixture
See [`packages/domain/src/domain/sap/README.md#adding-a-new-elmhurst-conformance-fixture`](../../packages/domain/src/domain/sap/README.md#adding-a-new-elmhurst-conformance-fixture) See [`domain/sap10_calculator/README.md#adding-a-new-elmhurst-conformance-fixture`](../../domain/sap10_calculator/README.md#adding-a-new-elmhurst-conformance-fixture)
for the step-by-step cookbook. Summary: for the step-by-step cookbook. Summary:
1. Drop a fixture module at `worksheet/tests/_elmhurst_worksheet_NNNNNN.py` 1. Drop a fixture module at `worksheet/tests/_elmhurst_worksheet_NNNNNN.py`

View file

@ -50,7 +50,7 @@ The assessor exports two PDFs from Elmhurst's RdSAP tool:
5. **Run the conformance tests**: 5. **Run the conformance tests**:
``` ```
python -m pytest packages/domain/src/domain/sap/worksheet/tests/ \ python -m pytest domain/sap10_calculator/worksheet/tests/ \
-k elmhurst --no-cov -v -k elmhurst --no-cov -v
``` ```
Each fixture appears 3× (one parametrize per section), pytest id = the cert ref number. Each fixture appears 3× (one parametrize per section), pytest id = the cert ref number.

View file

@ -3,7 +3,7 @@
Drives the 12-month heat-balance loop from a typed `CalculatorInputs` Drives the 12-month heat-balance loop from a typed `CalculatorInputs`
aggregate and emits a typed `SapResult`. This module is the physics aggregate and emits a typed `SapResult`. This module is the physics
assembly only the RdSAP certinputs mapping lives in assembly only the RdSAP certinputs mapping lives in
`domain.sap.rdsap.cert_to_inputs`. Splitting the two keeps orchestration `domain.sap10_calculator.rdsap.cert_to_inputs`. Splitting the two keeps orchestration
testable against synthetic inputs without dragging in cert-shape testable against synthetic inputs without dragging in cert-shape
assumptions. assumptions.
@ -44,15 +44,15 @@ from __future__ import annotations
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Final, Optional, TYPE_CHECKING from typing import Final, Optional, TYPE_CHECKING
from domain.sap.climate.appendix_u import external_temperature_c from domain.sap10_calculator.climate.appendix_u import external_temperature_c
if TYPE_CHECKING: if TYPE_CHECKING:
from datatypes.epc.domain.epc_property_data import EpcPropertyData from datatypes.epc.domain.epc_property_data import EpcPropertyData
from domain.sap.worksheet.dimensions import Dimensions from domain.sap10_calculator.worksheet.dimensions import Dimensions
from domain.sap.worksheet.energy_requirements import EnergyRequirementsResult from domain.sap10_calculator.worksheet.energy_requirements import EnergyRequirementsResult
from domain.sap.worksheet.fuel_cost import FuelCostResult from domain.sap10_calculator.worksheet.fuel_cost import FuelCostResult
from domain.sap.worksheet.heat_transmission import HeatTransmission from domain.sap10_calculator.worksheet.heat_transmission import HeatTransmission
from domain.sap.worksheet.rating import ( from domain.sap10_calculator.worksheet.rating import (
ECF_LOG_THRESHOLD, ECF_LOG_THRESHOLD,
ENERGY_COST_DEFLATOR, ENERGY_COST_DEFLATOR,
FLOOR_AREA_OFFSET_M2, FLOOR_AREA_OFFSET_M2,
@ -621,6 +621,6 @@ class Sap10Calculator:
""" """
def calculate(self, epc: "EpcPropertyData") -> SapResult: def calculate(self, epc: "EpcPropertyData") -> SapResult:
from domain.sap.rdsap.cert_to_inputs import cert_to_inputs from domain.sap10_calculator.rdsap.cert_to_inputs import cert_to_inputs
return calculate_sap_from_inputs(cert_to_inputs(epc)) return calculate_sap_from_inputs(cert_to_inputs(epc))

View file

@ -20,7 +20,7 @@ from __future__ import annotations
from typing import Final from typing import Final
from domain.sap.tables.pcdb.postcode_weather import PostcodeClimate from domain.sap10_calculator.tables.pcdb.postcode_weather import PostcodeClimate
# Table U1 — Mean external temperature (°C), 22 regions × 12 months. # Table U1 — Mean external temperature (°C), 22 regions × 12 months.

View file

@ -8,7 +8,7 @@ global solar irradiance on a horizontal plane and monthly solar declination.
import pytest import pytest
from domain.sap.climate.appendix_u import ( from domain.sap10_calculator.climate.appendix_u import (
external_temperature_c, external_temperature_c,
horizontal_solar_irradiance_w_per_m2, horizontal_solar_irradiance_w_per_m2,
solar_declination_deg, solar_declination_deg,

View file

@ -67,82 +67,82 @@ from domain.ml.sap_efficiencies import (
seasonal_efficiency, seasonal_efficiency,
water_heating_efficiency as _legacy_water_heating_efficiency, water_heating_efficiency as _legacy_water_heating_efficiency,
) )
from domain.sap.calculator import CalculatorInputs from domain.sap10_calculator.calculator import CalculatorInputs
from domain.sap.tables.pcdb import gas_oil_boiler_record from domain.sap10_calculator.tables.pcdb import gas_oil_boiler_record
from domain.sap.tables.pcdb.parser import GasOilBoilerRecord from domain.sap10_calculator.tables.pcdb.parser import GasOilBoilerRecord
from domain.sap.tables.pcdb.postcode_weather import ( from domain.sap10_calculator.tables.pcdb.postcode_weather import (
PostcodeClimate, PostcodeClimate,
postcode_climate, postcode_climate,
) )
from domain.sap.tables.table_12 import ( from domain.sap10_calculator.tables.table_12 import (
co2_monthly_factors_kg_per_kwh, co2_monthly_factors_kg_per_kwh,
co2_factor_kg_per_kwh, co2_factor_kg_per_kwh,
pe_monthly_factors_kwh_per_kwh, pe_monthly_factors_kwh_per_kwh,
primary_energy_factor, primary_energy_factor,
unit_price_p_per_kwh, unit_price_p_per_kwh,
) )
from domain.sap.tables.table_12a import ( from domain.sap10_calculator.tables.table_12a import (
Tariff, Tariff,
tariff_from_meter_type, tariff_from_meter_type,
) )
from domain.sap.tables.table_32 import ( from domain.sap10_calculator.tables.table_32 import (
additional_standing_charges_gbp, additional_standing_charges_gbp,
unit_price_p_per_kwh as table_32_unit_price_p_per_kwh, unit_price_p_per_kwh as table_32_unit_price_p_per_kwh,
) )
from domain.sap.worksheet.fuel_cost import FuelCostResult, fuel_cost from domain.sap10_calculator.worksheet.fuel_cost import FuelCostResult, fuel_cost
from domain.sap.worksheet.rating import ( from domain.sap10_calculator.worksheet.rating import (
ENERGY_COST_DEFLATOR, ENERGY_COST_DEFLATOR,
energy_cost_factor, energy_cost_factor,
environmental_impact_rating, environmental_impact_rating,
sap_rating, sap_rating,
sap_rating_integer, sap_rating_integer,
) )
from domain.sap.worksheet.dimensions import dimensions_from_cert from domain.sap10_calculator.worksheet.dimensions import dimensions_from_cert
from domain.sap.worksheet.internal_gains import ( from domain.sap10_calculator.worksheet.internal_gains import (
InternalGainsResult, InternalGainsResult,
OvershadingCategory, OvershadingCategory,
internal_gains_from_cert, internal_gains_from_cert,
) )
from domain.sap.worksheet.solar_gains import ( from domain.sap10_calculator.worksheet.solar_gains import (
ORIENTATION_BY_SAP10_CODE, ORIENTATION_BY_SAP10_CODE,
RoofWindowInput, RoofWindowInput,
SolarGainsResult, SolarGainsResult,
solar_gains_from_cert, solar_gains_from_cert,
surface_solar_flux_w_per_m2, surface_solar_flux_w_per_m2,
) )
from domain.sap.worksheet.heat_transmission import ( from domain.sap10_calculator.worksheet.heat_transmission import (
DwellingExposure, DwellingExposure,
HeatTransmission, HeatTransmission,
_AREA_ROUND_DP, _AREA_ROUND_DP,
_round_half_up, _round_half_up,
heat_transmission_from_cert, heat_transmission_from_cert,
) )
from domain.sap.climate.appendix_u import external_temperature_c from domain.sap10_calculator.climate.appendix_u import external_temperature_c
from domain.sap.worksheet.mean_internal_temperature import ( from domain.sap10_calculator.worksheet.mean_internal_temperature import (
MeanInternalTemperatureResult, MeanInternalTemperatureResult,
mean_internal_temperature_monthly, mean_internal_temperature_monthly,
) )
from domain.sap.worksheet.energy_requirements import ( from domain.sap10_calculator.worksheet.energy_requirements import (
EnergyRequirementsResult, EnergyRequirementsResult,
space_heating_fuel_monthly_kwh, space_heating_fuel_monthly_kwh,
) )
from domain.sap.worksheet.fabric_energy_efficiency import ( from domain.sap10_calculator.worksheet.fabric_energy_efficiency import (
fabric_energy_efficiency_kwh_per_m2_yr, fabric_energy_efficiency_kwh_per_m2_yr,
) )
from domain.sap.worksheet.space_cooling import ( from domain.sap10_calculator.worksheet.space_cooling import (
SpaceCoolingResult, SpaceCoolingResult,
space_cooling_monthly_kwh, space_cooling_monthly_kwh,
) )
from domain.sap.worksheet.space_heating import ( from domain.sap10_calculator.worksheet.space_heating import (
SpaceHeatingResult, SpaceHeatingResult,
space_heating_monthly_kwh, space_heating_monthly_kwh,
) )
from domain.sap.worksheet.ventilation import ( from domain.sap10_calculator.worksheet.ventilation import (
MechanicalVentilationKind, MechanicalVentilationKind,
VentilationResult, VentilationResult,
ventilation_from_inputs, ventilation_from_inputs,
) )
from domain.sap.worksheet.water_heating import ( from domain.sap10_calculator.worksheet.water_heating import (
TABLE_J1_TCOLD_FROM_MAINS_C, TABLE_J1_TCOLD_FROM_MAINS_C,
WaterHeatingResult, WaterHeatingResult,
combi_loss_monthly_kwh_table_3b_row_1_instantaneous, combi_loss_monthly_kwh_table_3b_row_1_instantaneous,

View file

@ -27,15 +27,15 @@ from domain.ml.tests._fixtures import (
make_sap_heating, make_sap_heating,
make_window, make_window,
) )
from domain.sap.calculator import Sap10Calculator, SapResult from domain.sap10_calculator.calculator import Sap10Calculator, SapResult
from domain.sap.rdsap.cert_to_inputs import ( from domain.sap10_calculator.rdsap.cert_to_inputs import (
cert_to_demand_inputs, cert_to_demand_inputs,
cert_to_inputs, cert_to_inputs,
pcdb_combi_loss_override, pcdb_combi_loss_override,
) )
from domain.sap.tables.pcdb import GasOilBoilerRecord, gas_oil_boiler_record from domain.sap10_calculator.tables.pcdb import GasOilBoilerRecord, gas_oil_boiler_record
from domain.sap.worksheet.tests import _elmhurst_worksheet_000477 as _w000477 from domain.sap10_calculator.worksheet.tests import _elmhurst_worksheet_000477 as _w000477
from domain.sap.worksheet.water_heating import ( from domain.sap10_calculator.worksheet.water_heating import (
combi_loss_monthly_kwh_table_3b_row_1_instantaneous, combi_loss_monthly_kwh_table_3b_row_1_instantaneous,
combi_loss_monthly_kwh_table_3c_two_profile_instantaneous, combi_loss_monthly_kwh_table_3c_two_profile_instantaneous,
) )

View file

@ -36,8 +36,8 @@ from typing import Any
import pytest import pytest
from datatypes.epc.domain.mapper import EpcPropertyDataMapper from datatypes.epc.domain.mapper import EpcPropertyDataMapper
from domain.sap.calculator import calculate_sap_from_inputs from domain.sap10_calculator.calculator import calculate_sap_from_inputs
from domain.sap.rdsap.cert_to_inputs import ( from domain.sap10_calculator.rdsap.cert_to_inputs import (
SAP_10_2_SPEC_PRICES, SAP_10_2_SPEC_PRICES,
cert_to_demand_inputs, cert_to_demand_inputs,
cert_to_inputs, cert_to_inputs,

View file

@ -27,13 +27,13 @@ import json
from pathlib import Path from pathlib import Path
from typing import Final, Optional from typing import Final, Optional
from domain.sap.tables.pcdb.parser import GasOilBoilerRecord from domain.sap10_calculator.tables.pcdb.parser import GasOilBoilerRecord
__all__ = ["GasOilBoilerRecord", "gas_oil_boiler_record"] __all__ = ["GasOilBoilerRecord", "gas_oil_boiler_record"]
_REPO_SAP_SPEC_DIR: Final[Path] = ( _REPO_SAP_SPEC_DIR: Final[Path] = (
Path(__file__).resolve().parents[7] / "docs" / "sap-spec" Path(__file__).resolve().parents[4] / "docs" / "sap-spec"
) )
_TABLE_105_JSONL: Final[Path] = ( _TABLE_105_JSONL: Final[Path] = (
_REPO_SAP_SPEC_DIR / "pcdb_table_105_gas_oil_boilers.jsonl" _REPO_SAP_SPEC_DIR / "pcdb_table_105_gas_oil_boilers.jsonl"

View file

@ -2,7 +2,7 @@
Idempotent. Re-run when BRE publishes an updated pcdb10.dat. JSON files Idempotent. Re-run when BRE publishes an updated pcdb10.dat. JSON files
are committed in-repo alongside the source .dat so callers can load are committed in-repo alongside the source .dat so callers can load
without a build step. Run via `python -m domain.sap.tables.pcdb.etl`. without a build step. Run via `python -m domain.sap10_calculator.tables.pcdb.etl`.
Reference: BRE PCDB pcdb10.dat (April 2026 revision). Reference: BRE PCDB pcdb10.dat (April 2026 revision).
""" """
@ -13,7 +13,7 @@ import json
from dataclasses import asdict from dataclasses import asdict
from pathlib import Path from pathlib import Path
from domain.sap.tables.pcdb.parser import ( from domain.sap10_calculator.tables.pcdb.parser import (
GasOilBoilerRecord, GasOilBoilerRecord,
RawPcdbRecord, RawPcdbRecord,
parse_table_105, parse_table_105,
@ -75,7 +75,7 @@ def run_etl(*, source: Path, output_dir: Path) -> None:
if __name__ == "__main__": # pragma: no cover — manual ETL invocation if __name__ == "__main__": # pragma: no cover — manual ETL invocation
repo_root = Path(__file__).resolve().parents[7] repo_root = Path(__file__).resolve().parents[4]
run_etl( run_etl(
source=repo_root / "docs" / "sap-spec" / "pcdb10.dat", source=repo_root / "docs" / "sap-spec" / "pcdb10.dat",
output_dir=repo_root / "docs" / "sap-spec", output_dir=repo_root / "docs" / "sap-spec",

View file

@ -21,7 +21,7 @@ from typing import Final, Optional
_PCDB_DAT_PATH: Final[Path] = ( _PCDB_DAT_PATH: Final[Path] = (
Path(__file__).resolve().parents[7] / "docs" / "sap-spec" / "pcdb10.dat" Path(__file__).resolve().parents[4] / "docs" / "sap-spec" / "pcdb10.dat"
) )
_TABLE_172_TAG: Final[str] = "$172" _TABLE_172_TAG: Final[str] = "$172"

View file

@ -14,7 +14,7 @@ factors are largely unchanged. When the corpus migrates to SAP 10.3
this module re-points to those values. this module re-points to those values.
The Energy Cost Deflator stays at 0.36 (used in ECF see The Energy Cost Deflator stays at 0.36 (used in ECF see
`domain.sap.worksheet.rating`). `domain.sap10_calculator.worksheet.rating`).
""" """
from __future__ import annotations from __future__ import annotations

View file

@ -7,14 +7,14 @@ targets RdSAP10 cost per ADR-0010 amendment.
CO2 emission factors and primary energy factors are unchanged from CO2 emission factors and primary energy factors are unchanged from
SAP10.2 Table 12 (RdSAP10 §19.2), so they continue to live in SAP10.2 Table 12 (RdSAP10 §19.2), so they continue to live in
`domain.sap.tables.table_12` rather than being duplicated here. `domain.sap10_calculator.tables.table_12` rather than being duplicated here.
""" """
from __future__ import annotations from __future__ import annotations
from typing import Final, Optional from typing import Final, Optional
from domain.sap.tables.table_12a import Tariff from domain.sap10_calculator.tables.table_12a import Tariff
_DEFAULT_P_PER_KWH: Final[float] = 3.48 # fall back to mains gas _DEFAULT_P_PER_KWH: Final[float] = 3.48 # fall back to mains gas

View file

@ -23,17 +23,17 @@ from __future__ import annotations
import pytest import pytest
from domain.sap.calculator import ( from domain.sap10_calculator.calculator import (
CalculatorInputs, CalculatorInputs,
calculate_sap_from_inputs, calculate_sap_from_inputs,
) )
from domain.sap.climate.appendix_u import external_temperature_c from domain.sap10_calculator.climate.appendix_u import external_temperature_c
from domain.sap.worksheet.dimensions import Dimensions from domain.sap10_calculator.worksheet.dimensions import Dimensions
from domain.sap.worksheet.heat_transmission import HeatTransmission from domain.sap10_calculator.worksheet.heat_transmission import HeatTransmission
from domain.sap.worksheet.mean_internal_temperature import ( from domain.sap10_calculator.worksheet.mean_internal_temperature import (
mean_internal_temperature_monthly, mean_internal_temperature_monthly,
) )
from domain.sap.worksheet.space_heating import space_heating_monthly_kwh from domain.sap10_calculator.worksheet.space_heating import space_heating_monthly_kwh
def _baseline_dwelling() -> CalculatorInputs: def _baseline_dwelling() -> CalculatorInputs:

View file

@ -20,18 +20,18 @@ from dataclasses import replace
import pytest import pytest
from domain.sap.calculator import ( from domain.sap10_calculator.calculator import (
CalculatorInputs, CalculatorInputs,
SapResult, SapResult,
calculate_sap_from_inputs, calculate_sap_from_inputs,
) )
from domain.sap.climate.appendix_u import external_temperature_c from domain.sap10_calculator.climate.appendix_u import external_temperature_c
from domain.sap.worksheet.dimensions import Dimensions from domain.sap10_calculator.worksheet.dimensions import Dimensions
from domain.sap.worksheet.heat_transmission import HeatTransmission from domain.sap10_calculator.worksheet.heat_transmission import HeatTransmission
from domain.sap.worksheet.mean_internal_temperature import ( from domain.sap10_calculator.worksheet.mean_internal_temperature import (
mean_internal_temperature_monthly, mean_internal_temperature_monthly,
) )
from domain.sap.worksheet.space_heating import space_heating_monthly_kwh from domain.sap10_calculator.worksheet.space_heating import space_heating_monthly_kwh
def _baseline_inputs() -> CalculatorInputs: def _baseline_inputs() -> CalculatorInputs:

View file

@ -15,15 +15,15 @@ from pathlib import Path
import pytest import pytest
from domain.sap.tables.pcdb.etl import run_etl from domain.sap10_calculator.tables.pcdb.etl import run_etl
from domain.sap.tables.pcdb.parser import ( from domain.sap10_calculator.tables.pcdb.parser import (
parse_table_105, parse_table_105,
parse_table_105_row, parse_table_105_row,
parse_table_raw, parse_table_raw,
) )
_REPO_ROOT: Path = Path(__file__).resolve().parents[6] _REPO_ROOT: Path = Path(__file__).resolve().parents[3]
_PCDB_DAT_PATH: Path = _REPO_ROOT / "docs" / "sap-spec" / "pcdb10.dat" _PCDB_DAT_PATH: Path = _REPO_ROOT / "docs" / "sap-spec" / "pcdb10.dat"

View file

@ -10,7 +10,7 @@ Reference: BRE PCDB pcdb10.dat (April 2026); user-verified records.
from __future__ import annotations from __future__ import annotations
from domain.sap.tables.pcdb import gas_oil_boiler_record from domain.sap10_calculator.tables.pcdb import gas_oil_boiler_record
def test_gas_oil_boiler_record_returns_verified_baxi_98() -> None: def test_gas_oil_boiler_record_returns_verified_baxi_98() -> None:

View file

@ -10,7 +10,7 @@ Reference: BRE PCDB pcdb10.dat Table 172 (Postcodes).
""" """
from __future__ import annotations from __future__ import annotations
from domain.sap.tables.pcdb.postcode_weather import ( from domain.sap10_calculator.tables.pcdb.postcode_weather import (
PostcodeClimate, PostcodeClimate,
postcode_climate, postcode_climate,
) )

View file

@ -15,7 +15,7 @@ from __future__ import annotations
import pytest import pytest
from domain.sap.tables.table_12 import ( from domain.sap10_calculator.tables.table_12 import (
co2_factor_kg_per_kwh, co2_factor_kg_per_kwh,
primary_energy_factor, primary_energy_factor,
unit_price_p_per_kwh, unit_price_p_per_kwh,

View file

@ -12,7 +12,7 @@ from __future__ import annotations
import pytest import pytest
from domain.sap.tables.table_12a import ( from domain.sap10_calculator.tables.table_12a import (
OtherUse, OtherUse,
Table12aSystem, Table12aSystem,
Tariff, Tariff,

View file

@ -13,8 +13,8 @@ from __future__ import annotations
import pytest import pytest
from domain.sap.tables.table_12a import Tariff from domain.sap10_calculator.tables.table_12a import Tariff
from domain.sap.tables.table_32 import ( from domain.sap10_calculator.tables.table_32 import (
additional_standing_charges_gbp, additional_standing_charges_gbp,
standing_charge_gbp, standing_charge_gbp,
unit_price_p_per_kwh, unit_price_p_per_kwh,

View file

@ -15,7 +15,7 @@ from __future__ import annotations
import pytest import pytest
from domain.sap.validation.parity_report import ( from domain.sap10_calculator.validation.parity_report import (
ParityCase, ParityCase,
ParityReport, ParityReport,
build_parity_report, build_parity_report,

View file

@ -30,7 +30,7 @@ will retire envelope.py in favour of this module (ADR-0009 §"Module
layout"). layout").
U-value lookups cascade through `domain.ml.rdsap_uvalues` migrating to U-value lookups cascade through `domain.ml.rdsap_uvalues` migrating to
`domain.sap.rdsap.cascade_defaults` in Session B. `domain.sap10_calculator.rdsap.cascade_defaults` in Session B.
Reference: SAP 10.2 specification §3 (pages 17-22); RdSAP 10 §5 (Tables Reference: SAP 10.2 specification §3 (pages 17-22); RdSAP 10 §5 (Tables
6-24); xlsx worked example at `2026-05-19-17-18 RdSap10Worksheet.xlsx`, 6-24); xlsx worked example at `2026-05-19-17-18 RdSap10Worksheet.xlsx`,

View file

@ -23,7 +23,7 @@ from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from typing import Final from typing import Final
from domain.sap.worksheet.utilisation_factor import utilisation_factor from domain.sap10_calculator.worksheet.utilisation_factor import utilisation_factor
_T_H1_C: Final[float] = 21.0 _T_H1_C: Final[float] = 21.0

View file

@ -6,7 +6,7 @@ Two layers:
implements the §U3.2 polynomial that converts the horizontal solar implements the §U3.2 polynomial that converts the horizontal solar
irradiance from Table U3 into a per-orientation per-pitch surface flux. irradiance from Table U3 into a per-orientation per-pitch surface flux.
Reads: Reads:
- S_h,m from Appendix U Table U3 (already in `domain.sap.climate.appendix_u`) - S_h,m from Appendix U Table U3 (already in `domain.sap10_calculator.climate.appendix_u`)
- δ from Appendix U Table U3 footer (already in `appendix_u.solar_declination_deg`) - δ from Appendix U Table U3 footer (already in `appendix_u.solar_declination_deg`)
- φ from Appendix U Table U4 (this module) - φ from Appendix U Table U4 (this module)
- k1..k9 from Appendix U Table U5 (this module) - k1..k9 from Appendix U Table U5 (this module)
@ -35,12 +35,12 @@ from math import cos, floor, radians, sin
from typing import Final from typing import Final
from datatypes.epc.domain.epc_property_data import EpcPropertyData, SapWindow from datatypes.epc.domain.epc_property_data import EpcPropertyData, SapWindow
from domain.sap.tables.pcdb.postcode_weather import PostcodeClimate from domain.sap10_calculator.tables.pcdb.postcode_weather import PostcodeClimate
from domain.sap.climate.appendix_u import ( from domain.sap10_calculator.climate.appendix_u import (
horizontal_solar_irradiance_w_per_m2, horizontal_solar_irradiance_w_per_m2,
solar_declination_deg, solar_declination_deg,
) )
from domain.sap.worksheet.internal_gains import OvershadingCategory from domain.sap10_calculator.worksheet.internal_gains import OvershadingCategory
def _round_area_2dp(value: float) -> float: def _round_area_2dp(value: float) -> float:

View file

@ -18,7 +18,7 @@ from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from typing import Final from typing import Final
from domain.sap.worksheet.heat_transmission import _round_half_up from domain.sap10_calculator.worksheet.heat_transmission import _round_half_up
_MIN_KWH_PER_MONTH: Final[float] = 1.0 _MIN_KWH_PER_MONTH: Final[float] = 1.0

View file

@ -24,7 +24,7 @@ SECTION_8C_INTERMITTENCY_MONTHLY: tuple[float, ...] = (
0.0, 0.0, 0.0, 0.0, 0.0, 0.25, 0.25, 0.25, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.25, 0.25, 0.25, 0.0, 0.0, 0.0, 0.0,
) )
from domain.sap.worksheet.tests import ( from domain.sap10_calculator.worksheet.tests import (
_elmhurst_worksheet_000474 as w000474, _elmhurst_worksheet_000474 as w000474,
_elmhurst_worksheet_000477 as w000477, _elmhurst_worksheet_000477 as w000477,
_elmhurst_worksheet_000480 as w000480, _elmhurst_worksheet_000480 as w000480,

View file

@ -37,11 +37,11 @@ from domain.ml.tests._fixtures import (
make_sap_heating, make_sap_heating,
make_window, make_window,
) )
from domain.sap.worksheet.solar_gains import RoofWindowInput, RooflightInput from domain.sap10_calculator.worksheet.solar_gains import RoofWindowInput, RooflightInput
from domain.sap.worksheet.ventilation import MechanicalVentilationKind from domain.sap10_calculator.worksheet.ventilation import MechanicalVentilationKind
from domain.sap.worksheet.water_heating import TABLE_J1_TCOLD_FROM_MAINS_C from domain.sap10_calculator.worksheet.water_heating import TABLE_J1_TCOLD_FROM_MAINS_C
from domain.sap.worksheet.tests._elmhurst_fixtures import ( from domain.sap10_calculator.worksheet.tests._elmhurst_fixtures import (
SECTION_8C_ALL_ZERO_MONTHLY, SECTION_8C_ALL_ZERO_MONTHLY,
SECTION_8C_ETA_LOSS_ALL_ONE, SECTION_8C_ETA_LOSS_ALL_ONE,
SECTION_8C_INTERMITTENCY_MONTHLY, SECTION_8C_INTERMITTENCY_MONTHLY,

View file

@ -35,11 +35,11 @@ from domain.ml.tests._fixtures import (
make_sap_heating, make_sap_heating,
make_window, make_window,
) )
from domain.sap.worksheet.solar_gains import RoofWindowInput, RooflightInput from domain.sap10_calculator.worksheet.solar_gains import RoofWindowInput, RooflightInput
from domain.sap.worksheet.ventilation import MechanicalVentilationKind from domain.sap10_calculator.worksheet.ventilation import MechanicalVentilationKind
from domain.sap.worksheet.water_heating import TABLE_J1_TCOLD_FROM_MAINS_C from domain.sap10_calculator.worksheet.water_heating import TABLE_J1_TCOLD_FROM_MAINS_C
from domain.sap.worksheet.tests._elmhurst_fixtures import ( from domain.sap10_calculator.worksheet.tests._elmhurst_fixtures import (
SECTION_8C_ALL_ZERO_MONTHLY, SECTION_8C_ALL_ZERO_MONTHLY,
SECTION_8C_ETA_LOSS_ALL_ONE, SECTION_8C_ETA_LOSS_ALL_ONE,
SECTION_8C_INTERMITTENCY_MONTHLY, SECTION_8C_INTERMITTENCY_MONTHLY,

View file

@ -36,11 +36,11 @@ from domain.ml.tests._fixtures import (
make_sap_heating, make_sap_heating,
make_window, make_window,
) )
from domain.sap.worksheet.solar_gains import RoofWindowInput, RooflightInput from domain.sap10_calculator.worksheet.solar_gains import RoofWindowInput, RooflightInput
from domain.sap.worksheet.ventilation import MechanicalVentilationKind from domain.sap10_calculator.worksheet.ventilation import MechanicalVentilationKind
from domain.sap.worksheet.water_heating import TABLE_J1_TCOLD_FROM_MAINS_C from domain.sap10_calculator.worksheet.water_heating import TABLE_J1_TCOLD_FROM_MAINS_C
from domain.sap.worksheet.tests._elmhurst_fixtures import ( from domain.sap10_calculator.worksheet.tests._elmhurst_fixtures import (
SECTION_8C_ALL_ZERO_MONTHLY, SECTION_8C_ALL_ZERO_MONTHLY,
SECTION_8C_ETA_LOSS_ALL_ONE, SECTION_8C_ETA_LOSS_ALL_ONE,
SECTION_8C_INTERMITTENCY_MONTHLY, SECTION_8C_INTERMITTENCY_MONTHLY,

View file

@ -34,8 +34,8 @@ from domain.ml.tests._fixtures import (
make_sap_heating, make_sap_heating,
make_window, make_window,
) )
from domain.sap.worksheet.solar_gains import RoofWindowInput, RooflightInput from domain.sap10_calculator.worksheet.solar_gains import RoofWindowInput, RooflightInput
from domain.sap.worksheet.water_heating import TABLE_J1_TCOLD_FROM_MAINS_C from domain.sap10_calculator.worksheet.water_heating import TABLE_J1_TCOLD_FROM_MAINS_C
# RdSAP wall_construction code seen in the cert→worksheet mapping. The # RdSAP wall_construction code seen in the cert→worksheet mapping. The
# Summary lists "CA Cavity" for both main and extension walls. The alt # Summary lists "CA Cavity" for both main and extension walls. The alt
@ -258,9 +258,9 @@ def build_epc() -> EpcPropertyData:
# on the EpcPropertyData domain object — we pass these into # on the EpcPropertyData domain object — we pass these into
# `ventilation_from_inputs` alongside the cert-derived geometry). # `ventilation_from_inputs` alongside the cert-derived geometry).
# ============================================================================ # ============================================================================
from domain.sap.worksheet.ventilation import MechanicalVentilationKind from domain.sap10_calculator.worksheet.ventilation import MechanicalVentilationKind
from domain.sap.worksheet.tests._elmhurst_fixtures import ( from domain.sap10_calculator.worksheet.tests._elmhurst_fixtures import (
SECTION_8C_ALL_ZERO_MONTHLY, SECTION_8C_ALL_ZERO_MONTHLY,
SECTION_8C_ETA_LOSS_ALL_ONE, SECTION_8C_ETA_LOSS_ALL_ONE,
SECTION_8C_INTERMITTENCY_MONTHLY, SECTION_8C_INTERMITTENCY_MONTHLY,

View file

@ -39,11 +39,11 @@ from domain.ml.tests._fixtures import (
make_sap_heating, make_sap_heating,
make_window, make_window,
) )
from domain.sap.worksheet.solar_gains import RoofWindowInput, RooflightInput from domain.sap10_calculator.worksheet.solar_gains import RoofWindowInput, RooflightInput
from domain.sap.worksheet.ventilation import MechanicalVentilationKind from domain.sap10_calculator.worksheet.ventilation import MechanicalVentilationKind
from domain.sap.worksheet.water_heating import TABLE_J1_TCOLD_FROM_MAINS_C from domain.sap10_calculator.worksheet.water_heating import TABLE_J1_TCOLD_FROM_MAINS_C
from domain.sap.worksheet.tests._elmhurst_fixtures import ( from domain.sap10_calculator.worksheet.tests._elmhurst_fixtures import (
SECTION_8C_ALL_ZERO_MONTHLY, SECTION_8C_ALL_ZERO_MONTHLY,
SECTION_8C_ETA_LOSS_ALL_ONE, SECTION_8C_ETA_LOSS_ALL_ONE,
SECTION_8C_INTERMITTENCY_MONTHLY, SECTION_8C_INTERMITTENCY_MONTHLY,

View file

@ -42,11 +42,11 @@ from domain.ml.tests._fixtures import (
make_sap_heating, make_sap_heating,
make_window, make_window,
) )
from domain.sap.worksheet.solar_gains import Orientation, RoofWindowInput, RooflightInput from domain.sap10_calculator.worksheet.solar_gains import Orientation, RoofWindowInput, RooflightInput
from domain.sap.worksheet.ventilation import MechanicalVentilationKind from domain.sap10_calculator.worksheet.ventilation import MechanicalVentilationKind
from domain.sap.worksheet.water_heating import TABLE_J1_TCOLD_FROM_MAINS_C from domain.sap10_calculator.worksheet.water_heating import TABLE_J1_TCOLD_FROM_MAINS_C
from domain.sap.worksheet.tests._elmhurst_fixtures import ( from domain.sap10_calculator.worksheet.tests._elmhurst_fixtures import (
SECTION_8C_ALL_ZERO_MONTHLY, SECTION_8C_ALL_ZERO_MONTHLY,
SECTION_8C_ETA_LOSS_ALL_ONE, SECTION_8C_ETA_LOSS_ALL_ONE,
SECTION_8C_INTERMITTENCY_MONTHLY, SECTION_8C_INTERMITTENCY_MONTHLY,

View file

@ -1,6 +1,6 @@
"""Reader for the canonical SAP10.2 worksheet Excel — the source of """Reader for the canonical SAP10.2 worksheet Excel — the source of
truth used to build line-by-line conformance tests against truth used to build line-by-line conformance tests against
`packages/domain/src/domain/sap/worksheet/`. `domain/sap10_calculator/worksheet/`.
The Excel file lives at the repo root and has two sheets both contain The Excel file lives at the repo root and has two sheets both contain
the full worksheet, differing only on weather-data source: the full worksheet, differing only on weather-data source:
@ -20,7 +20,7 @@ from typing import Any, Iterable
import openpyxl import openpyxl
_REPO_ROOT = Path(__file__).resolve().parents[7] _REPO_ROOT = Path(__file__).resolve().parents[4]
WORKSHEET_XLSX_PATH = _REPO_ROOT / "2026-05-19-17-18 RdSap10Worksheet.xlsx" WORKSHEET_XLSX_PATH = _REPO_ROOT / "2026-05-19-17-18 RdSap10Worksheet.xlsx"

View file

@ -26,8 +26,8 @@ from domain.ml.tests._fixtures import (
make_floor_dimension, make_floor_dimension,
make_minimal_sap10_epc, make_minimal_sap10_epc,
) )
from domain.sap.worksheet.dimensions import Dimensions, dimensions_from_cert from domain.sap10_calculator.worksheet.dimensions import Dimensions, dimensions_from_cert
from domain.sap.worksheet.tests._xlsx_loader import load_cells from domain.sap10_calculator.worksheet.tests._xlsx_loader import load_cells
_RIR_FIXTURES_DIR = Path(__file__).parent / "fixtures" / "rir" _RIR_FIXTURES_DIR = Path(__file__).parent / "fixtures" / "rir"
@ -507,7 +507,7 @@ def test_all_rir_shapes_apply_section_1_2_45m_convention_uniformly(
from types import ModuleType # noqa: E402 (kept near the Elmhurst tests) from types import ModuleType # noqa: E402 (kept near the Elmhurst tests)
from domain.sap.worksheet.tests._elmhurst_fixtures import ( # noqa: E402 from domain.sap10_calculator.worksheet.tests._elmhurst_fixtures import ( # noqa: E402
ALL_FIXTURES as _ELMHURST_FIXTURES, ALL_FIXTURES as _ELMHURST_FIXTURES,
fixture_id as _elmhurst_fixture_id, fixture_id as _elmhurst_fixture_id,
) )

View file

@ -24,9 +24,9 @@ from typing import Final
import pytest import pytest
from domain.sap.calculator import Sap10Calculator from domain.sap10_calculator.calculator import Sap10Calculator
from domain.sap.rdsap.cert_to_inputs import cert_to_inputs from domain.sap10_calculator.rdsap.cert_to_inputs import cert_to_inputs
from domain.sap.worksheet.tests import ( from domain.sap10_calculator.worksheet.tests import (
_elmhurst_worksheet_000474 as _w000474, _elmhurst_worksheet_000474 as _w000474,
_elmhurst_worksheet_000477 as _w000477, _elmhurst_worksheet_000477 as _w000477,
_elmhurst_worksheet_000480 as _w000480, _elmhurst_worksheet_000480 as _w000480,
@ -35,7 +35,7 @@ from domain.sap.worksheet.tests import (
_elmhurst_worksheet_000516 as _w000516, _elmhurst_worksheet_000516 as _w000516,
_elmhurst_worksheet_001479 as _w001479, _elmhurst_worksheet_001479 as _w001479,
) )
from domain.sap.worksheet.tests._elmhurst_fixtures import ( from domain.sap10_calculator.worksheet.tests._elmhurst_fixtures import (
ALL_FIXTURES as _ELMHURST_FIXTURES, ALL_FIXTURES as _ELMHURST_FIXTURES,
fixture_id as _elmhurst_fixture_id, fixture_id as _elmhurst_fixture_id,
) )

View file

@ -6,7 +6,7 @@ Reference: SAP 10.2 specification (14-03-2025) worksheet block §9a
from __future__ import annotations from __future__ import annotations
from domain.sap.worksheet.energy_requirements import ( from domain.sap10_calculator.worksheet.energy_requirements import (
space_heating_fuel_monthly_kwh, space_heating_fuel_monthly_kwh,
) )

View file

@ -10,10 +10,10 @@ from types import ModuleType
import pytest import pytest
from domain.sap.worksheet.fabric_energy_efficiency import ( from domain.sap10_calculator.worksheet.fabric_energy_efficiency import (
fabric_energy_efficiency_kwh_per_m2_yr, fabric_energy_efficiency_kwh_per_m2_yr,
) )
from domain.sap.worksheet.tests._elmhurst_fixtures import ALL_FIXTURES, fixture_id from domain.sap10_calculator.worksheet.tests._elmhurst_fixtures import ALL_FIXTURES, fixture_id
def test_fabric_energy_efficiency_sums_heating_per_m2_and_cooling_per_m2() -> None: def test_fabric_energy_efficiency_sums_heating_per_m2_and_cooling_per_m2() -> None:

View file

@ -8,8 +8,8 @@ from __future__ import annotations
import pytest import pytest
from domain.sap.rdsap.cert_to_inputs import cert_to_inputs from domain.sap10_calculator.rdsap.cert_to_inputs import cert_to_inputs
from domain.sap.worksheet.fuel_cost import FuelCostResult, fuel_cost from domain.sap10_calculator.worksheet.fuel_cost import FuelCostResult, fuel_cost
def test_single_rate_main_only_bills_kwh_at_high_rate_price() -> None: def test_single_rate_main_only_bills_kwh_at_high_rate_price() -> None:

View file

@ -7,7 +7,7 @@ a typed `HeatTransmission` breakdown so callers can audit each
worksheet contribution. worksheet contribution.
U-values cascade through the RdSAP 10 §5 defaults (currently implemented U-values cascade through the RdSAP 10 §5 defaults (currently implemented
in `domain.ml.rdsap_uvalues` migrates to `domain.sap.rdsap.cascade_defaults` in `domain.ml.rdsap_uvalues` migrates to `domain.sap10_calculator.rdsap.cascade_defaults`
in Session B). in Session B).
Reference: SAP 10.3 specification §3 (pages 17-22); Reference: SAP 10.3 specification §3 (pages 17-22);
@ -30,7 +30,7 @@ from domain.ml.tests._fixtures import (
make_floor_dimension, make_floor_dimension,
make_minimal_sap10_epc, make_minimal_sap10_epc,
) )
from domain.sap.worksheet.heat_transmission import ( from domain.sap10_calculator.worksheet.heat_transmission import (
DwellingExposure, DwellingExposure,
HeatTransmission, HeatTransmission,
heat_transmission_from_cert, heat_transmission_from_cert,
@ -1132,11 +1132,11 @@ def test_real_corpus_basement_cert_has_part_with_has_basement_true() -> None:
from types import ModuleType # noqa: E402 from types import ModuleType # noqa: E402
from domain.sap.worksheet.tests._elmhurst_fixtures import ( # noqa: E402 from domain.sap10_calculator.worksheet.tests._elmhurst_fixtures import ( # noqa: E402
ALL_FIXTURES as _ELMHURST_FIXTURES, ALL_FIXTURES as _ELMHURST_FIXTURES,
fixture_id as _elmhurst_fixture_id, fixture_id as _elmhurst_fixture_id,
) )
from domain.sap.worksheet.tests import ( # noqa: E402 from domain.sap10_calculator.worksheet.tests import ( # noqa: E402
_elmhurst_worksheet_000474 as _w000474, _elmhurst_worksheet_000474 as _w000474,
_elmhurst_worksheet_000490 as _w000490, _elmhurst_worksheet_000490 as _w000490,
) )

View file

@ -1,6 +1,6 @@
"""Tests for SAP 10.2 §5 + Appendix L — internal gains. """Tests for SAP 10.2 §5 + Appendix L — internal gains.
Worksheet line refs (66)..(73) land in `domain.sap.worksheet.internal_gains` Worksheet line refs (66)..(73) land in `domain.sap10_calculator.worksheet.internal_gains`
as monthly 12-tuple outputs. Each leaf function is unit-tested against the as monthly 12-tuple outputs. Each leaf function is unit-tested against the
SAP 10.2 spec formula; the orchestrator is parametrized against every SAP 10.2 spec formula; the orchestrator is parametrized against every
Elmhurst conformance fixture in `_elmhurst_fixtures.ALL_FIXTURES`. Elmhurst conformance fixture in `_elmhurst_fixtures.ALL_FIXTURES`.
@ -12,7 +12,7 @@ Table 5a + Appendix L (lighting/appliances/cooking) + Appendix J Table 1b
import pytest import pytest
from domain.sap.worksheet.internal_gains import ( from domain.sap10_calculator.worksheet.internal_gains import (
InternalGainsResult, InternalGainsResult,
OvershadingCategory, OvershadingCategory,
PumpDateCategory, PumpDateCategory,
@ -44,7 +44,7 @@ from datatypes.epc.domain.epc_property_data import (
SapWindow, SapWindow,
) )
from domain.ml.tests._fixtures import make_minimal_sap10_epc from domain.ml.tests._fixtures import make_minimal_sap10_epc
from domain.sap.worksheet.tests._elmhurst_fixtures import ALL_FIXTURES, fixture_id from domain.sap10_calculator.worksheet.tests._elmhurst_fixtures import ALL_FIXTURES, fixture_id
def test_metabolic_gains_are_60w_per_occupant_constant_across_months() -> None: def test_metabolic_gains_are_60w_per_occupant_constant_across_months() -> None:

View file

@ -14,14 +14,14 @@ from types import ModuleType
import pytest import pytest
from domain.sap.climate.appendix_u import external_temperature_c from domain.sap10_calculator.climate.appendix_u import external_temperature_c
from domain.sap.worksheet.mean_internal_temperature import ( from domain.sap10_calculator.worksheet.mean_internal_temperature import (
MeanInternalTemperatureResult, MeanInternalTemperatureResult,
elsewhere_heating_temperature_c, elsewhere_heating_temperature_c,
mean_internal_temperature_monthly, mean_internal_temperature_monthly,
off_period_temperature_reduction_c, off_period_temperature_reduction_c,
) )
from domain.sap.worksheet.tests._elmhurst_fixtures import ALL_FIXTURES, fixture_id from domain.sap10_calculator.worksheet.tests._elmhurst_fixtures import ALL_FIXTURES, fixture_id
# UK-average climate (region 0) external temperatures, Appendix U Table U1. # UK-average climate (region 0) external temperatures, Appendix U Table U1.

View file

@ -19,7 +19,7 @@ target is SAP 10.2.
import pytest import pytest
from domain.sap.worksheet.rating import ( from domain.sap10_calculator.worksheet.rating import (
energy_cost_factor, energy_cost_factor,
environmental_impact_rating, environmental_impact_rating,
sap_rating, sap_rating,

View file

@ -16,7 +16,7 @@ from typing import Final
import pytest import pytest
from domain.sap.rdsap.cert_to_inputs import ( from domain.sap10_calculator.rdsap.cert_to_inputs import (
cert_to_inputs, cert_to_inputs,
energy_requirements_section_from_cert, energy_requirements_section_from_cert,
environmental_section_from_cert, environmental_section_from_cert,
@ -34,8 +34,8 @@ from domain.sap.rdsap.cert_to_inputs import (
ventilation_from_cert, ventilation_from_cert,
water_heating_section_from_cert, water_heating_section_from_cert,
) )
from domain.sap.worksheet.dimensions import dimensions_from_cert from domain.sap10_calculator.worksheet.dimensions import dimensions_from_cert
from domain.sap.worksheet.tests import ( from domain.sap10_calculator.worksheet.tests import (
_elmhurst_worksheet_000474 as _w000474, _elmhurst_worksheet_000474 as _w000474,
_elmhurst_worksheet_000477 as _w000477, _elmhurst_worksheet_000477 as _w000477,
_elmhurst_worksheet_000480 as _w000480, _elmhurst_worksheet_000480 as _w000480,

View file

@ -19,8 +19,8 @@ from datatypes.epc.domain.epc_property_data import (
WindowTransmissionDetails, WindowTransmissionDetails,
) )
from domain.ml.tests._fixtures import make_minimal_sap10_epc, make_window from domain.ml.tests._fixtures import make_minimal_sap10_epc, make_window
from domain.sap.worksheet.internal_gains import OvershadingCategory from domain.sap10_calculator.worksheet.internal_gains import OvershadingCategory
from domain.sap.worksheet.solar_gains import ( from domain.sap10_calculator.worksheet.solar_gains import (
Orientation, Orientation,
RoofWindowInput, RoofWindowInput,
RooflightInput, RooflightInput,
@ -29,7 +29,7 @@ from domain.sap.worksheet.solar_gains import (
window_solar_gain_w, window_solar_gain_w,
z_solar_for_overshading, z_solar_for_overshading,
) )
from domain.sap.worksheet.tests._elmhurst_fixtures import ALL_FIXTURES, fixture_id from domain.sap10_calculator.worksheet.tests._elmhurst_fixtures import ALL_FIXTURES, fixture_id
# Worksheet U985-0001-000490 reference (UK-avg weather, region 0): # Worksheet U985-0001-000490 reference (UK-avg weather, region 0):

View file

@ -9,12 +9,12 @@ from types import ModuleType
import pytest import pytest
from domain.sap.climate.appendix_u import external_temperature_c from domain.sap10_calculator.climate.appendix_u import external_temperature_c
from domain.sap.worksheet.space_cooling import ( from domain.sap10_calculator.worksheet.space_cooling import (
space_cooling_monthly_kwh, space_cooling_monthly_kwh,
utilisation_factor_loss, utilisation_factor_loss,
) )
from domain.sap.worksheet.tests._elmhurst_fixtures import ALL_FIXTURES, fixture_id from domain.sap10_calculator.worksheet.tests._elmhurst_fixtures import ALL_FIXTURES, fixture_id
_FULLY_INACTIVE_GAINS_WINTER_TE_C: float = -10.0 _FULLY_INACTIVE_GAINS_WINTER_TE_C: float = -10.0

View file

@ -11,13 +11,13 @@ from types import ModuleType
import pytest import pytest
from domain.sap.climate.appendix_u import external_temperature_c from domain.sap10_calculator.climate.appendix_u import external_temperature_c
from domain.sap.worksheet.space_heating import ( from domain.sap10_calculator.worksheet.space_heating import (
SpaceHeatingResult, SpaceHeatingResult,
monthly_heat_requirement_kwh, monthly_heat_requirement_kwh,
space_heating_monthly_kwh, space_heating_monthly_kwh,
) )
from domain.sap.worksheet.tests._elmhurst_fixtures import ALL_FIXTURES, fixture_id from domain.sap10_calculator.worksheet.tests._elmhurst_fixtures import ALL_FIXTURES, fixture_id
_UK_AVG_EXT_TEMP_C: tuple[float, ...] = tuple( _UK_AVG_EXT_TEMP_C: tuple[float, ...] = tuple(

View file

@ -9,7 +9,7 @@ Reference: SAP 10.3 specification (13-01-2026) Table 9a (page 184).
import pytest import pytest
from domain.sap.worksheet.utilisation_factor import utilisation_factor from domain.sap10_calculator.worksheet.utilisation_factor import utilisation_factor
def test_small_gain_loss_ratio_returns_eta_close_to_one() -> None: def test_small_gain_loss_ratio_returns_eta_close_to_one() -> None:

View file

@ -12,8 +12,8 @@ Canonical worked example: `2026-05-19-17-18 RdSap10Worksheet.xlsx`,
import pytest import pytest
from domain.sap.worksheet.tests._xlsx_loader import load_cells from domain.sap10_calculator.worksheet.tests._xlsx_loader import load_cells
from domain.sap.worksheet.ventilation import ( from domain.sap10_calculator.worksheet.ventilation import (
MechanicalVentilationKind, MechanicalVentilationKind,
TABLE_U2_NON_REGIONAL_WIND_SPEED_M_S, TABLE_U2_NON_REGIONAL_WIND_SPEED_M_S,
VentilationResult, VentilationResult,
@ -461,7 +461,7 @@ def test_excel_worksheet_conformance_section_2_lines_6a_to_25m() -> None:
from types import ModuleType # noqa: E402 from types import ModuleType # noqa: E402
from domain.sap.worksheet.tests._elmhurst_fixtures import ( # noqa: E402 from domain.sap10_calculator.worksheet.tests._elmhurst_fixtures import ( # noqa: E402
ALL_FIXTURES as _ELMHURST_FIXTURES, ALL_FIXTURES as _ELMHURST_FIXTURES,
fixture_id as _elmhurst_fixture_id, fixture_id as _elmhurst_fixture_id,
) )
@ -479,7 +479,7 @@ def test_section_2_matches_elmhurst_worksheet(fixture: ModuleType) -> None:
worksheet's (9) value. worksheet's (9) value.
""" """
# Arrange # Arrange
from domain.sap.worksheet.dimensions import dimensions_from_cert from domain.sap10_calculator.worksheet.dimensions import dimensions_from_cert
dims = dimensions_from_cert(fixture.build_epc()) dims = dimensions_from_cert(fixture.build_epc())
assert dims.storey_count == fixture.LINE_9_STOREYS, ( assert dims.storey_count == fixture.LINE_9_STOREYS, (
f"dims.storey_count={dims.storey_count} should equal worksheet (9) " f"dims.storey_count={dims.storey_count} should equal worksheet (9) "

View file

@ -1,6 +1,6 @@
"""Tests for SAP 10.2 §4 — water heating energy requirements. """Tests for SAP 10.2 §4 — water heating energy requirements.
Worksheet line refs land in `domain.sap.worksheet.water_heating`. Each Worksheet line refs land in `domain.sap10_calculator.worksheet.water_heating`. Each
test asserts a single line-ref output against the canonical xlsx worked test asserts a single line-ref output against the canonical xlsx worked
example and/or Elmhurst conformance fixtures. example and/or Elmhurst conformance fixtures.
@ -12,14 +12,14 @@ from types import ModuleType
import pytest import pytest
from domain.sap.worksheet.tests import ( from domain.sap10_calculator.worksheet.tests import (
_elmhurst_worksheet_000474 as _w000474, _elmhurst_worksheet_000474 as _w000474,
_elmhurst_worksheet_000477 as _w000477, _elmhurst_worksheet_000477 as _w000477,
_elmhurst_worksheet_000490 as _w000490, _elmhurst_worksheet_000490 as _w000490,
) )
from domain.sap.worksheet.tests._elmhurst_fixtures import ALL_FIXTURES, fixture_id from domain.sap10_calculator.worksheet.tests._elmhurst_fixtures import ALL_FIXTURES, fixture_id
from domain.sap.worksheet.tests._xlsx_loader import load_cells from domain.sap10_calculator.worksheet.tests._xlsx_loader import load_cells
from domain.sap.worksheet.water_heating import ( from domain.sap10_calculator.worksheet.water_heating import (
TABLE_J1_TCOLD_FROM_MAINS_C, TABLE_J1_TCOLD_FROM_MAINS_C,
annual_average_hot_water_l_per_day, annual_average_hot_water_l_per_day,
annual_average_hot_water_other_uses_l_per_day, annual_average_hot_water_other_uses_l_per_day,
@ -654,7 +654,7 @@ def test_000474_cert_to_inputs_hot_water_kwh_closes_within_1pct_post_slice_2() -
Equation D1 monthly cascade effective annual η ~88% (vs the Equation D1 monthly cascade effective annual η ~88% (vs the
87.0% summer scalar) HW kWh 2320 ~2290 (+0% target).""" 87.0% summer scalar) HW kWh 2320 ~2290 (+0% target)."""
# Arrange # Arrange
from domain.sap.rdsap.cert_to_inputs import cert_to_inputs from domain.sap10_calculator.rdsap.cert_to_inputs import cert_to_inputs
epc = _w000474.build_epc() epc = _w000474.build_epc()
@ -675,8 +675,8 @@ def test_000490_cert_to_inputs_hot_water_kwh_closes_via_equation_d1() -> None:
(+6.2%). Post-slice-2 Equation D1 cascade HW kWh closes toward (+6.2%). Post-slice-2 Equation D1 cascade HW kWh closes toward
2851 (target ±2%).""" 2851 (target ±2%)."""
# Arrange # Arrange
from domain.sap.rdsap.cert_to_inputs import cert_to_inputs from domain.sap10_calculator.rdsap.cert_to_inputs import cert_to_inputs
from domain.sap.worksheet.tests import _elmhurst_worksheet_000490 as _w000490 from domain.sap10_calculator.worksheet.tests import _elmhurst_worksheet_000490 as _w000490
epc = _w000490.build_epc() epc = _w000490.build_epc()

View file

@ -1,4 +0,0 @@
"""Shared domain types for the Ara modelling pipeline and sibling Domna services.
No persistence, no IO, no business logic. See README.md for layout.
"""

View file

@ -232,7 +232,7 @@ def predicted_lighting_kwh(
Missing counts treated as zero. Missing counts treated as zero.
DEPRECATED for SAP rating use. The spec-faithful Appendix L L1-L11 DEPRECATED for SAP rating use. The spec-faithful Appendix L L1-L11
cascade is in `domain.sap.worksheet.internal_gains.annual_lighting_kwh` cascade is in `domain.sap10_calculator.worksheet.internal_gains.annual_lighting_kwh`
and is what `cert_to_inputs` now plumbs into `inputs.lighting_kwh_per_yr`. and is what `cert_to_inputs` now plumbs into `inputs.lighting_kwh_per_yr`.
This heuristic over-counts ~3× on the Elmhurst cohort (528 vs 140 kWh This heuristic over-counts ~3× on the Elmhurst cohort (528 vs 140 kWh
on 000474). Kept only for `domain.ml.ecf.energy_cost_factor` and on 000474). Kept only for `domain.ml.ecf.energy_cost_factor` and