mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
§7 slice 4: calculator + cert_to_inputs wired to §7 orchestrator (atomic)
Adds CalculatorInputs.mean_internal_temp_monthly_c (93)m and CalculatorInputs.utilisation_factor_monthly (94)m. _solve_month indexes directly into both — the 2-pass η fixed-point loop is gone (SAP10.2 §7 Table 9c is sequential, not iterative). cert_to_inputs computes per-month HTC = transmission HLC + 0.33·V·(25)m, sums (73)m + (83)m for total gains, and calls mean_internal_temperature_monthly to populate both new fields. Single codepath for all callers. Synthetic test fixtures (_baseline_inputs, _baseline_dwelling) compute their MIT + η via the §7 orchestrator too — preserves consistency with the cert path while keeping the BRE worked-example trace asserting the new spec-correct per-zone η values. Atomic with cert_to_inputs (originally planned as slice 4 + slice 5): introducing the calculator fields without populating them in cert_to_inputs would break every cert-driven test. e2e SAP-score tests (000490 within 1 point, 000474 within 7 points) still pass with the new sequential η path. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
ff5d8c70c1
commit
8ec9da4742
4 changed files with 146 additions and 48 deletions
|
|
@ -88,6 +88,13 @@ class CalculatorInputs:
|
|||
# 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, ...]
|
||||
# SAP10.2 (93)m — adjusted mean internal temperature °C per month, and
|
||||
# (94)m — utilisation factor (whole-dwelling Ti) per month. Both come
|
||||
# from §7 orchestrator `mean_internal_temperature_monthly` upstream.
|
||||
# The calculator stops iterating η in _solve_month — Table 9c is a
|
||||
# sequential chain (steps 1-9), not a fixed-point loop.
|
||||
mean_internal_temp_monthly_c: tuple[float, ...]
|
||||
utilisation_factor_monthly: tuple[float, ...]
|
||||
region: int
|
||||
control_type: int
|
||||
responsiveness: float
|
||||
|
|
@ -182,30 +189,14 @@ def _solve_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,
|
||||
# recompute η from the resulting loss rate, then once more.
|
||||
eta = 1.0
|
||||
t_int = 0.0
|
||||
loss_rate_w = 0.0
|
||||
for _ in range(_ETA_ITERATIONS):
|
||||
t_int = mean_internal_temperature_c(
|
||||
external_temp_c=t_ext,
|
||||
heat_transfer_coefficient_w_per_k=hlc_w_per_k,
|
||||
total_gains_w=g_total,
|
||||
utilisation_factor=eta,
|
||||
time_constant_h=time_constant_h,
|
||||
heat_loss_parameter=heat_loss_parameter,
|
||||
living_area_fraction=inputs.living_area_fraction,
|
||||
control_type=inputs.control_type,
|
||||
responsiveness=inputs.responsiveness,
|
||||
control_temperature_adjustment_c=inputs.control_temperature_adjustment_c,
|
||||
)
|
||||
loss_rate_w = max(0.0, hlc_w_per_k * (t_int - t_ext))
|
||||
eta = utilisation_factor(
|
||||
total_gains_w=g_total,
|
||||
heat_loss_rate_w=loss_rate_w,
|
||||
time_constant_h=time_constant_h,
|
||||
)
|
||||
# SAP 10.2 §7 Table 9c is a sequential chain (steps 1-9); the §7
|
||||
# orchestrator computes (93)m and (94)m upstream and the calculator
|
||||
# consumes them by index. No fixed-point iteration here.
|
||||
_ = time_constant_h # τ now lives inside the §7 orchestrator
|
||||
_ = heat_loss_parameter
|
||||
t_int = inputs.mean_internal_temp_monthly_c[month - 1]
|
||||
eta = inputs.utilisation_factor_monthly[month - 1]
|
||||
loss_rate_w = max(0.0, hlc_w_per_k * (t_int - t_ext))
|
||||
|
||||
q_heat = monthly_heat_requirement_kwh(
|
||||
heat_transfer_coefficient_w_per_k=hlc_w_per_k,
|
||||
|
|
|
|||
|
|
@ -67,6 +67,10 @@ from domain.sap.worksheet.heat_transmission import (
|
|||
DwellingExposure,
|
||||
heat_transmission_from_cert,
|
||||
)
|
||||
from domain.sap.climate.appendix_u import external_temperature_c
|
||||
from domain.sap.worksheet.mean_internal_temperature import (
|
||||
mean_internal_temperature_monthly,
|
||||
)
|
||||
from domain.sap.worksheet.solar_gains import solar_gains_from_cert
|
||||
from domain.sap.worksheet.ventilation import (
|
||||
MechanicalVentilationKind,
|
||||
|
|
@ -886,6 +890,40 @@ def cert_to_inputs(
|
|||
internal_gains_result.total_internal_gains_monthly_w
|
||||
)
|
||||
|
||||
solar_gains_monthly_w = solar_gains_from_cert(
|
||||
epc=epc,
|
||||
region=_region_index(epc.region_code),
|
||||
overshading=_INTERNAL_GAINS_DEFAULT_OVERSHADING,
|
||||
).total_solar_gains_monthly_w
|
||||
|
||||
# SAP10.2 §7 — compose (93)m + (94)m via the orchestrator. Per-month HTC
|
||||
# = transmission HLC + 0.33·V·(25)m. Table 4e control adjustment is 0
|
||||
# for the Elmhurst corpus (cert-side mapping is a future slice).
|
||||
control_type_value = _control_type(main)
|
||||
responsiveness_value = _responsiveness(main)
|
||||
living_area_fraction_value = _living_area_fraction(epc.habitable_rooms_count)
|
||||
monthly_total_gains_w = tuple(
|
||||
internal_gains_monthly_w[m] + solar_gains_monthly_w[m] for m in range(12)
|
||||
)
|
||||
monthly_htc_w_per_k = tuple(
|
||||
ht.total_w_per_k + 0.33 * dim.volume_m3 * ventilation.effective_monthly_ach[m]
|
||||
for m in range(12)
|
||||
)
|
||||
mit_result = mean_internal_temperature_monthly(
|
||||
monthly_external_temp_c=tuple(
|
||||
external_temperature_c(_region_index(epc.region_code), m)
|
||||
for m in range(1, 13)
|
||||
),
|
||||
monthly_total_gains_w=monthly_total_gains_w,
|
||||
monthly_heat_transfer_coefficient_w_per_k=monthly_htc_w_per_k,
|
||||
thermal_mass_parameter_kj_per_m2_k=_DEFAULT_THERMAL_MASS_PARAMETER_KJ_PER_M2_K,
|
||||
total_floor_area_m2=dim.total_floor_area_m2,
|
||||
control_type=control_type_value,
|
||||
responsiveness=responsiveness_value,
|
||||
living_area_fraction=living_area_fraction_value,
|
||||
control_temperature_adjustment_c=0.0,
|
||||
)
|
||||
|
||||
return CalculatorInputs(
|
||||
dimensions=dim,
|
||||
heat_transmission=ht,
|
||||
|
|
@ -900,15 +938,15 @@ def cert_to_inputs(
|
|||
# (Elmhurst data shows them all as `window_location = External wall`);
|
||||
# both pass-throughs are empty. Per-fixture §6 conformance is
|
||||
# exercised separately in `test_solar_gains.py`.
|
||||
solar_gains_monthly_w=solar_gains_from_cert(
|
||||
epc=epc,
|
||||
region=_region_index(epc.region_code),
|
||||
overshading=_INTERNAL_GAINS_DEFAULT_OVERSHADING,
|
||||
).total_solar_gains_monthly_w,
|
||||
solar_gains_monthly_w=solar_gains_monthly_w,
|
||||
# SAP10.2 (93)m + (94)m — adjusted MIT and whole-dwelling η. From
|
||||
# the §7 orchestrator above (Table 9c steps 1-9 sequential, per-zone η).
|
||||
mean_internal_temp_monthly_c=mit_result.adjusted_mean_internal_temp_monthly,
|
||||
utilisation_factor_monthly=mit_result.utilisation_factor_whole_monthly,
|
||||
region=_region_index(epc.region_code),
|
||||
control_type=_control_type(main),
|
||||
responsiveness=_responsiveness(main),
|
||||
living_area_fraction=_living_area_fraction(epc.habitable_rooms_count),
|
||||
control_type=control_type_value,
|
||||
responsiveness=responsiveness_value,
|
||||
living_area_fraction=living_area_fraction_value,
|
||||
control_temperature_adjustment_c=0.0,
|
||||
thermal_mass_parameter_kj_per_m2_k=_DEFAULT_THERMAL_MASS_PARAMETER_KJ_PER_M2_K,
|
||||
main_heating_efficiency=eff,
|
||||
|
|
|
|||
|
|
@ -27,8 +27,12 @@ from domain.sap.calculator import (
|
|||
CalculatorInputs,
|
||||
calculate_sap_from_inputs,
|
||||
)
|
||||
from domain.sap.climate.appendix_u import external_temperature_c
|
||||
from domain.sap.worksheet.dimensions import Dimensions
|
||||
from domain.sap.worksheet.heat_transmission import HeatTransmission
|
||||
from domain.sap.worksheet.mean_internal_temperature import (
|
||||
mean_internal_temperature_monthly,
|
||||
)
|
||||
|
||||
|
||||
def _baseline_dwelling() -> CalculatorInputs:
|
||||
|
|
@ -60,18 +64,36 @@ def _baseline_dwelling() -> CalculatorInputs:
|
|||
total_external_element_area_m2=200.0, # synthetic placeholder
|
||||
total_w_per_k=150.0,
|
||||
)
|
||||
internal_gains_monthly_w = (450.0,) * 12
|
||||
solar_gains_monthly_w = (
|
||||
70.1510, 118.4419, 161.4420, 202.5589, 231.7608, 232.9177,
|
||||
223.3279, 200.6543, 175.3023, 130.5274, 83.7805, 60.2212,
|
||||
)
|
||||
ext_temp_monthly_c = tuple(external_temperature_c(0, m) for m in range(1, 13))
|
||||
total_gains_monthly_w = tuple(
|
||||
internal_gains_monthly_w[m] + solar_gains_monthly_w[m] for m in range(12)
|
||||
)
|
||||
htc_monthly_w_per_k = tuple(
|
||||
ht.total_w_per_k + 0.33 * dim.volume_m3 * 0.7 for _ in range(12)
|
||||
)
|
||||
mit_result = mean_internal_temperature_monthly(
|
||||
monthly_external_temp_c=ext_temp_monthly_c,
|
||||
monthly_total_gains_w=total_gains_monthly_w,
|
||||
monthly_heat_transfer_coefficient_w_per_k=htc_monthly_w_per_k,
|
||||
thermal_mass_parameter_kj_per_m2_k=250.0,
|
||||
total_floor_area_m2=dim.total_floor_area_m2,
|
||||
control_type=2,
|
||||
responsiveness=1.0,
|
||||
living_area_fraction=0.30,
|
||||
)
|
||||
return CalculatorInputs(
|
||||
dimensions=dim,
|
||||
heat_transmission=ht,
|
||||
monthly_infiltration_ach=(0.7,) * 12,
|
||||
internal_gains_monthly_w=(450.0,) * 12,
|
||||
# Hand-computed solar (S + N 4 m² panes, g⊥=0.63 FF=0.7 Z=0.77,
|
||||
# UK-avg region 0, vertical) — captured at HEAD so the trace fixture
|
||||
# matches the pre-§6-wiring numerical baseline.
|
||||
solar_gains_monthly_w=(
|
||||
70.1510, 118.4419, 161.4420, 202.5589, 231.7608, 232.9177,
|
||||
223.3279, 200.6543, 175.3023, 130.5274, 83.7805, 60.2212,
|
||||
),
|
||||
internal_gains_monthly_w=internal_gains_monthly_w,
|
||||
solar_gains_monthly_w=solar_gains_monthly_w,
|
||||
mean_internal_temp_monthly_c=mit_result.adjusted_mean_internal_temp_monthly,
|
||||
utilisation_factor_monthly=mit_result.utilisation_factor_whole_monthly,
|
||||
region=0,
|
||||
control_type=2,
|
||||
responsiveness=1.0,
|
||||
|
|
|
|||
|
|
@ -25,8 +25,12 @@ from domain.sap.calculator import (
|
|||
SapResult,
|
||||
calculate_sap_from_inputs,
|
||||
)
|
||||
from domain.sap.climate.appendix_u import external_temperature_c
|
||||
from domain.sap.worksheet.dimensions import Dimensions
|
||||
from domain.sap.worksheet.heat_transmission import HeatTransmission
|
||||
from domain.sap.worksheet.mean_internal_temperature import (
|
||||
mean_internal_temperature_monthly,
|
||||
)
|
||||
|
||||
|
||||
def _baseline_inputs() -> CalculatorInputs:
|
||||
|
|
@ -56,6 +60,28 @@ def _baseline_inputs() -> CalculatorInputs:
|
|||
total_external_element_area_m2=200.0, # synthetic placeholder
|
||||
total_w_per_k=150.0,
|
||||
)
|
||||
internal_gains_monthly_w = (450.0,) * 12
|
||||
solar_gains_monthly_w = (
|
||||
70.1510, 118.4419, 161.4420, 202.5589, 231.7608, 232.9177,
|
||||
223.3279, 200.6543, 175.3023, 130.5274, 83.7805, 60.2212,
|
||||
)
|
||||
ext_temp_monthly_c = tuple(external_temperature_c(0, m) for m in range(1, 13))
|
||||
total_gains_monthly_w = tuple(
|
||||
internal_gains_monthly_w[m] + solar_gains_monthly_w[m] for m in range(12)
|
||||
)
|
||||
htc_monthly_w_per_k = tuple(
|
||||
ht.total_w_per_k + 0.33 * dim.volume_m3 * 0.7 for _ in range(12)
|
||||
)
|
||||
mit_result = mean_internal_temperature_monthly(
|
||||
monthly_external_temp_c=ext_temp_monthly_c,
|
||||
monthly_total_gains_w=total_gains_monthly_w,
|
||||
monthly_heat_transfer_coefficient_w_per_k=htc_monthly_w_per_k,
|
||||
thermal_mass_parameter_kj_per_m2_k=250.0,
|
||||
total_floor_area_m2=dim.total_floor_area_m2,
|
||||
control_type=2,
|
||||
responsiveness=1.0,
|
||||
living_area_fraction=0.30,
|
||||
)
|
||||
return CalculatorInputs(
|
||||
dimensions=dim,
|
||||
heat_transmission=ht,
|
||||
|
|
@ -63,15 +89,14 @@ def _baseline_inputs() -> CalculatorInputs:
|
|||
# Synthetic baseline internal gains: 450 W constant. Real
|
||||
# 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,
|
||||
internal_gains_monthly_w=internal_gains_monthly_w,
|
||||
# Hand-computed solar (S + N 4 m² panes, g⊥=0.63 FF=0.7 Z=0.77,
|
||||
# UK-avg region 0, vertical) — captured from §6 leaves at HEAD
|
||||
# so this synthetic baseline keeps the same heat-balance gains
|
||||
# as before the §6 wiring slice.
|
||||
solar_gains_monthly_w=(
|
||||
70.1510, 118.4419, 161.4420, 202.5589, 231.7608, 232.9177,
|
||||
223.3279, 200.6543, 175.3023, 130.5274, 83.7805, 60.2212,
|
||||
),
|
||||
# UK-avg region 0, vertical) — captured from §6 leaves at HEAD.
|
||||
solar_gains_monthly_w=solar_gains_monthly_w,
|
||||
# §7 (93)m + (94)m precomputed from the orchestrator above so the
|
||||
# baseline reflects spec-correct sequential per-zone η.
|
||||
mean_internal_temp_monthly_c=mit_result.adjusted_mean_internal_temp_monthly,
|
||||
utilisation_factor_monthly=mit_result.utilisation_factor_whole_monthly,
|
||||
region=0,
|
||||
control_type=2,
|
||||
responsiveness=1.0,
|
||||
|
|
@ -108,6 +133,28 @@ def test_calculator_consumes_solar_gains_monthly_w_field_for_per_month_solar() -
|
|||
assert monthly.solar_gains_w == 100.0
|
||||
|
||||
|
||||
def test_calculator_consumes_mean_internal_temp_and_utilisation_monthly_fields() -> None:
|
||||
# Arrange — replace baseline inputs' MIT + η with explicit known 12-tuples.
|
||||
# The §7 orchestrator produces these upstream; the calculator must just
|
||||
# look them up, not iterate or recompute. 18.0 °C MIT + 0.8 η constant
|
||||
# everywhere — distinct enough that any leftover iteration would drift.
|
||||
explicit_mit = (18.0,) * 12
|
||||
explicit_eta = (0.8,) * 12
|
||||
inputs = replace(
|
||||
_baseline_inputs(),
|
||||
mean_internal_temp_monthly_c=explicit_mit,
|
||||
utilisation_factor_monthly=explicit_eta,
|
||||
)
|
||||
|
||||
# Act
|
||||
result = calculate_sap_from_inputs(inputs)
|
||||
|
||||
# Assert
|
||||
for monthly in result.monthly:
|
||||
assert monthly.internal_temp_c == 18.0
|
||||
assert monthly.utilisation_factor == 0.8
|
||||
|
||||
|
||||
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()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue