Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions cross_platform/docs/PACE_Controller_Manual.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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.

Expand Down
5 changes: 5 additions & 0 deletions cross_platform/src/pace_controller/leak.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
16 changes: 11 additions & 5 deletions cross_platform/tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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]
Expand Down
19 changes: 18 additions & 1 deletion cross_platform/tests/test_indenting_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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