mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
Slice 26b: §6 solar gains cascade pin + SapRoofWindow solar attrs
Added `solar_gains_section_from_cert` and 12 strict pin cases for §6 LINE_83 (total solar W) and LINE_84 (total internal + solar gains). Extended SapRoofWindow with the solar attrs needed for line (82) roof- window monthly gain: `orientation` (SAP10.2 code 1..8), `pitch_deg`, `g_perpendicular`, `frame_factor`. Defaults match the modal RdSAP roof window (45° pitch, DG g⊥=0.76, PVC FF=0.70, N). 000516 lodges orientation=2 (NE) + pitch=45 from the U985 cert. Plumbed `_roof_windows_for_solar_gains` through both `solar_gains_ section_from_cert` and the internal `cert_to_inputs` cascade so the production §6 cascade now picks up 000516's NE roof window contribution to (82). Exposed `ORIENTATION_BY_SAP10_CODE` from solar_gains for the SAP10.2 code → Orientation enum mapping the cascade needs. §6 cascade (LINE_83 monthly): fixture | LINE_83 | LINE_84 000474 | ✓ | ✓ 000477 | ✓ | ✗ (cascaded §4 LINE_65 → §5 LINE_72/73) 000480 | ✓ | ✓ 000487 | ✓ | ✗ (cascaded HW lodgement defect, slice 25) 000490 | ✓ | ✓ 000516 | ✓ | ✓ (roof window now feeding (82)) Scoreboard: section_cascade_pins: 220 → 230 PASS (+10; 12 new tests, 2 fail) e2e SapResult: 30 → 32 PASS (+2, downstream of §6 closure) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
9cb79d9c98
commit
1e9654ce28
6 changed files with 155 additions and 24 deletions
|
|
@ -172,16 +172,25 @@ class WindowTransmissionDetails:
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class SapRoofWindow:
|
class SapRoofWindow:
|
||||||
"""RdSAP10 §3 worksheet line (27a) — a pitched roof window cut into a
|
"""RdSAP10 worksheet roof window — feeds §3 (27a) heat transmission
|
||||||
storey-below roof. Heat-transmission contribution is A × U_eff where
|
and §6 (82) solar gain. Heat-transmission contribution is A × U_eff
|
||||||
U_eff applies the SAP10.2 §3.2 curtain resistance (R=0.04 m²K/W) to
|
where U_eff applies the SAP10.2 §3.2 curtain resistance (R=0.04
|
||||||
`u_value_raw`. Roof windows draw their U-value from RdSAP 10 Table 24
|
m²K/W) to `u_value_raw`. Roof windows draw their U-value from RdSAP
|
||||||
(p.50/113) "Roof window" column — distinct from the standard-window
|
10 Table 24 (p.50/113) "Roof window" column (e.g. double-glazed roof
|
||||||
column (e.g. double-glazed roof window U=3.4, vs 2.8 for standard).
|
window U=3.4 vs 2.8 for standard).
|
||||||
|
|
||||||
|
Solar fields (orientation, pitch, g_perpendicular, frame_factor)
|
||||||
|
feed `solar_gains_from_cert` — defaults match the modal RdSAP roof
|
||||||
|
window (45° pitch, manufacturer-default DG g⊥=0.76, PVC FF=0.70,
|
||||||
|
N-facing) and are intended to be overridden per-fixture.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
area_m2: float
|
area_m2: float
|
||||||
u_value_raw: float # RdSAP10 Table 24 roof-window column, pre-curtain.
|
u_value_raw: float # RdSAP10 Table 24 roof-window column, pre-curtain.
|
||||||
|
orientation: int = 1 # SAP10.2 code: 1=N, 2=NE, 3=E, 4=SE, 5=S, 6=SW, 7=W, 8=NW.
|
||||||
|
pitch_deg: float = 45.0
|
||||||
|
g_perpendicular: float = 0.76
|
||||||
|
frame_factor: float = 0.70
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|
|
||||||
|
|
@ -133,12 +133,12 @@ Two test files contain the strict pins:
|
||||||
Total: **169 PASS / 83 FAIL** across the strict pins. 4 of 6 fixtures fully
|
Total: **169 PASS / 83 FAIL** across the strict pins. 4 of 6 fixtures fully
|
||||||
close §1+§2+§4. 000487 is the worst (RR fixture defect propagates everywhere).
|
close §1+§2+§4. 000487 is the worst (RR fixture defect propagates everywhere).
|
||||||
|
|
||||||
(Post-slice-26: section_cascade_pins 220 PASS / 20 FAIL, e2e SapResult
|
(Post-slice-26b: section_cascade_pins 230 PASS / 22 FAIL, e2e SapResult
|
||||||
30 PASS / 42 FAIL. §3 + §5 fully close for 5 of 6 fixtures at abs=1e-4.
|
32 PASS / 40 FAIL. §3 + §5 + §6 fully close for 5 of 6 fixtures at
|
||||||
Remaining cascade failures: §4 monthly (000477/487 HW defects, slice 25),
|
abs=1e-4. Remaining cascade failures: §4 monthly (000477/487 HW defects,
|
||||||
§3 + §5 LINE_72/73 (000487 RR + 000477 LINE_61 cascade defects, slice 25),
|
slice 25), §5 LINE_72/73 + §6 LINE_84 on 000477/487 (cascaded from §4),
|
||||||
and downstream SapResult pins still drifting because of §6–§9a precision
|
§3 (000487 RR defect, slice 25), and downstream SapResult pins still
|
||||||
not yet pinned.)
|
drifting because of §7–§9a precision not yet pinned.)
|
||||||
|
|
||||||
### B.2 SapResult pin matrix (post-slice-22/23)
|
### B.2 SapResult pin matrix (post-slice-22/23)
|
||||||
|
|
||||||
|
|
@ -199,6 +199,7 @@ fixture | section §4 pin status
|
||||||
### B.5 Recent slices (in reverse order — newest first)
|
### B.5 Recent slices (in reverse order — newest first)
|
||||||
|
|
||||||
```
|
```
|
||||||
|
Slice 26b: §6 solar gains cascade pin (12 cases, 10 PASS) + SapRoofWindow solar attrs + plumb to §6 cascade
|
||||||
Slice 26: §5 internal gains cascade pin (54 cases, 50 PASS / 4 FAIL) + rooflight plumb to daylight factor
|
Slice 26: §5 internal gains cascade pin (54 cases, 50 PASS / 4 FAIL) + rooflight plumb to daylight factor
|
||||||
Slice 27b: §3 element-area + door-area rounding to 2 d.p. per RdSAP10 §15 (p.66)
|
Slice 27b: §3 element-area + door-area rounding to 2 d.p. per RdSAP10 §15 (p.66)
|
||||||
Slice 27: BS EN ISO 13370 floor U rounded to 2 d.p. per RdSAP10 §5.12
|
Slice 27: BS EN ISO 13370 floor U rounded to 2 d.p. per RdSAP10 §5.12
|
||||||
|
|
@ -266,7 +267,7 @@ The cascade pin work continues in worksheet order. For each section:
|
||||||
|
|
||||||
Sections still to pin:
|
Sections still to pin:
|
||||||
- ~~**§5 internal gains** (lines 66-73 + 232 lighting kWh)~~ DONE (slice 26)
|
- ~~**§5 internal gains** (lines 66-73 + 232 lighting kWh)~~ DONE (slice 26)
|
||||||
- **§6 solar gains** (lines 83-84). 2 monthly tuples.
|
- ~~**§6 solar gains** (lines 83-84)~~ DONE (slice 26b — 5/6 fixtures close, 000477/487 cascade from §4)
|
||||||
- **§7 mean internal temperature** (lines 85-94). 10 line refs, mostly monthly.
|
- **§7 mean internal temperature** (lines 85-94). 10 line refs, mostly monthly.
|
||||||
- **§8 space heating** (lines 95-99). 4 monthly + 2 annual.
|
- **§8 space heating** (lines 95-99). 4 monthly + 2 annual.
|
||||||
- **§9a energy requirements** (lines 201, 206-208, 211-215, 219). 5 scalar + 2
|
- **§9a energy requirements** (lines 201, 206-208, 211-215, 219). 5 scalar + 2
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,12 @@ from domain.sap.worksheet.internal_gains import (
|
||||||
OvershadingCategory,
|
OvershadingCategory,
|
||||||
internal_gains_from_cert,
|
internal_gains_from_cert,
|
||||||
)
|
)
|
||||||
|
from domain.sap.worksheet.solar_gains import (
|
||||||
|
ORIENTATION_BY_SAP10_CODE,
|
||||||
|
RoofWindowInput,
|
||||||
|
SolarGainsResult,
|
||||||
|
solar_gains_from_cert,
|
||||||
|
)
|
||||||
from domain.sap.worksheet.heat_transmission import (
|
from domain.sap.worksheet.heat_transmission import (
|
||||||
DwellingExposure,
|
DwellingExposure,
|
||||||
HeatTransmission,
|
HeatTransmission,
|
||||||
|
|
@ -84,7 +90,6 @@ from domain.sap.climate.appendix_u import external_temperature_c
|
||||||
from domain.sap.worksheet.mean_internal_temperature import (
|
from domain.sap.worksheet.mean_internal_temperature import (
|
||||||
mean_internal_temperature_monthly,
|
mean_internal_temperature_monthly,
|
||||||
)
|
)
|
||||||
from domain.sap.worksheet.solar_gains import solar_gains_from_cert
|
|
||||||
from domain.sap.worksheet.energy_requirements import (
|
from domain.sap.worksheet.energy_requirements import (
|
||||||
EnergyRequirementsResult,
|
EnergyRequirementsResult,
|
||||||
space_heating_fuel_monthly_kwh,
|
space_heating_fuel_monthly_kwh,
|
||||||
|
|
@ -798,6 +803,51 @@ def internal_gains_section_from_cert(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _roof_windows_for_solar_gains(
|
||||||
|
epc: EpcPropertyData,
|
||||||
|
) -> tuple[RoofWindowInput, ...]:
|
||||||
|
"""Convert `epc.sap_roof_windows` (SapRoofWindow) to the §6 calc's
|
||||||
|
`RoofWindowInput` tuple — projecting area + orientation + pitch +
|
||||||
|
g_perp + frame_factor for line (82) monthly solar gain.
|
||||||
|
|
||||||
|
Roof-window U-value lives on SapRoofWindow but doesn't flow into §6;
|
||||||
|
it's a §3 (27a) heat-transmission input handled by
|
||||||
|
`heat_transmission_from_cert` separately."""
|
||||||
|
return tuple(
|
||||||
|
RoofWindowInput(
|
||||||
|
area_m2=float(rw.area_m2),
|
||||||
|
orientation=ORIENTATION_BY_SAP10_CODE.get(
|
||||||
|
rw.orientation, list(ORIENTATION_BY_SAP10_CODE.values())[0]
|
||||||
|
),
|
||||||
|
g_perpendicular=float(rw.g_perpendicular),
|
||||||
|
frame_factor=float(rw.frame_factor),
|
||||||
|
pitch_deg=float(rw.pitch_deg),
|
||||||
|
)
|
||||||
|
for rw in epc.sap_roof_windows or []
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def solar_gains_section_from_cert(epc: EpcPropertyData) -> SolarGainsResult:
|
||||||
|
"""SAP 10.2 §6 cert→inputs cascade for `solar_gains_from_cert`.
|
||||||
|
|
||||||
|
Returns the full `SolarGainsResult` (every (74)..(83) per-orientation
|
||||||
|
line ref + (82)/(82a) roof-window/rooflight monthly tuples) computed
|
||||||
|
from the cert's `sap_windows` (vertical wall windows) and
|
||||||
|
`sap_roof_windows` (pitched roof windows for line (82)) at default
|
||||||
|
AVERAGE overshading and UK-average region (matches cert_to_inputs'
|
||||||
|
internal cascade for the SAP-rating pass).
|
||||||
|
|
||||||
|
Rooflights (horizontal Z=1.0 glazing) are not yet lodged on the cert
|
||||||
|
datatype distinct from roof windows — they pass through as empty.
|
||||||
|
"""
|
||||||
|
return solar_gains_from_cert(
|
||||||
|
epc=epc,
|
||||||
|
region=_region_index(epc.region_code),
|
||||||
|
overshading=_INTERNAL_GAINS_DEFAULT_OVERSHADING,
|
||||||
|
roof_windows=_roof_windows_for_solar_gains(epc),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def ventilation_from_cert(epc: EpcPropertyData) -> VentilationResult:
|
def ventilation_from_cert(epc: EpcPropertyData) -> VentilationResult:
|
||||||
"""SAP 10.2 §2 cert→inputs cascade for `ventilation_from_inputs`.
|
"""SAP 10.2 §2 cert→inputs cascade for `ventilation_from_inputs`.
|
||||||
|
|
||||||
|
|
@ -1266,6 +1316,7 @@ def cert_to_inputs(
|
||||||
epc=epc,
|
epc=epc,
|
||||||
region=_region_index(epc.region_code),
|
region=_region_index(epc.region_code),
|
||||||
overshading=_INTERNAL_GAINS_DEFAULT_OVERSHADING,
|
overshading=_INTERNAL_GAINS_DEFAULT_OVERSHADING,
|
||||||
|
roof_windows=_roof_windows_for_solar_gains(epc),
|
||||||
).total_solar_gains_monthly_w
|
).total_solar_gains_monthly_w
|
||||||
|
|
||||||
# SAP10.2 §7 — compose (93)m + (94)m via the orchestrator. Per-month HTC
|
# SAP10.2 §7 — compose (93)m + (94)m via the orchestrator. Per-month HTC
|
||||||
|
|
|
||||||
|
|
@ -189,7 +189,7 @@ _FRAME_FACTOR_DEFAULT: Final[float] = 0.7
|
||||||
# SAP10 octant code → Orientation enum. Cert windows with a code outside 1..8
|
# SAP10 octant code → Orientation enum. Cert windows with a code outside 1..8
|
||||||
# (e.g. 0, "NR") are dropped — no solar gain contribution, mirroring the
|
# (e.g. 0, "NR") are dropped — no solar gain contribution, mirroring the
|
||||||
# legacy `cert_to_inputs._window_inputs` shortcut.
|
# legacy `cert_to_inputs._window_inputs` shortcut.
|
||||||
_ORIENTATION_BY_SAP10_CODE: Final[dict[int, Orientation]] = {
|
ORIENTATION_BY_SAP10_CODE: Final[dict[int, Orientation]] = {
|
||||||
1: Orientation.N,
|
1: Orientation.N,
|
||||||
2: Orientation.NE,
|
2: Orientation.NE,
|
||||||
3: Orientation.E,
|
3: Orientation.E,
|
||||||
|
|
@ -283,8 +283,8 @@ def _frame_factor(w: SapWindow) -> float:
|
||||||
|
|
||||||
def _orientation(w: SapWindow) -> Orientation | None:
|
def _orientation(w: SapWindow) -> Orientation | None:
|
||||||
"""Map cert `orientation` code (1..8) to enum; None for unmapped."""
|
"""Map cert `orientation` code (1..8) to enum; None for unmapped."""
|
||||||
if isinstance(w.orientation, int) and w.orientation in _ORIENTATION_BY_SAP10_CODE:
|
if isinstance(w.orientation, int) and w.orientation in ORIENTATION_BY_SAP10_CODE:
|
||||||
return _ORIENTATION_BY_SAP10_CODE[w.orientation]
|
return ORIENTATION_BY_SAP10_CODE[w.orientation]
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -131,13 +131,23 @@ def build_epc() -> EpcPropertyData:
|
||||||
door_count=2,
|
door_count=2,
|
||||||
# 000516 PDF (27a) — single 1.18 m² roof window cut into the
|
# 000516 PDF (27a) — single 1.18 m² roof window cut into the
|
||||||
# storey-below "External roof Main". Cert lodges it as
|
# storey-below "External roof Main". Cert lodges it as
|
||||||
# "Manufacturer, Roof Window, Double glazed, FF=0.70" without a
|
# "Manufacturer, Roof Window, Double glazed, FF=0.70" with
|
||||||
# measured U; raw U=3.40 from RdSAP10 Table 24 (p.50/113) "Roof
|
# orientation NE and pitch 45° (see U985 txt line 49). Raw U=3.40
|
||||||
# window" column. After SAP10.2 §3.2 curtain transform (R=0.04
|
# from RdSAP10 Table 24 (p.50/113) "Roof window" column. After
|
||||||
# m²K/W): U_eff = 1/(1/3.40 + 0.04) = 2.9930, matching the PDF
|
# SAP10.2 §3.2 curtain transform (R=0.04 m²K/W): U_eff =
|
||||||
# (27a) lodgement exactly. Contribution = 1.18 × 2.9930 = 3.5317
|
# 1/(1/3.40 + 0.04) = 2.9930, matching the PDF (27a) lodgement
|
||||||
# W/K.
|
# exactly. Heat-transmission contribution = 1.18 × 2.9930 = 3.5317
|
||||||
sap_roof_windows=[SapRoofWindow(area_m2=1.18, u_value_raw=3.40)],
|
# W/K. Solar attrs feed §6 (82) roof-window monthly tuple.
|
||||||
|
sap_roof_windows=[
|
||||||
|
SapRoofWindow(
|
||||||
|
area_m2=1.18,
|
||||||
|
u_value_raw=3.40,
|
||||||
|
orientation=2, # NE
|
||||||
|
pitch_deg=45.0,
|
||||||
|
g_perpendicular=0.76,
|
||||||
|
frame_factor=0.70,
|
||||||
|
),
|
||||||
|
],
|
||||||
percent_draughtproofed=75,
|
percent_draughtproofed=75,
|
||||||
low_energy_fixed_lighting_bulbs_count=SECTION_5_BULB_COUNT_LEL,
|
low_energy_fixed_lighting_bulbs_count=SECTION_5_BULB_COUNT_LEL,
|
||||||
sap_windows=list(SECTION_6_VERTICAL_WINDOWS),
|
sap_windows=list(SECTION_6_VERTICAL_WINDOWS),
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ from domain.sap.rdsap.cert_to_inputs import (
|
||||||
cert_to_inputs,
|
cert_to_inputs,
|
||||||
heat_transmission_section_from_cert,
|
heat_transmission_section_from_cert,
|
||||||
internal_gains_section_from_cert,
|
internal_gains_section_from_cert,
|
||||||
|
solar_gains_section_from_cert,
|
||||||
ventilation_from_cert,
|
ventilation_from_cert,
|
||||||
water_heating_section_from_cert,
|
water_heating_section_from_cert,
|
||||||
)
|
)
|
||||||
|
|
@ -379,3 +380,62 @@ def test_section_5_line_232_lighting_kwh_per_yr_matches_pdf(
|
||||||
expected,
|
expected,
|
||||||
f"§5 (232) {fixture_name}",
|
f"§5 (232) {fixture_name}",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# §6 Solar gains — LINE_83 monthly total
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("fixture_name", list(_FIXTURES), ids=lambda x: x)
|
||||||
|
def test_section_6_line_83_total_solar_gains_match_pdf(
|
||||||
|
fixture_name: str,
|
||||||
|
) -> None:
|
||||||
|
"""§6 (83) monthly — total solar gains W per month, summed across
|
||||||
|
per-orientation contributions (74)..(81) + (82) roof windows +
|
||||||
|
(82a) rooflights.
|
||||||
|
|
||||||
|
Roof windows and rooflights pass through the cert cascade as empty
|
||||||
|
today — `SapRoofWindow` carries only area + raw U (slice 24) so
|
||||||
|
000516's NE roof window doesn't contribute to (82) here. A future
|
||||||
|
slice should extend SapRoofWindow with orientation/pitch/g_perp/
|
||||||
|
frame_factor; until then this pin fails on 000516 specifically.
|
||||||
|
"""
|
||||||
|
# Arrange
|
||||||
|
mod = _FIXTURES[fixture_name]
|
||||||
|
epc = mod.build_epc() # type: ignore[attr-defined]
|
||||||
|
expected = mod.LINE_83_M_TOTAL_SOLAR_W # type: ignore[attr-defined]
|
||||||
|
|
||||||
|
# Act
|
||||||
|
sg = solar_gains_section_from_cert(epc)
|
||||||
|
actual = sg.total_solar_gains_monthly_w
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
for m in range(12):
|
||||||
|
_pin(actual[m], expected[m], f"§6 (83)[{m+1}] {fixture_name}")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("fixture_name", list(_FIXTURES), ids=lambda x: x)
|
||||||
|
def test_section_6_line_84_total_gains_match_pdf(
|
||||||
|
fixture_name: str,
|
||||||
|
) -> None:
|
||||||
|
"""§6 (84) monthly — total internal + solar gains. Arithmetic identity
|
||||||
|
LINE_73 + LINE_83 = LINE_84, asserted on the cascade to catch any
|
||||||
|
drift between the §5 and §6 cascades."""
|
||||||
|
# Arrange
|
||||||
|
mod = _FIXTURES[fixture_name]
|
||||||
|
epc = mod.build_epc() # type: ignore[attr-defined]
|
||||||
|
expected = mod.LINE_84_M_TOTAL_GAINS_W # type: ignore[attr-defined]
|
||||||
|
|
||||||
|
# Act
|
||||||
|
ig = internal_gains_section_from_cert(epc)
|
||||||
|
sg = solar_gains_section_from_cert(epc)
|
||||||
|
assert ig is not None, f"{fixture_name}: internal_gains_from_cert returned None"
|
||||||
|
actual = tuple(
|
||||||
|
ig.total_internal_gains_monthly_w[m] + sg.total_solar_gains_monthly_w[m]
|
||||||
|
for m in range(12)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
for m in range(12):
|
||||||
|
_pin(actual[m], expected[m], f"§6 (84)[{m+1}] {fixture_name}")
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue