From 20424a2dca16a85da14cbb308b663f448d795dfd Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Fri, 22 May 2026 23:05:55 +0000 Subject: [PATCH] =?UTF-8?q?Slice=2021a:=20relabel=20ambient=20SAP=2010.3?= =?UTF-8?q?=20=E2=86=92=20SAP=2010.2=20in=20calculator=20docstrings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- packages/domain/src/domain/sap/calculator.py | 12 ++++++------ .../domain/src/domain/sap/climate/appendix_u.py | 12 ++++++------ .../domain/src/domain/sap/rdsap/cert_to_inputs.py | 14 +++++++------- .../src/domain/sap/validation/parity_report.py | 2 +- .../domain/src/domain/sap/worksheet/dimensions.py | 6 +++--- .../src/domain/sap/worksheet/heat_transmission.py | 2 +- .../sap/worksheet/mean_internal_temperature.py | 6 +++--- packages/domain/src/domain/sap/worksheet/rating.py | 8 ++++---- .../domain/src/domain/sap/worksheet/solar_gains.py | 8 ++++---- .../src/domain/sap/worksheet/space_heating.py | 6 +++--- .../src/domain/sap/worksheet/utilisation_factor.py | 6 +++--- 11 files changed, 41 insertions(+), 41 deletions(-) diff --git a/packages/domain/src/domain/sap/calculator.py b/packages/domain/src/domain/sap/calculator.py index c083d490..ff8f56fc 100644 --- a/packages/domain/src/domain/sap/calculator.py +++ b/packages/domain/src/domain/sap/calculator.py @@ -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. diff --git a/packages/domain/src/domain/sap/climate/appendix_u.py b/packages/domain/src/domain/sap/climate/appendix_u.py index 4811c081..654a7a8d 100644 --- a/packages/domain/src/domain/sap/climate/appendix_u.py +++ b/packages/domain/src/domain/sap/climate/appendix_u.py @@ -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/m²) 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] diff --git a/packages/domain/src/domain/sap/rdsap/cert_to_inputs.py b/packages/domain/src/domain/sap/rdsap/cert_to_inputs.py index a29be7d4..e0b4ef3b 100644 --- a/packages/domain/src/domain/sap/rdsap/cert_to_inputs.py +++ b/packages/domain/src/domain/sap/rdsap/cert_to_inputs.py @@ -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)) diff --git a/packages/domain/src/domain/sap/validation/parity_report.py b/packages/domain/src/domain/sap/validation/parity_report.py index 9a04a696..a7f0fd4e 100644 --- a/packages/domain/src/domain/sap/validation/parity_report.py +++ b/packages/domain/src/domain/sap/validation/parity_report.py @@ -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 diff --git a/packages/domain/src/domain/sap/worksheet/dimensions.py b/packages/domain/src/domain/sap/worksheet/dimensions.py index a2c58bbd..f48e24d5 100644 --- a/packages/domain/src/domain/sap/worksheet/dimensions.py +++ b/packages/domain/src/domain/sap/worksheet/dimensions.py @@ -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 diff --git a/packages/domain/src/domain/sap/worksheet/heat_transmission.py b/packages/domain/src/domain/sap/worksheet/heat_transmission.py index 06abcde3..671d4bf2 100644 --- a/packages/domain/src/domain/sap/worksheet/heat_transmission.py +++ b/packages/domain/src/domain/sap/worksheet/heat_transmission.py @@ -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. """ diff --git a/packages/domain/src/domain/sap/worksheet/mean_internal_temperature.py b/packages/domain/src/domain/sap/worksheet/mean_internal_temperature.py index cd72936c..8e3be03d 100644 --- a/packages/domain/src/domain/sap/worksheet/mean_internal_temperature.py +++ b/packages/domain/src/domain/sap/worksheet/mean_internal_temperature.py @@ -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 × τ diff --git a/packages/domain/src/domain/sap/worksheet/rating.py b/packages/domain/src/domain/sap/worksheet/rating.py index 5e15e2c0..ca132479 100644 --- a/packages/domain/src/domain/sap/worksheet/rating.py +++ b/packages/domain/src/domain/sap/worksheet/rating.py @@ -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: diff --git a/packages/domain/src/domain/sap/worksheet/solar_gains.py b/packages/domain/src/domain/sap/worksheet/solar_gains.py index caca48d5..498d4942 100644 --- a/packages/domain/src/domain/sap/worksheet/solar_gains.py +++ b/packages/domain/src/domain/sap/worksheet/solar_gains.py @@ -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 cert→inputs 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 diff --git a/packages/domain/src/domain/sap/worksheet/space_heating.py b/packages/domain/src/domain/sap/worksheet/space_heating.py index 2573aad2..34d9db53 100644 --- a/packages/domain/src/domain/sap/worksheet/space_heating.py +++ b/packages/domain/src/domain/sap/worksheet/space_heating.py @@ -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 diff --git a/packages/domain/src/domain/sap/worksheet/utilisation_factor.py b/packages/domain/src/domain/sap/worksheet/utilisation_factor.py index cb702027..16986673 100644 --- a/packages/domain/src/domain/sap/worksheet/utilisation_factor.py +++ b/packages/domain/src/domain/sap/worksheet/utilisation_factor.py @@ -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