Пример #1
0
    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))
Пример #2
0
    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()
Пример #3
0
    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)
Пример #4
0
    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()
Пример #5
0
    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])
Пример #6
0
    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)
Пример #7
0
    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]))
Пример #8
0
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))
Пример #9
0
    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])>")