Skip to content

Commit 2c7e26b

Browse files
authored
Allow disabling stats calculation (#36)
Add an API to the stats calculator table to disable calculation and plumb this through to the CSV viewer. This can reduce latency when capturing long data sets.
1 parent c929424 commit 2c7e26b

File tree

3 files changed

+105
-4
lines changed

3 files changed

+105
-4
lines changed

pyqtgraph_scope_plots/csv/csv_plots.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,10 @@ def _on_line_width_action(self) -> None:
247247
self._plots.set_thickness(value)
248248
self._table.set_thickness(value)
249249

250+
def _on_disable_stats(self, checked: bool) -> None:
251+
assert isinstance(self._table, StatsSignalsTable)
252+
self._table.disable_stats(checked)
253+
250254
def _make_controls(self) -> QWidget:
251255
button_load = QToolButton()
252256
button_load.setText("Load CSV")
@@ -295,6 +299,10 @@ def _make_controls(self) -> QWidget:
295299
line_width_action = QAction("Set Line Width", button_menu)
296300
line_width_action.triggered.connect(self._on_line_width_action)
297301
button_menu.addAction(line_width_action)
302+
self._disable_stats_action = QAction("Disable Stats", button_menu)
303+
self._disable_stats_action.setCheckable(True)
304+
self._disable_stats_action.toggled.connect(self._on_disable_stats)
305+
button_menu.addAction(self._disable_stats_action)
298306
animation_action = QAction("Create Animation", button_menu)
299307
animation_action.triggered.connect(partial(self._start_animation_ui_flow, ""))
300308
button_menu.addAction(animation_action)
@@ -503,6 +511,8 @@ def _do_load_config(self, filename: str, model: CsvLoaderStateModel) -> None:
503511
data = self._data
504512
self._set_data({}) # blank the data while updates happen, for performance
505513
self._load_model(model)
514+
assert isinstance(self._table, StatsSignalsTable)
515+
self._disable_stats_action.setChecked(self._table.stats_disabled())
506516

507517
# force-update data items and data
508518
data_items = [(name, int_color(i), data_type) for i, (name, data_type) in enumerate(self._data_items.items())]

pyqtgraph_scope_plots/stats_signals_table.py

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,23 @@
1515
import math
1616
import time
1717
import weakref
18-
from typing import Dict, Tuple, List, Any
18+
from typing import Dict, Tuple, List, Any, Optional
1919

2020
import numpy as np
2121
import numpy.typing as npt
22-
from PySide6.QtCore import Signal, QThread, QMutex, QMutexLocker, QThreadPool, QRunnable, QObject
22+
from PySide6.QtCore import Signal, QThread, QMutex, QMutexLocker, QThreadPool, QRunnable, QObject, Qt
2323
from PySide6.QtWidgets import QTableWidgetItem
24+
from pydantic import BaseModel
2425

2526
from .signals_table import HasRegionSignalsTable
26-
from .util import IdentityCacheDict, not_none
27+
from .util import IdentityCacheDict, HasSaveLoadDataConfig, not_none
2728

2829

29-
class StatsSignalsTable(HasRegionSignalsTable):
30+
class StatsTableStateModel(BaseModel):
31+
stats_disabled: Optional[bool] = None
32+
33+
34+
class StatsSignalsTable(HasRegionSignalsTable, HasSaveLoadDataConfig):
3035
"""Mixin into SignalsTable with statistics rows. Optional range to specify computation of statistics.
3136
Values passed into set_data must all be numeric."""
3237

@@ -44,6 +49,8 @@ class StatsSignalsTable(HasRegionSignalsTable):
4449
COL_STAT_STDEV,
4550
]
4651

52+
_MODEL_BASES = [StatsTableStateModel]
53+
4754
_FULL_RANGE = (-float("inf"), float("inf"))
4855

4956
class StatsCalculatorSignals(QObject):
@@ -80,6 +87,8 @@ def run(self) -> None:
8087
self._parent._last_region = request_region
8188

8289
for xs_ys_ref in request_data:
90+
if self._parent._stats_calculation_disabled: # terminate if disabled
91+
return
8392
with QMutexLocker(self._parent._request_mutex):
8493
if self._parent._debounce_target_ns != debounce_target_ns:
8594
return
@@ -125,6 +134,8 @@ def _init_table(self) -> None:
125134
self.setHorizontalHeaderItem(self.COL_STAT + self.COL_STAT_STDEV, QTableWidgetItem("StDev"))
126135

127136
def __init__(self, *args: Any, **kwargs: Any) -> None:
137+
self._stats_calculation_disabled = False
138+
128139
super().__init__(*args, **kwargs)
129140
# since calculating stats across the full range is VERY EXPENSIVE, cache the results
130141
self._full_range_stats = IdentityCacheDict[npt.NDArray[np.float64], Dict[int, float]]() # array -> stats dict
@@ -148,6 +159,43 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
148159
self._stats_signals = self.StatsCalculatorSignals()
149160
self._stats_signals.update.connect(self._on_stats_updated)
150161

162+
def _update(self) -> None:
163+
super()._update()
164+
self._update_stats_disabled()
165+
166+
def _write_model(self, model: BaseModel) -> None:
167+
assert isinstance(model, StatsTableStateModel)
168+
super()._write_model(model)
169+
model.stats_disabled = self._stats_calculation_disabled
170+
171+
def _load_model(self, model: BaseModel) -> None:
172+
assert isinstance(model, StatsTableStateModel)
173+
super()._load_model(model)
174+
if model.stats_disabled is not None:
175+
self.disable_stats(model.stats_disabled)
176+
177+
def stats_disabled(self) -> bool:
178+
"""Returns whether stats calculation is disabled."""
179+
return self._stats_calculation_disabled
180+
181+
def disable_stats(self, disable: bool = True) -> None:
182+
"""Call this to disable stats calculation and to blank the table, or re-enable the calculation."""
183+
self._stats_calculation_disabled = disable
184+
self._update_stats_disabled()
185+
if not disable:
186+
self._update_stats_task(0, True) # populate the table again
187+
188+
def _update_stats_disabled(self) -> None:
189+
"""Updates the table visuals for stats disabled"""
190+
for row, name in enumerate(self._data_items.keys()):
191+
for col in self.STATS_COLS:
192+
item = not_none(self.item(row, self.COL_STAT + col))
193+
if self._stats_calculation_disabled: # clear table on disable
194+
item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsEnabled)
195+
item.setText("")
196+
else:
197+
item.setFlags(item.flags() | Qt.ItemFlag.ItemIsEnabled)
198+
151199
def _on_stats_updated(
152200
self, input_arr: npt.NDArray[np.float64], input_region: Tuple[float, float], stats_dict: Dict[int, float]
153201
) -> None:
@@ -160,6 +208,9 @@ def _on_stats_updated(
160208
self._update_stats_display(False)
161209

162210
def _update_stats_task(self, delay_ms: int, clear_table: bool) -> None:
211+
if self._stats_calculation_disabled: # don't create a calculation task if disabled
212+
return
213+
163214
region = HasRegionSignalsTable._region_of_plot(self._plots)
164215
data_items = [ # filter out enum types
165216
(name, (xs, ys)) for name, (xs, ys) in self._plots._data.items() if np.issubdtype(ys.dtype, np.number)
@@ -183,6 +234,9 @@ def _update_stats_task(self, delay_ms: int, clear_table: bool) -> None:
183234
self._update_stats_display(clear_table)
184235

185236
def _update_stats_display(self, clear_table: bool) -> None:
237+
if self._stats_calculation_disabled: # don't update the display if disabled
238+
return
239+
186240
for row, name in enumerate(self._data_items.keys()):
187241
xs, ys = self._plots._data.get(name, (None, None))
188242
if xs is None or ys is None:

tests/test_stats_table.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14+
from typing import cast
1415

1516
import pytest
1617
from pytestqt.qtbot import QtBot
1718

1819

1920
from pyqtgraph_scope_plots import LinkedMultiPlotWidget, StatsSignalsTable
21+
from pyqtgraph_scope_plots.stats_signals_table import StatsTableStateModel
2022
from .common_testdata import DATA_ITEMS, DATA
2123

2224

@@ -90,3 +92,38 @@ def test_region_empty(qtbot: QtBot, table: StatsSignalsTable) -> None:
9092
assert table.item(2, table.COL_STAT + table.COL_STAT_AVG).text() == ""
9193
assert table.item(2, table.COL_STAT + table.COL_STAT_RMS).text() == ""
9294
assert table.item(2, table.COL_STAT + table.COL_STAT_STDEV).text() == ""
95+
96+
97+
def test_disable_enable(qtbot: QtBot, table: StatsSignalsTable) -> None:
98+
qtbot.waitUntil(lambda: table.item(0, table.COL_STAT + table.COL_STAT_MIN).text() != "")
99+
100+
table.disable_stats(True)
101+
qtbot.waitUntil(lambda: table.item(0, table.COL_STAT + table.COL_STAT_MIN).text() == "")
102+
for row in [0, 1, 2]:
103+
for col in table.STATS_COLS:
104+
assert table.item(row, table.COL_STAT + col).text() == ""
105+
106+
table.disable_stats(False)
107+
qtbot.waitUntil(lambda: table.item(0, table.COL_STAT + table.COL_STAT_MIN).text() != "")
108+
109+
110+
def test_stats_table_save(qtbot: QtBot, table: StatsSignalsTable) -> None:
111+
assert cast(StatsTableStateModel, table._dump_data_model([])).stats_disabled == False
112+
113+
table.disable_stats(True)
114+
assert cast(StatsTableStateModel, table._dump_data_model([])).stats_disabled == True
115+
116+
table.disable_stats(False)
117+
assert cast(StatsTableStateModel, table._dump_data_model([])).stats_disabled == False
118+
119+
120+
def test_stats_table_load(qtbot: QtBot, table: StatsSignalsTable) -> None:
121+
model = cast(StatsTableStateModel, table._dump_data_model([]))
122+
123+
model.stats_disabled = True
124+
table._load_model(model)
125+
qtbot.waitUntil(lambda: table.item(0, table.COL_STAT + table.COL_STAT_MIN).text() == "")
126+
127+
model.stats_disabled = False
128+
table._load_model(model)
129+
qtbot.waitUntil(lambda: table.item(0, table.COL_STAT + table.COL_STAT_MIN).text() != "")

0 commit comments

Comments
 (0)