mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
§4 HW slice 1: PCDB Table 3b combi-loss override
Closes the dominant ~92% of the 000474 HW kWh +14.4% residual that the post-§10a Table 32 cost-side fix exposed (pre-§10a wrong prices had been masking it). 000474 HW fuel kWh tightens 2622 → 2320 (+1.2% over PDF 2292); remaining +1.2% closes when slice 2 (Eq D1 monthly cascade) lands. 000490 unaffected — PCDB 10328 lodges separate_dhw_ tests=0 (no Table 3b/3c data), falls through to existing Table 3a default. - tables/pcdb/parser.py: GasOilBoilerRecord gains 7 typed fields per BRE PCDF Spec v1.0 §7.11 — subsidiary_type (field 16), store_type (field 39), separate_dhw_tests (field 48), rejected_energy_ proportion_r1 (field 51), loss_factor_f1_kwh_per_day (field 52), loss_factor_f2_kwh_per_day (field 56), rejected_factor_f3_per_ litre (field 57). Field positions cross-verified against PDF Σ(61) = 337.27 vs 000474 worksheet pin 337.19 (Δ 0.02%). - worksheet/water_heating.py: combi_loss_monthly_kwh_table_3b_row_1_ instantaneous(r1, F1, energy_content (45)m, daily HW (44)m) — SAP10.2 Appendix J Table 3b row 1 formula (61)m = (45)m × r1 × fu + F1 × n_m. Other Table 3b rows (storage variants) and Table 3c (two-profile) deferred until a fixture exercises. - rdsap/cert_to_inputs.py: _pcdb_table_3b_combi_loss_override builds the (61)m override from the PCDB record when separate_dhw_tests=1 + subsidiary=0 + store_type=0 (instantaneous non-storage path). _hot_water_fuel_kwh_per_yr threaded with pcdb_record kwarg; calls water_heating_from_cert with the override when present. - docs/sap-spec/pcdb_table_105_gas_oil_boilers.jsonl: regenerated via the ETL to surface the new typed fields alongside the existing efficiency columns. 484 tests passing (was 479). e2e ceilings hold: 000474 SAP delta 4 → 3 (within current ceiling of 4 — will tighten further after slice 2 Eq D1 cascade lands). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
ae8c946179
commit
760e25dea9
7 changed files with 7487 additions and 7236 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -54,6 +54,7 @@ from domain.ml.sap_efficiencies import (
|
|||
)
|
||||
from domain.sap.calculator import CalculatorInputs
|
||||
from domain.sap.tables.pcdb import gas_oil_boiler_record
|
||||
from domain.sap.tables.pcdb.parser import GasOilBoilerRecord
|
||||
from domain.sap.tables.table_12 import (
|
||||
co2_factor_kg_per_kwh,
|
||||
primary_energy_factor,
|
||||
|
|
@ -97,6 +98,7 @@ from domain.sap.worksheet.ventilation import (
|
|||
)
|
||||
from domain.sap.worksheet.water_heating import (
|
||||
TABLE_J1_TCOLD_FROM_MAINS_C,
|
||||
combi_loss_monthly_kwh_table_3b_row_1_instantaneous,
|
||||
water_heating_from_cert,
|
||||
)
|
||||
|
||||
|
|
@ -709,12 +711,47 @@ def _has_bath_from_cert(epc: EpcPropertyData) -> bool:
|
|||
return n is None or n >= 1
|
||||
|
||||
|
||||
def _pcdb_table_3b_combi_loss_override(
|
||||
pcdb_record: Optional[GasOilBoilerRecord],
|
||||
*,
|
||||
energy_content_monthly_kwh: tuple[float, ...],
|
||||
daily_hot_water_monthly_l_per_day: tuple[float, ...],
|
||||
) -> Optional[tuple[float, ...]]:
|
||||
"""Build a Table 3b row-1 combi-loss override when the PCDB record
|
||||
lodges single-profile (Profile M only) test data for an instantaneous
|
||||
combi with non-storage FGHRS or without FGHRS. Returns None for
|
||||
every other PCDB combi configuration so the worksheet falls back to
|
||||
the Table 3a default. Other Table 3b/3c rows (storage variants,
|
||||
integral FGHRS, two-profile Table 3c) are deferred until a fixture
|
||||
exercises them — defaulting to Table 3a is safe (matches the pre-
|
||||
§4 behaviour) but loses spec accuracy for those configurations."""
|
||||
if pcdb_record is None:
|
||||
return None
|
||||
if pcdb_record.separate_dhw_tests != 1:
|
||||
return None
|
||||
if pcdb_record.subsidiary_type not in (None, 0):
|
||||
return None
|
||||
if pcdb_record.store_type not in (None, 0):
|
||||
return None
|
||||
r1 = pcdb_record.rejected_energy_proportion_r1
|
||||
f1 = pcdb_record.loss_factor_f1_kwh_per_day
|
||||
if r1 is None or f1 is None:
|
||||
return None
|
||||
return combi_loss_monthly_kwh_table_3b_row_1_instantaneous(
|
||||
rejected_energy_proportion_r1=r1,
|
||||
loss_factor_f1_kwh_per_day=f1,
|
||||
energy_content_monthly_kwh=energy_content_monthly_kwh,
|
||||
daily_hot_water_monthly_l_per_day=daily_hot_water_monthly_l_per_day,
|
||||
)
|
||||
|
||||
|
||||
def _hot_water_fuel_kwh_per_yr(
|
||||
*,
|
||||
epc: EpcPropertyData,
|
||||
water_efficiency_pct: float,
|
||||
is_instantaneous: bool,
|
||||
primary_age: Optional[str],
|
||||
pcdb_record: Optional[GasOilBoilerRecord] = None,
|
||||
) -> tuple[float, tuple[float, ...]]:
|
||||
"""Annual hot water FUEL kWh (the slot calculator.CalculatorInputs
|
||||
expects). Wires the SAP10.2 §4 worksheet orchestrator into the cert→
|
||||
|
|
@ -753,6 +790,25 @@ def _hot_water_fuel_kwh_per_yr(
|
|||
has_solar_water_heating=epc.solar_water_heating,
|
||||
)
|
||||
return legacy_kwh, zero_monthly
|
||||
# If the PCDB record carries Profile-M combi-test data (separate_dhw_
|
||||
# tests=1, instantaneous non-storage), pre-build the (61)m override
|
||||
# so `water_heating_from_cert` uses Table 3b row 1 instead of the
|
||||
# Table 3a default. Requires (45)m and (44)m from a prior orchestrator
|
||||
# invocation; cheapest to call the orchestrator twice (once to derive
|
||||
# the inputs to the override, once to land the final result with the
|
||||
# override in place).
|
||||
bootstrap = water_heating_from_cert(
|
||||
epc=epc,
|
||||
mixer_shower_flow_rates_l_per_min=_mixer_shower_flow_rates_from_cert(epc),
|
||||
has_bath=_has_bath_from_cert(epc),
|
||||
cold_water_temps_c=TABLE_J1_TCOLD_FROM_MAINS_C,
|
||||
low_water_use=False,
|
||||
)
|
||||
combi_loss_override = _pcdb_table_3b_combi_loss_override(
|
||||
pcdb_record,
|
||||
energy_content_monthly_kwh=bootstrap.energy_content_monthly_kwh,
|
||||
daily_hot_water_monthly_l_per_day=bootstrap.daily_hot_water_l_per_day_monthly,
|
||||
)
|
||||
result = water_heating_from_cert(
|
||||
epc=epc,
|
||||
mixer_shower_flow_rates_l_per_min=_mixer_shower_flow_rates_from_cert(epc),
|
||||
|
|
@ -762,6 +818,7 @@ def _hot_water_fuel_kwh_per_yr(
|
|||
# a domain-model field + plumb-through in a future slice.
|
||||
cold_water_temps_c=TABLE_J1_TCOLD_FROM_MAINS_C,
|
||||
low_water_use=False,
|
||||
combi_loss_monthly_kwh_override=combi_loss_override,
|
||||
)
|
||||
if water_efficiency_pct <= 0:
|
||||
return 0.0, result.heat_gains_monthly_kwh
|
||||
|
|
@ -1035,6 +1092,7 @@ def cert_to_inputs(
|
|||
water_efficiency_pct=water_eff,
|
||||
is_instantaneous=is_instantaneous,
|
||||
primary_age=primary_age,
|
||||
pcdb_record=pcdb_main,
|
||||
)
|
||||
lighting_kwh = predicted_lighting_kwh(
|
||||
total_floor_area_m2=epc.total_floor_area_m2,
|
||||
|
|
|
|||
|
|
@ -61,6 +61,13 @@ def _load_table_105() -> dict[int, GasOilBoilerRecord]:
|
|||
comparative_hot_water_efficiency_pct=data["comparative_hot_water_efficiency_pct"],
|
||||
output_kw_max=data["output_kw_max"],
|
||||
final_year_of_manufacture=data["final_year_of_manufacture"],
|
||||
subsidiary_type=data.get("subsidiary_type"),
|
||||
store_type=data.get("store_type"),
|
||||
separate_dhw_tests=data.get("separate_dhw_tests"),
|
||||
rejected_energy_proportion_r1=data.get("rejected_energy_proportion_r1"),
|
||||
loss_factor_f1_kwh_per_day=data.get("loss_factor_f1_kwh_per_day"),
|
||||
loss_factor_f2_kwh_per_day=data.get("loss_factor_f2_kwh_per_day"),
|
||||
rejected_factor_f3_per_litre=data.get("rejected_factor_f3_per_litre"),
|
||||
raw=tuple(data["raw"]),
|
||||
)
|
||||
records_by_id[record.pcdb_id] = record
|
||||
|
|
|
|||
|
|
@ -64,6 +64,24 @@ class GasOilBoilerRecord:
|
|||
comparative_hot_water_efficiency_pct: Optional[float]
|
||||
output_kw_max: Optional[float]
|
||||
final_year_of_manufacture: Optional[int]
|
||||
# SAP10.2 Appendix J Table 3b/3c — combi-loss fields per BRE PCDF
|
||||
# Spec v1.0 §7.11 fields 48 / 51 / 52 / 56 / 57. Populated only for
|
||||
# boilers EN 13203-2 / OPS 26 tested; SAP-default boilers leave them
|
||||
# all blank → `separate_dhw_tests=0` and (61)m falls back to Table 3a.
|
||||
# BRE PCDF Spec v1.0 §7.11 field 16 (0-idx 15): 0=normal, 1=integral
|
||||
# FGHRS, 2=combined HP+boiler, 3=combined HP+boiler+FGHRS. Gates the
|
||||
# Table 3b/3c row selection — only `subsidiary_type=0` exercises the
|
||||
# "Instantaneous with non-storage FGHRS or without FGHRS" row 1.
|
||||
subsidiary_type: Optional[int]
|
||||
# BRE PCDF Spec v1.0 §7.11 field 39 (0-idx 38): 0=not storage combi,
|
||||
# 1=primary water store, 2=secondary store, 3=CPSU. Gates storage-
|
||||
# combi rows in Table 3b/3c (deferred until a fixture exercises).
|
||||
store_type: Optional[int]
|
||||
separate_dhw_tests: Optional[int]
|
||||
rejected_energy_proportion_r1: Optional[float]
|
||||
loss_factor_f1_kwh_per_day: Optional[float]
|
||||
loss_factor_f2_kwh_per_day: Optional[float]
|
||||
rejected_factor_f3_per_litre: Optional[float]
|
||||
raw: tuple[str, ...]
|
||||
|
||||
|
||||
|
|
@ -141,5 +159,12 @@ def parse_table_105_row(row: str) -> GasOilBoilerRecord:
|
|||
winter_efficiency_pct=_parse_optional_float(fields[25]),
|
||||
summer_efficiency_pct=_parse_optional_float(fields[26]),
|
||||
comparative_hot_water_efficiency_pct=_parse_optional_float(fields[28]),
|
||||
subsidiary_type=_parse_optional_int(fields[15]),
|
||||
store_type=_parse_optional_int(fields[38]),
|
||||
separate_dhw_tests=_parse_optional_int(fields[47]),
|
||||
rejected_energy_proportion_r1=_parse_optional_float(fields[50]),
|
||||
loss_factor_f1_kwh_per_day=_parse_optional_float(fields[51]),
|
||||
loss_factor_f2_kwh_per_day=_parse_optional_float(fields[55]),
|
||||
rejected_factor_f3_per_litre=_parse_optional_float(fields[56]),
|
||||
raw=fields,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -34,6 +34,22 @@ _BAXI_98_RAW: str = (
|
|||
",,0,,0,,0,,,,,0,,,,,,,,,,,,,0000,,,,,,,,,,,,,,,"
|
||||
)
|
||||
|
||||
# Verified by ground-truth arithmetic against PDF Σ(61) = 337.19 for 000474
|
||||
# Elmhurst fixture (Vaillant ecoTEC pro 28 VUW GB 286/5-3, pcdb_id 16839):
|
||||
# Table 3b row 1 → Σ(61) = (45) × r1 × fu + F1 × 365
|
||||
# = 1680.84 × 0.0025 × 1.0 + 0.91251 × 365 = 337.27.
|
||||
# Combi-loss fields (BRE PCDF Spec v1.0 §7.11 fields 48/51/52/56/57):
|
||||
# separate_dhw_tests = 1 (one test, profile M → Table 3b)
|
||||
# rejected_energy_proportion_r1 = 0.0025
|
||||
# loss_factor_f1_kwh_per_day = 0.91251
|
||||
# loss_factor_f2 / rejected_factor_f3 = blank (Table 3c not used)
|
||||
_VAILLANT_16839_RAW: str = (
|
||||
"016839,000031,0,2019/Mar/04 10:28,Vaillant,Vaillant,ecoTEC pro 28,"
|
||||
"VUW GB 286/5-3,GC 47-044-45,2005,2015,1,2,1,2,0,,,2,2,2,24.4,24.4,,,"
|
||||
"88.7,87.0,,75.1,,2,,,104,1,2,105,2,0,,,,0,,,,,1,7.012,0.133,0.0025,"
|
||||
"0.91251,,,,,,1,1,,0045,,,,,,,,,89.0,98.0,,,,,96.3"
|
||||
)
|
||||
|
||||
|
||||
def test_table_105_parser_extracts_baxi_98_known_fields() -> None:
|
||||
"""Decode the user-verified Baxi 000098 Wm 20/3rs record. Field positions
|
||||
|
|
@ -116,6 +132,63 @@ def test_table_105_parser_extracts_other_user_verified_records(
|
|||
assert getattr(record, key) == value, f"field {key}"
|
||||
|
||||
|
||||
def test_table_105_parser_extracts_separate_dhw_tests_profile_flag() -> None:
|
||||
"""BRE PCDF Spec v1.0 §7.11 field 48 (0-indexed 47) "Separate DHW
|
||||
tests" encodes the profile-flag for PCDB Table 3b/3c combi-loss
|
||||
selection: 0 = none / not applicable, 1 = one test profile M
|
||||
(Table 3b), 2 = two tests profiles M+L (Table 3c), 3 = two tests
|
||||
profiles M+S (Table 3c). 16839 lodges flag=1 → Table 3b path."""
|
||||
# Arrange
|
||||
raw_row = _VAILLANT_16839_RAW
|
||||
|
||||
# Act
|
||||
record = parse_table_105_row(raw_row)
|
||||
|
||||
# Assert
|
||||
assert record.separate_dhw_tests == 1
|
||||
|
||||
|
||||
def test_table_105_parser_extracts_table_3b_3c_combi_loss_coefficients() -> None:
|
||||
"""BRE PCDF Spec v1.0 §7.11 fields 51 / 52 / 56 / 57 (0-indexed
|
||||
50 / 51 / 55 / 56) carry the Table 3b/3c combi-loss coefficients:
|
||||
rejected energy r1, loss factor F1 (Table 3b), loss factor F2
|
||||
(Table 3c), rejected factor F3 (Table 3c, can be negative).
|
||||
16839 lodges profile M only, so F2/F3 are absent (blank). Cross-
|
||||
verified by arithmetic: Σ(61) = (45) × r1 × fu + F1 × 365
|
||||
= 1680.84 × 0.0025 × 1.0 + 0.91251 × 365 = 337.27 kWh/yr against
|
||||
the 000474 worksheet's PDF pin Σ(61) = 337.19 (Δ 0.02%)."""
|
||||
# Arrange
|
||||
raw_row = _VAILLANT_16839_RAW
|
||||
|
||||
# Act
|
||||
record = parse_table_105_row(raw_row)
|
||||
|
||||
# Assert
|
||||
assert record.rejected_energy_proportion_r1 == 0.0025
|
||||
assert record.loss_factor_f1_kwh_per_day == 0.91251
|
||||
assert record.loss_factor_f2_kwh_per_day is None
|
||||
assert record.rejected_factor_f3_per_litre is None
|
||||
|
||||
|
||||
def test_table_105_parser_leaves_combi_loss_fields_none_for_sap_default_boilers() -> None:
|
||||
"""Baxi 000098 is a SAP-default boiler (no EN 13203-2 / OPS 26 tests),
|
||||
so the Table 3b/3c combi-loss fields are blank in pcdb10.dat. The
|
||||
parser exposes them as None to signal Table 3a fallback (the
|
||||
pre-§4-HW default 600 kWh/yr behaviour)."""
|
||||
# Arrange
|
||||
raw_row = _BAXI_98_RAW
|
||||
|
||||
# Act
|
||||
record = parse_table_105_row(raw_row)
|
||||
|
||||
# Assert
|
||||
assert record.separate_dhw_tests == 0
|
||||
assert record.rejected_energy_proportion_r1 is None
|
||||
assert record.loss_factor_f1_kwh_per_day is None
|
||||
assert record.loss_factor_f2_kwh_per_day is None
|
||||
assert record.rejected_factor_f3_per_litre is None
|
||||
|
||||
|
||||
def test_parse_table_105_walks_section_skipping_headers_and_comments() -> None:
|
||||
"""The .dat file demarcates each table with a `$<id>,<format>,...`
|
||||
header line, intersperses `#`-prefixed comments, and ends the table
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ from domain.sap.worksheet.water_heating import (
|
|||
annual_average_hot_water_other_uses_l_per_day,
|
||||
assumed_occupancy,
|
||||
combi_loss_monthly_kwh_table_3a_keep_hot_time_clock,
|
||||
combi_loss_monthly_kwh_table_3b_row_1_instantaneous,
|
||||
distribution_loss_monthly_kwh,
|
||||
energy_content_of_hot_water_monthly_kwh,
|
||||
heat_gains_from_water_heating_monthly_kwh,
|
||||
|
|
@ -474,6 +475,60 @@ def test_distribution_loss_zero_for_instantaneous_point_of_use_water_heating() -
|
|||
assert all(v == pytest.approx(0.0, abs=1e-9) for v in loss)
|
||||
|
||||
|
||||
def test_combi_loss_table_3b_row_1_matches_elmhurst_000474_pcdb_arithmetic() -> None:
|
||||
"""SAP10.2 Appendix J Table 3b row 1 (Instantaneous with non-storage
|
||||
FGHRS or without FGHRS):
|
||||
(61)m = (45)m × r1 × fu + [F1 × n_m]
|
||||
|
||||
where r1, F1 are PCDB Table 105 fields and fu = V_d,m/100 for daily
|
||||
HW < 100 L/day, else 1.0.
|
||||
|
||||
For 000474 (Vaillant ecoTEC pro 28, PCDB 16839): r1 = 0.0025,
|
||||
F1 = 0.91251 kWh/day, V_d,m > 100 ⇒ fu = 1.0 every month.
|
||||
Σ(61) = 0.0025 × Σ(45) + 0.91251 × 365 = 4.20 + 333.07 = 337.27
|
||||
against PDF pin Σ(61) = 337.19 (Δ 0.02%, rounding-floor)."""
|
||||
# Arrange
|
||||
r1 = 0.0025
|
||||
f1 = 0.91251
|
||||
energy_content_45 = _w000474.LINE_45_M_HW_ENERGY_CONTENT_KWH
|
||||
# Daily HW > 100 L/day every month for 000474 → fu collapses to 1.0.
|
||||
daily_hw_44 = _w000474.LINE_44_M_DAILY_HW_USAGE_L
|
||||
|
||||
# Act
|
||||
monthly = combi_loss_monthly_kwh_table_3b_row_1_instantaneous(
|
||||
rejected_energy_proportion_r1=r1,
|
||||
loss_factor_f1_kwh_per_day=f1,
|
||||
energy_content_monthly_kwh=energy_content_45,
|
||||
daily_hot_water_monthly_l_per_day=daily_hw_44,
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert sum(monthly) == pytest.approx(337.27, abs=0.10)
|
||||
# Per-month also: Jan = (174.4 × 0.0025 × 1.0) + (0.91251 × 31) = 28.7271
|
||||
assert monthly[0] == pytest.approx(_w000474.LINE_61_M_COMBI_LOSS_KWH[0], abs=0.05)
|
||||
|
||||
|
||||
def test_000474_cert_to_inputs_hot_water_kwh_closes_within_1_5pct_via_pcdb_table_3b() -> None:
|
||||
"""Cert-round-trip conformance: 000474 mid-terrace combi-gas (PDF
|
||||
HW fuel = 2291.78 kWh/yr). Pre-§4 slice 1: cert_to_inputs used
|
||||
Table 3a default 600 kWh/yr combi loss → HW fuel 2621.65 (+14.4%).
|
||||
Post-§4 slice 1: cert_to_inputs reads PCDB Table 105 r1/F1 fields
|
||||
and routes through Table 3b row 1 (Σ(61) = 337.27) → HW fuel 2319.7
|
||||
(+1.2%). The remaining ~1.2% residual closes when slice 2 promotes
|
||||
`water_efficiency_pct` from the scalar summer efficiency to the
|
||||
monthly Equation D1 cascade (Appendix D §D2.1 (2))."""
|
||||
# Arrange
|
||||
from domain.sap.rdsap.cert_to_inputs import cert_to_inputs
|
||||
|
||||
epc = _w000474.build_epc()
|
||||
|
||||
# Act
|
||||
inputs = cert_to_inputs(epc)
|
||||
|
||||
# Assert
|
||||
assert inputs.hot_water_kwh_per_yr == pytest.approx(2291.78, rel=0.015)
|
||||
|
||||
|
||||
def test_combi_loss_table_3a_time_clock_keep_hot_matches_elmhurst_000490() -> None:
|
||||
"""SAP10.2 §4 line (61)m via Table 3a row "Instantaneous, with keep-hot
|
||||
facility controlled by time clock":
|
||||
|
|
|
|||
|
|
@ -264,6 +264,39 @@ def distribution_loss_monthly_kwh(
|
|||
return tuple(0.15 * e for e in monthly_energy_content_kwh)
|
||||
|
||||
|
||||
def combi_loss_monthly_kwh_table_3b_row_1_instantaneous(
|
||||
*,
|
||||
rejected_energy_proportion_r1: float,
|
||||
loss_factor_f1_kwh_per_day: float,
|
||||
energy_content_monthly_kwh: tuple[float, ...],
|
||||
daily_hot_water_monthly_l_per_day: tuple[float, ...],
|
||||
) -> tuple[float, ...]:
|
||||
"""SAP 10.2 Appendix J Table 3b row 1 (Instantaneous combi with non-
|
||||
storage FGHRS or without FGHRS, profile M only):
|
||||
|
||||
(61)m = (45)m × r1 × fu + [F1 × n_m]
|
||||
|
||||
where r1 = rejected energy proportion (PCDB Table 105 field 51),
|
||||
F1 = loss factor in kWh/day (PCDB field 52), and fu = V_d,m / 100
|
||||
when daily hot-water usage V_d,m < 100 L/day, else fu = 1.0.
|
||||
|
||||
Applies only to combi boilers EN 13203-2 / OPS 26 tested with one
|
||||
profile (Separate DHW tests = 1). Other Table 3b rows (storage
|
||||
combis, storage-FGHRS variants) and Table 3c (two-profile tests)
|
||||
are deferred until a fixture exercises them. Untested combis fall
|
||||
back to the existing Table 3a path.
|
||||
"""
|
||||
return tuple(
|
||||
e * rejected_energy_proportion_r1 * (v / 100.0 if v < 100.0 else 1.0)
|
||||
+ loss_factor_f1_kwh_per_day * n
|
||||
for e, v, n in zip(
|
||||
energy_content_monthly_kwh,
|
||||
daily_hot_water_monthly_l_per_day,
|
||||
_DAYS_IN_MONTH,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def combi_loss_monthly_kwh_table_3a_keep_hot_time_clock() -> tuple[float, ...]:
|
||||
"""SAP 10.2 §4 line (61)m — Table 3a row "Instantaneous, with keep-hot
|
||||
facility controlled by time clock": 600 × n_m / 365 kWh/month.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue