From 13c2c6514f760691e2bee353fbfe5e819e60d3bb Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 20 May 2026 21:30:37 +0000 Subject: [PATCH] =?UTF-8?q?=C2=A77=20slice=202:=20two-main=20case=201=20we?= =?UTF-8?q?ighted-R=20per=20Table=209b?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds secondary_fraction (203) + secondary_responsiveness orchestrator params. When both main systems heat the whole house (Table 9c case 1), the u-formula consumes a weighted responsiveness: R_eff = (1 - (203)) × R_primary + (203) × R_secondary Synthetic equivalence test pins the contract: any (frac, R_primary, R_secondary) call lands the same MIT as a single-main call with the weighted R. No fixture exercises case 1 (all 6 Elmhurst = single combi), so secondary_fraction defaults to 0 → identity behaviour. Case 2 (different parts heated separately) deferred — needs (203) > 1-(91) branch + conditional T_2 averaging + per-system Table 4e adjustment. No fixture data to drive. Co-Authored-By: Claude Opus 4.7 --- .../worksheet/mean_internal_temperature.py | 15 ++++++- .../tests/test_mean_internal_temperature.py | 40 +++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) 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 674a018a..3eec3bb9 100644 --- a/packages/domain/src/domain/sap/worksheet/mean_internal_temperature.py +++ b/packages/domain/src/domain/sap/worksheet/mean_internal_temperature.py @@ -237,6 +237,8 @@ def mean_internal_temperature_monthly( responsiveness: float, living_area_fraction: float, control_temperature_adjustment_c: float = 0.0, + secondary_fraction: float = 0.0, + secondary_responsiveness: float = 1.0, ) -> MeanInternalTemperatureResult: """SAP 10.2 §7 orchestrator — chain Table 9c steps 1–9 for all 12 months. @@ -255,7 +257,16 @@ def mean_internal_temperature_monthly( control_temperature_adjustment_c (93) Table 4e adj (defaults 0 — all 6 Elmhurst fixtures have (93) = (92), so this is a known shortcut; cert-side mapping is a future slice). + secondary_fraction (203) fraction of main heat from second main system + when both heat the whole house (Table 9c case 1). + Defaults 0 (single-main); used to compute weighted R + per Table 9b: R_eff = (1-frac)·R_primary + frac·R_secondary. + Case 2 (different parts heated) deferred — no fixture. """ + effective_responsiveness = ( + (1.0 - secondary_fraction) * responsiveness + + secondary_fraction * secondary_responsiveness + ) elsewhere_off_hours = ( _ELSEWHERE_OFF_HOURS_TYPE_3 if control_type == 3 else _ELSEWHERE_OFF_HOURS_TYPE_12 ) @@ -285,7 +296,7 @@ def mean_internal_temperature_monthly( heating_temperature_c=_T_H1_C, off_hours_first=_LIVING_AREA_OFF_HOURS[0], off_hours_second=_LIVING_AREA_OFF_HOURS[1], - external_temp_c=ext, responsiveness=responsiveness, + external_temp_c=ext, responsiveness=effective_responsiveness, total_gains_w=gains, heat_transfer_coefficient_w_per_k=h, time_constant_h=tau, ) @@ -301,7 +312,7 @@ def mean_internal_temperature_monthly( heating_temperature_c=t_h2_m, off_hours_first=elsewhere_off_hours[0], off_hours_second=elsewhere_off_hours[1], - external_temp_c=ext, responsiveness=responsiveness, + external_temp_c=ext, responsiveness=effective_responsiveness, total_gains_w=gains, heat_transfer_coefficient_w_per_k=h, time_constant_h=tau, ) diff --git a/packages/domain/src/domain/sap/worksheet/tests/test_mean_internal_temperature.py b/packages/domain/src/domain/sap/worksheet/tests/test_mean_internal_temperature.py index 135a4af3..b980461b 100644 --- a/packages/domain/src/domain/sap/worksheet/tests/test_mean_internal_temperature.py +++ b/packages/domain/src/domain/sap/worksheet/tests/test_mean_internal_temperature.py @@ -65,6 +65,46 @@ def test_mean_internal_temperature_monthly_reproduces_000490_line_92_jan() -> No assert result.mean_internal_temp_monthly[0] == pytest.approx(15.1899, abs=5e-3) +def test_two_main_systems_case_1_uses_weighted_responsiveness_per_table_9b() -> None: + # Arrange — SAP10.2 Table 9b weighted-R formula when both main systems + # heat the whole house (same control type, shared T_h2 + Th-time grid): + # R = (203) × R_system2 + [1 - (203)] × R_system1 + # Equivalent-R contract: passing secondary_fraction + secondary_R must + # produce the same MIT as a single-main run with the weighted R. + secondary_fraction = 0.3 # (203) + primary_r = 1.0 # gas wet system (Table 4d) + secondary_r = 0.0 # extreme — exaggerates the weighting effect + expected_r = (1.0 - secondary_fraction) * primary_r + secondary_fraction * secondary_r + + common_kwargs = dict( + monthly_external_temp_c=_W000490_EXT_TEMP_C, + monthly_total_gains_w=_W000490_TOTAL_GAINS_W, + monthly_heat_transfer_coefficient_w_per_k=_W000490_HTC_W_PER_K, + thermal_mass_parameter_kj_per_m2_k=_W000490_TMP_KJ_PER_M2_K, + total_floor_area_m2=_W000490_TFA_M2, + control_type=_W000490_CONTROL_TYPE, + living_area_fraction=_W000490_LIVING_AREA_FRACTION, + ) + + # Act + two_system = mean_internal_temperature_monthly( + responsiveness=primary_r, + secondary_fraction=secondary_fraction, + secondary_responsiveness=secondary_r, + **common_kwargs, + ) + equivalent_single = mean_internal_temperature_monthly( + responsiveness=expected_r, + **common_kwargs, + ) + + # Assert — every month identical to floating-point precision + for m in range(12): + assert two_system.mean_internal_temp_monthly[m] == pytest.approx( + equivalent_single.mean_internal_temp_monthly[m], abs=1e-12 + ), f"(92) month {m+1} weighted-R contract" + + def test_elsewhere_temperature_control_type_2_uses_quadratic_drop() -> None: # Arrange — Table 9 elsewhere formula for control type 2 (programmer + # room thermostat, default for boiler systems with reasonable control):