diff --git a/cross_platform/docs/PACE_Controller_Manual.md b/cross_platform/docs/PACE_Controller_Manual.md index c7788f5..e0ce1d0 100644 --- a/cross_platform/docs/PACE_Controller_Manual.md +++ b/cross_platform/docs/PACE_Controller_Manual.md @@ -303,7 +303,7 @@ Only decreases are considered pressure losses. Increases are clipped to zero los ### 13.2 Operating condition -Sample-side history is collected in MEASURE with no active automation. Cylinder/positive-source history is collected both in MEASURE and during steady, in-limits CONTROL, but is reset while the target is moving. The loss rate is shown as soon as two valid readings at different times are available; there is no three-minute waiting period. +Sample-side history is collected in MEASURE with no active automation. Cylinder/positive-source history is collected both in MEASURE and during steady, in-limits CONTROL, but is reset while the target is moving. The numeric loss rate is shown as soon as two valid readings at different times are available. Colour warnings and classification labels wait for three minutes of history, as described below. ### 13.3 Calculation and default classes @@ -316,7 +316,7 @@ A linear least-squares fit of pressure versus time is calculated over all readin | WARNING: pressure leak | Orange | `>=0.3` and `<=0.6 bar/h` | | WARNING: SIGNIFICANT PRESSURE LEAK | Red | `>0.6 bar/h` | -The SETTINGS page directly edits the three colour boundaries in **bar/h**, with a live band summary and a **Restore defaults** button. Press **Save settings** to apply. Required ordering is `0 < green boundary < yellow boundary < orange boundary`. All colours are available as soon as a fitted rate exists, without a separate green waiting period. These thresholds apply to both sides and both short and long term measurements. Existing custom reference-drop/time settings are converted to equivalent hourly limits; old factory defaults become the new 0.1 / 0.3 / 0.6 bar/h defaults. +The SETTINGS page directly edits the three colour boundaries in **bar/h**, with a live band summary and a **Restore defaults** button. Press **Save settings** to apply. Required ordering is `0 < green boundary < yellow boundary < orange boundary`. The fitted numeric rate is visible immediately. Colours and leak classification labels appear only after at least three minutes of available history; before that, both short and long term results remain neutral and show ASSESSING with the rate and window counter. Each history reset restarts this three-minute period. These thresholds apply to both sides and both short and long term measurements. Existing custom reference-drop/time settings are converted to equivalent hourly limits; old factory defaults become the new 0.1 / 0.3 / 0.6 bar/h defaults. A pressure trend does not uniquely identify a physical leak. Thermal equilibration, regulator hysteresis, pressure-medium behaviour, and sensor noise can produce similar signals. Confirm warnings using an appropriate laboratory leak-test procedure. diff --git a/cross_platform/src/pace_controller/leak.py b/cross_platform/src/pace_controller/leak.py index 0baff5a..8e3b485 100644 --- a/cross_platform/src/pace_controller/leak.py +++ b/cross_platform/src/pace_controller/leak.py @@ -94,6 +94,11 @@ def add(self, timestamp: float, value: float, enabled: bool) -> LeakAssessment: slope = fsum((x - mean_x) * (y - mean_y) for x, y in zip(xs, ys)) / denominator rate = max(0.0, -slope) + # Show the fitted number immediately, but wait for three minutes of + # available history before assigning any colour or leak classification. + if elapsed_minutes < 3.0: + return LeakAssessment("assessing", rate, elapsed_minutes) + t = self.thresholds rate_hour = rate * 60.0 # Normalize numerical roundoff at the inclusive colour boundaries. diff --git a/cross_platform/tests/test_core.py b/cross_platform/tests/test_core.py index dd13967..17572b1 100644 --- a/cross_platform/tests/test_core.py +++ b/cross_platform/tests/test_core.py @@ -308,14 +308,18 @@ def test_leak_monitor_pauses_during_control() -> None: ("rate", "expected"), [(0.0025, "slight_leak"), (0.0075, "pressure_leak"), (0.015, "significant_leak")], ) -def test_leak_rate_and_warnings_are_available_immediately(rate: float, expected: str) -> None: +def test_leak_rate_is_immediate_but_warnings_wait_three_minutes(rate: float, expected: str) -> None: monitor = LeakMonitor(LeakThresholds()) first = monitor.add(0.0, 50.0, True) assert first.observation_minutes == 0.0 early = monitor.add(1.0, 50.0 - rate / 60.0, True) - assert early.level == expected + assert early.level == "assessing" assert early.observation_minutes == pytest.approx(1.0 / 60.0) assert early.rate_bar_min == pytest.approx(rate) + before = monitor.add(179.999, 50.0 - rate * 179.999 / 60.0, True) + assert before.level == "assessing" + ready = monitor.add(180.0, 50.0 - rate * 3.0, True) + assert ready.level == expected def test_five_minute_average_excludes_old_loss_and_keeps_green_confirmation() -> None: @@ -330,7 +334,8 @@ def test_five_minute_average_excludes_old_loss_and_keeps_green_confirmation() -> assert monitor.samples[0][0] == 300.0 monitor.add(601.0, 49.0, False) assert monitor.add(602.0, 49.0, True).level == "assessing" - assert monitor.add(603.0, 49.0, True).level == "no_leak" + assert monitor.add(603.0, 49.0, True).level == "assessing" + assert monitor.add(782.0, 49.0, True).level == "no_leak" def test_regression_interpolates_boundary_and_uses_irregular_timestamps() -> None: @@ -371,9 +376,10 @@ def test_ui_shows_early_loss_rate_and_autonomy() -> None: card = SimpleNamespace(set_level=lambda level, text: displayed.append((level, text))) window = SimpleNamespace(t=Translator("en")) MainWindow._apply_leak( - window, card, LeakAssessment("pressure_leak", 0.003, 1.0 / 60.0), 10.0 + window, card, LeakAssessment("assessing", 0.003, 1.0 / 60.0), 10.0 ) - assert displayed[0][0] == "pressure_leak" + assert displayed[0][0] == "assessing" + assert "LEAK" not in displayed[0][1] assert "0.180 bar/h" in displayed[0][1] assert "10.0 h" in displayed[0][1] assert "0.0 / 5 min" in displayed[0][1] diff --git a/cross_platform/tests/test_indenting_settings.py b/cross_platform/tests/test_indenting_settings.py index fbe3aad..6158f51 100644 --- a/cross_platform/tests/test_indenting_settings.py +++ b/cross_platform/tests/test_indenting_settings.py @@ -13,7 +13,7 @@ def test_hourly_threshold_boundaries(rate, level): monitor = LeakMonitor(LeakThresholds()) monitor.add(0, 50, True) - assert monitor.add(60, 50-rate/60, True).level == level + assert monitor.add(180, 50-rate/20, True).level == level def test_migration_and_settings_roundtrip(monkeypatch, tmp_path): @@ -144,3 +144,20 @@ def test_ui_settings_and_pause_editability(monkeypatch, tmp_path): finally: window.close() app.processEvents() + + +@pytest.mark.parametrize("window", [5, 120]) +@pytest.mark.parametrize("rate,level", [(0, "no_leak"), (0.2, "slight_leak"), (0.4, "pressure_leak"), (0.8, "significant_leak")]) +def test_three_minute_classification_delay_restarts_after_pressure_step(window, rate, level): + monitor = LeakMonitor(LeakThresholds(), window_minutes=window) + for stamp in (0, 1, 179.999, 180): + result = monitor.add(stamp, 50 - rate * stamp / 3600, True) + assert result.level == (level if stamp == 180 else "assessing") + if stamp: + assert result.rate_bar_min * 60 == pytest.approx(rate) + reset = monitor.add(240, 52, True) + assert reset.history_reset and reset.level == "assessing" + early = monitor.add(241, 52 - rate / 3600, True) + assert early.level == "assessing" + assert early.rate_bar_min * 60 == pytest.approx(rate) + assert monitor.add(420, 52 - rate / 20, True).level == level