# Repository: https://gitlab.com/quantify-os/quantify-core
# Licensed according to the LICENCE file on the main branch
"""Module containing the pyqtgraph based plotting monitor."""
import time
import warnings
import pyqtgraph.multiprocess as pgmp
from qcodes import Instrument, Parameter
from qcodes import validators as vals
from qcodes.utils.helpers import strip_attrs
from quantify_core.data.handling import get_datadir
from quantify_core.measurement.control import _DATASET_LOCKS_DIR
# pylint: disable=invalid-name
[docs]
class PlotMonitor_pyqt(Instrument):
"""
Pyqtgraph based plot monitor instrument.
A plot monitor is intended to provide a real-time visualization of a dataset.
The interaction with this virtual instrument are virtually instantaneous.
All the heavier computations and plotting happens in a separate QtProcess.
"""
[docs]
def __init__(self, name: str):
"""
Creates an instance of the Measurement Control.
Parameters
----------
name
Name of this instrument instance
"""
super().__init__(name=name)
# pyqtgraph multiprocessing
# We setup a remote process which creates a queue to which
# "commands" will be sent
self.proc = pgmp.QtProcess(processRequests=False)
# quantify_core module(s) in the remote process
timeout = 60
self._remote_quantify = self.proc._import("quantify_core", timeout=timeout)
self._remote_ppr = self.proc._import(
"quantify_core.visualization.pyqt_plotmon_remote", timeout=timeout
)
# the interface to the remote object
self._remote_plotmon = self._remote_ppr.RemotePlotmon(
instr_name=self.name, dataset_locks_dir=_DATASET_LOCKS_DIR
)
# `initial_cache_value` avoid set_cmd being called at __init__
self.tuids_max_num = Parameter(
vals=vals.Ints(min_value=1, max_value=100),
set_cmd=self._set_tuids_max_num,
get_cmd=self._get_tuids_max_num,
initial_cache_value=3,
name="tuids_max_num",
instrument=self,
)
"""The maximum number of auto-accumulated datasets in :attr:`.tuids`.
Older dataset are discarded when :attr:`.tuids_append` is called [directly or
from :meth:`.update`]."""
# `initial_cache_value` avoid set_cmd being called at __init__
self.tuids = Parameter(
initial_cache_value=[],
vals=vals.Lists(elt_validator=vals.Strings()),
get_cmd=self._get_tuids,
set_cmd=self._set_tuids,
name="tuids",
instrument=self,
)
"""The tuids of the auto-accumulated previous datasets when specified through
:attr:`.tuids_append`.
Can be set to a list ``['tuid_one', 'tuid_two', ...]``.
Can be reset by setting to ``[]``.
See also :attr:`.tuids_extra`."""
# `initial_cache_value` avoid set_cmd being called at __init__
self.tuids_extra = Parameter(
initial_cache_value=[],
vals=vals.Lists(elt_validator=vals.Strings()),
set_cmd=self._set_tuids_extra,
get_cmd=self._get_tuids_extra,
name="tuids_extra",
instrument=self,
)
"""Extra tuids whose datasets are never affected by :attr:`.tuids_append` or
:attr:`.tuids_max_num`.
As opposed to the :attr:`.tuids`, these ones never vanish.
Can be reset by setting to ``[]``. Intended to perform realtime measurements and
have a live comparison with previously measured datasets."""
# Jupyter notebook support
# pylint: disable=invalid-name
self.main_QtPlot = QtPlotObjForJupyter(self._remote_plotmon, "main_QtPlot")
"""Retrieves the image of the main window when used as the final statement in a
cell of a Jupyter-like notebook."""
# pylint: disable=invalid-name
self.secondary_QtPlot = QtPlotObjForJupyter(
self._remote_plotmon, "secondary_QtPlot"
)
"""Retrieves the image of the secondary window when used as the final statement
in a cell of a Jupyter-like notebook."""
# Wrappers for the remote methods
# We just put "commands" on a queue that will be consumed by the
# remote_plotmon
# the commands are just a tuple:
# (
# <str: attr to be called in the remote process>,
# <tuple: a tuple with the arguments passed to the attr>
# )
# see `remote_plotmon._exec_queue`
# For consistency we mirror the label of all methods and set_cmd/get_cmd's
# with the remote_plotmon
# NB: before implementing the queue, _callSync="off" could be used
# to avoid waiting for a return
# e.g. self._remote_plotmon.update(tuid, _callSync="off")
[docs]
def create_plot_monitor(self):
"""
Creates the PyQtGraph plotting monitors.
Can also be used to recreate these when plotting has crashed.
"""
self._remote_plotmon.queue.put(("create_plot_monitor", tuple()))
# Without queue it will be:
# self._remote_plotmon.create_plot_monitor()
[docs]
def update(self, tuid: str = None):
"""
Updates the curves/heatmaps of a specific dataset.
If the dataset is not specified the latest dataset in :attr:`.tuids` is used.
If :attr:`.tuids` is empty and ``tuid`` is provided
then :meth:`tuids_append(tuid) <.tuids_append>` will be called.
NB: this is intended mainly for MC to avoid issues when the file
was not yet created or is empty.
"""
try:
self._remote_plotmon.queue.put(("update", (tuid, get_datadir())))
except Exception as e: # pylint: disable=broad-except
warnings.warn(f"At update encountered: {e}", Warning)
[docs]
def tuids_append(self, tuid: str = None):
"""
Appends a tuid to :attr:`.tuids` and also discards older datasets
according to :attr:`.tuids_max_num`.
The the corresponding data will be plotted in the main window
with blue circles.
NB: do not call before the corresponding dataset file was created and filled
with data
"""
self._remote_plotmon.queue.put(("tuids_append", (tuid, get_datadir())))
def _set_tuids_max_num(self, val):
self._remote_plotmon.queue.put(("_set_tuids_max_num", (val,)))
def _set_tuids(self, tuids: list):
self._remote_plotmon.queue.put(("_set_tuids", (tuids, get_datadir())))
def _set_tuids_extra(self, tuids: list):
self._remote_plotmon.queue.put(("_set_tuids_extra", (tuids, get_datadir())))
# Blocking calls
# For this ones we wait to get the return
def _get_tuids_max_num(self):
# wait to finish the queue
self._remote_plotmon._exec_queue()
return self._remote_plotmon._get_tuids_max_num()
def _get_tuids(self):
# wait to finish the queue
self._remote_plotmon._exec_queue()
return self._remote_plotmon._get_tuids()
def _get_tuids_extra(self):
# wait to finish the queue
self._remote_plotmon._exec_queue()
return self._remote_plotmon._get_tuids_extra()
# Workaround for test due to pickling issues of certain objects
def _get_curves_config(self):
# wait to finish the queue
self._remote_plotmon._exec_queue()
return self._remote_plotmon._get_curves_config()
def _get_traces_config(self, which="main_QtPlot"):
# wait to finish the queue
self._remote_plotmon._exec_queue()
return self._remote_plotmon._get_traces_config(which)
[docs]
def close(self) -> None:
"""
(Modified from Instrument class)
Irreversibly stop this instrument and free its resources.
Subclasses should override this if they have other specific
resources to close.
"""
# Stop the RemotePlotmon, so the timer_queue QTimer is stopped before closing the process
self._remote_plotmon.stop()
# is_stopped.wait() cannot be used (here or in .stop()) because it blocks the QTimer and causes a deadlock. For
# the same reason, this sleep loop can also not be in .stop().
# To solve this, the QTimer could be put in a separate thread.
while not self._remote_plotmon.is_stopped.is_set():
time.sleep(self._remote_plotmon._update_interval_ms / 1000 / 10)
if hasattr(self, "connection") and hasattr(self.connection, "close"):
self.connection.close()
# Essential!!! Close the process
self.proc.join()
strip_attrs(self, whitelist=["_name"])
self.remove_instance(self)
# pylint: disable=invalid-name
[docs]
def setGeometry_main(self, x: int, y: int, w: int, h: int):
"""Set the geometry of the main plotmon
Parameters
----------
x
Horizontal position of the top-left corner of the window
y
Vertical position of the top-left corner of the window
w
Width of the window
h
Height of the window
"""
# wait to finish the queue
self._remote_plotmon._exec_queue()
self._remote_plotmon._set_qt_plot_geometry(x, y, w, h, which="main_QtPlot")
# pylint: disable=invalid-name
[docs]
def setGeometry_secondary(self, x: int, y: int, w: int, h: int):
"""Set the geometry of the secondary plotmon
Parameters
----------
x
Horizontal position of the top-left corner of the window
y
Vertical position of the top-left corner of the window
w
Width of the window
h
Height of the window
"""
# wait to finish the queue
self._remote_plotmon._exec_queue()
self._remote_plotmon._set_qt_plot_geometry(x, y, w, h, which="secondary_QtPlot")
# pylint: disable=too-few-public-methods
[docs]
class QtPlotObjForJupyter:
"""
A wrapper to be able to display a QtPlot window in Jupyter notebooks
"""
def __init__(self, remote_plotmon, attr_name):
# Save reference of the remote object
self._remote_plotmon = remote_plotmon
self.attr_name = attr_name
def _repr_png_(self):
# wait to finish the queue
self._remote_plotmon._exec_queue()
# always get the remote object, avoid keeping object references
return getattr(self._remote_plotmon, self.attr_name)._repr_png_()