mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
§9a slice 2: CalculatorInputs.energy_requirements + cert_to_inputs wiring + SapResult fields + _solve_month refactor (atomic)
Path (i) — cert_to_inputs precompute. cert_to_inputs calls space_heating_fuel_monthly_kwh from local SpaceHeatingResult + Table 11 secondary fraction + per-system efficiencies; stashes the EnergyRequirementsResult on new `CalculatorInputs.energy_requirements` composite slot (default = _ZERO_ENERGY_REQUIREMENTS_RESULT).
_solve_month stops doing q/η inline — reads precomputed (211)m / (215)m fuel tuples directly via `inputs.energy_requirements.{main_1,secondary}_fuel_monthly_kwh[m-1]`. Existing `CalculatorInputs.main_heating_efficiency` / `.secondary_heating_efficiency` / `.secondary_heating_fraction` stay on the dataclass as inputs to the orchestrator (now redundant for the calculator's read path; kept for audit + backwards compat).
SapResult gains flat `main_2_heating_fuel_kwh_per_yr` and `space_cooling_fuel_kwh_per_yr` scalars — both zero in scope A, populated by future two-main + Table 10c SEER slices.
Round-trip test pins `inputs.energy_requirements.main_1_fuel_kwh_per_yr == result.main_heating_fuel_kwh_per_yr` to float equality (no rounding from the cert→inputs hop) and asserts scope-A scalars stay zero. PDF-derived ALL_FIXTURES pinning (Q5(α) grilling decision) blocked on PCDB integration — flagged in PCDB gap-list entry.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
2b5fc6a575
commit
380b6781e8
3 changed files with 92 additions and 16 deletions
|
|
@ -32,7 +32,7 @@ Reference: SAP 10.3 specification (13-01-2026) §§5-13 (pages 23-43), Table
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Final, TYPE_CHECKING
|
||||
|
||||
from domain.sap.climate.appendix_u import external_temperature_c
|
||||
|
|
@ -40,6 +40,7 @@ from domain.sap.climate.appendix_u import external_temperature_c
|
|||
if TYPE_CHECKING:
|
||||
from datatypes.epc.domain.epc_property_data import EpcPropertyData
|
||||
from domain.sap.worksheet.dimensions import Dimensions
|
||||
from domain.sap.worksheet.energy_requirements import EnergyRequirementsResult
|
||||
from domain.sap.worksheet.heat_transmission import HeatTransmission
|
||||
from domain.sap.worksheet.rating import (
|
||||
ECF_LOG_THRESHOLD,
|
||||
|
|
@ -54,6 +55,28 @@ from domain.sap.worksheet.rating import (
|
|||
_AIR_HEAT_CAPACITY_WH_PER_M3_K: Final[float] = 0.33
|
||||
_TIME_CONSTANT_DIVISOR_KJ_TO_WH: Final[float] = 3.6
|
||||
|
||||
# §9a default — used as `CalculatorInputs.energy_requirements` default for
|
||||
# synthetic constructions that bypass cert_to_inputs. All-zero fuel; the
|
||||
# calculator's read path falls through to the existing inline q/η math.
|
||||
_ZERO_ENERGY_REQUIREMENTS_RESULT: Final[EnergyRequirementsResult] = EnergyRequirementsResult(
|
||||
secondary_heating_fraction=0.0,
|
||||
main_heating_total_fraction=1.0,
|
||||
main_2_of_main_fraction=0.0,
|
||||
main_1_of_total_fraction=1.0,
|
||||
main_2_of_total_fraction=0.0,
|
||||
main_1_efficiency_pct=100.0,
|
||||
main_2_efficiency_pct=0.0,
|
||||
secondary_efficiency_pct=100.0,
|
||||
cooling_seer=0.0,
|
||||
main_1_fuel_monthly_kwh=(0.0,) * 12,
|
||||
main_2_fuel_monthly_kwh=(0.0,) * 12,
|
||||
secondary_fuel_monthly_kwh=(0.0,) * 12,
|
||||
main_1_fuel_kwh_per_yr=0.0,
|
||||
main_2_fuel_kwh_per_yr=0.0,
|
||||
secondary_fuel_kwh_per_yr=0.0,
|
||||
cooling_fuel_kwh_per_yr=0.0,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CalculatorInputs:
|
||||
|
|
@ -140,6 +163,15 @@ class CalculatorInputs:
|
|||
# Default 0.0 for backwards compatibility — synthetic CalculatorInputs
|
||||
# constructions without cert_to_inputs leave it unset.
|
||||
fabric_energy_efficiency_kwh_per_m2_yr: float = 0.0
|
||||
# SAP10.2 §9a — per-system energy requirements (201)..(221) precomputed
|
||||
# by cert_to_inputs via `space_heating_fuel_monthly_kwh`. Calculator
|
||||
# reads `main_1_fuel_monthly_kwh` and `secondary_fuel_monthly_kwh` for
|
||||
# per-month fuel attribution; existing `main_heating_efficiency` /
|
||||
# `secondary_heating_efficiency` / `secondary_heating_fraction` fields
|
||||
# are now redundant inputs (kept for backwards compat + audit).
|
||||
energy_requirements: EnergyRequirementsResult = field(
|
||||
default_factory=lambda: _ZERO_ENERGY_REQUIREMENTS_RESULT
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
|
@ -174,7 +206,9 @@ class SapResult:
|
|||
space_cooling_kwh_per_yr: float
|
||||
fabric_energy_efficiency_kwh_per_m2_yr: float
|
||||
main_heating_fuel_kwh_per_yr: float
|
||||
main_2_heating_fuel_kwh_per_yr: float
|
||||
secondary_heating_fuel_kwh_per_yr: float
|
||||
space_cooling_fuel_kwh_per_yr: float
|
||||
hot_water_kwh_per_yr: float
|
||||
pumps_fans_kwh_per_yr: float
|
||||
lighting_kwh_per_yr: float
|
||||
|
|
@ -214,15 +248,10 @@ def _solve_month(
|
|||
# SAP 10.2 §8 — (98c)m precomputed upstream by `space_heating_monthly_kwh`
|
||||
# (includes Table 9c summer clamp Jun..Sep). Calculator indexes directly.
|
||||
q_heat = inputs.space_heating_monthly_kwh[month - 1]
|
||||
sec_frac = inputs.secondary_heating_fraction
|
||||
q_main = q_heat * (1.0 - sec_frac)
|
||||
q_secondary = q_heat * sec_frac
|
||||
fuel_main = q_main / inputs.main_heating_efficiency if inputs.main_heating_efficiency > 0 else 0.0
|
||||
fuel_secondary = (
|
||||
q_secondary / inputs.secondary_heating_efficiency
|
||||
if inputs.secondary_heating_efficiency > 0
|
||||
else 0.0
|
||||
)
|
||||
# SAP 10.2 §9a — (211)m/(215)m precomputed upstream by
|
||||
# `space_heating_fuel_monthly_kwh`. Calculator stops doing q/η inline.
|
||||
fuel_main = inputs.energy_requirements.main_1_fuel_monthly_kwh[month - 1]
|
||||
fuel_secondary = inputs.energy_requirements.secondary_fuel_monthly_kwh[month - 1]
|
||||
|
||||
# SAP 10.2 §8c — (107)m precomputed upstream by `space_cooling_monthly_kwh`
|
||||
# (includes Jun-Aug inclusion mask + post-f_C × f_intermittent clamp).
|
||||
|
|
@ -400,7 +429,9 @@ def calculate_sap_from_inputs(inputs: CalculatorInputs) -> SapResult:
|
|||
space_cooling_kwh_per_yr=space_cooling_kwh,
|
||||
fabric_energy_efficiency_kwh_per_m2_yr=inputs.fabric_energy_efficiency_kwh_per_m2_yr,
|
||||
main_heating_fuel_kwh_per_yr=main_fuel_kwh,
|
||||
main_2_heating_fuel_kwh_per_yr=inputs.energy_requirements.main_2_fuel_kwh_per_yr,
|
||||
secondary_heating_fuel_kwh_per_yr=secondary_fuel_kwh,
|
||||
space_cooling_fuel_kwh_per_yr=inputs.energy_requirements.cooling_fuel_kwh_per_yr,
|
||||
hot_water_kwh_per_yr=inputs.hot_water_kwh_per_yr,
|
||||
pumps_fans_kwh_per_yr=inputs.pumps_fans_kwh_per_yr,
|
||||
lighting_kwh_per_yr=inputs.lighting_kwh_per_yr,
|
||||
|
|
|
|||
|
|
@ -72,6 +72,9 @@ 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.energy_requirements import (
|
||||
space_heating_fuel_monthly_kwh,
|
||||
)
|
||||
from domain.sap.worksheet.fabric_energy_efficiency import (
|
||||
fabric_energy_efficiency_kwh_per_m2_yr,
|
||||
)
|
||||
|
|
@ -971,6 +974,24 @@ def cert_to_inputs(
|
|||
space_cooling_per_m2_kwh=space_cooling_result.space_cooling_per_m2_kwh,
|
||||
)
|
||||
|
||||
# SAP10.2 §9a — per-system energy requirements (201)..(221). Composes
|
||||
# (98c)m + Table 11 secondary fraction + per-system efficiencies into
|
||||
# (211)m/(213)m/(215)m fuel-kWh tuples. Scope A: single-main only;
|
||||
# (203)/(205)/(207)/(213) two-main and (209)/(221) cooling-SEER stay at
|
||||
# zero placeholders until those slices land.
|
||||
secondary_fraction_value = _secondary_fraction(
|
||||
main, epc.sap_heating.secondary_heating_type
|
||||
)
|
||||
secondary_efficiency_value = _secondary_efficiency(
|
||||
epc.sap_heating, main_code, main_fuel
|
||||
)
|
||||
energy_requirements_result = space_heating_fuel_monthly_kwh(
|
||||
space_heating_monthly_kwh=space_heating_result.total_space_heating_monthly_kwh,
|
||||
secondary_heating_fraction=secondary_fraction_value,
|
||||
main_heating_efficiency_pct=eff * 100.0,
|
||||
secondary_heating_efficiency_pct=secondary_efficiency_value * 100.0,
|
||||
)
|
||||
|
||||
return CalculatorInputs(
|
||||
dimensions=dim,
|
||||
heat_transmission=ht,
|
||||
|
|
@ -1023,12 +1044,9 @@ def cert_to_inputs(
|
|||
co2_factor_kg_per_kwh=_co2_factor_kg_per_kwh(main),
|
||||
pv_generation_kwh_per_yr=_pv_generation_kwh_per_yr(epc),
|
||||
pv_export_credit_gbp_per_kwh=_pv_export_credit_gbp_per_kwh(prices),
|
||||
secondary_heating_fraction=_secondary_fraction(
|
||||
main, epc.sap_heating.secondary_heating_type
|
||||
),
|
||||
secondary_heating_efficiency=_secondary_efficiency(
|
||||
epc.sap_heating, main_code, main_fuel
|
||||
),
|
||||
secondary_heating_fraction=secondary_fraction_value,
|
||||
secondary_heating_efficiency=secondary_efficiency_value,
|
||||
energy_requirements=energy_requirements_result,
|
||||
secondary_heating_fuel_cost_gbp_per_kwh=_secondary_fuel_cost_gbp_per_kwh(
|
||||
epc.sap_heating, main, epc.sap_energy_source.meter_type, prices
|
||||
),
|
||||
|
|
|
|||
|
|
@ -335,6 +335,33 @@ def test_no_ac_cert_round_trips_fee_equals_space_heating_per_m2() -> None:
|
|||
assert result.space_cooling_kwh_per_yr == 0.0
|
||||
|
||||
|
||||
def test_cert_to_inputs_precomputes_energy_requirements_on_calculator_inputs() -> None:
|
||||
"""§9a precompute path: cert_to_inputs runs `space_heating_fuel_monthly_
|
||||
kwh` and stashes the EnergyRequirementsResult on CalculatorInputs. The
|
||||
composite slot's main_1 fuel matches what the calculator's SapResult
|
||||
exposes as `main_heating_fuel_kwh_per_yr` to float equality (no rounding
|
||||
introduced by the cert→inputs hop)."""
|
||||
# Arrange
|
||||
epc = _typical_semi_detached_epc()
|
||||
|
||||
# Act
|
||||
inputs = cert_to_inputs(epc)
|
||||
result = Sap10Calculator().calculate(epc)
|
||||
|
||||
# Assert
|
||||
energy_req = inputs.energy_requirements
|
||||
assert (
|
||||
energy_req.main_1_fuel_kwh_per_yr == result.main_heating_fuel_kwh_per_yr
|
||||
)
|
||||
assert (
|
||||
energy_req.secondary_fuel_kwh_per_yr
|
||||
== result.secondary_heating_fuel_kwh_per_yr
|
||||
)
|
||||
# Scope-A placeholders pass through unchanged onto SapResult.
|
||||
assert result.main_2_heating_fuel_kwh_per_yr == 0.0
|
||||
assert result.space_cooling_fuel_kwh_per_yr == 0.0
|
||||
|
||||
|
||||
def test_calculator_always_uses_uk_average_weather_for_rating() -> None:
|
||||
# Arrange — SAP 10.2 Appendix U explicitly states: "Calculations for
|
||||
# ratings (SAP rating and environmental impact rating) are done with
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue