From 55a29f5a1cd7ceec70ca458ab7bdf6ba0f886b3f Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Sat, 30 May 2026 20:39:36 +0000 Subject: [PATCH] =?UTF-8?q?Slice=20S0380.118:=20cohort=20LINE=5Fxx=20pins?= =?UTF-8?q?=20=E2=86=92=20abs=3D1e-4=20+=20=C2=A715-rounded=20RR=20test=20?= =?UTF-8?q?expecteds?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../worksheet/tests/test_heat_transmission.py | 46 ++++++++++++------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/domain/sap10_calculator/worksheet/tests/test_heat_transmission.py b/domain/sap10_calculator/worksheet/tests/test_heat_transmission.py index f3b1dee3..3e2660b2 100644 --- a/domain/sap10_calculator/worksheet/tests/test_heat_transmission.py +++ b/domain/sap10_calculator/worksheet/tests/test_heat_transmission.py @@ -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: