def test_sizes(self): r = RingBuffer(5, dtype=(int, 2)) self.assertEqual(r.maxlen, 5) self.assertEqual(len(r), 0) self.assertEqual(r.shape, (0, 2)) r.append([0, 0]) self.assertEqual(r.maxlen, 5) self.assertEqual(len(r), 1) self.assertEqual(r.shape, (1, 2))
def test_degenerate(self): r = RingBuffer(0) np.testing.assert_equal(r, np.array([])) # this does not error with deque(maxlen=0), so should not error here try: r.append(0) r.appendleft(0) r.extend([0]) r.extendleft([0]) except IndexError: self.fail()
def test_iter(self): r = RingBuffer(5) for i in range(3): r.append(i) for i, j in zip(r, range(3)): self.assertEqual(i, j) r.clear() for i in range(5): r.append(i) for i, j in zip(r, range(5)): self.assertEqual(i, j)
def test_pops(self): r = RingBuffer(3) r.append(1) r.appendleft(2) r.append(3) np.testing.assert_equal(r, np.array([2, 1, 3])) self.assertEqual(r.pop(), 3) np.testing.assert_equal(r, np.array([2, 1])) self.assertEqual(r.popleft(), 2) np.testing.assert_equal(r, np.array([1])) # test empty pops empty = RingBuffer(1) with self.assertRaisesRegex(IndexError, "empty"): empty.pop() with self.assertRaisesRegex(IndexError, "empty"): empty.popleft()
def test_2d(self): r = RingBuffer(5, dtype=(float, 2)) r.append([1, 2]) np.testing.assert_equal(r, np.array([[1, 2]])) self.assertEqual(len(r), 1) self.assertEqual(np.shape(r), (1, 2)) r.append([3, 4]) np.testing.assert_equal(r, np.array([[1, 2], [3, 4]])) self.assertEqual(len(r), 2) self.assertEqual(np.shape(r), (2, 2)) r.appendleft([5, 6]) np.testing.assert_equal(r, np.array([[5, 6], [1, 2], [3, 4]])) self.assertEqual(len(r), 3) self.assertEqual(np.shape(r), (3, 2)) np.testing.assert_equal(r[0], [5, 6]) np.testing.assert_equal(r[0, :], [5, 6]) np.testing.assert_equal(r[:, 0], [5, 1, 3])
def test_append(self): r = RingBuffer(5) r.append(1) np.testing.assert_equal(r, np.array([1])) self.assertEqual(len(r), 1) r.append(2) np.testing.assert_equal(r, np.array([1, 2])) self.assertEqual(len(r), 2) r.append(3) r.append(4) r.append(5) np.testing.assert_equal(r, np.array([1, 2, 3, 4, 5])) self.assertEqual(len(r), 5) r.append(6) np.testing.assert_equal(r, np.array([2, 3, 4, 5, 6])) self.assertEqual(len(r), 5) self.assertEqual(r[4], 6) self.assertEqual(r[-1], 6)
def test_no_overwrite(self): r = RingBuffer(3, allow_overwrite=False) r.append(1) r.append(2) r.appendleft(3) with self.assertRaisesRegex(IndexError, "overwrite"): r.appendleft(4) with self.assertRaisesRegex(IndexError, "overwrite"): r.extendleft([4]) r.extendleft([]) np.testing.assert_equal(r, np.array([3, 1, 2])) with self.assertRaisesRegex(IndexError, "overwrite"): r.append(4) with self.assertRaisesRegex(IndexError, "overwrite"): r.extend([4]) r.extend([]) # works fine if we pop the surplus r.pop() r.append(4) np.testing.assert_equal(r, np.array([3, 1, 4]))
class ThreadSafeCurve(object): """Provides the base class for a thread-safe plot *curve* to which (x, y)-data can be safely appended or set from out of any thread. It will wrap around the passed argument ``linked_curve`` of type ``pyqtgraph.PlotDataItem`` and will manage the (x, y)-data buffers underlying the curve. Intended multi-threaded operation: One or more threads push new data into the ``ThreadSafeCurve``-buffers. Another thread performs the GUI refresh by calling ``update()`` which will redraw the curve according to the current buffer contents. Args: capacity (``int``, optional): When an integer is supplied it defines the maximum number op points each of the x-data and y-data buffers can store. The x-data buffer and the y-data buffer are each a ring buffer. New readings are placed at the end (right-side) of the buffer, pushing out the oldest readings when the buffer has reached its maximum capacity (FIFO). Use methods ``appendData()`` and ``extendData()`` to push in new data. When ``None`` is supplied the x-data and y-data buffers are each a regular array buffer of undefined length. Use method ``setData()`` to set the data. linked_curve (``pyqtgraph.PlotDataItem``): Instance of ``pyqtgraph.PlotDataItem`` to plot the buffered data out into. shift_right_x_to_zero (``bool``, optional): When plotting, should the x-data be shifted such that the right-side is always set to 0? Useful for history charts. Default: False use_ringbuffer (``bool``, deprecated): Deprecated since v3.1.0. Defined for backwards compatibility. Simply supply a value for ``capacity`` to enable use of a ring buffer. Attributes: x_axis_divisor (``float``): The x-data in the buffer will be divided by this factor when the plot curve is drawn. Useful to, e.g., transform the x-axis units from milliseconds to seconds or minutes. Default: 1 y_axis_divisor (``float``): Same functionality as ``x_axis_divisor``. Default: 1 """ def __init__( self, capacity: Optional[int], linked_curve: pg.PlotDataItem, shift_right_x_to_zero: bool = False, use_ringbuffer=None, # Deprecated arg for backwards compatibility # pylint: disable=unused-argument ): self.capacity = capacity self.curve = linked_curve self.opts = self.curve.opts # Use for read-only self._shift_right_x_to_zero = shift_right_x_to_zero self._use_ringbuffer = capacity is not None self._mutex = QtCore.QMutex() # To allow proper multithreading self.x_axis_divisor = 1 self.y_axis_divisor = 1 if self._use_ringbuffer: self._buffer_x = RingBuffer(capacity=capacity) self._buffer_y = RingBuffer(capacity=capacity) else: self._buffer_x = np.array([]) self._buffer_y = np.array([]) self._snapshot_x = np.array([]) self._snapshot_y = np.array([]) def appendData(self, x, y): """Append a single (x, y)-data point to the ring buffer. """ if self._use_ringbuffer: locker = QtCore.QMutexLocker(self._mutex) self._buffer_x.append(x) self._buffer_y.append(y) locker.unlock() def extendData(self, x_list, y_list): """Extend the ring buffer with a list of (x, y)-data points. """ if self._use_ringbuffer: locker = QtCore.QMutexLocker(self._mutex) self._buffer_x.extend(x_list) self._buffer_y.extend(y_list) locker.unlock() def setData(self, x_list, y_list): """Set the (x, y)-data of the regular array buffer. """ if not self._use_ringbuffer: locker = QtCore.QMutexLocker(self._mutex) self._buffer_x = x_list self._buffer_y = y_list locker.unlock() def update(self, create_snapshot: bool = True): """Update the data behind the curve by creating a snapshot of the current contents of the buffer, and redraw the curve on screen. Args: create_snapshot (``bool``): You can suppress updating the data behind the curve by setting this parameter to False. The curve will then only be redrawn based on the old data. This is useful when the plot is paused. Default: True """ # Create a snapshot of the currently buffered data. Fast operation. if create_snapshot: locker = QtCore.QMutexLocker(self._mutex) self._snapshot_x = np.copy(self._buffer_x) self._snapshot_y = np.copy(self._buffer_y) # print("numel x: %d, numel y: %d" % # (self._snapshot_x.size, self._snapshot_y.size)) locker.unlock() # Now update the data behind the curve and redraw it on screen. # Note: .setData() will internally emit a PyQt signal to redraw the # curve, once it has updated its data members. That's why .setData() # returns almost immediately, but the curve still has to get redrawn by # the Qt event engine, which will happen automatically, eventually. if len(self._snapshot_x) == 0: self.curve.setData([], []) else: x_0 = self._snapshot_x[-1] if self._shift_right_x_to_zero else 0 x = (self._snapshot_x - x_0) / float(self.x_axis_divisor) y = self._snapshot_y / float(self.y_axis_divisor) # self.curve.setData(x,y) # No! Read below. # PyQt5 >= 5.12.3 causes a bug in PyQtGraph where a curve won't # render if it contains NaNs (but only in the case when OpenGL is # disabled). See for more information: # https://github.com/pyqtgraph/pyqtgraph/pull/1287/commits/5d58ec0a1b59f402526e2533977344d043b306d8 # # My approach is slightly different: # NaN values are allowed in the source x and y arrays, but we need # to filter them such that the drawn curve is displayed as # *fragmented* whenever NaN is encountered. The parameter `connect` # will help us out here. # NOTE: When OpenGL is used to paint the curve by setting # pg.setConfigOptions(useOpenGL=True) # pg.setConfigOptions(enableExperimental=True) # the `connect` argument will get ignored and the curve fragments # are connected together into a continuous curve, linearly # interpolating the gaps. Seems to be little I can do about that, # apart from modifying the pyqtgraph source-code in # `pyqtgraph.plotCurveItem.paintGL()`. # # UPDATE 07-08-2020: # Using parameter `connect` as used below will cause: # ValueError: could not broadcast input array from shape ('N') into shape ('< N') # --> arr[1:-1]['c'] = connect # in ``pyqtgraph.functinos.arrayToQPath()`` # This happens when ClipToView is enabled and the curve data extends # past the viewbox limits, when not using OpenGL. # We simply comment out those lines. This results in 100% working # code again, though the curve is no longer shown fragmented but # continuous (with linear interpolation) at each NaN value. That's # okay. finite = np.logical_and(np.isfinite(x), np.isfinite(y)) # connect = np.logical_and(finite, np.roll(finite, -1)) x_finite = x[finite] y_finite = y[finite] # connect = connect[finite] self.curve.setData(x_finite, y_finite) # , connect=connect) @QtCore.pyqtSlot() def clear(self): """Clear the contents of the curve and redraw. """ locker = QtCore.QMutexLocker(self._mutex) if self._use_ringbuffer: self._buffer_x.clear() self._buffer_y.clear() else: self._buffer_x = np.array([]) self._buffer_y = np.array([]) locker.unlock() self.update() def name(self): """Get the name of the curve. """ return self.curve.name() def isVisible(self) -> bool: return self.curve.isVisible() def setVisible(self, state: bool = True): self.curve.setVisible(state) def setDownsampling(self, *args, **kwargs): """All arguments will be passed onto method ``pyqtgraph.PlotDataItem.setDownsampling()`` of the underlying curve. """ self.curve.setDownsampling(*args, **kwargs) @property def size(self) -> Tuple[int, int]: """Number of elements currently contained in the underlying (x, y)- buffers of the curve. Note that this is not necessarily the number of elements of the currently drawn curve. Instead, it reflects the current sizes of the data buffers behind it that will be drawn onto screen by the next call to ``update()``. """ # fmt: off locker = QtCore.QMutexLocker(self._mutex) # pylint: disable=unused-variable # fmt: on return (len(self._buffer_x), len(self._buffer_y))
def test_repr(self): r = RingBuffer(5, dtype=int) for i in range(5): r.append(i) self.assertEqual(repr(r), "<RingBuffer of array([0, 1, 2, 3, 4])>")