mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
S0380.205: SAP 10.2 p.186 two-systems-different-parts MIT (weighted R + elsewhere blend)
When two main heating systems heat different parts of a dwelling, SAP 10.2 §7 (PDF p.186) adapts the mean-internal-temperature calculation: - Table 9b weighted responsiveness: R = (1−(203))·R_sys1 + (203)·R_sys2. - Rest-of-dwelling temperature (90)m = weighted average of T2 computed under EACH system's control schedule, weights (203)/[1−(91)] for sys2 and [1−(203)−(91)]/[1−(91)] for sys1 (or sys2's control alone when (203) ≥ 1−(91)). The cascade used Main 1's control + R=1.0 for the whole dwelling, over-stating MIT by +0.037 °C on simulated case 6 (Main 1 radiators/2106 type 2 living + Main 2 underfloor/2110 type 3 elsewhere, R 1.0/0.75). That inflated (97) heat loss by ~11 W → demand +61 kWh/yr. `mean_internal_temperature_monthly` gains `main_2_control_type`, `main_2_fraction`, `main_2_responsiveness`; cert_to_inputs derives them from the second main detail (gated on main_heating_fraction > 0, so single-main / DHW-only second mains pass the defaults → unchanged). Case 6: (87) living, (90) elsewhere, (98c) demand 11991.96 and per-system fuel (211)=7741.6458 / (213)=6995.3106 all match the worksheet to 1e-4. Re-pin: golden 0240 (same 2106/2110 archetype, API-only) — PE +2.1519 → +1.6893, CO2 +0.1051 → +0.0815 (both closer to zero; SAP 72 unchanged). Single-main certs unchanged (2360 pass + 0 fail). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
2b1afa7339
commit
e440e2df2e
5 changed files with 127 additions and 7 deletions
|
|
@ -6250,12 +6250,33 @@ def cert_to_inputs(
|
|||
# = 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, tariff=tariff_from_meter_type(epc.sap_energy_source.meter_type),
|
||||
)
|
||||
_mit_tariff = tariff_from_meter_type(epc.sap_energy_source.meter_type)
|
||||
responsiveness_value = _responsiveness(main, tariff=_mit_tariff)
|
||||
living_area_fraction_value = _living_area_fraction(
|
||||
epc.habitable_rooms_count, dim.total_floor_area_m2
|
||||
)
|
||||
# SAP 10.2 Table 9b weighted R + p.186 two-systems-different-parts MIT.
|
||||
# A genuine second main (main_heating_fraction > 0 = (203)) contributes
|
||||
# its own responsiveness (Table 9b weighted average) and, when it
|
||||
# carries a different control type, its own rest-of-dwelling control
|
||||
# schedule. `_first_main_heating` is system 1 (living area); the second
|
||||
# detail is system 2. Single-main / DHW-only second mains (frac 0) pass
|
||||
# the None/0 defaults → unchanged single-system MIT.
|
||||
_mit_details = epc.sap_heating.main_heating_details if epc.sap_heating else []
|
||||
_mit_main_2 = _mit_details[1] if len(_mit_details) >= 2 else None
|
||||
main_2_control_type_value: Optional[int] = None
|
||||
main_2_fraction_value = 0.0
|
||||
main_2_responsiveness_value = 1.0
|
||||
if (
|
||||
_mit_main_2 is not None
|
||||
and _mit_main_2.main_heating_fraction is not None
|
||||
and _mit_main_2.main_heating_fraction > 0
|
||||
):
|
||||
main_2_control_type_value = _control_type(_mit_main_2)
|
||||
main_2_fraction_value = _mit_main_2.main_heating_fraction / 100.0
|
||||
main_2_responsiveness_value = _responsiveness(
|
||||
_mit_main_2, tariff=_mit_tariff
|
||||
)
|
||||
monthly_total_gains_w = tuple(
|
||||
internal_gains_monthly_w[m] + solar_gains_monthly_w[m] for m in range(12)
|
||||
)
|
||||
|
|
@ -6281,6 +6302,9 @@ def cert_to_inputs(
|
|||
responsiveness=responsiveness_value,
|
||||
living_area_fraction=living_area_fraction_value,
|
||||
control_temperature_adjustment_c=_control_temperature_adjustment_c(main),
|
||||
main_2_control_type=main_2_control_type_value,
|
||||
main_2_fraction=main_2_fraction_value,
|
||||
main_2_responsiveness=main_2_responsiveness_value,
|
||||
extended_heating_days_per_month=extended_heating_days,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -321,6 +321,9 @@ def mean_internal_temperature_monthly(
|
|||
control_temperature_adjustment_c: float = 0.0,
|
||||
secondary_fraction: float = 0.0,
|
||||
secondary_responsiveness: float = 1.0,
|
||||
main_2_control_type: Optional[int] = None,
|
||||
main_2_fraction: float = 0.0,
|
||||
main_2_responsiveness: float = 1.0,
|
||||
extended_heating_days_per_month: Optional[tuple[tuple[int, int], ...]] = None,
|
||||
) -> MeanInternalTemperatureResult:
|
||||
"""SAP 10.2 §7 orchestrator — chain Table 9c steps 1–9 for all 12 months.
|
||||
|
|
@ -354,13 +357,36 @@ def mean_internal_temperature_monthly(
|
|||
standard SAP heating schedule applies: T_zone
|
||||
= T_bimodal directly.
|
||||
"""
|
||||
# SAP 10.2 Table 9b (PDF p.183) — "where there are two main systems R
|
||||
# is a weighted average ... R = (203)·R_system2 + [1 − (203)]·R_system1".
|
||||
# (203) = `main_2_fraction`. Applied before the secondary-heating blend.
|
||||
main_responsiveness = responsiveness
|
||||
if main_2_control_type is not None and main_2_fraction > 0.0:
|
||||
main_responsiveness = (
|
||||
(1.0 - main_2_fraction) * responsiveness
|
||||
+ main_2_fraction * main_2_responsiveness
|
||||
)
|
||||
effective_responsiveness = (
|
||||
(1.0 - secondary_fraction) * responsiveness
|
||||
(1.0 - secondary_fraction) * main_responsiveness
|
||||
+ secondary_fraction * secondary_responsiveness
|
||||
)
|
||||
elsewhere_off_hours = (
|
||||
_ELSEWHERE_OFF_HOURS_TYPE_3 if control_type == 3 else _ELSEWHERE_OFF_HOURS_TYPE_12
|
||||
)
|
||||
# SAP 10.2 p.186 "two systems heat different parts of the house": when
|
||||
# the two mains carry different controls, the rest-of-dwelling (90)m is
|
||||
# the weighted average of T2 computed under EACH system's control. The
|
||||
# elsewhere off-hours for main system 2's control:
|
||||
two_main_different_parts = (
|
||||
main_2_control_type is not None
|
||||
and main_2_fraction > 0.0
|
||||
and main_2_control_type != control_type
|
||||
)
|
||||
elsewhere_off_hours_main_2 = (
|
||||
_ELSEWHERE_OFF_HOURS_TYPE_3
|
||||
if main_2_control_type == 3
|
||||
else _ELSEWHERE_OFF_HOURS_TYPE_12
|
||||
)
|
||||
|
||||
eta_living: list[float] = []
|
||||
t_1: list[float] = []
|
||||
|
|
@ -408,6 +434,34 @@ def mean_internal_temperature_monthly(
|
|||
)
|
||||
eta_elsewhere.append(eta_e)
|
||||
|
||||
# SAP 10.2 p.186 part 2 — two systems heat different parts: blend
|
||||
# the rest-of-dwelling temperature computed under each system's
|
||||
# control. Th2 + η are identical for control types 2/3 (Table 9
|
||||
# uses the same Th2 formula); only the off-hours differ, so the
|
||||
# second computation reuses t_h2_m and shares η. Weights:
|
||||
# sys2 control: (203) / [1 − (91)]
|
||||
# sys1 control: [1 − (203) − (91)] / [1 − (91)]
|
||||
# If (203) ≥ rest-of-house area [1 − (91)], use sys2's control
|
||||
# alone for elsewhere (per the spec's threshold clause).
|
||||
if two_main_different_parts:
|
||||
rest_of_house = 1.0 - living_area_fraction
|
||||
_, t_e_main_2 = _zone_mean_temp_with_per_zone_eta(
|
||||
heating_temperature_c=t_h2_m,
|
||||
off_hours_first=elsewhere_off_hours_main_2[0],
|
||||
off_hours_second=elsewhere_off_hours_main_2[1],
|
||||
external_temp_c=ext, responsiveness=effective_responsiveness,
|
||||
total_gains_w=gains, heat_transfer_coefficient_w_per_k=h,
|
||||
time_constant_h=tau,
|
||||
)
|
||||
if rest_of_house <= 0.0 or main_2_fraction >= rest_of_house:
|
||||
t_e_bimodal = t_e_main_2
|
||||
else:
|
||||
w_main_2 = main_2_fraction / rest_of_house
|
||||
w_main_1 = (
|
||||
rest_of_house - main_2_fraction
|
||||
) / rest_of_house
|
||||
t_e_bimodal = w_main_1 * t_e_bimodal + w_main_2 * t_e_main_2
|
||||
|
||||
# SAP 10.2 Appendix N3.5 Equation N5 — when the caller provides
|
||||
# per-month (N24,9, N16,9) day allocations, blend Th / T_unimodal
|
||||
# / T_bimodal for each zone. T_unimodal applies one 8-hour off
|
||||
|
|
|
|||
|
|
@ -83,8 +83,8 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = (
|
|||
cert_number="0240-0200-5706-2365-8010",
|
||||
actual_sap=73,
|
||||
expected_sap_resid=-1,
|
||||
expected_pe_resid_kwh_per_m2=+2.1519,
|
||||
expected_co2_resid_tonnes_per_yr=+0.1051,
|
||||
expected_pe_resid_kwh_per_m2=+1.6893,
|
||||
expected_co2_resid_tonnes_per_yr=+0.0815,
|
||||
notes=(
|
||||
"Detached house, TFA 118, age J, oil boiler PCDB-listed + PV + "
|
||||
"RR on BP[0]. Mapper DOES extract sap_room_in_roof.room_in_roof_"
|
||||
|
|
@ -169,7 +169,14 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = (
|
|||
"instead of the regular roof (the case-6 worksheet rule). 0240 "
|
||||
"is detailed-RR gables-only like case 6 → roof drops → space-"
|
||||
"heating demand falls → PE +2.5812 → +2.1519, CO2 +0.1269 → "
|
||||
"+0.1051 (both closer to zero; SAP integer 72 unchanged)."
|
||||
"+0.1051 (both closer to zero; SAP integer 72 unchanged). "
|
||||
"Slice S0380.205 applied the SAP 10.2 p.186 two-systems-"
|
||||
"different-parts MIT (Main 1 2106 type 2 / Main 2 2110 type 3, "
|
||||
"emitter 2 R=0.75): weighted responsiveness 0.8775 + elsewhere "
|
||||
"two-control blend. Lowers MIT ~0.037 °C → space-heating demand "
|
||||
"falls → PE +2.1519 → +1.6893, CO2 +0.1051 → +0.0815 (both "
|
||||
"closer to zero; SAP integer 72 unchanged). Verified 1e-4 "
|
||||
"against the case-6 worksheet (87)/(90)/(98c)."
|
||||
),
|
||||
),
|
||||
_GoldenExpectation(
|
||||
|
|
|
|||
|
|
@ -84,6 +84,14 @@ LINE_30_ROOF_W_PER_K: Final[float] = 19.0523
|
|||
# wired in `_table_4f_additive_components`).
|
||||
LINE_231_PUMPS_FANS_KWH: Final[float] = 356.0
|
||||
|
||||
# Worksheet (211)/(213) per-system space-heating fuel (kWh/yr). The dual
|
||||
# oil boiler heats different parts (Main 1 radiators/2106 living 51%, Main
|
||||
# 2 underfloor/2110 elsewhere 49%) — the SAP 10.2 p.186 two-systems-
|
||||
# different-parts MIT (weighted R 0.8775 + elsewhere two-control blend)
|
||||
# lands (98c) demand 11991.96 exact, so the per-system fuels pin.
|
||||
LINE_211_MAIN_1_FUEL_KWH: Final[float] = 7741.6458
|
||||
LINE_213_MAIN_2_FUEL_KWH: Final[float] = 6995.3106
|
||||
|
||||
# Worksheet (70) "Pumps, fans" internal-gain (W), heating-season only
|
||||
# (Jun-Sep = 0). = 10 W = the two-main-system central-heating-pump pair
|
||||
# per SAP 10.2 Table 5a note a): Main 1 ("2013 or later" → 3 W) + Main 2
|
||||
|
|
|
|||
|
|
@ -301,6 +301,33 @@ def test_case6_main_2_emitter_and_control_extracted() -> None:
|
|||
assert main_2.main_heating_control == 2110
|
||||
|
||||
|
||||
def test_section_9a_per_system_fuel_case6_match_pdf() -> None:
|
||||
"""(211)/(213) per-system space-heating fuel for simulated case 6. The
|
||||
dual oil boiler heats different parts (Main 1 radiators/2106 living,
|
||||
Main 2 underfloor/2110 elsewhere), so SAP 10.2 p.186 applies the
|
||||
two-systems-different-parts MIT: weighted responsiveness R = 0.51·1.0
|
||||
+ 0.49·0.75 = 0.8775 (Table 9b) and a rest-of-dwelling temperature
|
||||
blended from each system's control schedule. That lands (98c) demand
|
||||
11991.96 exact, so the per-system fuels pin. Pre-S0380.205 the cascade
|
||||
used Main 1's control + R=1.0 for the whole dwelling → MIT +0.037 °C →
|
||||
demand +61 kWh → both legs ~+1.3 % high."""
|
||||
# Arrange / Act — pin the REAL cascade (the §2.4 section helper skips
|
||||
# the interlock penalty + two-system MIT params, so use cert_to_inputs).
|
||||
er = cert_to_inputs(_w001431_case6.build_epc()).energy_requirements
|
||||
|
||||
# Assert
|
||||
_pin(
|
||||
er.main_1_fuel_kwh_per_yr,
|
||||
_w001431_case6.LINE_211_MAIN_1_FUEL_KWH,
|
||||
"§9a (211) case6",
|
||||
)
|
||||
_pin(
|
||||
er.main_2_fuel_kwh_per_yr,
|
||||
_w001431_case6.LINE_213_MAIN_2_FUEL_KWH,
|
||||
"§9a (213) case6",
|
||||
)
|
||||
|
||||
|
||||
def test_section_4f_pumps_fans_case6_match_pdf() -> None:
|
||||
"""(231) pumps/fans pin for simulated case 6 — a DUAL-oil-boiler
|
||||
detached dwelling. Worksheet (231) = 356 = (230c) central heating
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue