From 07720e909e86fd224264967f81891fec2003bdcb Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Sat, 30 May 2026 21:44:43 +0000 Subject: [PATCH] Slice S0380.122: tighten test_ventilation tolerances MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 17 hand-crafted ventilation tests had abs=0.001-0.01 tolerances that masked the actual diff (always 0 or 1e-16 for these direct-arithmetic formulas). Tightened to abs=1e-12 (essentially exact). 10 cohort cert pins (`LINE_8`/`LINE_10`/.../`LINE_25` against U985 PDF) had mixed abs=0.0001-0.0005; standardised to abs=1e-4 (PDF 4-d.p. display floor per [[feedback-e2e-validation-philosophy]]). The looser 0.0005 pins on (8), (16), (18), (21), (22b), (25) admitted up to half a 4-d.p. unit of drift that the cascade isn't producing — actual cascade diffs are ~5e-5 (one notch under display precision). Test movement: all 26 tests pass at the new tolerances. Net pyright change: 69 → 69. Per [[feedback-zero-error-strict]] tolerance widening is forbidden; this slice goes the other way — every pin tightened to its actual precision floor. Co-Authored-By: Claude Opus 4.7 --- .../worksheet/tests/test_ventilation.py | 67 ++++++++++--------- 1 file changed, 34 insertions(+), 33 deletions(-) diff --git a/domain/sap10_calculator/worksheet/tests/test_ventilation.py b/domain/sap10_calculator/worksheet/tests/test_ventilation.py index 16f363a2..f307b354 100644 --- a/domain/sap10_calculator/worksheet/tests/test_ventilation.py +++ b/domain/sap10_calculator/worksheet/tests/test_ventilation.py @@ -42,7 +42,7 @@ def test_bare_masonry_detached_returns_baseline_line_16_of_0_65() -> None: # Assert assert isinstance(result, VentilationResult) - assert result.infiltration_rate_ach == pytest.approx(0.65, abs=0.01) + assert result.infiltration_rate_ach == pytest.approx(0.65, abs=1e-12) def test_open_chimney_adds_80_per_volume_to_line_8_openings() -> None: @@ -60,8 +60,8 @@ def test_open_chimney_adds_80_per_volume_to_line_8_openings() -> None: ) # Assert - assert result.openings_ach == pytest.approx(0.40, abs=0.005) - assert result.infiltration_rate_ach == pytest.approx(1.05, abs=0.01) + assert result.openings_ach == pytest.approx(0.40, abs=1e-12) + assert result.infiltration_rate_ach == pytest.approx(1.05, abs=1e-12) def test_two_storey_dwelling_adds_0_1_via_line_10() -> None: @@ -77,8 +77,8 @@ def test_two_storey_dwelling_adds_0_1_via_line_10() -> None: ) # Assert - assert result.additional_ach == pytest.approx(0.1, abs=0.001) - assert result.infiltration_rate_ach == pytest.approx(0.75, abs=0.01) + assert result.additional_ach == pytest.approx(0.1, abs=1e-12) + assert result.infiltration_rate_ach == pytest.approx(0.75, abs=1e-12) def test_timber_frame_uses_line_11_structural_0_25_not_0_35() -> None: @@ -94,8 +94,8 @@ def test_timber_frame_uses_line_11_structural_0_25_not_0_35() -> None: ) # Assert - assert result.structural_ach == pytest.approx(0.25, abs=0.001) - assert result.infiltration_rate_ach == pytest.approx(0.55, abs=0.01) + assert result.structural_ach == pytest.approx(0.25, abs=1e-12) + assert result.infiltration_rate_ach == pytest.approx(0.55, abs=1e-12) def test_suspended_timber_floor_line_12_unsealed_vs_sealed() -> None: @@ -116,8 +116,8 @@ def test_suspended_timber_floor_line_12_unsealed_vs_sealed() -> None: ) # Assert - assert unsealed.floor_ach == pytest.approx(0.2, abs=0.001) - assert sealed.floor_ach == pytest.approx(0.1, abs=0.001) + assert unsealed.floor_ach == pytest.approx(0.2, abs=1e-12) + assert sealed.floor_ach == pytest.approx(0.1, abs=1e-12) def test_draught_lobby_present_zeros_line_13() -> None: @@ -131,7 +131,7 @@ def test_draught_lobby_present_zeros_line_13() -> None: ) # Assert - assert result.draught_lobby_ach == pytest.approx(0.0, abs=0.001) + assert result.draught_lobby_ach == pytest.approx(0.0, abs=1e-12) def test_window_draught_proofed_line_15_is_linear_in_pct() -> None: @@ -149,8 +149,8 @@ def test_window_draught_proofed_line_15_is_linear_in_pct() -> None: ) # Assert - assert full.window_ach == pytest.approx(0.05, abs=0.005) - assert half.window_ach == pytest.approx(0.15, abs=0.005) + assert full.window_ach == pytest.approx(0.05, abs=1e-12) + assert half.window_ach == pytest.approx(0.15, abs=1e-12) def test_openings_sum_each_table_2_1_rate_independently() -> None: @@ -167,7 +167,7 @@ def test_openings_sum_each_table_2_1_rate_independently() -> None: ) # Assert - assert result.openings_ach == pytest.approx(0.825, abs=0.01) + assert result.openings_ach == pytest.approx(0.825, abs=1e-12) def test_zero_or_negative_volume_raises_value_error() -> None: @@ -199,7 +199,7 @@ def test_pressure_test_ap50_uses_line_18a_formula() -> None: ) # Assert - assert result.pressure_test_ach == pytest.approx(0.25, abs=0.001) + assert result.pressure_test_ach == pytest.approx(0.25, abs=1e-12) def test_pressure_test_ap4_uses_line_18b_formula() -> None: @@ -214,7 +214,7 @@ def test_pressure_test_ap4_uses_line_18b_formula() -> None: ) # Assert - assert result.pressure_test_ach == pytest.approx(0.263 * (4.0 ** 0.924), abs=0.001) + assert result.pressure_test_ach == pytest.approx(0.263 * (4.0 ** 0.924), abs=1e-12) def test_shelter_factor_line_20_clamps_sides_to_0_4() -> None: @@ -303,8 +303,8 @@ def test_mvhr_24a_subtracts_efficiency_from_system_air_change() -> None: for i in range(12): delta_90 = mvhr_90.effective_monthly_ach[i] - mvhr_90.monthly_wind_adjusted_ach[i] delta_0 = mvhr_0.effective_monthly_ach[i] - mvhr_0.monthly_wind_adjusted_ach[i] - assert delta_90 == pytest.approx(0.05, abs=0.001) - assert delta_0 == pytest.approx(0.5, abs=0.001) + assert delta_90 == pytest.approx(0.05, abs=1e-12) + assert delta_0 == pytest.approx(0.5, abs=1e-12) def test_balanced_mv_24b_adds_full_system_ach_each_month() -> None: @@ -500,24 +500,25 @@ def test_section_2_matches_elmhurst_worksheet(fixture: ModuleType) -> None: mv_kind=fixture.MV_KIND, ) - # Assert — line-by-line vs Elmhurst output. - assert result.openings_ach == pytest.approx(fixture.LINE_8_OPENINGS_ACH, abs=0.0005) - assert result.additional_ach == pytest.approx(fixture.LINE_10_ADDITIONAL_ACH, abs=0.0001) - assert result.structural_ach == pytest.approx(fixture.LINE_11_STRUCTURAL_ACH, abs=0.0001) - assert result.floor_ach == pytest.approx(fixture.LINE_12_FLOOR_ACH, abs=0.0001) - assert result.draught_lobby_ach == pytest.approx(fixture.LINE_13_DRAUGHT_LOBBY_ACH, abs=0.0001) - assert result.window_ach == pytest.approx(fixture.LINE_15_WINDOW_ACH, abs=0.0001) - assert result.infiltration_rate_ach == pytest.approx(fixture.LINE_16_INFILTRATION_RATE_ACH, abs=0.0005) - assert result.pressure_test_ach == pytest.approx(fixture.LINE_18_PRESSURE_TEST_ACH, abs=0.0005) - assert result.shelter_factor == pytest.approx(fixture.LINE_20_SHELTER_FACTOR, abs=0.0001) - assert result.shelter_adjusted_ach == pytest.approx(fixture.LINE_21_SHELTER_ADJUSTED_ACH, abs=0.0005) + # Assert — line-by-line vs Elmhurst output. All pins ride at abs=1e-4 + # (PDF 4-d.p. display floor); cascade lands at ~5e-5 for monthly tuples. + assert result.openings_ach == pytest.approx(fixture.LINE_8_OPENINGS_ACH, abs=1e-4) + assert result.additional_ach == pytest.approx(fixture.LINE_10_ADDITIONAL_ACH, abs=1e-4) + assert result.structural_ach == pytest.approx(fixture.LINE_11_STRUCTURAL_ACH, abs=1e-4) + assert result.floor_ach == pytest.approx(fixture.LINE_12_FLOOR_ACH, abs=1e-4) + assert result.draught_lobby_ach == pytest.approx(fixture.LINE_13_DRAUGHT_LOBBY_ACH, abs=1e-4) + assert result.window_ach == pytest.approx(fixture.LINE_15_WINDOW_ACH, abs=1e-4) + assert result.infiltration_rate_ach == pytest.approx(fixture.LINE_16_INFILTRATION_RATE_ACH, abs=1e-4) + assert result.pressure_test_ach == pytest.approx(fixture.LINE_18_PRESSURE_TEST_ACH, abs=1e-4) + assert result.shelter_factor == pytest.approx(fixture.LINE_20_SHELTER_FACTOR, abs=1e-4) + assert result.shelter_adjusted_ach == pytest.approx(fixture.LINE_21_SHELTER_ADJUSTED_ACH, abs=1e-4) - # Monthly arrays — every month. + # Monthly arrays — every month at abs=1e-4 (PDF 4-d.p. display). for i in range(12): - assert result.monthly_wind_speed_m_s[i] == pytest.approx(fixture.LINE_22_WIND_SPEED_M_S[i], abs=0.001) - assert result.monthly_wind_factor[i] == pytest.approx(fixture.LINE_22A_WIND_FACTOR[i], abs=0.001) - assert result.monthly_wind_adjusted_ach[i] == pytest.approx(fixture.LINE_22B_WIND_ADJUSTED_ACH[i], abs=0.0005) - assert result.effective_monthly_ach[i] == pytest.approx(fixture.LINE_25_EFFECTIVE_ACH[i], abs=0.0005) + assert result.monthly_wind_speed_m_s[i] == pytest.approx(fixture.LINE_22_WIND_SPEED_M_S[i], abs=1e-4) + assert result.monthly_wind_factor[i] == pytest.approx(fixture.LINE_22A_WIND_FACTOR[i], abs=1e-4) + assert result.monthly_wind_adjusted_ach[i] == pytest.approx(fixture.LINE_22B_WIND_ADJUSTED_ACH[i], abs=1e-4) + assert result.effective_monthly_ach[i] == pytest.approx(fixture.LINE_25_EFFECTIVE_ACH[i], abs=1e-4) def test_table_u2_default_matches_worksheet_g86_to_r86() -> None: