Slice 21a: relabel ambient SAP 10.3 → SAP 10.2 in calculator docstrings

The codebase targets SAP 10.2 (14-03-2025) per ADR-0010 and the values
match SAP 10.2 (grid CO2 = 0.136 not 0.086, ECF deflator = 0.42, etc.).
But ~35 docstrings/comments labelled formulas / sections / appendices
as "SAP 10.3 (13-01-2026)" — mis-labeling without affecting behaviour.

Relabels all of them to "SAP 10.2 specification (14-03-2025)" where the
formula being implemented is identical between 10.2 and 10.3 (which is
the vast majority — §1-§9 heat balance, §11/§13 SAP rating equations,
Appendix U climate tables, Table 9a/9c utilisation factor).

Intentionally retained:
- `worksheet/rating.py:14` — explicit comparison "SAP 10.3 widens these
  to 0.36 / 16.21 / 108.8 / 120.5" annotating where 10.3 values would
  differ from the 10.2 values we ship.
- `tables/table_12.py` — its docstring explicitly compares 10.2 vs 10.3
  CO2 / PEF differences; the file's purpose is the 10.2 → 10.3 reference
  table, so the 10.3 label is intentional discussion.

All 515 passing tests continue to pass (only the 48 known cascade-pin
failures from slice 19a remain — those are real residuals, not label
issues).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-05-22 23:05:55 +00:00
parent e2d9f77d0f
commit 20424a2dca
11 changed files with 41 additions and 41 deletions

View file

@ -1,4 +1,4 @@
"""SAP 10.3 synthetic-input calculator orchestrator.
"""SAP 10.2 synthetic-input calculator orchestrator.
Drives the 12-month heat-balance loop from a typed `CalculatorInputs`
aggregate and emits a typed `SapResult`. This module is the physics
@ -26,7 +26,7 @@ Annual totals = month sums; ECF = §13 Table 12 deflator × total cost /
emission factor × delivered fuel (single-fuel approximation in this
slice slice S-A8 splits hot-water/lighting onto per-fuel factors).
Reference: SAP 10.3 specification (13-01-2026) §§5-13 (pages 23-43), Table
Reference: SAP 10.2 specification (14-03-2025) §§5-13 (pages 23-43), Table
9a/9b/9c (pages 184-186), Table 12 (page 191), Appendix L + U.
"""
@ -120,7 +120,7 @@ _ZERO_FUEL_COST_RESULT: Final[FuelCostResult] = FuelCostResult(
@dataclass(frozen=True)
class CalculatorInputs:
"""Synthetic SAP 10.3 calculator inputs. The cert→inputs mapper
"""Synthetic SAP 10.2 calculator inputs. The cert→inputs mapper
(S-A7b) produces one of these from an `EpcPropertyData`.
Fuel-cost fields are per-end-use because SAP §12 / Table 32 charges
@ -224,7 +224,7 @@ class CalculatorInputs:
@dataclass(frozen=True)
class MonthlyEntry:
"""Per-month worksheet outputs for downstream audit. SAP 10.3 §§5-9."""
"""Per-month worksheet outputs for downstream audit. SAP 10.2 §§5-9."""
month: int
external_temp_c: float
@ -321,7 +321,7 @@ def _solve_month(
def calculate_sap_from_inputs(inputs: CalculatorInputs) -> SapResult:
"""Run SAP 10.3 §§5-13 monthly loop on synthetic inputs; return a
"""Run SAP 10.2 §§5-13 monthly loop on synthetic inputs; return a
typed `SapResult`. Cert-shape mapping is the job of `cert_to_inputs`
(S-A7b); this entry point is pure physics."""
tfa = inputs.dimensions.total_floor_area_m2
@ -521,7 +521,7 @@ def calculate_sap_from_inputs(inputs: CalculatorInputs) -> SapResult:
class Sap10Calculator:
"""Deterministic SAP 10.3 calculator entry point. Maps an
"""Deterministic SAP 10.2 calculator entry point. Maps an
`EpcPropertyData` to typed `CalculatorInputs` via the RdSAP-driven
`cert_to_inputs` mapper and runs the 12-month worksheet loop.

View file

@ -1,7 +1,7 @@
"""SAP 10.3 Appendix U — climate data lookups.
"""SAP 10.2 Appendix U — climate data lookups.
Source: BRE, *The Government's Standard Assessment Procedure for Energy
Rating of Dwellings, SAP 10.3* (13-01-2026), Appendix U.
Rating of Dwellings, SAP 10.2* (14-03-2025), Appendix U.
Three monthly tables across 22 SAP climate regions (index 0 = UK average,
1-21 = named regions per Table U6 postcode mapping):
@ -11,7 +11,7 @@ Three monthly tables across 22 SAP climate regions (index 0 = UK average,
- Table U3: Mean global solar irradiance on a horizontal plane (W/)
plus monthly solar declination (°)
Month is 1-12 (January = 1). Region indices map to the SAP 10.3 region
Month is 1-12 (January = 1). Region indices map to the SAP 10.2 region
names; lookup helpers raise `ValueError` on out-of-range inputs so callers
can fail fast.
"""
@ -109,7 +109,7 @@ def wind_speed_m_per_s(region: int, month: int) -> float:
# Table U3 — Mean global solar irradiance on a horizontal plane (W/m²),
# 22 regions × 12 months. Used (with Table U3 declination + per-window
# orientation/pitch) to derive surface flux for solar-gains calculation
# (SAP 10.3 §6.1).
# (SAP 10.2 §6.1).
_TABLE_U3: Final[tuple[tuple[float, ...], ...]] = (
(26, 54, 96, 150, 192, 200, 189, 157, 115, 66, 33, 21), # 0 UK average
(30, 56, 98, 157, 195, 217, 203, 173, 127, 73, 39, 24), # 1 Thames
@ -139,7 +139,7 @@ _TABLE_U3: Final[tuple[tuple[float, ...], ...]] = (
def horizontal_solar_irradiance_w_per_m2(region: int, month: int) -> float:
"""Mean global solar irradiance on a horizontal plane (W/m²) for a SAP
climate region in a month. The starting point for the per-orientation
surface-flux calculation in SAP 10.3 §6.1."""
surface-flux calculation in SAP 10.2 §6.1."""
_validate(region, month)
return float(_TABLE_U3[region][month - 1])
@ -153,7 +153,7 @@ _SOLAR_DECLINATION: Final[tuple[float, ...]] = (
def solar_declination_deg(month: int) -> float:
"""Solar declination angle (°) for the given month. SAP 10.3 Appendix U
"""Solar declination angle (°) for the given month. SAP 10.2 Appendix U
Table U3 footer independent of region."""
_validate_month(month)
return _SOLAR_DECLINATION[month - 1]

View file

@ -1,4 +1,4 @@
"""RdSAP 10 cert → SAP 10.3 CalculatorInputs mapping.
"""RdSAP 10 cert → SAP 10.2 CalculatorInputs mapping.
Reads `EpcPropertyData` (the gov EPC API / site-notes domain model) and
produces the typed `CalculatorInputs` the synthetic-input orchestrator
@ -13,7 +13,7 @@ Defaulting rules per RdSAP 10 (10-06-2025):
- Heat transmission: §5 (port in `worksheet/heat_transmission.py`)
- Infiltration: §4 Table 5 (port in `worksheet/ventilation.py`)
- Living-area fraction: Table 27 by `habitable_rooms_count`
- Heating efficiency: SAP 10.3 Tables 4a/4b (existing
- Heating efficiency: SAP 10.2 Tables 4a/4b (existing
`domain.ml.sap_efficiencies.seasonal_efficiency` cascade)
- Hot-water demand: Appendix J (existing `domain.ml.demand`)
- Lighting demand: Appendix L simplified (`domain.ml.demand`)
@ -30,7 +30,7 @@ Edge cases deliberately deferred to Session B:
- control_temperature_adjustment from main_heating_control code 2101/2103/2106
(defaults to 0)
Reference: RdSAP 10 specification (10-06-2025); SAP 10.3 specification
Reference: RdSAP 10 specification (10-06-2025); SAP 10.2 specification
(13-01-2026) Tables 4a/4b/4e/12.
"""
@ -277,7 +277,7 @@ SAP_10_2_SPEC_PRICES: Final[PriceTable] = PriceTable(
)
# SAP 10.3 Table 9 main_heating_control codes → control type (1/2/3).
# SAP 10.2 Table 9 main_heating_control codes → control type (1/2/3).
# Type 1: no time + temp control, or one but not both.
# Type 2: programmer + room thermostat (+/ TRVs).
# Type 3: time-and-temperature zone control (e.g. separate living-zone
@ -376,7 +376,7 @@ def _first_main_heating(epc: EpcPropertyData) -> Optional[MainHeatingDetail]:
def _control_type(main: Optional[MainHeatingDetail]) -> int:
"""SAP 10.3 §7.1 / Table 9 control type 1/2/3 from the
"""SAP 10.2 §7.1 / Table 9 control type 1/2/3 from the
`main_heating_control` code on `MainHeatingDetail`. Defaults to 2
(programmer + room thermostat) when the code is missing the modal
RdSAP case."""
@ -389,7 +389,7 @@ def _control_type(main: Optional[MainHeatingDetail]) -> int:
def _responsiveness(main: Optional[MainHeatingDetail]) -> float:
"""SAP 10.3 Table 9b responsiveness R ∈ [0, 1]. Radiators ≈ 1.0;
"""SAP 10.2 Table 9b responsiveness R ∈ [0, 1]. Radiators ≈ 1.0;
underfloor 0.25. Defaults to radiators."""
if main is None:
return 1.0
@ -656,7 +656,7 @@ def _water_efficiency_with_category_inherit(
def _co2_factor_kg_per_kwh(main: Optional[MainHeatingDetail]) -> float:
"""SAP 10.3 Table 12 CO2 emission factor by fuel code."""
"""SAP 10.2 Table 12 CO2 emission factor by fuel code."""
return co2_factor_kg_per_kwh(_main_fuel_code(main))

View file

@ -1,4 +1,4 @@
"""Parity-validation report for the deterministic SAP 10.3 calculator.
"""Parity-validation report for the deterministic SAP 10.2 calculator.
ADR-0009 Session B compares `Sap10Calculator.calculate(epc).sap_score`
to the cert's `energy_rating_current` across a 1000-cert stratified

View file

@ -1,4 +1,4 @@
"""SAP 10.3 §1 — dwelling dimensions.
"""SAP 10.2 §1 — dwelling dimensions.
Builds the typed `Dimensions` aggregate that the rest of the worksheet
reads: total floor area, volume, gross/party wall areas, ground and top
@ -7,7 +7,7 @@ floor areas, perimeter. Geometry is summed across every entry in
N parts produces totals over all N. Room-in-roof contributes one
additional storey per part where present (RdSAP §1.8 + §3.9).
Reference: SAP 10.3 specification (13-01-2026), §1 (pages 10-12); for
Reference: SAP 10.2 specification (14-03-2025), §1 (pages 10-12); for
existing dwellings see RdSAP 10 §3 (areas and dimensions).
Edge cases explicitly out of scope for the first slice (see ADR-0009
@ -34,7 +34,7 @@ _RR_SIMPLIFIED_STOREY_HEIGHT_M: Final[float] = 2.45
@dataclass(frozen=True)
class Dimensions:
"""SAP 10.3 §1 geometric inputs to the monthly heat-balance loop."""
"""SAP 10.2 §1 geometric inputs to the monthly heat-balance loop."""
total_floor_area_m2: float
volume_m3: float

View file

@ -98,7 +98,7 @@ class DwellingExposure:
dwelling. Houses + bungalows expose both floor and roof; flats expose
only the surfaces that aren't party with neighbouring dwellings.
SAP 10.3 §3 / RdSAP 10 §5: heat-transmission excludes party surfaces;
SAP 10.2 §3 / RdSAP 10 §5: heat-transmission excludes party surfaces;
`party_walls_w_per_k` already captures the party-wall channel using
its own U_party.
"""

View file

@ -1,4 +1,4 @@
"""SAP 10.3 mean internal temperature (Tables 9, 9b, 9c).
"""SAP 10.2 mean internal temperature (Tables 9, 9b, 9c).
The dwelling has two temperature zones during heating periods:
@ -14,7 +14,7 @@ Standard SAP heating schedule:
- 9 hours heating per day, two off-periods of 7 h and 8 h (control types 1, 2)
- Control type 3 zones the heating: 9 h and 8 h off in 'elsewhere'
Reference: SAP 10.3 specification (13-01-2026) Table 9 (page 183),
Reference: SAP 10.2 specification (14-03-2025) Table 9 (page 183),
Table 9b (page 184), Table 9c (page 185).
"""
@ -63,7 +63,7 @@ def off_period_temperature_reduction_c(
utilisation_factor: float,
time_constant_h: float,
) -> float:
"""SAP 10.3 Table 9b — temperature reduction `u` during a heating-off
"""SAP 10.2 Table 9b — temperature reduction `u` during a heating-off
period, in °C below the heating-period temperature.
t_c = 4 + 0.25 × τ

View file

@ -43,12 +43,12 @@ def energy_cost_factor(
total_cost_gbp: float,
total_floor_area_m2: float,
) -> float:
"""SAP 10.3 §13 equation (7): ECF = 0.36 × cost / (TFA + 45)."""
"""SAP 10.2 §13 equation (7): ECF = 0.36 × cost / (TFA + 45)."""
return ENERGY_COST_DEFLATOR * total_cost_gbp / (total_floor_area_m2 + FLOOR_AREA_OFFSET_M2)
def sap_rating(*, ecf: float) -> float:
"""SAP 10.3 §13 equations (8)/(9). Un-rounded result so callers can
"""SAP 10.2 §13 equations (8)/(9). Un-rounded result so callers can
inspect the continuous value; `sap_rating_integer` rounds and clamps."""
if ecf >= ECF_LOG_THRESHOLD:
return _SAP_LOG_INTERCEPT - _SAP_LOG_SLOPE * log10(ecf)
@ -56,7 +56,7 @@ def sap_rating(*, ecf: float) -> float:
def sap_rating_integer(*, ecf: float) -> int:
"""SAP 10.3 §13: round the continuous SAP rating to the nearest integer
"""SAP 10.2 §13: round the continuous SAP rating to the nearest integer
and clamp to a minimum of 1 ("if the result of the calculation is less
than 1 the rating should be quoted as 1"). The integer value is the
one published on the EPC."""
@ -68,7 +68,7 @@ def environmental_impact_rating(
co2_emissions_kg_per_yr: float,
total_floor_area_m2: float,
) -> float:
"""SAP 10.3 §14 equations (10)-(12). Un-rounded EI rating; mirrors the
"""SAP 10.2 §14 equations (10)-(12). Un-rounded EI rating; mirrors the
SAP rating curve but uses CO2 emissions per (TFA + 45) as the input."""
cf = co2_emissions_kg_per_yr / (total_floor_area_m2 + FLOOR_AREA_OFFSET_M2)
if cf >= _CF_LOG_THRESHOLD:

View file

@ -1,4 +1,4 @@
"""SAP 10.3 §6 + Appendix U §U3.2 — solar gains.
"""SAP 10.2 §6 + Appendix U §U3.2 — solar gains.
Two layers:
@ -22,7 +22,7 @@ Defaults for g⊥ (Table 6b), FF (Table 6c), Z (Table 6d) are deferred to
the certinputs mapper slice this module takes them as caller inputs so
its physics is independent of cert-shape assumptions.
Reference: SAP 10.3 specification §6 + Appendix U §§U3.2-3.3 (pages
Reference: SAP 10.2 specification §6 + Appendix U §§U3.2-3.3 (pages
127-129); Table U5 columns map 8 cardinal cert orientations to 5
coefficient sets (N, NE/NW, E/W, SE/SW, S).
"""
@ -116,7 +116,7 @@ def surface_solar_flux_w_per_m2(
) -> float:
"""Per-orientation per-pitch monthly solar flux on a surface (W/m²).
SAP 10.3 Appendix U §U3.2 polynomial conversion from the horizontal
SAP 10.2 Appendix U §U3.2 polynomial conversion from the horizontal
irradiance in Table U3 to any orientation/tilt combination.
"""
s_h = horizontal_solar_irradiance_w_per_m2(region, month)
@ -146,7 +146,7 @@ def window_solar_gain_w(
frame_factor: float,
overshading_factor: float,
) -> float:
"""Solar gain through a window (W). SAP 10.3 §6.1 equation (5):
"""Solar gain through a window (W). SAP 10.2 §6.1 equation (5):
G_solar = 0.9 × A_w × S × g × FF × Z

View file

@ -1,4 +1,4 @@
"""SAP 10.3 Table 9c step 10 — monthly space-heating requirement.
"""SAP 10.2 Table 9c step 10 — monthly space-heating requirement.
Final step of the heating worksheet: the heat the heating system has to
deliver to maintain the mean internal temperature, given the loss rate to
@ -10,7 +10,7 @@ the outside and the gains the dwelling has already accumulated.
If Q_heat would be negative or below 1 kWh in any month, set it to 0 per
the Table 9c clamp.
Reference: SAP 10.3 specification (13-01-2026) Table 9c (page 185).
Reference: SAP 10.2 specification (14-03-2025) Table 9c (page 185).
"""
from __future__ import annotations
@ -37,7 +37,7 @@ def monthly_heat_requirement_kwh(
total_gains_w: float,
days_in_month: int,
) -> float:
"""SAP 10.3 Table 9c step 10. Returns delivered kWh required for the
"""SAP 10.2 Table 9c step 10. Returns delivered kWh required for the
month; clamps to 0 when below 1 kWh or negative."""
loss_rate_w = heat_transfer_coefficient_w_per_k * (
internal_temperature_c - external_temperature_c

View file

@ -1,4 +1,4 @@
"""SAP 10.3 Table 9a — heating utilisation factor η.
"""SAP 10.2 Table 9a — heating utilisation factor η.
η reduces the contribution of internal + solar gains when they outpace the
dwelling's heat-loss rate. A well-insulated dwelling with large solar gains
@ -16,7 +16,7 @@ The time constant τ = TMP / (3.6 × HLP) comes from the dwelling's thermal
mass parameter and heat-loss parameter; computed by the orchestrator and
passed in here.
Reference: SAP 10.3 specification (13-01-2026) Table 9a (page 184).
Reference: SAP 10.2 specification (14-03-2025) Table 9a (page 184).
"""
from __future__ import annotations
@ -28,7 +28,7 @@ def utilisation_factor(
heat_loss_rate_w: float,
time_constant_h: float,
) -> float:
"""SAP 10.3 Table 9a heating utilisation factor η.
"""SAP 10.2 Table 9a heating utilisation factor η.
γ = total_gains_w / heat_loss_rate_w; η (0, 1]. When the heat-loss
rate is non-positive (dwelling already balanced or gaining), η = 1