From 53aba1332ed91ada48ecb7c95d25f27ad2d5aedc Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 20 May 2026 18:38:46 +0000 Subject: [PATCH] =?UTF-8?q?=C2=A75=20slice=208:=20(73)=20total=5Finternal?= =?UTF-8?q?=5Fgains=5Fmonthly=5Fw=20+=20InternalGainsResult?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the §5 leaf-function surface: - total_internal_gains_monthly_w sums (66) + (67) + (68) + (69) + (70) + (71) + (72) element-wise. (71) carries negative sign so the losses term subtracts. - InternalGainsResult frozen dataclass bundles all 7 line refs plus the total as 12-tuples — the typed payload returned by the orchestrator. Verified against Elmhurst U985-0001-000490 (73)m to ≤1e-2 W/month. Co-Authored-By: Claude Opus 4.7 --- .../domain/sap/worksheet/internal_gains.py | 51 ++++++++++++++ .../worksheet/tests/test_internal_gains.py | 68 +++++++++++++++++++ 2 files changed, 119 insertions(+) diff --git a/packages/domain/src/domain/sap/worksheet/internal_gains.py b/packages/domain/src/domain/sap/worksheet/internal_gains.py index 2a9d6d3d..45327e95 100644 --- a/packages/domain/src/domain/sap/worksheet/internal_gains.py +++ b/packages/domain/src/domain/sap/worksheet/internal_gains.py @@ -297,6 +297,57 @@ def pumps_fans_monthly_w( ) +def total_internal_gains_monthly_w( + *, + metabolic_monthly_w: tuple[float, ...], + lighting_monthly_w: tuple[float, ...], + appliances_monthly_w: tuple[float, ...], + cooking_monthly_w: tuple[float, ...], + pumps_fans_monthly_w: tuple[float, ...], + losses_monthly_w: tuple[float, ...], + water_heating_gains_monthly_w: tuple[float, ...], +) -> tuple[float, ...]: + """SAP10.2 §5 line (73) — total internal gains. + + (73)m = (66)m + (67)m + (68)m + (69)m + (70)m + (71)m + (72)m + + Pure element-wise sum across the seven gain streams. (71)m carries a + negative sign (losses) so the contribution is a subtraction. + """ + return tuple( + m + l + a + c + p + s + w + for m, l, a, c, p, s, w in zip( + metabolic_monthly_w, + lighting_monthly_w, + appliances_monthly_w, + cooking_monthly_w, + pumps_fans_monthly_w, + losses_monthly_w, + water_heating_gains_monthly_w, + ) + ) + + +@dataclass(frozen=True) +class InternalGainsResult: + """SAP10.2 §5 line refs (66)..(73), each a 12-tuple of watts per month. + + Returned by `internal_gains_from_cert`. Downstream §6/§7/§9 calculators + consume `total_internal_gains_monthly_w` directly; the per-line tuples + are exposed for worksheet conformance + audit. Field names mirror the + SAP10.2 line refs. + """ + + metabolic_monthly_w: tuple[float, ...] # line (66) + lighting_monthly_w: tuple[float, ...] # line (67) + appliances_monthly_w: tuple[float, ...] # line (68) + cooking_monthly_w: tuple[float, ...] # line (69) + pumps_fans_monthly_w: tuple[float, ...] # line (70) + losses_monthly_w: tuple[float, ...] # line (71) + water_heating_gains_monthly_w: tuple[float, ...] # line (72) + total_internal_gains_monthly_w: tuple[float, ...] # line (73) + + def water_heating_gains_monthly_w( *, heat_gains_from_water_heating_monthly_kwh: tuple[float, ...], diff --git a/packages/domain/src/domain/sap/worksheet/tests/test_internal_gains.py b/packages/domain/src/domain/sap/worksheet/tests/test_internal_gains.py index 044dac53..38c7fafe 100644 --- a/packages/domain/src/domain/sap/worksheet/tests/test_internal_gains.py +++ b/packages/domain/src/domain/sap/worksheet/tests/test_internal_gains.py @@ -13,6 +13,7 @@ Table 5a + Appendix L (lighting/appliances/cooking) + Appendix J Table 1b import pytest from domain.sap.worksheet.internal_gains import ( + InternalGainsResult, PumpDateCategory, appliances_monthly_w, balanced_mv_no_hr_fan_w, @@ -26,6 +27,7 @@ from domain.sap.worksheet.internal_gains import ( metabolic_monthly_w, piv_fan_w, pumps_fans_monthly_w, + total_internal_gains_monthly_w, warm_air_heating_fan_w, water_heating_gains_monthly_w, ) @@ -344,3 +346,69 @@ def test_heat_interface_unit_converts_kwh_per_day_to_constant_watts() -> None: # Assert assert w == pytest.approx(20.0, abs=1e-9) + + +def test_total_internal_gains_sums_seven_components_per_month_for_000490() -> None: + """SAP10.2 §5 line (73): G_total,m = G_M + G_L + G_A + G_C + G_pumps + + G_losses + G_WH per month. (71) is negative — net subtraction. + + Verified against Elmhurst U985-0001-000490 worksheet (73)m row. + """ + # Arrange — component tuples from the 000490 worksheet rows (66)..(72) + metabolic = (128.8087,) * 12 + lighting = (24.2665, 21.5533, 17.5283, 13.2701, 9.9195, 8.3745, + 9.0489, 11.7621, 15.7871, 20.0454, 23.3959, 24.9410) + appliances = (280.4965, 283.4071, 276.0723, 260.4574, 240.7463, 222.2207, + 209.8445, 206.9338, 214.2686, 229.8835, 249.5946, 268.1202) + cooking = (50.0277,) * 12 + pumps = (7.0, 7.0, 7.0, 7.0, 7.0, 0.0, 0.0, 0.0, 0.0, 7.0, 7.0, 7.0) + losses = (-85.8725,) * 12 + water_heating = (101.0798, 98.8663, 94.6647, 85.5412, 79.9343, 74.0821, + 70.5249, 73.5203, 77.0253, 83.5192, 92.2698, 99.9197) + expected_total = ( + 505.8067, 503.7906, 488.2293, 459.2325, 430.5641, 397.6412, + 382.3822, 385.1801, 400.0449, 433.4120, 465.2242, 492.9448, + ) + + # Act + total = total_internal_gains_monthly_w( + metabolic_monthly_w=metabolic, + lighting_monthly_w=lighting, + appliances_monthly_w=appliances, + cooking_monthly_w=cooking, + pumps_fans_monthly_w=pumps, + losses_monthly_w=losses, + water_heating_gains_monthly_w=water_heating, + ) + + # Assert + assert len(total) == 12 + for m, (actual, exp) in enumerate(zip(total, expected_total)): + assert actual == pytest.approx(exp, abs=1e-2), f"month {m+1}" + + +def test_internal_gains_result_dataclass_holds_all_seven_lines_plus_total() -> None: + """InternalGainsResult bundles every line (66)..(73) as a 12-tuple so + downstream §6/§7/§9 callers receive a single typed payload from the + orchestrator. Field names mirror the worksheet line refs.""" + # Arrange + zeros = (0.0,) * 12 + + # Act + result = InternalGainsResult( + metabolic_monthly_w=zeros, + lighting_monthly_w=zeros, + appliances_monthly_w=zeros, + cooking_monthly_w=zeros, + pumps_fans_monthly_w=zeros, + losses_monthly_w=zeros, + water_heating_gains_monthly_w=zeros, + total_internal_gains_monthly_w=zeros, + ) + + # Assert — every field is a 12-tuple + assert all(len(getattr(result, f)) == 12 for f in ( + "metabolic_monthly_w", "lighting_monthly_w", "appliances_monthly_w", + "cooking_monthly_w", "pumps_fans_monthly_w", "losses_monthly_w", + "water_heating_gains_monthly_w", "total_internal_gains_monthly_w", + ))