From 376cdb6bc308f480d5461707e748bf038d06c208 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 20 May 2026 20:51:49 +0000 Subject: [PATCH] =?UTF-8?q?=C2=A76=20slice=205:=20CalculatorInputs.solar?= =?UTF-8?q?=5Fgains=5Fmonthly=5Fw=20+=20per-month=20index=20lookup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the §6 (83)m output as a required 12-tuple field on CalculatorInputs; _solve_month indexes into it directly instead of recomputing solar each month via _solar_gains_w(windows, region, month). Test (test_calculator_consumes_solar_gains_monthly_w_field_for_per_month_solar) pins the read path: an explicit non-zero monthly tuple flows through calculate_sap_from_inputs unchanged. cert_to_inputs preserves identical behaviour during the migration by computing the new field via the legacy _solar_gains_w leaf per month. Slice 6 swaps that for solar_gains_from_cert; slice 7 deletes the legacy leaf + WindowInput + windows field. Co-Authored-By: Claude Opus 4.7 --- packages/domain/src/domain/sap/calculator.py | 6 ++++- .../src/domain/sap/rdsap/cert_to_inputs.py | 14 ++++++++++- .../sap/tests/test_bre_worked_examples.py | 4 ++++ .../src/domain/sap/tests/test_calculator.py | 23 +++++++++++++++++++ 4 files changed, 45 insertions(+), 2 deletions(-) diff --git a/packages/domain/src/domain/sap/calculator.py b/packages/domain/src/domain/sap/calculator.py index 9bd7c855..9eb75d73 100644 --- a/packages/domain/src/domain/sap/calculator.py +++ b/packages/domain/src/domain/sap/calculator.py @@ -105,6 +105,10 @@ class CalculatorInputs: # zero out in summer per Table 5a. Produced by §5 orchestrator # `internal_gains_from_cert` (called from cert_to_inputs). internal_gains_monthly_w: tuple[float, ...] + # SAP10.2 (83)m — total solar gains W per month (Jan..Dec). Produced + # by §6 orchestrator `solar_gains_from_cert` upstream; the calculator + # only indexes into it per month, no recomputation here. + solar_gains_monthly_w: tuple[float, ...] region: int windows: tuple[WindowInput, ...] control_type: int @@ -218,7 +222,7 @@ def _solve_month( ) -> MonthlyEntry: t_ext = external_temperature_c(inputs.region, month) g_int = inputs.internal_gains_monthly_w[month - 1] - g_sol = _solar_gains_w(windows=inputs.windows, region=inputs.region, month=month) + g_sol = inputs.solar_gains_monthly_w[month - 1] g_total = g_int + g_sol # SAP 10.3 §7.3: two-pass iteration. Seed η = 1, compute T_internal, diff --git a/packages/domain/src/domain/sap/rdsap/cert_to_inputs.py b/packages/domain/src/domain/sap/rdsap/cert_to_inputs.py index af00d1fc..c6892866 100644 --- a/packages/domain/src/domain/sap/rdsap/cert_to_inputs.py +++ b/packages/domain/src/domain/sap/rdsap/cert_to_inputs.py @@ -52,7 +52,7 @@ from domain.ml.sap_efficiencies import ( seasonal_efficiency, water_heating_efficiency as _legacy_water_heating_efficiency, ) -from domain.sap.calculator import CalculatorInputs, WindowInput +from domain.sap.calculator import CalculatorInputs, WindowInput, _solar_gains_w from domain.sap.tables.table_12 import ( co2_factor_kg_per_kwh, primary_energy_factor, @@ -998,6 +998,18 @@ def cert_to_inputs( # SAP10.2 line (73)m — total internal gains W/month from §5 # orchestrator (composed above). internal_gains_monthly_w=internal_gains_monthly_w, + # SAP10.2 line (83)m — total solar gains W/month. Computed here via + # the legacy `_solar_gains_w` per-month leaf to preserve identical + # behaviour during the §6 wiring migration; the next slice swaps + # this for the §6 orchestrator `solar_gains_from_cert`. + solar_gains_monthly_w=tuple( + _solar_gains_w( + windows=_window_inputs(epc.sap_windows), + region=_region_index(epc.region_code), + month=m, + ) + for m in range(1, 13) + ), region=_region_index(epc.region_code), windows=_window_inputs(epc.sap_windows), control_type=_control_type(main), diff --git a/packages/domain/src/domain/sap/tests/test_bre_worked_examples.py b/packages/domain/src/domain/sap/tests/test_bre_worked_examples.py index a02a8bf3..6da003b4 100644 --- a/packages/domain/src/domain/sap/tests/test_bre_worked_examples.py +++ b/packages/domain/src/domain/sap/tests/test_bre_worked_examples.py @@ -26,6 +26,7 @@ import pytest from domain.sap.calculator import ( CalculatorInputs, WindowInput, + _solar_gains_w, calculate_sap_from_inputs, ) from domain.sap.worksheet.dimensions import Dimensions @@ -85,6 +86,9 @@ def _baseline_dwelling() -> CalculatorInputs: heat_transmission=ht, monthly_infiltration_ach=(0.7,) * 12, internal_gains_monthly_w=(450.0,) * 12, + solar_gains_monthly_w=tuple( + _solar_gains_w(windows=windows, region=0, month=m) for m in range(1, 13) + ), region=0, windows=windows, control_type=2, diff --git a/packages/domain/src/domain/sap/tests/test_calculator.py b/packages/domain/src/domain/sap/tests/test_calculator.py index ee47c7af..5ee88aae 100644 --- a/packages/domain/src/domain/sap/tests/test_calculator.py +++ b/packages/domain/src/domain/sap/tests/test_calculator.py @@ -24,6 +24,7 @@ from domain.sap.calculator import ( CalculatorInputs, SapResult, WindowInput, + _solar_gains_w, calculate_sap_from_inputs, ) from domain.sap.worksheet.dimensions import Dimensions @@ -84,6 +85,9 @@ def _baseline_inputs() -> CalculatorInputs: # per-month variation lives in §5 orchestrator output; tracer # tests don't need the modulation to verify the SAP loop. internal_gains_monthly_w=(450.0,) * 12, + solar_gains_monthly_w=tuple( + _solar_gains_w(windows=windows, region=0, month=m) for m in range(1, 13) + ), region=0, windows=windows, control_type=2, @@ -102,6 +106,25 @@ def _baseline_inputs() -> CalculatorInputs: ) +def test_calculator_consumes_solar_gains_monthly_w_field_for_per_month_solar() -> None: + # Arrange — replace the baseline inputs' solar with an explicit known + # 12-tuple. The §6 orchestrator produces this upstream; the calculator + # must just look it up, not recompute from the legacy `windows` field. + # 100 W constant solar everywhere — distinct enough that any leftover + # _solar_gains_w(windows, ...) recomputation would land elsewhere. + explicit_solar = (100.0,) * 12 + inputs = replace( + _baseline_inputs(), solar_gains_monthly_w=explicit_solar, + ) + + # Act + result = calculate_sap_from_inputs(inputs) + + # Assert + for monthly in result.monthly: + assert monthly.solar_gains_w == 100.0 + + def test_calculator_returns_twelve_month_breakdown_and_plausible_sap_score() -> None: # Arrange — baseline 100 m² gas-boiler dwelling in UK-average climate. inputs = _baseline_inputs()