mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
Slice 26: §5 internal gains cascade pin (50/54 PASS) + rooflight daylight plumb
Added `internal_gains_section_from_cert` helper composing §1 (volume) + §4 (heat_gains line 65)m → §5 orchestrator, and 54 strict pin cases for worksheet lines (66)..(73) monthly + (232) annual lighting kWh. Also fixed a missing input plumb: cert_to_inputs was passing `rooflight_total_area_m2=0` to `internal_gains_from_cert`, so the 000516 roof window (lodged on `epc.sap_roof_windows` since slice 24) wasn't contributing to the L2a daylight factor. Added `_rooflight_total_area_m2_from_cert` and routed it through both the public cert→inputs cascade and the new §5 section helper. §5 cascade: field | 474 | 477 | 480 | 487 | 490 | 516 LINE_66 | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ LINE_67 | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ (rooflight plumb) LINE_68 | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ LINE_69 | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ LINE_70 | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ LINE_71 | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ LINE_72 | ✓ | ✗ | ✓ | ✗ | ✓ | ✓ LINE_73 | ✓ | ✗ | ✓ | ✗ | ✓ | ✓ LINE_232 | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ Remaining failures are 000477 + 000487 LINE_72/73 — cascaded from §4 LINE_65 heat_gains residuals (000477 combi loss, 000487 HW lodgement defect). Both fixtures are slice 25 territory. Scoreboard: section_cascade_pins: 170 → 220 PASS (+50; 54 new tests, 4 fail) e2e SapResult: 29 → 30 PASS (+1, downstream from rooflight plumb) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
d4c090fc7c
commit
9cb79d9c98
3 changed files with 123 additions and 8 deletions
|
|
@ -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-27b: section_cascade_pins 170 PASS / 16 FAIL, e2e SapResult
|
(Post-slice-26: section_cascade_pins 220 PASS / 20 FAIL, e2e SapResult
|
||||||
29 PASS / 43 FAIL. §3 fully closes for 5 of 6 fixtures at abs=1e-4 — every
|
30 PASS / 42 FAIL. §3 + §5 fully close for 5 of 6 fixtures at abs=1e-4.
|
||||||
LINE_31/33/36/37 pin passes on 000474/477/480/490/516. Remaining cascade
|
Remaining cascade failures: §4 monthly (000477/487 HW defects, slice 25),
|
||||||
failures are §4 monthly (000477/487 HW defects, slice 25), §3 (000487 RR
|
§3 + §5 LINE_72/73 (000487 RR + 000477 LINE_61 cascade defects, slice 25),
|
||||||
defect, slice 25), and downstream SapResult pins still drifting because
|
and downstream SapResult pins still drifting because of §6–§9a precision
|
||||||
of §5–§9a precision not yet pinned.)
|
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 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
|
||||||
Slice 24: rooflight (line 27a) — SapRoofWindow datatype + 000516 cascade closure
|
Slice 24: rooflight (line 27a) — SapRoofWindow datatype + 000516 cascade closure
|
||||||
|
|
@ -256,14 +257,15 @@ The cascade pin work continues in worksheet order. For each section:
|
||||||
1. Identify the cert→inputs cascade entry point. May need to extract a
|
1. Identify the cert→inputs cascade entry point. May need to extract a
|
||||||
`<section>_from_cert(epc)` helper from `cert_to_inputs` (mirroring slice
|
`<section>_from_cert(epc)` helper from `cert_to_inputs` (mirroring slice
|
||||||
21c's `ventilation_from_cert`, 21d's `heat_transmission_section_from_cert`,
|
21c's `ventilation_from_cert`, 21d's `heat_transmission_section_from_cert`,
|
||||||
21e's `water_heating_section_from_cert`).
|
21e's `water_heating_section_from_cert`, 26's
|
||||||
|
`internal_gains_section_from_cert`).
|
||||||
2. Map fixture `LINE_X_<NAME>` constants to result struct attributes.
|
2. Map fixture `LINE_X_<NAME>` constants to result struct attributes.
|
||||||
3. Add scalar + monthly pin tests at abs=1e-4 to `test_section_cascade_pins.py`.
|
3. Add scalar + monthly pin tests at abs=1e-4 to `test_section_cascade_pins.py`.
|
||||||
4. Run, see failures, diagnose. Fixture defect or calculator bug — fix in place,
|
4. Run, see failures, diagnose. Fixture defect or calculator bug — fix in place,
|
||||||
no widening.
|
no widening.
|
||||||
|
|
||||||
Sections still to pin:
|
Sections still to pin:
|
||||||
- **§5 internal gains** (lines 66-73 + 232 lighting kWh). 6 monthly + 1 annual.
|
- ~~**§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). 2 monthly tuples.
|
||||||
- **§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.
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,7 @@ from domain.sap.tables.table_32 import (
|
||||||
from domain.sap.worksheet.fuel_cost import FuelCostResult, fuel_cost
|
from domain.sap.worksheet.fuel_cost import FuelCostResult, fuel_cost
|
||||||
from domain.sap.worksheet.dimensions import dimensions_from_cert
|
from domain.sap.worksheet.dimensions import dimensions_from_cert
|
||||||
from domain.sap.worksheet.internal_gains import (
|
from domain.sap.worksheet.internal_gains import (
|
||||||
|
InternalGainsResult,
|
||||||
OvershadingCategory,
|
OvershadingCategory,
|
||||||
internal_gains_from_cert,
|
internal_gains_from_cert,
|
||||||
)
|
)
|
||||||
|
|
@ -755,6 +756,48 @@ def heat_transmission_section_from_cert(epc: EpcPropertyData) -> HeatTransmissio
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _rooflight_total_area_m2_from_cert(epc: EpcPropertyData) -> float:
|
||||||
|
"""Σ area of `epc.sap_roof_windows` for §5 daylight-factor L2a +
|
||||||
|
§6 horizontal solar gain. Returns 0.0 when none are lodged.
|
||||||
|
|
||||||
|
Roof windows behave as rooflights for §5 L2a (Z_L = 1.0 per Table 6d
|
||||||
|
note 2) — same treatment as horizontal rooflights for the daylight
|
||||||
|
bonus. Areas are 2-d.p.-rounded inputs (RdSAP10 §15) when lodged on
|
||||||
|
the SapRoofWindow datatype."""
|
||||||
|
return sum(float(rw.area_m2) for rw in epc.sap_roof_windows or [])
|
||||||
|
|
||||||
|
|
||||||
|
def internal_gains_section_from_cert(
|
||||||
|
epc: EpcPropertyData,
|
||||||
|
) -> Optional[InternalGainsResult]:
|
||||||
|
"""SAP 10.2 §5 cert→inputs cascade for `internal_gains_from_cert`.
|
||||||
|
|
||||||
|
Composes §1 (dim.volume_m3) + §4 (heat_gains_from_water_heating
|
||||||
|
monthly_kwh, line (65)m) and threads them through the §5 orchestrator
|
||||||
|
— exactly as `cert_to_inputs` computes internally. Returns the full
|
||||||
|
`InternalGainsResult` (every (66)..(73) line ref + annual lighting kWh
|
||||||
|
line (232)) so cascade pin tests can assert each §5 line ref against
|
||||||
|
the U985 PDF.
|
||||||
|
|
||||||
|
Returns `None` when TFA is missing (matches the §4 helper contract;
|
||||||
|
tests using this helper should skip those fixtures).
|
||||||
|
"""
|
||||||
|
if epc.total_floor_area_m2 is None:
|
||||||
|
return None
|
||||||
|
dim = dimensions_from_cert(epc)
|
||||||
|
wh = water_heating_section_from_cert(epc)
|
||||||
|
hw_heat_gains_monthly_kwh = (
|
||||||
|
wh.heat_gains_monthly_kwh if wh is not None else (0.0,) * 12
|
||||||
|
)
|
||||||
|
return internal_gains_from_cert(
|
||||||
|
epc=epc,
|
||||||
|
dwelling_volume_m3=dim.volume_m3,
|
||||||
|
heat_gains_from_water_heating_monthly_kwh=hw_heat_gains_monthly_kwh,
|
||||||
|
overshading=_INTERNAL_GAINS_DEFAULT_OVERSHADING,
|
||||||
|
rooflight_total_area_m2=_rooflight_total_area_m2_from_cert(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`.
|
||||||
|
|
||||||
|
|
@ -1212,6 +1255,7 @@ def cert_to_inputs(
|
||||||
dwelling_volume_m3=dim.volume_m3,
|
dwelling_volume_m3=dim.volume_m3,
|
||||||
heat_gains_from_water_heating_monthly_kwh=hw_heat_gains_monthly_kwh,
|
heat_gains_from_water_heating_monthly_kwh=hw_heat_gains_monthly_kwh,
|
||||||
overshading=_INTERNAL_GAINS_DEFAULT_OVERSHADING,
|
overshading=_INTERNAL_GAINS_DEFAULT_OVERSHADING,
|
||||||
|
rooflight_total_area_m2=_rooflight_total_area_m2_from_cert(epc),
|
||||||
)
|
)
|
||||||
internal_gains_monthly_w = (
|
internal_gains_monthly_w = (
|
||||||
internal_gains_result.total_internal_gains_monthly_w
|
internal_gains_result.total_internal_gains_monthly_w
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import pytest
|
||||||
from domain.sap.rdsap.cert_to_inputs import (
|
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,
|
||||||
ventilation_from_cert,
|
ventilation_from_cert,
|
||||||
water_heating_section_from_cert,
|
water_heating_section_from_cert,
|
||||||
)
|
)
|
||||||
|
|
@ -310,3 +311,71 @@ def test_section_4_monthly_line_refs_match_pdf(
|
||||||
# Assert
|
# Assert
|
||||||
for m in range(12):
|
for m in range(12):
|
||||||
_pin(actual[m], expected[m], f"§4 {fixture_attr}[{m+1}] {fixture_name}")
|
_pin(actual[m], expected[m], f"§4 {fixture_attr}[{m+1}] {fixture_name}")
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# §5 Internal gains — LINE_66..LINE_73 monthly + LINE_232 annual
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
_SECTION_5_MONTHLY_PINS: Final[tuple[tuple[str, str], ...]] = (
|
||||||
|
("LINE_66_M_METABOLIC_W", "metabolic_monthly_w"),
|
||||||
|
("LINE_67_M_LIGHTING_W", "lighting_monthly_w"),
|
||||||
|
("LINE_68_M_APPLIANCES_W", "appliances_monthly_w"),
|
||||||
|
("LINE_69_M_COOKING_W", "cooking_monthly_w"),
|
||||||
|
("LINE_70_M_PUMPS_FANS_W", "pumps_fans_monthly_w"),
|
||||||
|
("LINE_71_M_LOSSES_W", "losses_monthly_w"),
|
||||||
|
("LINE_72_M_WATER_HEATING_GAINS_W", "water_heating_gains_monthly_w"),
|
||||||
|
("LINE_73_M_TOTAL_INTERNAL_GAINS_W", "total_internal_gains_monthly_w"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"fixture_name,fixture_attr,result_attr",
|
||||||
|
[
|
||||||
|
(fix, line, attr)
|
||||||
|
for fix in _FIXTURES
|
||||||
|
for line, attr in _SECTION_5_MONTHLY_PINS
|
||||||
|
],
|
||||||
|
ids=lambda x: x if isinstance(x, str) else None,
|
||||||
|
)
|
||||||
|
def test_section_5_monthly_line_refs_match_pdf(
|
||||||
|
fixture_name: str, fixture_attr: str, result_attr: str
|
||||||
|
) -> None:
|
||||||
|
"""§5 monthly pins — every Jan..Dec value of (66)..(73) internal-gain
|
||||||
|
component matches the U985 PDF to abs=1e-4."""
|
||||||
|
# Arrange
|
||||||
|
mod = _FIXTURES[fixture_name]
|
||||||
|
epc = mod.build_epc() # type: ignore[attr-defined]
|
||||||
|
expected = getattr(mod, fixture_attr)
|
||||||
|
|
||||||
|
# Act
|
||||||
|
ig = internal_gains_section_from_cert(epc)
|
||||||
|
assert ig is not None, f"{fixture_name}: internal_gains_from_cert returned None"
|
||||||
|
actual = getattr(ig, result_attr)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
for m in range(12):
|
||||||
|
_pin(actual[m], expected[m], f"§5 {fixture_attr}[{m+1}] {fixture_name}")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("fixture_name", list(_FIXTURES), ids=lambda x: x)
|
||||||
|
def test_section_5_line_232_lighting_kwh_per_yr_matches_pdf(
|
||||||
|
fixture_name: str,
|
||||||
|
) -> None:
|
||||||
|
"""§5 (232) — annual lighting kWh from Appendix L, fuels the cost side
|
||||||
|
`inputs.lighting_kwh_per_yr`."""
|
||||||
|
# Arrange
|
||||||
|
mod = _FIXTURES[fixture_name]
|
||||||
|
epc = mod.build_epc() # type: ignore[attr-defined]
|
||||||
|
expected = mod.LINE_232_LIGHTING_KWH_PER_YR # type: ignore[attr-defined]
|
||||||
|
|
||||||
|
# Act
|
||||||
|
ig = internal_gains_section_from_cert(epc)
|
||||||
|
assert ig is not None, f"{fixture_name}: internal_gains_from_cert returned None"
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
_pin(
|
||||||
|
ig.lighting_kwh_per_yr,
|
||||||
|
expected,
|
||||||
|
f"§5 (232) {fixture_name}",
|
||||||
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue