mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
§6 slice 7: delete legacy _solar_gains_w + WindowInput + _window_inputs
Removes:
- calculator.WindowInput dataclass
- calculator.CalculatorInputs.windows field
- calculator._solar_gains_w function
- cert_to_inputs._window_inputs / _g_perpendicular / _frame_factor
- cert_to_inputs._G_PERPENDICULAR_BY_GLAZING_TYPE / _FRAME_FACTOR_BY_MATERIAL
/ _ORIENTATION_BY_CODE lookup tables (duplicated, spec-correct versions
live in solar_gains.py)
- 3 obsolete tests in test_cert_to_inputs.py that probed deleted internals;
one asserted the spec-incorrect Metal frame factor 0.83 (Table 6c spec
value is 0.8).
Test fixtures in test_calculator.py + test_bre_worked_examples.py pin the
prior synthetic solar 12-tuple verbatim so heat-balance numerics stay
identical pre/post §6 wiring.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
cd2bd9cedc
commit
a0ce45c98c
5 changed files with 15 additions and 250 deletions
|
|
@ -50,11 +50,6 @@ from domain.sap.worksheet.rating import (
|
||||||
sap_rating,
|
sap_rating,
|
||||||
sap_rating_integer,
|
sap_rating_integer,
|
||||||
)
|
)
|
||||||
from domain.sap.worksheet.solar_gains import (
|
|
||||||
Orientation,
|
|
||||||
surface_solar_flux_w_per_m2,
|
|
||||||
window_solar_gain_w,
|
|
||||||
)
|
|
||||||
from domain.sap.worksheet.space_heating import monthly_heat_requirement_kwh
|
from domain.sap.worksheet.space_heating import monthly_heat_requirement_kwh
|
||||||
from domain.sap.worksheet.utilisation_factor import utilisation_factor
|
from domain.sap.worksheet.utilisation_factor import utilisation_factor
|
||||||
|
|
||||||
|
|
@ -65,22 +60,6 @@ _TIME_CONSTANT_DIVISOR_KJ_TO_WH: Final[float] = 3.6
|
||||||
_ETA_ITERATIONS: Final[int] = 2
|
_ETA_ITERATIONS: Final[int] = 2
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class WindowInput:
|
|
||||||
"""One glazed opening contributing solar gain. Orientation maps to a
|
|
||||||
Table U5 column and to Table U4 latitude via `surface_solar_flux_w_per_m2`.
|
|
||||||
`g_perpendicular`, `frame_factor`, `overshading_factor` come from
|
|
||||||
Tables 6b/6c/6d — supplied by the caller so this module remains
|
|
||||||
physics-only."""
|
|
||||||
|
|
||||||
area_m2: float
|
|
||||||
orientation: Orientation
|
|
||||||
pitch_deg: float
|
|
||||||
g_perpendicular: float
|
|
||||||
frame_factor: float
|
|
||||||
overshading_factor: float
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class CalculatorInputs:
|
class CalculatorInputs:
|
||||||
"""Synthetic SAP 10.3 calculator inputs. The cert→inputs mapper
|
"""Synthetic SAP 10.3 calculator inputs. The cert→inputs mapper
|
||||||
|
|
@ -110,7 +89,6 @@ class CalculatorInputs:
|
||||||
# only indexes into it per month, no recomputation here.
|
# only indexes into it per month, no recomputation here.
|
||||||
solar_gains_monthly_w: tuple[float, ...]
|
solar_gains_monthly_w: tuple[float, ...]
|
||||||
region: int
|
region: int
|
||||||
windows: tuple[WindowInput, ...]
|
|
||||||
control_type: int
|
control_type: int
|
||||||
responsiveness: float
|
responsiveness: float
|
||||||
living_area_fraction: float
|
living_area_fraction: float
|
||||||
|
|
@ -185,27 +163,6 @@ class SapResult:
|
||||||
intermediate: dict[str, float]
|
intermediate: dict[str, float]
|
||||||
|
|
||||||
|
|
||||||
def _solar_gains_w(
|
|
||||||
*, windows: tuple[WindowInput, ...], region: int, month: int
|
|
||||||
) -> float:
|
|
||||||
total = 0.0
|
|
||||||
for w in windows:
|
|
||||||
s = surface_solar_flux_w_per_m2(
|
|
||||||
orientation=w.orientation,
|
|
||||||
pitch_deg=w.pitch_deg,
|
|
||||||
region=region,
|
|
||||||
month=month,
|
|
||||||
)
|
|
||||||
total += window_solar_gain_w(
|
|
||||||
area_m2=w.area_m2,
|
|
||||||
surface_flux_w_per_m2=s,
|
|
||||||
g_perpendicular=w.g_perpendicular,
|
|
||||||
frame_factor=w.frame_factor,
|
|
||||||
overshading_factor=w.overshading_factor,
|
|
||||||
)
|
|
||||||
return total
|
|
||||||
|
|
||||||
|
|
||||||
def _time_constant_h(*, tmp_kj_per_m2_k: float, tfa_m2: float, hlc_w_per_k: float) -> float:
|
def _time_constant_h(*, tmp_kj_per_m2_k: float, tfa_m2: float, hlc_w_per_k: float) -> float:
|
||||||
if hlc_w_per_k <= 0:
|
if hlc_w_per_k <= 0:
|
||||||
return float("inf")
|
return float("inf")
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ from domain.ml.sap_efficiencies import (
|
||||||
seasonal_efficiency,
|
seasonal_efficiency,
|
||||||
water_heating_efficiency as _legacy_water_heating_efficiency,
|
water_heating_efficiency as _legacy_water_heating_efficiency,
|
||||||
)
|
)
|
||||||
from domain.sap.calculator import CalculatorInputs, WindowInput
|
from domain.sap.calculator import CalculatorInputs
|
||||||
from domain.sap.tables.table_12 import (
|
from domain.sap.tables.table_12 import (
|
||||||
co2_factor_kg_per_kwh,
|
co2_factor_kg_per_kwh,
|
||||||
primary_energy_factor,
|
primary_energy_factor,
|
||||||
|
|
@ -67,7 +67,7 @@ from domain.sap.worksheet.heat_transmission import (
|
||||||
DwellingExposure,
|
DwellingExposure,
|
||||||
heat_transmission_from_cert,
|
heat_transmission_from_cert,
|
||||||
)
|
)
|
||||||
from domain.sap.worksheet.solar_gains import Orientation, solar_gains_from_cert
|
from domain.sap.worksheet.solar_gains import solar_gains_from_cert
|
||||||
from domain.sap.worksheet.ventilation import (
|
from domain.sap.worksheet.ventilation import (
|
||||||
MechanicalVentilationKind,
|
MechanicalVentilationKind,
|
||||||
ventilation_from_inputs,
|
ventilation_from_inputs,
|
||||||
|
|
@ -88,56 +88,6 @@ _LIVING_AREA_FRACTION_DEFAULT: Final[float] = 0.21
|
||||||
_LIVING_AREA_FRACTION_MIN: Final[float] = 0.13
|
_LIVING_AREA_FRACTION_MIN: Final[float] = 0.13
|
||||||
|
|
||||||
|
|
||||||
# SAP10 octant code → solar_gains.Orientation. Codes 1-8 only; anything
|
|
||||||
# else (0, "NR", arbitrary string) is treated as un-mapped and the window
|
|
||||||
# contributes no solar gain.
|
|
||||||
_ORIENTATION_BY_CODE: Final[dict[int, Orientation]] = {
|
|
||||||
1: Orientation.N,
|
|
||||||
2: Orientation.NE,
|
|
||||||
3: Orientation.E,
|
|
||||||
4: Orientation.SE,
|
|
||||||
5: Orientation.S,
|
|
||||||
6: Orientation.SW,
|
|
||||||
7: Orientation.W,
|
|
||||||
8: Orientation.NW,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# SAP 10.3 Table 6b — g_perpendicular (solar transmittance at normal
|
|
||||||
# incidence) by SAP10 glazing_type code. Default 0.72 (modern double
|
|
||||||
# glazing, no low-E) when the cert's glazing_type is missing or
|
|
||||||
# unrecognised — the modal RdSAP case.
|
|
||||||
_G_PERPENDICULAR_BY_GLAZING_TYPE: Final[dict[int, float]] = {
|
|
||||||
1: 0.85, # single
|
|
||||||
2: 0.72, # double 2002-2022 (no low-E)
|
|
||||||
3: 0.72, # double pre-2002
|
|
||||||
4: 0.63, # double low-E soft coat
|
|
||||||
5: 0.76, # secondary glazing
|
|
||||||
6: 0.68, # triple
|
|
||||||
}
|
|
||||||
_G_PERPENDICULAR_DEFAULT: Final[float] = 0.72
|
|
||||||
|
|
||||||
|
|
||||||
# SAP 10.3 Table 6c — frame factor (proportion of window area that is
|
|
||||||
# glazed, not frame) by frame material. The cert lodges this as a free-
|
|
||||||
# text string ("PVC", "Wood", "Metal", "Aluminium"); matching is case-
|
|
||||||
# insensitive and substring-based because site-notes capitalisation
|
|
||||||
# drifts.
|
|
||||||
_FRAME_FACTOR_BY_MATERIAL: Final[tuple[tuple[str, float], ...]] = (
|
|
||||||
("metal with thermal break", 0.80),
|
|
||||||
("metal", 0.83),
|
|
||||||
("aluminium with thermal break", 0.80),
|
|
||||||
("aluminium", 0.83),
|
|
||||||
("steel", 0.83),
|
|
||||||
("wood", 0.70),
|
|
||||||
("timber", 0.70),
|
|
||||||
("pvc", 0.70),
|
|
||||||
("upvc", 0.70),
|
|
||||||
("composite", 0.70),
|
|
||||||
)
|
|
||||||
_FRAME_FACTOR_DEFAULT: Final[float] = 0.70
|
|
||||||
|
|
||||||
|
|
||||||
_PENCE_TO_GBP: Final[float] = 0.01
|
_PENCE_TO_GBP: Final[float] = 0.01
|
||||||
_DEFAULT_THERMAL_MASS_PARAMETER_KJ_PER_M2_K: Final[float] = 250.0
|
_DEFAULT_THERMAL_MASS_PARAMETER_KJ_PER_M2_K: Final[float] = 250.0
|
||||||
_DEFAULT_PUMPS_FANS_KWH_PER_YR: Final[float] = 130.0
|
_DEFAULT_PUMPS_FANS_KWH_PER_YR: Final[float] = 130.0
|
||||||
|
|
@ -363,59 +313,6 @@ def _living_area_fraction(habitable_rooms_count: Optional[int]) -> float:
|
||||||
return _LIVING_AREA_FRACTION_MIN
|
return _LIVING_AREA_FRACTION_MIN
|
||||||
|
|
||||||
|
|
||||||
def _g_perpendicular(w: SapWindow) -> float:
|
|
||||||
"""Solar transmittance at normal incidence per SAP 10.3 Table 6b.
|
|
||||||
Prefer the cert's measured value (`window_transmission_details`);
|
|
||||||
otherwise look up by `glazing_type`."""
|
|
||||||
if w.window_transmission_details is not None:
|
|
||||||
return float(w.window_transmission_details.solar_transmittance)
|
|
||||||
if isinstance(w.glazing_type, int) and w.glazing_type in _G_PERPENDICULAR_BY_GLAZING_TYPE:
|
|
||||||
return _G_PERPENDICULAR_BY_GLAZING_TYPE[w.glazing_type]
|
|
||||||
return _G_PERPENDICULAR_DEFAULT
|
|
||||||
|
|
||||||
|
|
||||||
def _frame_factor(w: SapWindow) -> float:
|
|
||||||
"""SAP 10.3 Table 6c frame factor. Prefer the cert's measured value
|
|
||||||
(`SapWindow.frame_factor`); otherwise look up by `frame_material`."""
|
|
||||||
if w.frame_factor is not None:
|
|
||||||
return float(w.frame_factor)
|
|
||||||
material = (w.frame_material or "").lower()
|
|
||||||
for needle, ff in _FRAME_FACTOR_BY_MATERIAL:
|
|
||||||
if needle in material:
|
|
||||||
return ff
|
|
||||||
return _FRAME_FACTOR_DEFAULT
|
|
||||||
|
|
||||||
|
|
||||||
def _window_inputs(windows: list[SapWindow]) -> tuple[WindowInput, ...]:
|
|
||||||
"""Map each cert window with a known SAP octant to a `WindowInput`.
|
|
||||||
|
|
||||||
`g_perpendicular` follows SAP 10.3 Table 6b (by glazing_type when no
|
|
||||||
measured transmission details), `frame_factor` follows Table 6c (by
|
|
||||||
frame_material when no measured value). Overshading factor stays at
|
|
||||||
SAP's "average" default 0.77 because the cert doesn't lodge a per-
|
|
||||||
window overshading code in RdSAP 10. Pitch = 90° (vertical windows).
|
|
||||||
Windows whose orientation isn't in 1-8 are dropped, matching the
|
|
||||||
live ML feature-builder convention.
|
|
||||||
"""
|
|
||||||
out: list[WindowInput] = []
|
|
||||||
for w in windows:
|
|
||||||
orientation_code = w.orientation if isinstance(w.orientation, int) else None
|
|
||||||
if orientation_code is None or orientation_code not in _ORIENTATION_BY_CODE:
|
|
||||||
continue
|
|
||||||
area = float(w.window_width) * float(w.window_height)
|
|
||||||
out.append(
|
|
||||||
WindowInput(
|
|
||||||
area_m2=area,
|
|
||||||
orientation=_ORIENTATION_BY_CODE[orientation_code],
|
|
||||||
pitch_deg=90.0,
|
|
||||||
g_perpendicular=_g_perpendicular(w),
|
|
||||||
frame_factor=_frame_factor(w),
|
|
||||||
overshading_factor=0.77,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return tuple(out)
|
|
||||||
|
|
||||||
|
|
||||||
def _window_total_area_and_avg_u(windows: list[SapWindow]) -> tuple[float, Optional[float]]:
|
def _window_total_area_and_avg_u(windows: list[SapWindow]) -> tuple[float, Optional[float]]:
|
||||||
"""Area-weighted total + U-value for the conduction worksheet."""
|
"""Area-weighted total + U-value for the conduction worksheet."""
|
||||||
if not windows:
|
if not windows:
|
||||||
|
|
@ -1009,7 +906,6 @@ def cert_to_inputs(
|
||||||
overshading=_INTERNAL_GAINS_DEFAULT_OVERSHADING,
|
overshading=_INTERNAL_GAINS_DEFAULT_OVERSHADING,
|
||||||
).total_solar_gains_monthly_w,
|
).total_solar_gains_monthly_w,
|
||||||
region=_region_index(epc.region_code),
|
region=_region_index(epc.region_code),
|
||||||
windows=_window_inputs(epc.sap_windows),
|
|
||||||
control_type=_control_type(main),
|
control_type=_control_type(main),
|
||||||
responsiveness=_responsiveness(main),
|
responsiveness=_responsiveness(main),
|
||||||
living_area_fraction=_living_area_fraction(epc.habitable_rooms_count),
|
living_area_fraction=_living_area_fraction(epc.habitable_rooms_count),
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,6 @@ from domain.ml.tests._fixtures import (
|
||||||
)
|
)
|
||||||
from domain.sap.calculator import Sap10Calculator, SapResult
|
from domain.sap.calculator import Sap10Calculator, SapResult
|
||||||
from domain.sap.rdsap.cert_to_inputs import cert_to_inputs
|
from domain.sap.rdsap.cert_to_inputs import cert_to_inputs
|
||||||
from domain.sap.worksheet.solar_gains import Orientation
|
|
||||||
|
|
||||||
|
|
||||||
def _gas_boiler_detail(sap_main_heating_code: int = 102) -> MainHeatingDetail:
|
def _gas_boiler_detail(sap_main_heating_code: int = 102) -> MainHeatingDetail:
|
||||||
|
|
@ -319,22 +318,6 @@ def test_calculator_always_uses_uk_average_weather_for_rating() -> None:
|
||||||
assert inputs_default.region == 0
|
assert inputs_default.region == 0
|
||||||
|
|
||||||
|
|
||||||
def test_window_orientation_codes_map_to_solar_gains_orientation_enum() -> None:
|
|
||||||
# Arrange — SAP10 octant codes 1-8 (1=N, 5=S) must surface in the
|
|
||||||
# mapped WindowInput list as the matching `Orientation` enum members.
|
|
||||||
base = _typical_semi_detached_epc()
|
|
||||||
|
|
||||||
# Act
|
|
||||||
inputs = cert_to_inputs(base)
|
|
||||||
|
|
||||||
# Assert — south + north window from the fixture both land.
|
|
||||||
orientations = {w.orientation for w in inputs.windows}
|
|
||||||
assert orientations == {Orientation.S, Orientation.N}
|
|
||||||
south = next(w for w in inputs.windows if w.orientation == Orientation.S)
|
|
||||||
assert south.area_m2 == 2.0 * 1.2 # width × height from fixture
|
|
||||||
assert south.pitch_deg == 90.0
|
|
||||||
|
|
||||||
|
|
||||||
def test_open_chimneys_raise_infiltration_ach() -> None:
|
def test_open_chimneys_raise_infiltration_ach() -> None:
|
||||||
# Arrange — Direction check: chimneys add Table 2.1 volume to the
|
# Arrange — Direction check: chimneys add Table 2.1 volume to the
|
||||||
# infiltration calc, so an otherwise identical dwelling with 2 open
|
# infiltration calc, so an otherwise identical dwelling with 2 open
|
||||||
|
|
@ -464,42 +447,6 @@ def test_main_heating_control_code_maps_to_sap_control_type() -> None:
|
||||||
assert type_3.control_type == 3
|
assert type_3.control_type == 3
|
||||||
|
|
||||||
|
|
||||||
def test_window_g_perpendicular_uses_table_6b_by_glazing_type() -> None:
|
|
||||||
# Arrange — SAP 10.3 Table 6b: g⊥ depends on the glazing type when
|
|
||||||
# transmission details aren't measured. Single (code 1) → 0.85;
|
|
||||||
# double low-E soft coat (code 4) → 0.63; triple (code 6) → 0.68.
|
|
||||||
single = make_window(orientation=5, glazing_type=1, frame_material="PVC")
|
|
||||||
triple = make_window(orientation=5, glazing_type=6, frame_material="PVC")
|
|
||||||
low_e_double = make_window(orientation=5, glazing_type=4, frame_material="PVC")
|
|
||||||
base = _typical_semi_detached_epc()
|
|
||||||
base.sap_windows = [single, triple, low_e_double]
|
|
||||||
|
|
||||||
# Act
|
|
||||||
inputs = cert_to_inputs(base)
|
|
||||||
|
|
||||||
# Assert — same orientation, three different glazing types.
|
|
||||||
g_values = sorted(w.g_perpendicular for w in inputs.windows)
|
|
||||||
assert g_values == [0.63, 0.68, 0.85]
|
|
||||||
|
|
||||||
|
|
||||||
def test_window_frame_factor_uses_table_6c_by_frame_material() -> None:
|
|
||||||
# Arrange — Table 6c: PVC/Wood = 0.70; aluminium / steel = 0.83
|
|
||||||
# (no thermal break). Metal-with-thermal-break would be 0.80 but
|
|
||||||
# not tested here since cert strings rarely carry that distinction.
|
|
||||||
pvc = make_window(orientation=5, frame_material="PVC")
|
|
||||||
wood = make_window(orientation=5, frame_material="Wood")
|
|
||||||
aluminium = make_window(orientation=5, frame_material="Aluminium")
|
|
||||||
base = _typical_semi_detached_epc()
|
|
||||||
base.sap_windows = [pvc, wood, aluminium]
|
|
||||||
|
|
||||||
# Act
|
|
||||||
inputs = cert_to_inputs(base)
|
|
||||||
|
|
||||||
# Assert
|
|
||||||
ff_values = sorted(w.frame_factor for w in inputs.windows)
|
|
||||||
assert ff_values == [0.70, 0.70, 0.83]
|
|
||||||
|
|
||||||
|
|
||||||
def test_off_peak_meter_routes_electric_costs_to_low_rate() -> None:
|
def test_off_peak_meter_routes_electric_costs_to_low_rate() -> None:
|
||||||
# Arrange — RdSAP rule (per S-B15): we trust the cert's lodged
|
# Arrange — RdSAP rule (per S-B15): we trust the cert's lodged
|
||||||
# meter_type as the tariff source of truth. SAP10 code 2 = off-peak
|
# meter_type as the tariff source of truth. SAP10 code 2 = off-peak
|
||||||
|
|
|
||||||
|
|
@ -25,13 +25,10 @@ import pytest
|
||||||
|
|
||||||
from domain.sap.calculator import (
|
from domain.sap.calculator import (
|
||||||
CalculatorInputs,
|
CalculatorInputs,
|
||||||
WindowInput,
|
|
||||||
_solar_gains_w,
|
|
||||||
calculate_sap_from_inputs,
|
calculate_sap_from_inputs,
|
||||||
)
|
)
|
||||||
from domain.sap.worksheet.dimensions import Dimensions
|
from domain.sap.worksheet.dimensions import Dimensions
|
||||||
from domain.sap.worksheet.heat_transmission import HeatTransmission
|
from domain.sap.worksheet.heat_transmission import HeatTransmission
|
||||||
from domain.sap.worksheet.solar_gains import Orientation
|
|
||||||
|
|
||||||
|
|
||||||
def _baseline_dwelling() -> CalculatorInputs:
|
def _baseline_dwelling() -> CalculatorInputs:
|
||||||
|
|
@ -63,34 +60,19 @@ def _baseline_dwelling() -> CalculatorInputs:
|
||||||
total_external_element_area_m2=200.0, # synthetic placeholder
|
total_external_element_area_m2=200.0, # synthetic placeholder
|
||||||
total_w_per_k=150.0,
|
total_w_per_k=150.0,
|
||||||
)
|
)
|
||||||
windows = (
|
|
||||||
WindowInput(
|
|
||||||
area_m2=4.0,
|
|
||||||
orientation=Orientation.S,
|
|
||||||
pitch_deg=90.0,
|
|
||||||
g_perpendicular=0.63,
|
|
||||||
frame_factor=0.7,
|
|
||||||
overshading_factor=0.77,
|
|
||||||
),
|
|
||||||
WindowInput(
|
|
||||||
area_m2=4.0,
|
|
||||||
orientation=Orientation.N,
|
|
||||||
pitch_deg=90.0,
|
|
||||||
g_perpendicular=0.63,
|
|
||||||
frame_factor=0.7,
|
|
||||||
overshading_factor=0.77,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return CalculatorInputs(
|
return CalculatorInputs(
|
||||||
dimensions=dim,
|
dimensions=dim,
|
||||||
heat_transmission=ht,
|
heat_transmission=ht,
|
||||||
monthly_infiltration_ach=(0.7,) * 12,
|
monthly_infiltration_ach=(0.7,) * 12,
|
||||||
internal_gains_monthly_w=(450.0,) * 12,
|
internal_gains_monthly_w=(450.0,) * 12,
|
||||||
solar_gains_monthly_w=tuple(
|
# Hand-computed solar (S + N 4 m² panes, g⊥=0.63 FF=0.7 Z=0.77,
|
||||||
_solar_gains_w(windows=windows, region=0, month=m) for m in range(1, 13)
|
# 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,
|
||||||
),
|
),
|
||||||
region=0,
|
region=0,
|
||||||
windows=windows,
|
|
||||||
control_type=2,
|
control_type=2,
|
||||||
responsiveness=1.0,
|
responsiveness=1.0,
|
||||||
living_area_fraction=0.30,
|
living_area_fraction=0.30,
|
||||||
|
|
|
||||||
|
|
@ -23,13 +23,10 @@ import pytest
|
||||||
from domain.sap.calculator import (
|
from domain.sap.calculator import (
|
||||||
CalculatorInputs,
|
CalculatorInputs,
|
||||||
SapResult,
|
SapResult,
|
||||||
WindowInput,
|
|
||||||
_solar_gains_w,
|
|
||||||
calculate_sap_from_inputs,
|
calculate_sap_from_inputs,
|
||||||
)
|
)
|
||||||
from domain.sap.worksheet.dimensions import Dimensions
|
from domain.sap.worksheet.dimensions import Dimensions
|
||||||
from domain.sap.worksheet.heat_transmission import HeatTransmission
|
from domain.sap.worksheet.heat_transmission import HeatTransmission
|
||||||
from domain.sap.worksheet.solar_gains import Orientation
|
|
||||||
|
|
||||||
|
|
||||||
def _baseline_inputs() -> CalculatorInputs:
|
def _baseline_inputs() -> CalculatorInputs:
|
||||||
|
|
@ -59,24 +56,6 @@ def _baseline_inputs() -> CalculatorInputs:
|
||||||
total_external_element_area_m2=200.0, # synthetic placeholder
|
total_external_element_area_m2=200.0, # synthetic placeholder
|
||||||
total_w_per_k=150.0,
|
total_w_per_k=150.0,
|
||||||
)
|
)
|
||||||
windows = (
|
|
||||||
WindowInput(
|
|
||||||
area_m2=4.0,
|
|
||||||
orientation=Orientation.S,
|
|
||||||
pitch_deg=90.0,
|
|
||||||
g_perpendicular=0.63,
|
|
||||||
frame_factor=0.7,
|
|
||||||
overshading_factor=0.77,
|
|
||||||
),
|
|
||||||
WindowInput(
|
|
||||||
area_m2=4.0,
|
|
||||||
orientation=Orientation.N,
|
|
||||||
pitch_deg=90.0,
|
|
||||||
g_perpendicular=0.63,
|
|
||||||
frame_factor=0.7,
|
|
||||||
overshading_factor=0.77,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return CalculatorInputs(
|
return CalculatorInputs(
|
||||||
dimensions=dim,
|
dimensions=dim,
|
||||||
heat_transmission=ht,
|
heat_transmission=ht,
|
||||||
|
|
@ -85,11 +64,15 @@ def _baseline_inputs() -> CalculatorInputs:
|
||||||
# per-month variation lives in §5 orchestrator output; tracer
|
# per-month variation lives in §5 orchestrator output; tracer
|
||||||
# tests don't need the modulation to verify the SAP loop.
|
# tests don't need the modulation to verify the SAP loop.
|
||||||
internal_gains_monthly_w=(450.0,) * 12,
|
internal_gains_monthly_w=(450.0,) * 12,
|
||||||
solar_gains_monthly_w=tuple(
|
# Hand-computed solar (S + N 4 m² panes, g⊥=0.63 FF=0.7 Z=0.77,
|
||||||
_solar_gains_w(windows=windows, region=0, month=m) for m in range(1, 13)
|
# 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,
|
||||||
),
|
),
|
||||||
region=0,
|
region=0,
|
||||||
windows=windows,
|
|
||||||
control_type=2,
|
control_type=2,
|
||||||
responsiveness=1.0,
|
responsiveness=1.0,
|
||||||
living_area_fraction=0.30,
|
living_area_fraction=0.30,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue