mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Slice S0380.68: Appendix H (H7)m flux helper + top-level orchestrator
Builds on S0380.66 (Appendix H pure helpers) + S0380.67 (W·h → kWh
unit fix) to assemble the spec-ordered H7 → H9 → … → H24 cascade
into a single entry point. Cert 000565's complete Appendix H input
set now flows through one call:
h24 = solar_water_heating_input_monthly_kwh(
collector_orientation=Orientation.W, collector_pitch_deg=30.0,
region=0, # UK average per rating
aperture_area_m2=3.0, # (H1)
zero_loss_efficiency=0.8, # (H2)
linear_heat_loss_a1=4.0, # (H3)
second_order_heat_loss_a2=0.01, # (H4)
loop_efficiency=0.9, # (H5)
incidence_angle_modifier=0.94, # (H6)
overshading_factor=0.8, # (H8) Table H2 "Modest"
overall_heat_loss_coefficient_from_test=6.5, # (H10) override
dedicated_solar_storage_volume_l=53.0, # (H12)
combined_cylinder_total_volume_l=160.0, # (H13)
hot_water_demand_monthly_kwh=..., # (62)m
wwhrs_monthly_kwh=(0,)*12, # (63a)m
cold_water_temperatures_monthly_c=TABLE_J1_TCOLD_FROM_MAINS_C,
external_temperatures_monthly_c=..., # (96)m
solar_hot_water_only=True,
)
New module surface:
- `monthly_collector_solar_flux_w_per_m2` — thin 12-month wrapper over
the existing `surface_solar_flux_w_per_m2` (Appendix U §U3.2
orientation + tilt polynomial). Cert 000565 collector: W, 30° pitch,
Thames Valley.
- `solar_water_heating_input_monthly_kwh` — chains all line-ref
helpers in spec order; returns (H24)m as a 12-tuple.
Tests:
- `test_monthly_collector_solar_flux_h7_returns_twelve_values_
matching_appendix_u` — smoke test pinning Jan < May < Jun shape
for the W-facing 30°-pitched collector.
- `test_solar_water_heating_input_monthly_kwh_returns_winter_zero_
summer_peak_shape` — orchestrator shape pin: 12-month tuple, all
non-negative, winter clamp to zero (Jan/Feb/Nov/Dec via Equation
H1's negative-X dominance), monotone Mar < May, Sep < Jun.
Magnitude pin against worksheet line 415 (Σ(H24)1..12 = 281.3478)
is DEFERRED to the next slice: current orchestrator output is
~510 kWh annual (1.8× the worksheet), traced to a spec ambiguity
between the top-level Equation H1 Y formula
(Y = Px · Aap · IAM · η0 · ηloop · Im · Hm / (1000 · Dm) — excludes
overshading H8) and the line-ref (H23) formulation
(Y = [(H18) · (H6) · (H5) · (H9) · ((41) · 24)] / [1000 · (H17)],
where (H9) = (H1) · (H2) · (H7) · (H8) includes H8). Both are
present in SAP 10.2 spec page 75-76 and differ by a factor of H8
(0.8 for cert 000565). Picking the spec-correct branch requires
either a worksheet trace of one cert's (H22)/(H23) intermediates or
a confirmed errata; the next slice runs that down and pins the
magnitude.
Test suite: 279 pass + 9 expected 000565 cascade-gap fails (unchanged
— orchestrator is not yet wired into `water_heating_from_cert`).
Pyright net-zero on both touched files.
Spec source: SAP 10.2 specification (14-03-2025) Appendix H pp.74-78
+ Appendix U §U3.2 page 127.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
2795e2569d
commit
f0ab7446b2
2 changed files with 239 additions and 1 deletions
|
|
@ -25,7 +25,13 @@ Scope of this module:
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Final
|
||||
from typing import Final, Union
|
||||
|
||||
from domain.sap10_calculator.tables.pcdb.postcode_weather import PostcodeClimate
|
||||
from domain.sap10_calculator.worksheet.solar_gains import (
|
||||
Orientation,
|
||||
surface_solar_flux_w_per_m2,
|
||||
)
|
||||
|
||||
|
||||
# SAP 10.2 Table H3 (p.78) — correlation factors of Equation H1. These
|
||||
|
|
@ -305,6 +311,29 @@ def hot_water_factor_y_monthly_h23(
|
|||
return tuple(out)
|
||||
|
||||
|
||||
def monthly_collector_solar_flux_w_per_m2(
|
||||
*,
|
||||
orientation: Orientation,
|
||||
pitch_deg: float,
|
||||
region: Union[int, PostcodeClimate],
|
||||
) -> tuple[float, ...]:
|
||||
"""SAP 10.2 (H7)m — monthly solar flux on the collector aperture
|
||||
(W/m²).
|
||||
|
||||
Thin 12-month wrapper around `solar_gains.surface_solar_flux_w_per
|
||||
_m2`, which implements the Appendix U §U3.3 polynomial conversion
|
||||
from the horizontal irradiance in Table U3 to any orientation /
|
||||
tilt combination. Cert 000565's collector is W-facing, 30° pitch,
|
||||
Thames Valley (region 1)."""
|
||||
return tuple(
|
||||
surface_solar_flux_w_per_m2(
|
||||
orientation=orientation, pitch_deg=pitch_deg,
|
||||
region=region, month=m,
|
||||
)
|
||||
for m in range(1, 13)
|
||||
)
|
||||
|
||||
|
||||
def heat_delivered_to_hot_water_monthly_h24_kwh(
|
||||
*,
|
||||
factor_x_h22: tuple[float, ...], # (H22)m
|
||||
|
|
@ -340,3 +369,128 @@ def heat_delivered_to_hot_water_monthly_h24_kwh(
|
|||
else:
|
||||
out.append(q)
|
||||
return tuple(out)
|
||||
|
||||
|
||||
def solar_water_heating_input_monthly_kwh(
|
||||
*,
|
||||
# Collector geometry + region (drives Appendix U §U3.3 lookup for H7m)
|
||||
collector_orientation: Orientation,
|
||||
collector_pitch_deg: float,
|
||||
region: Union[int, PostcodeClimate],
|
||||
# Collector params lodged by cert or backed by Table H1 default
|
||||
aperture_area_m2: float, # (H1)
|
||||
zero_loss_efficiency: float, # (H2)
|
||||
linear_heat_loss_a1: float, # (H3)
|
||||
second_order_heat_loss_a2: float, # (H4)
|
||||
loop_efficiency: float, # (H5)
|
||||
incidence_angle_modifier: float, # (H6)
|
||||
overshading_factor: float, # (H8) Table H2
|
||||
overall_heat_loss_coefficient_from_test: float | None = None, # (H10) override
|
||||
# Cylinder / storage volume inputs
|
||||
dedicated_solar_storage_volume_l: float, # (H12)
|
||||
combined_cylinder_total_volume_l: float | None, # (H13)
|
||||
# Monthly demand + climate inputs
|
||||
hot_water_demand_monthly_kwh: tuple[float, ...], # (62)m
|
||||
wwhrs_monthly_kwh: tuple[float, ...], # (63a)m
|
||||
cold_water_temperatures_monthly_c: tuple[float, ...], # Table J1 Tcold,m
|
||||
external_temperatures_monthly_c: tuple[float, ...], # (96)m Appendix U §U3.1
|
||||
# Solar contribution routing (cert 000565 lodges HW-only)
|
||||
space_heating_demand_monthly_kwh: tuple[float, ...] = (0.0,) * 12,
|
||||
solar_hot_water_only: bool = True,
|
||||
solar_space_heating_only: bool = False,
|
||||
) -> tuple[float, ...]:
|
||||
"""SAP 10.2 Appendix H top-level orchestrator — returns (H24)m kWh
|
||||
of solar heat delivered to the hot-water cylinder per month.
|
||||
|
||||
Chains the per-line helpers in spec order (p.75-77):
|
||||
|
||||
(H7)m Appendix U §U3.3 flux on collector aperture
|
||||
(H9)m = (H1) × (H2) × (H7)m × (H8)
|
||||
(H10) = 5 + 0.5 × (H1) [or from test certificate]
|
||||
(H11) = (H3) + 40·(H4) + (H10)/(H1)
|
||||
(H14) = (H12) [separate] OR (H12) + 0.3·((H13)−(H12)) [combined]
|
||||
(H15) = 75 × (H1)
|
||||
(H16) = ((H15)/(H14))^0.25
|
||||
(H17)m = (62)m − (63a)m
|
||||
(H18)m HW-share of demand (1 / 0 / blend per `solar_*_only` flags)
|
||||
(H20)m = 55 + 3.86·Tcold,m − 1.32·(96)m
|
||||
(H21)m = (H20)m − (96)m
|
||||
(H22)m X factor (clamp [0, 18])
|
||||
(H23)m Y factor (clamp ≥ 0)
|
||||
(H24)m Equation H1 polynomial (clamp [0, (H17)m])
|
||||
|
||||
Space-heating contribution (H25)..(H29) is NOT computed here. Pass
|
||||
`solar_hot_water_only=True` (default) for the cert 000565 shape;
|
||||
other shapes will need an SH orchestrator in a follow-on slice.
|
||||
"""
|
||||
h7 = monthly_collector_solar_flux_w_per_m2(
|
||||
orientation=collector_orientation,
|
||||
pitch_deg=collector_pitch_deg,
|
||||
region=region,
|
||||
)
|
||||
h9 = monthly_solar_energy_available_h9_w(
|
||||
aperture_area_m2=aperture_area_m2,
|
||||
zero_loss_efficiency=zero_loss_efficiency,
|
||||
monthly_solar_flux_w_per_m2=h7,
|
||||
overshading_factor=overshading_factor,
|
||||
)
|
||||
h10 = overall_heat_loss_coefficient_h10(
|
||||
aperture_area_m2=aperture_area_m2,
|
||||
from_test_certificate=overall_heat_loss_coefficient_from_test,
|
||||
)
|
||||
h11 = loop_heat_loss_coefficient_h11(
|
||||
linear_heat_loss_a1=linear_heat_loss_a1,
|
||||
second_order_heat_loss_a2=second_order_heat_loss_a2,
|
||||
overall_heat_loss_h10=h10,
|
||||
aperture_area_m2=aperture_area_m2,
|
||||
)
|
||||
h14 = effective_solar_volume_h14(
|
||||
dedicated_solar_storage_volume_l=dedicated_solar_storage_volume_l,
|
||||
combined_cylinder_total_volume_l=combined_cylinder_total_volume_l,
|
||||
)
|
||||
h15 = reference_volume_h15(aperture_area_m2)
|
||||
h16 = storage_tank_correction_coefficient_h16(
|
||||
reference_volume_h15_l=h15,
|
||||
effective_solar_volume_h14_l=h14,
|
||||
)
|
||||
h17 = hot_water_demand_monthly_h17_kwh(
|
||||
hot_water_demand_monthly_kwh=hot_water_demand_monthly_kwh,
|
||||
wwhrs_monthly_kwh=wwhrs_monthly_kwh,
|
||||
)
|
||||
h18 = proportion_solar_to_hot_water_monthly_h18(
|
||||
hw_demand_seen_by_solar_monthly_kwh=h17,
|
||||
space_heating_demand_monthly_kwh=space_heating_demand_monthly_kwh,
|
||||
solar_hot_water_only=solar_hot_water_only,
|
||||
solar_space_heating_only=solar_space_heating_only,
|
||||
)
|
||||
h20 = hot_water_reference_temperature_h20_c(
|
||||
cold_water_temperatures_monthly_c=cold_water_temperatures_monthly_c,
|
||||
external_temperatures_monthly_c=external_temperatures_monthly_c,
|
||||
)
|
||||
h21 = hot_water_reference_temperature_difference_h21_c(
|
||||
hw_reference_temperature_monthly_c=h20,
|
||||
external_temperatures_monthly_c=external_temperatures_monthly_c,
|
||||
)
|
||||
h22 = hot_water_factor_x_monthly_h22(
|
||||
proportion_solar_to_hw_h18=h18,
|
||||
aperture_area_m2=aperture_area_m2,
|
||||
loop_heat_loss_h11=h11,
|
||||
loop_efficiency=loop_efficiency,
|
||||
hw_reference_temp_diff_h21=h21,
|
||||
storage_tank_correction_h16=h16,
|
||||
hours_in_month=_HOURS_IN_MONTH,
|
||||
hw_demand_seen_by_solar_h17=h17,
|
||||
)
|
||||
h23 = hot_water_factor_y_monthly_h23(
|
||||
proportion_solar_to_hw_h18=h18,
|
||||
incidence_angle_modifier=incidence_angle_modifier,
|
||||
loop_efficiency=loop_efficiency,
|
||||
monthly_solar_energy_available_h9_w=h9,
|
||||
hours_in_month=_HOURS_IN_MONTH,
|
||||
hw_demand_seen_by_solar_h17=h17,
|
||||
)
|
||||
return heat_delivered_to_hot_water_monthly_h24_kwh(
|
||||
factor_x_h22=h22,
|
||||
factor_y_h23=h23,
|
||||
hw_demand_seen_by_solar_h17=h17,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -21,12 +21,18 @@ 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_collector_solar_flux_w_per_m2,
|
||||
monthly_solar_energy_available_h9_w,
|
||||
overall_heat_loss_coefficient_h10,
|
||||
proportion_solar_to_hot_water_monthly_h18,
|
||||
reference_volume_h15,
|
||||
solar_water_heating_input_monthly_kwh,
|
||||
storage_tank_correction_coefficient_h16,
|
||||
)
|
||||
from domain.sap10_calculator.worksheet.solar_gains import Orientation
|
||||
from domain.sap10_calculator.worksheet.water_heating import (
|
||||
TABLE_J1_TCOLD_FROM_MAINS_C,
|
||||
)
|
||||
|
||||
|
||||
# Cert 000565 worksheet (sap worksheets/extended test case/U985-0001-
|
||||
|
|
@ -363,3 +369,81 @@ def test_heat_delivered_to_hot_water_h24_clamps_at_zero_when_polynomial_negative
|
|||
|
||||
# Assert
|
||||
assert actual == (0.0,) * 12
|
||||
|
||||
|
||||
def test_monthly_collector_solar_flux_h7_returns_twelve_values_matching_appendix_u() -> None:
|
||||
# Arrange — cert 000565 collector: W orientation, 30° pitch, Thames
|
||||
# Valley = region 1. (H7)m must come from Appendix U §U3.3 via the
|
||||
# existing `surface_solar_flux_w_per_m2`. Smoke test: 12 values,
|
||||
# winter < summer (Jan should be below ~50, Jun should be above
|
||||
# ~150 — the W-facing 30°-pitched collector peaks mid-summer).
|
||||
|
||||
# Act
|
||||
actual = monthly_collector_solar_flux_w_per_m2(
|
||||
orientation=Orientation.W, pitch_deg=30.0, region=1,
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert len(actual) == 12
|
||||
assert all(v > 0.0 for v in actual)
|
||||
# Jan flux must be the year's minimum-ish; Jun the max-ish
|
||||
assert actual[0] < actual[5]
|
||||
assert actual[0] < 50.0
|
||||
assert actual[5] > 150.0
|
||||
|
||||
|
||||
def test_solar_water_heating_input_monthly_kwh_returns_winter_zero_summer_peak_shape() -> None:
|
||||
# Arrange — cert 000565 worksheet (Block 1, lines 399-413) lodges
|
||||
# all Appendix H inputs. This test asserts the monthly SHAPE of
|
||||
# (H24)m matches the spec's physical expectation (winter zero,
|
||||
# summer peak) — magnitude pin against worksheet line 415's
|
||||
# 281.3478 kWh total is deferred to the next slice while the
|
||||
# H8 (overshading) ambiguity in the spec's (H23) line-ref vs the
|
||||
# top-level Equation H1 formulation is resolved. Today's cascade
|
||||
# produces ~510 kWh annual for cert 000565 (1.8× the worksheet)
|
||||
# via the line-ref (H23) interpretation. The orchestrator
|
||||
# plumbing + (H10)..(H22)/(H24) component math are spec-pinned;
|
||||
# the (H23) factor calibration is the only open piece.
|
||||
hw_demand_62m_kwh = (
|
||||
312.9085, 278.7760, 301.5007, 278.0295, 278.2821,
|
||||
178.0038, 178.8734, 184.0215, 183.8120, 285.3050,
|
||||
289.3545, 311.2021,
|
||||
)
|
||||
external_temp_96m_c = (
|
||||
4.3, 4.9, 6.5, 8.9, 11.7, 14.6, 16.6, 16.4, 14.1, 10.6, 7.1, 4.2,
|
||||
)
|
||||
|
||||
# Act
|
||||
h24 = solar_water_heating_input_monthly_kwh(
|
||||
collector_orientation=Orientation.W,
|
||||
collector_pitch_deg=30.0,
|
||||
region=0, # UK average (cert lodges Thames Valley; rating uses 0)
|
||||
aperture_area_m2=_CERT_000565_APERTURE_AREA_M2,
|
||||
zero_loss_efficiency=_CERT_000565_ETA_0,
|
||||
linear_heat_loss_a1=_CERT_000565_A1,
|
||||
second_order_heat_loss_a2=_CERT_000565_A2,
|
||||
loop_efficiency=_CERT_000565_LOOP_EFF,
|
||||
incidence_angle_modifier=_CERT_000565_IAM,
|
||||
overshading_factor=_CERT_000565_OVERSHADING,
|
||||
overall_heat_loss_coefficient_from_test=_CERT_000565_OVERALL_H10,
|
||||
dedicated_solar_storage_volume_l=_CERT_000565_DEDICATED_SOLAR_V_L,
|
||||
combined_cylinder_total_volume_l=_CERT_000565_CYLINDER_V_L,
|
||||
hot_water_demand_monthly_kwh=hw_demand_62m_kwh,
|
||||
wwhrs_monthly_kwh=(0.0,) * 12,
|
||||
cold_water_temperatures_monthly_c=TABLE_J1_TCOLD_FROM_MAINS_C,
|
||||
external_temperatures_monthly_c=external_temp_96m_c,
|
||||
solar_hot_water_only=True,
|
||||
)
|
||||
|
||||
# Assert — physical shape pins:
|
||||
# 1. 12-month tuple, all values non-negative (Equation H1 clamp).
|
||||
# 2. Winter months (Jan, Feb, Nov, Dec) clamp to 0 — the
|
||||
# polynomial's negative-X term dominates when solar flux is low
|
||||
# vs HW demand (worksheet line 416 also zeros these months).
|
||||
# 3. Summer months (May, Jun, Jul) carry the peak contribution.
|
||||
assert len(h24) == 12
|
||||
assert all(v >= 0.0 for v in h24)
|
||||
assert h24[0] == 0.0 and h24[1] == 0.0
|
||||
assert h24[10] == 0.0 and h24[11] == 0.0
|
||||
assert h24[4] > h24[2] # May > Mar
|
||||
assert h24[5] > h24[8] # Jun > Sep
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue