"""SAP 10.2 §8c Space cooling — Tables 10a (η_loss) + 10b (Q_cool). Spec lines 7864–7893 (worksheet block §8c) and 10274–10328 (Table 10a/10b algebra). All cooling lines are 0 for dwellings without a fixed air- conditioning system (cooled-area fraction f_C = 0 collapses Q_cool to 0). Inclusion rule: only June, July, August contribute; other months zeroed. Reference: SAP 10.2 specification (14-03-2025) Tables 10a/10b (page 186). """ from __future__ import annotations from dataclasses import dataclass from typing import Final _COOLING_INTERNAL_TEMP_C: Final[float] = 24.0 # spec line 10281 — fixed _MIN_KWH_PER_MONTH: Final[float] = 1.0 _WH_TO_KWH_PER_DAY: Final[float] = 0.024 _DAYS_IN_MONTH: Final[tuple[int, ...]] = (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) # Spec line 10325: include Jun..Aug, disregard Sep..May. Zero-based indices. _COOLING_MONTH_INDICES: Final[frozenset[int]] = frozenset({5, 6, 7}) def utilisation_factor_loss( *, total_gains_w: float, heat_loss_rate_w: float, time_constant_h: float, ) -> float: """SAP 10.2 Table 10a — cooling utilisation factor η_loss. Inverse of Table 9a heating η: γ = G/L (same definition) but exponents flip sign because Table 10a credits the LOSSES (cooling demand offset) rather than the gains (heating demand offset). η_loss ∈ (0, 1]; returns 1 when γ ≤ 0 (losses negative or zero → utilisation of losses undefined). """ if heat_loss_rate_w == 0.0: gamma = 1_000_000.0 # spec Table 10a L=0 sentinel (formula → ≈1) else: gamma = total_gains_w / heat_loss_rate_w gamma = round(gamma, 8) a = 1.0 + time_constant_h / 15.0 if gamma <= 0.0: return 1.0 if gamma == 1.0: return a / (a + 1.0) return (1.0 - gamma ** -a) / (1.0 - gamma ** -(a + 1.0)) def monthly_cool_requirement_kwh( *, heat_transfer_coefficient_w_per_k: float, external_temperature_c: float, total_gains_w: float, time_constant_h: float, days_in_month: int, ) -> tuple[float, float, float, float]: """SAP 10.2 Table 10b — Q_whole single month, pre f_C × f_intermittent. Returns the (100)..(104) line refs as a 4-tuple: (L_m, η_loss, useful loss = η×L, Q_whole). Q_whole is the signed (104)m value — spec line 10321 clamps the final Q_cool (107), not (104). Inclusion mask (Jun-Aug only) + the f_C × f_intermittent multiplication + the negative- or-sub-1-kWh clamp all live in the orchestrator. """ l_m = heat_transfer_coefficient_w_per_k * (_COOLING_INTERNAL_TEMP_C - external_temperature_c) eta = utilisation_factor_loss( total_gains_w=total_gains_w, heat_loss_rate_w=l_m, time_constant_h=time_constant_h, ) useful_loss_w = eta * l_m q_whole = _WH_TO_KWH_PER_DAY * (total_gains_w - useful_loss_w) * days_in_month return l_m, eta, useful_loss_w, q_whole @dataclass(frozen=True) class SpaceCoolingResult: """SAP 10.2 §8c worksheet line refs (100)..(108). Returned by `space_cooling_monthly_kwh`. Downstream calculator consumes `space_cooling_monthly_kwh` (107) directly; per-line tuples are exposed for worksheet conformance + audit. Field names mirror the line refs. """ heat_loss_rate_monthly_w: tuple[float, ...] # (100) utilisation_factor_loss_monthly: tuple[float, ...] # (101) useful_loss_monthly_w: tuple[float, ...] # (102) cooling_gains_monthly_w: tuple[float, ...] # (103) cooling_requirement_monthly_kwh: tuple[float, ...] # (104) cooling_requirement_kwh_per_yr: float # Σ(104) cooled_area_fraction: float # (105) intermittency_factor_monthly: tuple[float, ...] # (106) space_cooling_monthly_kwh: tuple[float, ...] # (107) space_cooling_kwh_per_yr: float # Σ(107) space_cooling_per_m2_kwh: float # (108) def space_cooling_monthly_kwh( *, monthly_heat_transfer_coefficient_w_per_k: tuple[float, ...], monthly_external_temperature_c: tuple[float, ...], monthly_total_gains_w: tuple[float, ...], total_floor_area_m2: float, thermal_mass_parameter_kj_per_m2_k: float, cooled_area_fraction: float = 0.0, intermittency_factor: float = 0.25, ) -> SpaceCoolingResult: """SAP 10.2 §8c orchestrator — produce (100)..(108) line refs. Inputs are length-12 Jan..Dec tuples; `monthly_total_gains_w` is the cooling-specific gains tuple (Table 5a items already excluded by caller — see `cert_to_inputs`). Internal temperature is fixed at 24 °C per Table 10a; not a parameter. Inclusion rule (spec line 10325): only Jun-Aug contribute to (104) and (107). Other months return 0 at those lines regardless of computed value. (100)..(103) and (106) follow worksheet shape — see Q4/Q7 grilling decisions for intermittency tuple semantics. """ l_list: list[float] = [] eta_list: list[float] = [] useful_loss_list: list[float] = [] q104_list: list[float] = [] intermittency_list: list[float] = [] q107_list: list[float] = [] hlp_m_per_m2_k = tuple( h / total_floor_area_m2 if total_floor_area_m2 > 0 else 0.0 for h in monthly_heat_transfer_coefficient_w_per_k ) tau_m_h = tuple( thermal_mass_parameter_kj_per_m2_k / (3.6 * hlp) if hlp > 0 else 0.0 for hlp in hlp_m_per_m2_k ) for m in range(12): l_m, eta_m, useful_loss_m, q_whole_m = monthly_cool_requirement_kwh( heat_transfer_coefficient_w_per_k=monthly_heat_transfer_coefficient_w_per_k[m], external_temperature_c=monthly_external_temperature_c[m], total_gains_w=monthly_total_gains_w[m], time_constant_h=tau_m_h[m], days_in_month=_DAYS_IN_MONTH[m], ) l_list.append(l_m) eta_list.append(eta_m) useful_loss_list.append(useful_loss_m) if m in _COOLING_MONTH_INDICES: q104_list.append(q_whole_m) intermittency_list.append(intermittency_factor) q_cool_m = q_whole_m * cooled_area_fraction * intermittency_factor # Spec Table 10b: "Set Qcool to zero if negative or less than 1 kWh." q107_list.append(q_cool_m if q_cool_m >= _MIN_KWH_PER_MONTH else 0.0) else: q104_list.append(0.0) intermittency_list.append(0.0) q107_list.append(0.0) annual_104 = sum(q104_list) annual_107 = sum(q107_list) per_m2 = annual_107 / total_floor_area_m2 if total_floor_area_m2 > 0 else 0.0 return SpaceCoolingResult( heat_loss_rate_monthly_w=tuple(l_list), utilisation_factor_loss_monthly=tuple(eta_list), useful_loss_monthly_w=tuple(useful_loss_list), cooling_gains_monthly_w=tuple(monthly_total_gains_w), cooling_requirement_monthly_kwh=tuple(q104_list), cooling_requirement_kwh_per_yr=annual_104, cooled_area_fraction=cooled_area_fraction, intermittency_factor_monthly=tuple(intermittency_list), space_cooling_monthly_kwh=tuple(q107_list), space_cooling_kwh_per_yr=annual_107, space_cooling_per_m2_kwh=per_m2, )