Slice S0380.118: cohort LINE_xx pins → abs=1e-4 + §15-rounded RR test expecteds

Two changes bundled (same file, same RdSAP 10 §15 spec citation):

1. Tighten cohort cert (000474 / 000490) heat_transmission LINE_xx
   pins from abs=0.01 / 0.1 → abs=1e-4 (4 pins). Pre-slice the cohort
   landed at 1e-4 of the U985 PDF but the test pins were holdovers
   from when the cascade was less precise. Per [[feedback-e2e-
   validation-philosophy]]:

     "per-component tests pin against U985 worksheet line refs at
      <1e-3 tolerance ... 1e-4 since PDF lodges 4 d.p."

   Probe data at HEAD post-§15:
     000474 LINE_33  cascade=209.108439 ws=209.1084 Δ=+4e-5
     000474 LINE_37  cascade=232.116939 ws=232.1169 Δ=+4e-5
     000490 LINE_33  cascade=211.893610 ws=211.8936 Δ=+1e-5
     000490 LINE_37  cascade=236.621110 ws=236.6211 Δ=+1e-5

2. Update `test_room_in_roof_simplified_type_1` and `..._type_2`
   expected-value formulas to round A_RR_shell to 2 d.p. per RdSAP
   10 §15 (p.66) — matching the cascade behaviour now enforced by
   Slice S0380.116. The unrounded expected was 100.9156 / 71.857;
   spec-correct rounded is 100.919 (39.5285 → 39.53) and 71.846
   (32.2749 → 32.27). Same abs=1e-4 pin enforces both arithmetic
   and rounding correctness.

   New import: `_round_half_up` from heat_transmission (the same
   helper the cascade uses for §15 rounding).

Net pyright change: 71 → 71. Net test change: 4 newly-tight pins,
2 newly-passing RR synthetic tests, 670 → 670 passing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-05-30 20:39:36 +00:00 committed by Jun-te Kim
parent 586bb27d95
commit 412525ae6f

View file

@ -36,6 +36,7 @@ from domain.sap10_calculator.worksheet.heat_transmission import (
heat_transmission_from_cert,
)
from domain.sap10_calculator.worksheet.heat_transmission import (
_round_half_up, # pyright: ignore[reportPrivateUsage]
_window_bp_index, # pyright: ignore[reportPrivateUsage]
)
@ -1367,12 +1368,15 @@ def test_section_3_non_rr_line_31_and_36_match_elmhurst_worksheet(
door_count=0,
)
# Assert
# Assert — per [[feedback-e2e-validation-philosophy]] cohort cert
# LINE_xx pins ride at abs=1e-4 to match the U985 PDF's 4-d.p.
# display precision; the cascade lands well inside that for both
# non-RR fixtures (000474 / 000490).
assert result.total_external_element_area_m2 == pytest.approx(
fixture.LINE_31_TOTAL_EXTERNAL_AREA_M2, abs=0.01
fixture.LINE_31_TOTAL_EXTERNAL_AREA_M2, abs=1e-4
)
assert result.thermal_bridging_w_per_k == pytest.approx(
fixture.LINE_36_THERMAL_BRIDGING_W_PER_K, abs=0.01
fixture.LINE_36_THERMAL_BRIDGING_W_PER_K, abs=1e-4
)
@ -1395,12 +1399,16 @@ def test_section_3_line_33_and_line_37_match_elmhurst_worksheet_000474() -> None
door_count=_w000474.DOOR_COUNT,
)
# Assert
# Assert — per [[feedback-e2e-validation-philosophy]] cohort cert
# LINE_33 / LINE_37 pins ride at abs=1e-4 to match the U985 PDF's
# 4-d.p. display precision. Pre-S0380.69 the cascade ran 0.05 W/K
# off here; the curtain-resistance + per-storey-perimeter fixes
# have closed those — the cascade lands at 4e-5 today.
assert result.fabric_heat_loss_w_per_k == pytest.approx(
_w000474.LINE_33_FABRIC_HEAT_LOSS_W_PER_K, abs=0.1
_w000474.LINE_33_FABRIC_HEAT_LOSS_W_PER_K, abs=1e-4
)
assert result.total_w_per_k == pytest.approx(
_w000474.LINE_37_TOTAL_FABRIC_HEAT_LOSS_W_PER_K, abs=0.1
_w000474.LINE_37_TOTAL_FABRIC_HEAT_LOSS_W_PER_K, abs=1e-4
)
@ -1425,12 +1433,14 @@ def test_section_3_line_33_and_line_37_match_elmhurst_worksheet_000490() -> None
door_count=_w000490.DOOR_COUNT,
)
# Assert
# Assert — per [[feedback-e2e-validation-philosophy]] cohort cert
# LINE_33 / LINE_37 pins ride at abs=1e-4 to match the U985 PDF's
# 4-d.p. display precision. Cascade lands at 1e-5 for 000490.
assert result.fabric_heat_loss_w_per_k == pytest.approx(
_w000490.LINE_33_FABRIC_HEAT_LOSS_W_PER_K, abs=0.1
_w000490.LINE_33_FABRIC_HEAT_LOSS_W_PER_K, abs=1e-4
)
assert result.total_w_per_k == pytest.approx(
_w000490.LINE_37_TOTAL_FABRIC_HEAT_LOSS_W_PER_K, abs=0.1
_w000490.LINE_37_TOTAL_FABRIC_HEAT_LOSS_W_PER_K, abs=1e-4
)
@ -1561,10 +1571,12 @@ def test_room_in_roof_simplified_type_1_adds_a_rr_timber_framed_area_to_roof_w_p
epc, window_total_area_m2=0.0, window_avg_u_value=None, door_count=0,
)
# Assert
a_rr = 12.5 * math.sqrt(15.0 / 1.5)
# Assert — per RdSAP 10 §15 (p.66) "All element areas (gross) ... 2
# d.p." the cascade rounds A_RR_shell before the (30) residual. For
# A_RR_floor = 15 m²: 12.5 × √10 = 39.5285 → 39.53 m² (HALF_UP).
a_rr = _round_half_up(12.5 * math.sqrt(15.0 / 1.5), 2)
expected_roof_w_per_k = (40.0 - 15.0) * 0.40 + a_rr * 2.30
assert result.roof_w_per_k == pytest.approx(expected_roof_w_per_k, abs=0.001)
assert result.roof_w_per_k == pytest.approx(expected_roof_w_per_k, abs=1e-4)
def test_room_in_roof_simplified_type_2_common_walls_route_to_walls_w_per_k() -> None:
@ -1626,14 +1638,16 @@ def test_room_in_roof_simplified_type_2_common_walls_route_to_walls_w_per_k() ->
epc, window_total_area_m2=0.0, window_avg_u_value=None, door_count=0,
)
# Assert
# Assert — per RdSAP 10 §15 (p.66) "All element areas (gross) ... 2
# d.p." the cascade rounds A_RR_shell before the (30) residual. For
# A_RR_floor = 10 m²: 12.5 × √(10/1.5) = 32.2749 → 32.27 m² (HALF_UP).
a_common = 5.0 * (0.25 + 1.0)
a_rr = 12.5 * math.sqrt(10.0 / 1.5)
a_rr = _round_half_up(12.5 * math.sqrt(10.0 / 1.5), 2)
a_rr_final = a_rr - a_common
expected_walls = 60.0 * 1.5 + a_common * 1.5
expected_roof = (40.0 - 10.0) * 0.40 + a_rr_final * 2.30
assert result.walls_w_per_k == pytest.approx(expected_walls, abs=0.001)
assert result.roof_w_per_k == pytest.approx(expected_roof, abs=0.001)
assert result.walls_w_per_k == pytest.approx(expected_walls, abs=1e-4)
assert result.roof_w_per_k == pytest.approx(expected_roof, abs=1e-4)
def test_room_in_roof_detailed_per_surface_lodgement_routes_each_to_correct_line_ref() -> None: