From 2795e2569d3cbc7f64ecff7a2ce3ae2b26d795a0 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Fri, 29 May 2026 09:46:12 +0000 Subject: [PATCH] =?UTF-8?q?Slice=20S0380.67:=20Appendix=20H=20(H9)=20helpe?= =?UTF-8?q?r=20+=20fix=20(H23)=20W=C2=B7h=20=E2=86=92=20kWh=20units?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit S0380.66 landed (H23) with an incorrect unit treatment: the spec formula on SAP 10.2 p.76 is Y_HW = [(H18)m × (H6) × (H5) × (H9)m × (41)m × 24] ÷ [1000 × (H17)m] and Appendix U (per `domain/sap10_calculator/climate/appendix_u. horizontal_solar_irradiance_w_per_m2`) returns (H7)m as a monthly- average flux in W/m². That makes (H9)m = (H1) × (H2) × (H7)m × (H8) an instantaneous power in W — the `× hours × 24 / 1000` factor in the (H23) formula is what time-integrates W·h → kWh so the Y_HW ratio lands dimensionless against (H17)m (kWh/month). S0380.66's (H23) elided the time integration by absorbing it into the input parameter (a kWh/m²/month name) — that broke unit consistency with the downstream Appendix U integration this module will consume in the next slice. Changes: - New `monthly_solar_energy_available_h9_w` — pure (H9)m calculator taking aperture, η₀, (H7)m flux tuple, and overshading. Returns W. - `hot_water_factor_y_monthly_h23`: parameter renamed `monthly_solar_energy_available_h9_w` (was `..._kwh_per_m2`); new `hours_in_month` parameter; formula now includes the spec's `× hours / 1000` time integration explicitly. Tests: - `test_monthly_solar_energy_available_h9_applies_spec_formula` — cert 000565 H1/H2/H8 with flat 100 W/m² flux → 192 W (the spec multiplicand 3 × 0.8 × 100 × 0.8). - `test_hot_water_factor_y_h23_applies_w_to_kwh_time_integration` — unit-consistency pin: H9=1000 W, hours=744, H17=744 kWh → Y=1.0. - `test_hot_water_factor_y_h23_clamps_lower_bound_at_zero` updated to the new parameter name and supplies `hours_in_month`. Test suite: 277 pass + 9 expected 000565 cascade-gap fails. Pyright net-zero on both touched files. Spec source: SAP 10.2 specification (14-03-2025) Appendix H p.76. Co-Authored-By: Claude Opus 4.7 --- .../worksheet/appendix_h_solar.py | 45 ++++++++++++++----- .../worksheet/tests/test_appendix_h_solar.py | 41 ++++++++++++++++- 2 files changed, 73 insertions(+), 13 deletions(-) diff --git a/domain/sap10_calculator/worksheet/appendix_h_solar.py b/domain/sap10_calculator/worksheet/appendix_h_solar.py index dee7fdfe..04b00382 100644 --- a/domain/sap10_calculator/worksheet/appendix_h_solar.py +++ b/domain/sap10_calculator/worksheet/appendix_h_solar.py @@ -247,13 +247,35 @@ def hot_water_factor_x_monthly_h22( return tuple(out) +def monthly_solar_energy_available_h9_w( + *, + aperture_area_m2: float, # (H1) + zero_loss_efficiency: float, # (H2) + monthly_solar_flux_w_per_m2: tuple[float, ...], # (H7)m from Appendix U §U3.3 + overshading_factor: float, # (H8) +) -> tuple[float, ...]: + """SAP 10.2 (H9)m — solar energy available on collector aperture (W). + + Spec p.76: "(H1) × (H2) × (H7)m × (H8)". The result is an + instantaneous (monthly-average) power in watts — the worksheet's + downstream (H22) and (H23) formulas multiply by `(41)m × 24` + (hours-in-month) and divide by 1000 to convert to kWh/month, so + keeping (H9)m in W here matches the spec's stepwise units. + """ + return tuple( + aperture_area_m2 * zero_loss_efficiency * flux * overshading_factor + for flux in monthly_solar_flux_w_per_m2 + ) + + def hot_water_factor_y_monthly_h23( *, - proportion_solar_to_hw_h18: tuple[float, ...], # (H18)m - incidence_angle_modifier: float, # (H6) - loop_efficiency: float, # (H5) - monthly_solar_energy_available_h9_kwh_per_m2: tuple[float, ...], # (H9)m as ENERGY in kWh/m²/month - hw_demand_seen_by_solar_h17: tuple[float, ...], # (H17)m + proportion_solar_to_hw_h18: tuple[float, ...], # (H18)m + incidence_angle_modifier: float, # (H6) + loop_efficiency: float, # (H5) + monthly_solar_energy_available_h9_w: tuple[float, ...], # (H9)m in W (per `monthly_solar_energy_available_h9_w`) + hours_in_month: tuple[int, ...], # (41)m × 24 + hw_demand_seen_by_solar_h17: tuple[float, ...], # (H17)m ) -> tuple[float, ...]: """SAP 10.2 (H23)m — HW factor Y. @@ -261,11 +283,9 @@ def hot_water_factor_y_monthly_h23( [1000 × (H17)m] Clamped to a lower bound of 0 per spec p.76 (`if Y < 0, enter zero`). - The `(H9)m × hours_in_month × 24 / 1000` shape in the spec captures - a flux-to-energy conversion; in this module (H9)m is supplied - directly as energy in kWh/m²/month (the upstream computation - `aperture × η₀ × overshading × monthly_radiation` already includes - the time integration), so the conversion factors collapse out. + (H9)m is in W (per `monthly_solar_energy_available_h9_w`); the + `× hours_in_month / 1000` factor here converts W·h → kWh so the + ratio Y_HW lands dimensionless against (H17)m (kWh/month). """ out: list[float] = [] for m in range(12): @@ -277,9 +297,10 @@ def hot_water_factor_y_monthly_h23( proportion_solar_to_hw_h18[m] * incidence_angle_modifier * loop_efficiency - * monthly_solar_energy_available_h9_kwh_per_m2[m] + * monthly_solar_energy_available_h9_w[m] + * hours_in_month[m] ) - y = numerator / h17 + y = numerator / (1000.0 * h17) out.append(max(0.0, y)) return tuple(out) diff --git a/domain/sap10_calculator/worksheet/tests/test_appendix_h_solar.py b/domain/sap10_calculator/worksheet/tests/test_appendix_h_solar.py index 257c1f42..2c821794 100644 --- a/domain/sap10_calculator/worksheet/tests/test_appendix_h_solar.py +++ b/domain/sap10_calculator/worksheet/tests/test_appendix_h_solar.py @@ -21,6 +21,7 @@ from domain.sap10_calculator.worksheet.appendix_h_solar import ( hot_water_reference_temperature_difference_h21_c, hot_water_reference_temperature_h20_c, loop_heat_loss_coefficient_h11, + monthly_solar_energy_available_h9_w, overall_heat_loss_coefficient_h10, proportion_solar_to_hot_water_monthly_h18, reference_volume_h15, @@ -257,6 +258,23 @@ def test_hot_water_factor_x_h22_clamps_upper_bound_at_18() -> None: assert actual == (18.0,) * 12 +def test_monthly_solar_energy_available_h9_applies_spec_formula() -> None: + # Arrange — SAP 10.2 (H9)m spec p.76: (H1) × (H2) × (H7)m × (H8) + # Cert 000565 worksheet (H1)=3.0, (H2)=0.8, (H8)=0.8. With a flat + # 100 W/m² flux input, expected = 3 × 0.8 × 100 × 0.8 = 192 W. + + # Act + actual = monthly_solar_energy_available_h9_w( + aperture_area_m2=_CERT_000565_APERTURE_AREA_M2, + zero_loss_efficiency=_CERT_000565_ETA_0, + monthly_solar_flux_w_per_m2=(100.0,) * 12, + overshading_factor=_CERT_000565_OVERSHADING, + ) + + # Assert — H1=3, H2=0.8, H7=100, H8=0.8 → 192 W/month + assert all(abs(h9 - 192.0) < 1e-9 for h9 in actual) + + def test_hot_water_factor_y_h23_clamps_lower_bound_at_zero() -> None: # Arrange — spec p.76 "if Y_HW < 0, enter zero". Negative solar # energy available (theoretical edge case) must not flow through. @@ -266,7 +284,8 @@ def test_hot_water_factor_y_h23_clamps_lower_bound_at_zero() -> None: proportion_solar_to_hw_h18=(1.0,) * 12, incidence_angle_modifier=0.94, loop_efficiency=0.9, - monthly_solar_energy_available_h9_kwh_per_m2=(-5.0,) * 12, + monthly_solar_energy_available_h9_w=(-5.0,) * 12, + hours_in_month=(744,) * 12, hw_demand_seen_by_solar_h17=(100.0,) * 12, ) @@ -274,6 +293,26 @@ def test_hot_water_factor_y_h23_clamps_lower_bound_at_zero() -> None: assert actual == (0.0,) * 12 +def test_hot_water_factor_y_h23_applies_w_to_kwh_time_integration() -> None: + # Arrange — spec p.76: Y_HW = [(H18) × (H6) × (H5) × (H9) × hours] + # ÷ [1000 × (H17)]. With H18=1, H6=1, H5=1, H9=1000 W, + # hours=744 (Jan), H17=744 kWh → numerator = 1000 × 744 = 744000 W·h + # ÷ 1000 = 744 kWh, ÷ H17 = 744/744 = 1.0 ✓ dimensionless. + + # Act + actual = hot_water_factor_y_monthly_h23( + proportion_solar_to_hw_h18=(1.0,) * 12, + incidence_angle_modifier=1.0, + loop_efficiency=1.0, + monthly_solar_energy_available_h9_w=(1000.0,) * 12, + hours_in_month=(744,) * 12, + hw_demand_seen_by_solar_h17=(744.0,) * 12, + ) + + # Assert + assert all(abs(y - 1.0) < 1e-9 for y in actual) + + def test_heat_delivered_to_hot_water_h24_applies_equation_h1_polynomial() -> None: # Arrange — spec Equation H1 with Table H3 (p.78) factors: # Q = (Ca·Y + Cb·X + Cc·Y² + Cd·X² + Ce·Y³ + Cf·X³) × demand