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.
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
calculator + cascade in isolation from the mapper. This file pins
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 datatypes.epc.domain.mapper import EpcPropertyDataMapper
from domain.sap.calculator import calculate_sap_from_inputs
from domain.sap.rdsap.cert_to_inputs import SAP_10_2_SPEC_PRICES, cert_to_inputs
from domain.sap.worksheet.tests import (
from domain.sap10_calculator.calculator import calculate_sap_from_inputs
from domain.sap10_calculator.rdsap.cert_to_inputs import SAP_10_2_SPEC_PRICES, cert_to_inputs
from domain.sap10_calculator.worksheet.tests import (
_elmhurst_worksheet_000474 as _w000474,
_elmhurst_worksheet_000477 as _w000477,
_elmhurst_worksheet_000480 as _w000480,
@ -63,7 +63,7 @@ _SUMMARY_001479_PDF = _FIXTURES / "Summary_001479.pdf"
# matches worksheet continuous SAP at 1e-4".
_API_001479_JSON = (
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"
)
@ -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:
# Arrange — cert U985-0001-000474 is a mid-terrace with 3 building
# 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
# extractor + mapper must yield the same count.
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)
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

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:
```
packages/domain/src/domain/sap/
domain/sap10_calculator/
__init__.py # Sap10Calculator entry point + SapResult dataclass
worksheet/
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)
```
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
@ -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.
- **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.
- **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.
## 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
- 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 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.

View file

@ -4,7 +4,7 @@
## 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.
@ -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.
`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).
@ -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.
`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
@ -84,9 +84,9 @@ The 000490 Elmhurst fixture had a recorded -12.5% cost gap (£706 vs £807 PDF)
### 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.
- **`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.
- **`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/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_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/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.
- 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)
```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_demand_inputs, # Demand cascade
local_climate_for_cert,
environmental_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)`
@ -101,8 +101,8 @@ side is what you need to map out:
schema versions (SAP-Schema-18/19, RdSAP-Schema-18) to
`EpcPropertyData` lives there.
3. **Is SAP scoring already wired to the API?** Search the backend for
imports of `domain.sap.rdsap.cert_to_inputs` or
`domain.sap.calculator`. If it's not yet wired, the integration test
imports of `domain.sap10_calculator.rdsap.cert_to_inputs` or
`domain.sap10_calculator.calculator`. If it's not yet wired, the integration test
is a forcing function for wiring it.
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
@ -132,10 +132,10 @@ expanding.
| File | Why |
|---|---|
| [`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 |
| [`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 |
| [`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 |
| [`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/calculator.py`](../../domain/sap10_calculator/calculator.py) | `SapResult` fields you'll assert against |
| [`domain/sap10_calculator/rdsap/cert_to_inputs.py`](../../domain/sap10_calculator/rdsap/cert_to_inputs.py) | The 3 public entry points + the section helpers |
| [`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 |
| [`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 |
| [`datatypes/epc/domain/mapper.py`](../../datatypes/epc/domain/mapper.py) | Schema → EpcPropertyData mappers |
@ -146,18 +146,18 @@ expanding.
```bash
# Confirm SAP calculator is still 930/930 green
python -m pytest \
packages/domain/src/domain/sap/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_section_cascade_pins.py \
domain/sap10_calculator/worksheet/tests/test_e2e_elmhurst_sap_score.py \
--no-cov --no-header --tb=no -q
# Show the 4 EPC outputs for fixture 000474
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,
environmental_section_from_cert, primary_energy_section_from_cert,
)
from domain.sap.calculator import calculate_sap_from_inputs
from domain.sap.worksheet.tests import _elmhurst_worksheet_000474 as w
from domain.sap10_calculator.calculator import calculate_sap_from_inputs
from domain.sap10_calculator.worksheet.tests import _elmhurst_worksheet_000474 as w
epc = w.build_epc()
pc = local_climate_for_cert(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.
If you find an additional cert-shape variation that breaks the
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.
- **BEDF fuel pricing.** The Fuel Bill on the EPC uses postcode-specific
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
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)
pages = _summary_pdf_to_textract_style_pages(Path('/workspaces/model/backend/documents_parser/tests/fixtures/Summary_001479.pdf'))
sn = ElmhurstSiteNotesExtractor(pages).extract()
@ -234,7 +234,7 @@ override field, (c) wait for more cert pairs to confirm pattern.
## 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-
21.0.1).
- `backend/documents_parser/tests/fixtures/Summary_001479.pdf`
@ -273,8 +273,8 @@ before this rewrite).
```bash
PYTHONPATH=/workspaces/model:/workspaces/model/packages/domain/src \
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 \
packages/domain/src/domain/sap/rdsap/tests/test_golden_fixtures.py \
domain/sap10_calculator/worksheet/tests/test_e2e_elmhurst_sap_score.py \
domain/sap10_calculator/rdsap/tests/test_golden_fixtures.py \
--no-cov -q
```
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).
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
Three entry points, all in `domain.sap.rdsap.cert_to_inputs`:
Three entry points, all in `domain.sap10_calculator.rdsap.cert_to_inputs`:
```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_demand_inputs, # Current Carbon + Current PE (postcode climate)
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)`
@ -93,11 +93,11 @@ upgrade wall insulation), re-run, observe the delta. The shape:
```python
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,
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):
"""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 plumbed through as either an `int` region index (0..21) or a
`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`,
`horizontal_solar_irradiance_w_per_m2`, plus `_latitude_deg` in
`worksheet/solar_gains.py`.
@ -192,7 +192,7 @@ on `CalculatorInputs`.
## 4. File map
```
packages/domain/src/domain/sap/
domain/sap10_calculator/
├── calculator.py # Top-level orchestrator (CalculatorInputs → SapResult)
├── README.md # Fixture authoring cookbook
├── rdsap/
@ -288,12 +288,12 @@ monthly_infiltration_ach 6/6
```bash
# 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)
python -m pytest \
packages/domain/src/domain/sap/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_section_cascade_pins.py \
domain/sap10_calculator/worksheet/tests/test_e2e_elmhurst_sap_score.py \
--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
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:
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**:
```
python -m pytest packages/domain/src/domain/sap/worksheet/tests/ \
python -m pytest domain/sap10_calculator/worksheet/tests/ \
-k elmhurst --no-cov -v
```
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`
aggregate and emits a typed `SapResult`. This module is the physics
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
assumptions.
@ -44,15 +44,15 @@ from __future__ import annotations
from dataclasses import dataclass, field
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:
from datatypes.epc.domain.epc_property_data import EpcPropertyData
from domain.sap.worksheet.dimensions import Dimensions
from domain.sap.worksheet.energy_requirements import EnergyRequirementsResult
from domain.sap.worksheet.fuel_cost import FuelCostResult
from domain.sap.worksheet.heat_transmission import HeatTransmission
from domain.sap.worksheet.rating import (
from domain.sap10_calculator.worksheet.dimensions import Dimensions
from domain.sap10_calculator.worksheet.energy_requirements import EnergyRequirementsResult
from domain.sap10_calculator.worksheet.fuel_cost import FuelCostResult
from domain.sap10_calculator.worksheet.heat_transmission import HeatTransmission
from domain.sap10_calculator.worksheet.rating import (
ECF_LOG_THRESHOLD,
ENERGY_COST_DEFLATOR,
FLOOR_AREA_OFFSET_M2,
@ -621,6 +621,6 @@ class Sap10Calculator:
"""
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))

View file

@ -20,7 +20,7 @@ from __future__ import annotations
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.

View file

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

View file

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

View file

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

View file

@ -27,13 +27,13 @@ import json
from pathlib import Path
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"]
_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] = (
_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
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).
"""
@ -13,7 +13,7 @@ import json
from dataclasses import asdict
from pathlib import Path
from domain.sap.tables.pcdb.parser import (
from domain.sap10_calculator.tables.pcdb.parser import (
GasOilBoilerRecord,
RawPcdbRecord,
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
repo_root = Path(__file__).resolve().parents[7]
repo_root = Path(__file__).resolve().parents[4]
run_etl(
source=repo_root / "docs" / "sap-spec" / "pcdb10.dat",
output_dir=repo_root / "docs" / "sap-spec",

View file

@ -21,7 +21,7 @@ from typing import Final, Optional
_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"

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.
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

View file

@ -7,14 +7,14 @@ targets RdSAP10 cost per ADR-0010 amendment.
CO2 emission factors and primary energy factors are unchanged from
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 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

View file

@ -23,17 +23,17 @@ from __future__ import annotations
import pytest
from domain.sap.calculator import (
from domain.sap10_calculator.calculator import (
CalculatorInputs,
calculate_sap_from_inputs,
)
from domain.sap.climate.appendix_u import external_temperature_c
from domain.sap.worksheet.dimensions import Dimensions
from domain.sap.worksheet.heat_transmission import HeatTransmission
from domain.sap.worksheet.mean_internal_temperature import (
from domain.sap10_calculator.climate.appendix_u import external_temperature_c
from domain.sap10_calculator.worksheet.dimensions import Dimensions
from domain.sap10_calculator.worksheet.heat_transmission import HeatTransmission
from domain.sap10_calculator.worksheet.mean_internal_temperature import (
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:

View file

@ -20,18 +20,18 @@ from dataclasses import replace
import pytest
from domain.sap.calculator import (
from domain.sap10_calculator.calculator import (
CalculatorInputs,
SapResult,
calculate_sap_from_inputs,
)
from domain.sap.climate.appendix_u import external_temperature_c
from domain.sap.worksheet.dimensions import Dimensions
from domain.sap.worksheet.heat_transmission import HeatTransmission
from domain.sap.worksheet.mean_internal_temperature import (
from domain.sap10_calculator.climate.appendix_u import external_temperature_c
from domain.sap10_calculator.worksheet.dimensions import Dimensions
from domain.sap10_calculator.worksheet.heat_transmission import HeatTransmission
from domain.sap10_calculator.worksheet.mean_internal_temperature import (
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:

View file

@ -15,15 +15,15 @@ from pathlib import Path
import pytest
from domain.sap.tables.pcdb.etl import run_etl
from domain.sap.tables.pcdb.parser import (
from domain.sap10_calculator.tables.pcdb.etl import run_etl
from domain.sap10_calculator.tables.pcdb.parser import (
parse_table_105,
parse_table_105_row,
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"

View file

@ -10,7 +10,7 @@ Reference: BRE PCDB pcdb10.dat (April 2026); user-verified records.
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:

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -30,7 +30,7 @@ will retire envelope.py in favour of this module (ADR-0009 §"Module
layout").
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
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 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

View file

@ -6,7 +6,7 @@ Two layers:
implements the §U3.2 polynomial that converts the horizontal solar
irradiance from Table U3 into a per-orientation per-pitch surface flux.
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 U4 (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 datatypes.epc.domain.epc_property_data import EpcPropertyData, SapWindow
from domain.sap.tables.pcdb.postcode_weather import PostcodeClimate
from domain.sap.climate.appendix_u import (
from domain.sap10_calculator.tables.pcdb.postcode_weather import PostcodeClimate
from domain.sap10_calculator.climate.appendix_u import (
horizontal_solar_irradiance_w_per_m2,
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:

View file

@ -18,7 +18,7 @@ from __future__ import annotations
from dataclasses import dataclass
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

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,
)
from domain.sap.worksheet.tests import (
from domain.sap10_calculator.worksheet.tests import (
_elmhurst_worksheet_000474 as w000474,
_elmhurst_worksheet_000477 as w000477,
_elmhurst_worksheet_000480 as w000480,

View file

@ -37,11 +37,11 @@ from domain.ml.tests._fixtures import (
make_sap_heating,
make_window,
)
from domain.sap.worksheet.solar_gains import RoofWindowInput, RooflightInput
from domain.sap.worksheet.ventilation import MechanicalVentilationKind
from domain.sap.worksheet.water_heating import TABLE_J1_TCOLD_FROM_MAINS_C
from domain.sap10_calculator.worksheet.solar_gains import RoofWindowInput, RooflightInput
from domain.sap10_calculator.worksheet.ventilation import MechanicalVentilationKind
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_ETA_LOSS_ALL_ONE,
SECTION_8C_INTERMITTENCY_MONTHLY,

View file

@ -35,11 +35,11 @@ from domain.ml.tests._fixtures import (
make_sap_heating,
make_window,
)
from domain.sap.worksheet.solar_gains import RoofWindowInput, RooflightInput
from domain.sap.worksheet.ventilation import MechanicalVentilationKind
from domain.sap.worksheet.water_heating import TABLE_J1_TCOLD_FROM_MAINS_C
from domain.sap10_calculator.worksheet.solar_gains import RoofWindowInput, RooflightInput
from domain.sap10_calculator.worksheet.ventilation import MechanicalVentilationKind
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_ETA_LOSS_ALL_ONE,
SECTION_8C_INTERMITTENCY_MONTHLY,

View file

@ -36,11 +36,11 @@ from domain.ml.tests._fixtures import (
make_sap_heating,
make_window,
)
from domain.sap.worksheet.solar_gains import RoofWindowInput, RooflightInput
from domain.sap.worksheet.ventilation import MechanicalVentilationKind
from domain.sap.worksheet.water_heating import TABLE_J1_TCOLD_FROM_MAINS_C
from domain.sap10_calculator.worksheet.solar_gains import RoofWindowInput, RooflightInput
from domain.sap10_calculator.worksheet.ventilation import MechanicalVentilationKind
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_ETA_LOSS_ALL_ONE,
SECTION_8C_INTERMITTENCY_MONTHLY,

View file

@ -34,8 +34,8 @@ from domain.ml.tests._fixtures import (
make_sap_heating,
make_window,
)
from domain.sap.worksheet.solar_gains import RoofWindowInput, RooflightInput
from domain.sap.worksheet.water_heating import TABLE_J1_TCOLD_FROM_MAINS_C
from domain.sap10_calculator.worksheet.solar_gains import RoofWindowInput, RooflightInput
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
# 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
# `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_ETA_LOSS_ALL_ONE,
SECTION_8C_INTERMITTENCY_MONTHLY,

View file

@ -39,11 +39,11 @@ from domain.ml.tests._fixtures import (
make_sap_heating,
make_window,
)
from domain.sap.worksheet.solar_gains import RoofWindowInput, RooflightInput
from domain.sap.worksheet.ventilation import MechanicalVentilationKind
from domain.sap.worksheet.water_heating import TABLE_J1_TCOLD_FROM_MAINS_C
from domain.sap10_calculator.worksheet.solar_gains import RoofWindowInput, RooflightInput
from domain.sap10_calculator.worksheet.ventilation import MechanicalVentilationKind
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_ETA_LOSS_ALL_ONE,
SECTION_8C_INTERMITTENCY_MONTHLY,

View file

@ -42,11 +42,11 @@ from domain.ml.tests._fixtures import (
make_sap_heating,
make_window,
)
from domain.sap.worksheet.solar_gains import Orientation, RoofWindowInput, RooflightInput
from domain.sap.worksheet.ventilation import MechanicalVentilationKind
from domain.sap.worksheet.water_heating import TABLE_J1_TCOLD_FROM_MAINS_C
from domain.sap10_calculator.worksheet.solar_gains import Orientation, RoofWindowInput, RooflightInput
from domain.sap10_calculator.worksheet.ventilation import MechanicalVentilationKind
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_ETA_LOSS_ALL_ONE,
SECTION_8C_INTERMITTENCY_MONTHLY,

View file

@ -1,6 +1,6 @@
"""Reader for the canonical SAP10.2 worksheet Excel — the source of
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 full worksheet, differing only on weather-data source:
@ -20,7 +20,7 @@ from typing import Any, Iterable
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"

View file

@ -26,8 +26,8 @@ from domain.ml.tests._fixtures import (
make_floor_dimension,
make_minimal_sap10_epc,
)
from domain.sap.worksheet.dimensions import Dimensions, dimensions_from_cert
from domain.sap.worksheet.tests._xlsx_loader import load_cells
from domain.sap10_calculator.worksheet.dimensions import Dimensions, dimensions_from_cert
from domain.sap10_calculator.worksheet.tests._xlsx_loader import load_cells
_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 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,
fixture_id as _elmhurst_fixture_id,
)

View file

@ -24,9 +24,9 @@ from typing import Final
import pytest
from domain.sap.calculator import Sap10Calculator
from domain.sap.rdsap.cert_to_inputs import cert_to_inputs
from domain.sap.worksheet.tests import (
from domain.sap10_calculator.calculator import Sap10Calculator
from domain.sap10_calculator.rdsap.cert_to_inputs import cert_to_inputs
from domain.sap10_calculator.worksheet.tests import (
_elmhurst_worksheet_000474 as _w000474,
_elmhurst_worksheet_000477 as _w000477,
_elmhurst_worksheet_000480 as _w000480,
@ -35,7 +35,7 @@ from domain.sap.worksheet.tests import (
_elmhurst_worksheet_000516 as _w000516,
_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,
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 domain.sap.worksheet.energy_requirements import (
from domain.sap10_calculator.worksheet.energy_requirements import (
space_heating_fuel_monthly_kwh,
)

View file

@ -10,10 +10,10 @@ from types import ModuleType
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,
)
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:

View file

@ -8,8 +8,8 @@ from __future__ import annotations
import pytest
from domain.sap.rdsap.cert_to_inputs import cert_to_inputs
from domain.sap.worksheet.fuel_cost import FuelCostResult, fuel_cost
from domain.sap10_calculator.rdsap.cert_to_inputs import cert_to_inputs
from domain.sap10_calculator.worksheet.fuel_cost import FuelCostResult, fuel_cost
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.
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).
Reference: SAP 10.3 specification §3 (pages 17-22);
@ -30,7 +30,7 @@ from domain.ml.tests._fixtures import (
make_floor_dimension,
make_minimal_sap10_epc,
)
from domain.sap.worksheet.heat_transmission import (
from domain.sap10_calculator.worksheet.heat_transmission import (
DwellingExposure,
HeatTransmission,
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 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,
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_000490 as _w000490,
)

View file

@ -1,6 +1,6 @@
"""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
SAP 10.2 spec formula; the orchestrator is parametrized against every
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
from domain.sap.worksheet.internal_gains import (
from domain.sap10_calculator.worksheet.internal_gains import (
InternalGainsResult,
OvershadingCategory,
PumpDateCategory,
@ -44,7 +44,7 @@ from datatypes.epc.domain.epc_property_data import (
SapWindow,
)
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:

View file

@ -14,14 +14,14 @@ from types import ModuleType
import pytest
from domain.sap.climate.appendix_u import external_temperature_c
from domain.sap.worksheet.mean_internal_temperature import (
from domain.sap10_calculator.climate.appendix_u import external_temperature_c
from domain.sap10_calculator.worksheet.mean_internal_temperature import (
MeanInternalTemperatureResult,
elsewhere_heating_temperature_c,
mean_internal_temperature_monthly,
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.

View file

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

View file

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

View file

@ -19,8 +19,8 @@ from datatypes.epc.domain.epc_property_data import (
WindowTransmissionDetails,
)
from domain.ml.tests._fixtures import make_minimal_sap10_epc, make_window
from domain.sap.worksheet.internal_gains import OvershadingCategory
from domain.sap.worksheet.solar_gains import (
from domain.sap10_calculator.worksheet.internal_gains import OvershadingCategory
from domain.sap10_calculator.worksheet.solar_gains import (
Orientation,
RoofWindowInput,
RooflightInput,
@ -29,7 +29,7 @@ from domain.sap.worksheet.solar_gains import (
window_solar_gain_w,
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):

View file

@ -9,12 +9,12 @@ from types import ModuleType
import pytest
from domain.sap.climate.appendix_u import external_temperature_c
from domain.sap.worksheet.space_cooling import (
from domain.sap10_calculator.climate.appendix_u import external_temperature_c
from domain.sap10_calculator.worksheet.space_cooling import (
space_cooling_monthly_kwh,
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

View file

@ -11,13 +11,13 @@ from types import ModuleType
import pytest
from domain.sap.climate.appendix_u import external_temperature_c
from domain.sap.worksheet.space_heating import (
from domain.sap10_calculator.climate.appendix_u import external_temperature_c
from domain.sap10_calculator.worksheet.space_heating import (
SpaceHeatingResult,
monthly_heat_requirement_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(

View file

@ -9,7 +9,7 @@ Reference: SAP 10.3 specification (13-01-2026) Table 9a (page 184).
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:

View file

@ -12,8 +12,8 @@ Canonical worked example: `2026-05-19-17-18 RdSap10Worksheet.xlsx`,
import pytest
from domain.sap.worksheet.tests._xlsx_loader import load_cells
from domain.sap.worksheet.ventilation import (
from domain.sap10_calculator.worksheet.tests._xlsx_loader import load_cells
from domain.sap10_calculator.worksheet.ventilation import (
MechanicalVentilationKind,
TABLE_U2_NON_REGIONAL_WIND_SPEED_M_S,
VentilationResult,
@ -461,7 +461,7 @@ def test_excel_worksheet_conformance_section_2_lines_6a_to_25m() -> None:
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,
fixture_id as _elmhurst_fixture_id,
)
@ -479,7 +479,7 @@ def test_section_2_matches_elmhurst_worksheet(fixture: ModuleType) -> None:
worksheet's (9) value.
"""
# 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())
assert dims.storey_count == fixture.LINE_9_STOREYS, (
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.
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
example and/or Elmhurst conformance fixtures.
@ -12,14 +12,14 @@ from types import ModuleType
import pytest
from domain.sap.worksheet.tests import (
from domain.sap10_calculator.worksheet.tests import (
_elmhurst_worksheet_000474 as _w000474,
_elmhurst_worksheet_000477 as _w000477,
_elmhurst_worksheet_000490 as _w000490,
)
from domain.sap.worksheet.tests._elmhurst_fixtures import ALL_FIXTURES, fixture_id
from domain.sap.worksheet.tests._xlsx_loader import load_cells
from domain.sap.worksheet.water_heating import (
from domain.sap10_calculator.worksheet.tests._elmhurst_fixtures import ALL_FIXTURES, fixture_id
from domain.sap10_calculator.worksheet.tests._xlsx_loader import load_cells
from domain.sap10_calculator.worksheet.water_heating import (
TABLE_J1_TCOLD_FROM_MAINS_C,
annual_average_hot_water_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
87.0% summer scalar) HW kWh 2320 ~2290 (+0% target)."""
# 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()
@ -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
2851 (target ±2%)."""
# Arrange
from domain.sap.rdsap.cert_to_inputs import cert_to_inputs
from domain.sap.worksheet.tests import _elmhurst_worksheet_000490 as _w000490
from domain.sap10_calculator.rdsap.cert_to_inputs import cert_to_inputs
from domain.sap10_calculator.worksheet.tests import _elmhurst_worksheet_000490 as _w000490
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.
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`.
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