§7 slice 2: two-main case 1 weighted-R per Table 9b

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 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-05-20 21:30:37 +00:00
parent fa49d7b946
commit 13c2c6514f
2 changed files with 53 additions and 2 deletions

View file

@ -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 19 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,
)

View file

@ -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):