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` Drives the 12-month heat-balance loop from a typed `CalculatorInputs`
aggregate and emits a typed `SapResult`. This module is the physics aggregate and emits a typed `SapResult`. This module is the physics
@ -26,7 +26,7 @@ Annual totals = month sums; ECF = §13 Table 12 deflator × total cost /
emission factor × delivered fuel (single-fuel approximation in this emission factor × delivered fuel (single-fuel approximation in this
slice slice S-A8 splits hot-water/lighting onto per-fuel factors). 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. 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) @dataclass(frozen=True)
class CalculatorInputs: 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`. (S-A7b) produces one of these from an `EpcPropertyData`.
Fuel-cost fields are per-end-use because SAP §12 / Table 32 charges Fuel-cost fields are per-end-use because SAP §12 / Table 32 charges
@ -224,7 +224,7 @@ class CalculatorInputs:
@dataclass(frozen=True) @dataclass(frozen=True)
class MonthlyEntry: 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 month: int
external_temp_c: float external_temp_c: float
@ -321,7 +321,7 @@ def _solve_month(
def calculate_sap_from_inputs(inputs: CalculatorInputs) -> SapResult: 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` typed `SapResult`. Cert-shape mapping is the job of `cert_to_inputs`
(S-A7b); this entry point is pure physics.""" (S-A7b); this entry point is pure physics."""
tfa = inputs.dimensions.total_floor_area_m2 tfa = inputs.dimensions.total_floor_area_m2
@ -521,7 +521,7 @@ def calculate_sap_from_inputs(inputs: CalculatorInputs) -> SapResult:
class Sap10Calculator: 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 `EpcPropertyData` to typed `CalculatorInputs` via the RdSAP-driven
`cert_to_inputs` mapper and runs the 12-month worksheet loop. `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 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, Three monthly tables across 22 SAP climate regions (index 0 = UK average,
1-21 = named regions per Table U6 postcode mapping): 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/) - Table U3: Mean global solar irradiance on a horizontal plane (W/)
plus monthly solar declination (°) 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 names; lookup helpers raise `ValueError` on out-of-range inputs so callers
can fail fast. 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²), # Table U3 — Mean global solar irradiance on a horizontal plane (W/m²),
# 22 regions × 12 months. Used (with Table U3 declination + per-window # 22 regions × 12 months. Used (with Table U3 declination + per-window
# orientation/pitch) to derive surface flux for solar-gains calculation # 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, ...], ...]] = ( _TABLE_U3: Final[tuple[tuple[float, ...], ...]] = (
(26, 54, 96, 150, 192, 200, 189, 157, 115, 66, 33, 21), # 0 UK average (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 (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: 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 """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 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) _validate(region, month)
return float(_TABLE_U3[region][month - 1]) return float(_TABLE_U3[region][month - 1])
@ -153,7 +153,7 @@ _SOLAR_DECLINATION: Final[tuple[float, ...]] = (
def solar_declination_deg(month: int) -> 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.""" Table U3 footer independent of region."""
_validate_month(month) _validate_month(month)
return _SOLAR_DECLINATION[month - 1] 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 Reads `EpcPropertyData` (the gov EPC API / site-notes domain model) and
produces the typed `CalculatorInputs` the synthetic-input orchestrator 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`) - Heat transmission: §5 (port in `worksheet/heat_transmission.py`)
- Infiltration: §4 Table 5 (port in `worksheet/ventilation.py`) - Infiltration: §4 Table 5 (port in `worksheet/ventilation.py`)
- Living-area fraction: Table 27 by `habitable_rooms_count` - 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) `domain.ml.sap_efficiencies.seasonal_efficiency` cascade)
- Hot-water demand: Appendix J (existing `domain.ml.demand`) - Hot-water demand: Appendix J (existing `domain.ml.demand`)
- Lighting demand: Appendix L simplified (`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 - control_temperature_adjustment from main_heating_control code 2101/2103/2106
(defaults to 0) (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. (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 1: no time + temp control, or one but not both.
# Type 2: programmer + room thermostat (+/ TRVs). # Type 2: programmer + room thermostat (+/ TRVs).
# Type 3: time-and-temperature zone control (e.g. separate living-zone # 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: 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 `main_heating_control` code on `MainHeatingDetail`. Defaults to 2
(programmer + room thermostat) when the code is missing the modal (programmer + room thermostat) when the code is missing the modal
RdSAP case.""" RdSAP case."""
@ -389,7 +389,7 @@ def _control_type(main: Optional[MainHeatingDetail]) -> int:
def _responsiveness(main: Optional[MainHeatingDetail]) -> float: 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.""" underfloor 0.25. Defaults to radiators."""
if main is None: if main is None:
return 1.0 return 1.0
@ -656,7 +656,7 @@ def _water_efficiency_with_category_inherit(
def _co2_factor_kg_per_kwh(main: Optional[MainHeatingDetail]) -> float: 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)) 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` ADR-0009 Session B compares `Sap10Calculator.calculate(epc).sap_score`
to the cert's `energy_rating_current` across a 1000-cert stratified 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 Builds the typed `Dimensions` aggregate that the rest of the worksheet
reads: total floor area, volume, gross/party wall areas, ground and top 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 N parts produces totals over all N. Room-in-roof contributes one
additional storey per part where present (RdSAP §1.8 + §3.9). 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). existing dwellings see RdSAP 10 §3 (areas and dimensions).
Edge cases explicitly out of scope for the first slice (see ADR-0009 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) @dataclass(frozen=True)
class Dimensions: 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 total_floor_area_m2: float
volume_m3: float volume_m3: float

View file

@ -98,7 +98,7 @@ class DwellingExposure:
dwelling. Houses + bungalows expose both floor and roof; flats expose dwelling. Houses + bungalows expose both floor and roof; flats expose
only the surfaces that aren't party with neighbouring dwellings. 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 `party_walls_w_per_k` already captures the party-wall channel using
its own U_party. 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: 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) - 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' - 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). Table 9b (page 184), Table 9c (page 185).
""" """
@ -63,7 +63,7 @@ def off_period_temperature_reduction_c(
utilisation_factor: float, utilisation_factor: float,
time_constant_h: float, time_constant_h: float,
) -> 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. period, in °C below the heating-period temperature.
t_c = 4 + 0.25 × τ t_c = 4 + 0.25 × τ

View file

@ -43,12 +43,12 @@ def energy_cost_factor(
total_cost_gbp: float, total_cost_gbp: float,
total_floor_area_m2: float, total_floor_area_m2: float,
) -> 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) return ENERGY_COST_DEFLATOR * total_cost_gbp / (total_floor_area_m2 + FLOOR_AREA_OFFSET_M2)
def sap_rating(*, ecf: float) -> float: 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.""" inspect the continuous value; `sap_rating_integer` rounds and clamps."""
if ecf >= ECF_LOG_THRESHOLD: if ecf >= ECF_LOG_THRESHOLD:
return _SAP_LOG_INTERCEPT - _SAP_LOG_SLOPE * log10(ecf) 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: 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 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 than 1 the rating should be quoted as 1"). The integer value is the
one published on the EPC.""" one published on the EPC."""
@ -68,7 +68,7 @@ def environmental_impact_rating(
co2_emissions_kg_per_yr: float, co2_emissions_kg_per_yr: float,
total_floor_area_m2: float, total_floor_area_m2: float,
) -> 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.""" 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) cf = co2_emissions_kg_per_yr / (total_floor_area_m2 + FLOOR_AREA_OFFSET_M2)
if cf >= _CF_LOG_THRESHOLD: 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: 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 the certinputs mapper slice this module takes them as caller inputs so
its physics is independent of cert-shape assumptions. 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 127-129); Table U5 columns map 8 cardinal cert orientations to 5
coefficient sets (N, NE/NW, E/W, SE/SW, S). coefficient sets (N, NE/NW, E/W, SE/SW, S).
""" """
@ -116,7 +116,7 @@ def surface_solar_flux_w_per_m2(
) -> float: ) -> float:
"""Per-orientation per-pitch monthly solar flux on a surface (W/m²). """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. irradiance in Table U3 to any orientation/tilt combination.
""" """
s_h = horizontal_solar_irradiance_w_per_m2(region, month) s_h = horizontal_solar_irradiance_w_per_m2(region, month)
@ -146,7 +146,7 @@ def window_solar_gain_w(
frame_factor: float, frame_factor: float,
overshading_factor: float, overshading_factor: float,
) -> 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 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 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 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 If Q_heat would be negative or below 1 kWh in any month, set it to 0 per
the Table 9c clamp. 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 from __future__ import annotations
@ -37,7 +37,7 @@ def monthly_heat_requirement_kwh(
total_gains_w: float, total_gains_w: float,
days_in_month: int, days_in_month: int,
) -> float: ) -> 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.""" month; clamps to 0 when below 1 kWh or negative."""
loss_rate_w = heat_transfer_coefficient_w_per_k * ( loss_rate_w = heat_transfer_coefficient_w_per_k * (
internal_temperature_c - external_temperature_c 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 η reduces the contribution of internal + solar gains when they outpace the
dwelling's heat-loss rate. A well-insulated dwelling with large solar gains 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 mass parameter and heat-loss parameter; computed by the orchestrator and
passed in here. 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 from __future__ import annotations
@ -28,7 +28,7 @@ def utilisation_factor(
heat_loss_rate_w: float, heat_loss_rate_w: float,
time_constant_h: float, time_constant_h: float,
) -> 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 γ = total_gains_w / heat_loss_rate_w; η (0, 1]. When the heat-loss
rate is non-positive (dwelling already balanced or gaining), η = 1 rate is non-positive (dwelling already balanced or gaining), η = 1