mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +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
|
||||
class SapRoofWindow:
|
||||
"""RdSAP10 §3 worksheet line (27a) — a pitched roof window cut into a
|
||||
storey-below roof. Heat-transmission contribution is A × U_eff where
|
||||
U_eff applies the SAP10.2 §3.2 curtain resistance (R=0.04 m²K/W) to
|
||||
`u_value_raw`. Roof windows draw their U-value from RdSAP 10 Table 24
|
||||
(p.50/113) "Roof window" column — distinct from the standard-window
|
||||
column (e.g. double-glazed roof window U=3.4, vs 2.8 for standard).
|
||||
"""RdSAP10 worksheet roof window — feeds §3 (27a) heat transmission
|
||||
and §6 (82) solar gain. Heat-transmission contribution is A × U_eff
|
||||
where U_eff applies the SAP10.2 §3.2 curtain resistance (R=0.04
|
||||
m²K/W) to `u_value_raw`. Roof windows draw their U-value from RdSAP
|
||||
10 Table 24 (p.50/113) "Roof window" column (e.g. double-glazed roof
|
||||
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
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
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
|
||||
30 PASS / 42 FAIL. §3 + §5 fully close for 5 of 6 fixtures at abs=1e-4.
|
||||
Remaining cascade failures: §4 monthly (000477/487 HW defects, slice 25),
|
||||
§3 + §5 LINE_72/73 (000487 RR + 000477 LINE_61 cascade defects, slice 25),
|
||||
and downstream SapResult pins still drifting because of §6–§9a precision
|
||||
not yet pinned.)
|
||||
(Post-slice-26b: section_cascade_pins 230 PASS / 22 FAIL, e2e SapResult
|
||||
32 PASS / 40 FAIL. §3 + §5 + §6 fully close for 5 of 6 fixtures at
|
||||
abs=1e-4. Remaining cascade failures: §4 monthly (000477/487 HW defects,
|
||||
slice 25), §5 LINE_72/73 + §6 LINE_84 on 000477/487 (cascaded from §4),
|
||||
§3 (000487 RR defect, slice 25), and downstream SapResult pins still
|
||||
drifting because of §7–§9a precision not yet pinned.)
|
||||
|
||||
### 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)
|
||||
|
||||
```
|
||||
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 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
|
||||
|
|
@ -266,7 +267,7 @@ The cascade pin work continues in worksheet order. For each section:
|
|||
|
||||
Sections still to pin:
|
||||
- ~~**§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.
|
||||
- **§8 space heating** (lines 95-99). 4 monthly + 2 annual.
|
||||
- **§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,
|
||||
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 (
|
||||
DwellingExposure,
|
||||
HeatTransmission,
|
||||
|
|
@ -84,7 +90,6 @@ from domain.sap.climate.appendix_u import external_temperature_c
|
|||
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 (
|
||||
EnergyRequirementsResult,
|
||||
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:
|
||||
"""SAP 10.2 §2 cert→inputs cascade for `ventilation_from_inputs`.
|
||||
|
||||
|
|
@ -1266,6 +1316,7 @@ def cert_to_inputs(
|
|||
epc=epc,
|
||||
region=_region_index(epc.region_code),
|
||||
overshading=_INTERNAL_GAINS_DEFAULT_OVERSHADING,
|
||||
roof_windows=_roof_windows_for_solar_gains(epc),
|
||||
).total_solar_gains_monthly_w
|
||||
|
||||
# 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
|
||||
# (e.g. 0, "NR") are dropped — no solar gain contribution, mirroring the
|
||||
# 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,
|
||||
2: Orientation.NE,
|
||||
3: Orientation.E,
|
||||
|
|
@ -283,8 +283,8 @@ def _frame_factor(w: SapWindow) -> float:
|
|||
|
||||
def _orientation(w: SapWindow) -> Orientation | None:
|
||||
"""Map cert `orientation` code (1..8) to enum; None for unmapped."""
|
||||
if isinstance(w.orientation, int) and w.orientation in _ORIENTATION_BY_SAP10_CODE:
|
||||
return _ORIENTATION_BY_SAP10_CODE[w.orientation]
|
||||
if isinstance(w.orientation, int) and w.orientation in ORIENTATION_BY_SAP10_CODE:
|
||||
return ORIENTATION_BY_SAP10_CODE[w.orientation]
|
||||
return None
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -131,13 +131,23 @@ def build_epc() -> EpcPropertyData:
|
|||
door_count=2,
|
||||
# 000516 PDF (27a) — single 1.18 m² roof window cut into the
|
||||
# storey-below "External roof Main". Cert lodges it as
|
||||
# "Manufacturer, Roof Window, Double glazed, FF=0.70" without a
|
||||
# measured U; raw U=3.40 from RdSAP10 Table 24 (p.50/113) "Roof
|
||||
# window" column. After SAP10.2 §3.2 curtain transform (R=0.04
|
||||
# m²K/W): U_eff = 1/(1/3.40 + 0.04) = 2.9930, matching the PDF
|
||||
# (27a) lodgement exactly. Contribution = 1.18 × 2.9930 = 3.5317
|
||||
# W/K.
|
||||
sap_roof_windows=[SapRoofWindow(area_m2=1.18, u_value_raw=3.40)],
|
||||
# "Manufacturer, Roof Window, Double glazed, FF=0.70" with
|
||||
# orientation NE and pitch 45° (see U985 txt line 49). Raw U=3.40
|
||||
# from RdSAP10 Table 24 (p.50/113) "Roof window" column. After
|
||||
# SAP10.2 §3.2 curtain transform (R=0.04 m²K/W): U_eff =
|
||||
# 1/(1/3.40 + 0.04) = 2.9930, matching the PDF (27a) lodgement
|
||||
# exactly. Heat-transmission contribution = 1.18 × 2.9930 = 3.5317
|
||||
# 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,
|
||||
low_energy_fixed_lighting_bulbs_count=SECTION_5_BULB_COUNT_LEL,
|
||||
sap_windows=list(SECTION_6_VERTICAL_WINDOWS),
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ from domain.sap.rdsap.cert_to_inputs import (
|
|||
cert_to_inputs,
|
||||
heat_transmission_section_from_cert,
|
||||
internal_gains_section_from_cert,
|
||||
solar_gains_section_from_cert,
|
||||
ventilation_from_cert,
|
||||
water_heating_section_from_cert,
|
||||
)
|
||||
|
|
@ -379,3 +380,62 @@ def test_section_5_line_232_lighting_kwh_per_yr_matches_pdf(
|
|||
expected,
|
||||
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