diff --git a/.github/actions/install-dependencies-and-plottr/action.yml b/.github/actions/install-dependencies-and-plottr/action.yml index 22621410..ce6eb391 100644 --- a/.github/actions/install-dependencies-and-plottr/action.yml +++ b/.github/actions/install-dependencies-and-plottr/action.yml @@ -8,5 +8,5 @@ runs: python -m pip install --upgrade pip setuptools wheel pip install -r requirements.txt pip install -r test_requirements.txt - pip install .[pyqt5] + pip install .[pyside6] shell: bash diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 3519a2cd..d4c1364c 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -18,9 +18,16 @@ jobs: DISPLAY: ':99.0' steps: - - name: setup ubuntu-latest xvfb +# - name: setup ubuntu-latest xvfb +# run: | +# sudo apt install libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0 x11-utils libgl1 libegl1 libdbus-1-3 libegl1-mesa-dev +# /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -screen 0 1920x1200x24 -ac +extension GLX + # Source - https://stackoverflow.com/questions/75497408/github-action-pytest-exit-code-134 + # Posted by Justin Buiel + # Retrieved 2026-01-03, License - CC BY-SA 4.0 + - uses: tlambert03/setup-qt-libs@v1 + - name: build "display" run: | - sudo apt install libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0 x11-utils /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -screen 0 1920x1200x24 -ac +extension GLX - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} diff --git a/doc/Makefile b/doc/Makefile deleted file mode 100644 index d4bb2cbb..00000000 --- a/doc/Makefile +++ /dev/null @@ -1,20 +0,0 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line, and also -# from the environment for the first two. -SPHINXOPTS ?= -SPHINXBUILD ?= sphinx-build -SOURCEDIR = . -BUILDDIR = _build - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -.PHONY: help Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/doc/api/data.rst b/doc/api/data.rst deleted file mode 100644 index 74d16b02..00000000 --- a/doc/api/data.rst +++ /dev/null @@ -1,5 +0,0 @@ -Data format: DataDict ---------------------- - -.. automodule:: plottr.data.datadict - :members: \ No newline at end of file diff --git a/doc/api/index.rst b/doc/api/index.rst deleted file mode 100644 index 4b4f0a66..00000000 --- a/doc/api/index.rst +++ /dev/null @@ -1,10 +0,0 @@ -API documentation -================= - -.. toctree:: - :maxdepth: 2 - :caption: Contents: - - node - plot - data \ No newline at end of file diff --git a/doc/api/node.rst b/doc/api/node.rst deleted file mode 100644 index 3c24ca52..00000000 --- a/doc/api/node.rst +++ /dev/null @@ -1,7 +0,0 @@ -Node and Flowchart core elements --------------------------------- - -Node essentials: the node module -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -.. automodule:: plottr.node.node - :members: diff --git a/doc/api/plot.rst b/doc/api/plot.rst deleted file mode 100644 index faa67bb7..00000000 --- a/doc/api/plot.rst +++ /dev/null @@ -1,67 +0,0 @@ -Plotting elements -################# - -.. _Base plot API: - -Base plotting elements -^^^^^^^^^^^^^^^^^^^^^^ - -Overview -======== - -Classes for plotting functionality ----------------------------------- - -* :class:`.PlotNode` : The base class for a `.Node` with the purpose of receiving data for visualization. -* :class:`.PlotWidgetContainer` : A class that contains a `PlotWidget` (and can change it during runtime) -* :class:`.PlotWidget` : An abstract widget that can be inherited to implement actual plotting. -* :class:`.AutoFigureMaker` : A convenience class for semi-automatic generation of figures. - The purpose is to keep actual plotting code out of the plot widget. This is not mandatory, just convenient. - -Data structures ---------------- - -* :class:`.PlotDataType` : Enum with types of data that can be plotted. -* :class:`.ComplexRepresentation`: Enum with ways to represent complex-valued data. - - -Additional tools ----------------- - -* :func:`.makeFlowchartWithPlot` : convenience function for creating a flowchart that leads to a plot node. -* :func:`.determinePlotDataType` : try to infer which type of plot data is in a data set. - -Object Documentation -==================== - -.. automodule:: plottr.plot.base - :members: - -.. _MPL plot API: - -Matplotlib plotting tools -^^^^^^^^^^^^^^^^^^^^^^^^^ - -Overview -======== - -.. automodule:: plottr.plot.mpl - :members: - -Object Documentation -==================== - -General Widgets ---------------- -.. automodule:: plottr.plot.mpl.widgets - :members: - -General plotting tools ----------------------- -.. automodule:: plottr.plot.mpl.plotting - :members: - -Autoplot --------- -.. automodule:: plottr.plot.mpl.autoplot - :members: diff --git a/doc/concepts/data.rst b/doc/concepts/data.rst deleted file mode 100644 index c37020ae..00000000 --- a/doc/concepts/data.rst +++ /dev/null @@ -1,59 +0,0 @@ -.. documentation of the internal data formats. - -Data formats -++++++++++++ - -The main format we're using within plottr is the ``DataDict``. While most of the actual numeric data will typically live in numpy arrays (or lists, or similar), they don't typically capture easily arbitrary metadata and relationships between arrays. Say, for example, we have some data ``z`` that depends on two other variables, ``x`` and ``y``. This information has be stored somewhere, and numpy doesn't offer readily a solution here. There are various extensions, for example `xarray `_ or the `MetaArray class `_. Those however typically have a grid format in mind, which we do not want to impose. Instead, we use a wrapper around the python dictionary that contains all the required meta information to infer the relevant relationships, and that uses numpy arrays internally to store the numeric data. Additionally we can store any other arbitrary meta data. - -A DataDict container (a `dataset`) can contain multiple `data fields` (or variables), that have values and can contain their own meta information. Importantly, we distinct between independent fields (the `axes`) and dependent fields (the `data`). - -Despite the naming, `axes` is not meant to imply that the `data` have to have a certain shape (but the degree to which this is true depends on the class used). A list of classes for different shapes of data can be found below. - -The basic structure of data conceptually looks like this (we inherit from `dict`) :: - - { - 'data_1' : { - 'axes' : ['ax1', 'ax2'], - 'unit' : 'some unit', - 'values' : [ ... ], - '__meta__' : 'This is very important data', - ... - }, - 'ax1' : { - 'axes' : [], - 'unit' : 'some other unit', - 'values' : [ ... ], - ..., - }, - 'ax2' : { - 'axes' : [], - 'unit' : 'a third unit', - 'values' : [ ... ], - ..., - }, - '__globalmeta__' : 'some information about this data set', - '__moremeta__' : 1234, - ... - } - -In this case we have one dependent variable, ``data_1``, that depends on two axes, ``ax1`` and ``ax2``. This concept is restricted only in the following way: - -* a dependent can depend on any number of independents -* an independent cannot depend on other fields itself -* any field that does not depend on another, is treated as an axis - -Note that meta information is contained in entries whose keys start and end with double underscores. Both the DataDict itself, as well as each field can contain meta information. - -In the most basic implementation, the only restriction on the data values is that they need to be contained in a sequence (typically as list, or numpy array), and that the length of all values in the data set (the number of `records`) must be equal. Note that this does not preclude nested sequences! - - -Relevant data classes ---------------------- -:DataDictBase: The main base class. Only checks for correct dependencies. Any - requirements on data structure is left to the inheriting classes. The class contains methods for easy access to data and metadata. -:DataDict: The only requirement for valid data is that the number of records is the - same for all data fields. Contains some tools for expansion of data. -:MeshgridDataDict: For data that lives on a grid (not necessarily regular). - -For more information, see the API documentation. - diff --git a/doc/concepts/index.rst b/doc/concepts/index.rst deleted file mode 100644 index 0f0cfb44..00000000 --- a/doc/concepts/index.rst +++ /dev/null @@ -1,13 +0,0 @@ -Working principles of plottr -============================ - -This section documents how plottr works internally. -It should enable anyone who's interested in writing their own components -to get the basic ideas and find their way through the code. - -.. toctree:: - :maxdepth: 2 - :caption: Contents: - - nodes - data diff --git a/doc/concepts/nodes.bak.rst b/doc/concepts/nodes.bak.rst deleted file mode 100644 index 42638660..00000000 --- a/doc/concepts/nodes.bak.rst +++ /dev/null @@ -1,117 +0,0 @@ -.. documentation for nodes and flowchart. - -Nodes and Flowcharts -==================== - -Contents --------- - -* `Setting up flowcharts`_ -* `Create you own nodes`_ - - - -The basic concept of modular data analyis as we use it in plottr consists of `Nodes` that are connected directionally to form a `Flowchart`. This terminology is adopted from the great `pyqtgraph `_ project; we currently use their `Node` and `Flowchart` API under the hood as well. Executing the flowchart means that data flows through the nodes via connections that have been made between them, and gets modified in some way by each node along the way. The end product is then the fully processed data. This whole process is typically on-demand: If a modification of the data flow occurs somewhere in the flowchart -- e.g., due to user input -- then only 'downstream' nodes need to re-process data in order to keep the flowchart output up to date. - - -.. _Setting up flowcharts: - -Setting up flowcharts ---------------------- - -TBD. - -.. _Create you own nodes: - -Creating custom nodes ---------------------- - -The following are some general notes. For an example see the notebook ``Custom nodes`` under ``doc/examples``. - -The class :class:`plottr.node.node.Node` forms the basis for all nodes in plottr. It is an extension of ``pyqtgraph``'s Node class with some additional tools, and defaults. - - -Basics: -^^^^^^^ -The actual data processing the node is supposed to do is implemented in :meth:`plottr.node.node.Node.process`. - - -Defaults: -^^^^^^^^^ - -Per default, we use an input terminal (``dataIn``), and one output terminal (``dataOut``). Can be overwritten via the attribute :attr:`plottr.node.node.Node.terminals`. - -User options: -^^^^^^^^^^^^^ - -We use ``property`` for user options. i.e., we implement a setter and getter function (e.g., with the ``@property`` decorator). The setter can be decorated with :meth:`plottr.node.node.updateOption` to automatically process the option change on assignment. - -Synchronizing Node and UI: -^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The UI widget is automatically instantiated when :attr:`plottr.node.node.Node.uiClass` is set to an appropriate node widget class, and :attr:`plottr.node.node.Node.useUi` is ``True``. - -Messaging between Node and Node UI is implemented through Qt signals/slots. Any update to a node property is signalled automatically when the property setter is decorated with :meth:`plottr.node.node.updateOption`. A setter decorated with ``@updateOption('myOption')`` will, on assignment of the new value, call the function assigned to ``plottr.node.node.NodeWidget.optSetter['myOption']``. - -Vice versa, there are tools to notify the node of changes made through the UI. Any trigger (such as a widget signal) can be connected to the UI by calling the functions :meth:`plottr.node.node.NodeWidget.signalOption` with the option name (say, ``myOption``) as argument, or :meth:`plottr.node.node.NodeWidget.signalAllOptions`. In the first case, the value of the option is taken by calling ``plottr.node.node.NodeWidget.optGetter['myOption']()``, and then the name of the option and that value are emitted through :meth:`plottr.node.node.updateGuiFromNode`; this is connected to :meth:`plottr.node.node.Node.setOption` by default. Similarly, :meth:`plottr.node.node.NodeWidget.signalAllOptions` results in a signal leading to :meth:`plottr.node.node.Node.setOptions`. - -The implementation of the suitable triggers for emitting the option value and assigning functions to entries in ``optSetters`` and ``optGetters`` is up to the re-implementation. - - -Example implementation: -^^^^^^^^^^^^^^^^^^^^^^^ - -The implementation of a custom node with GUI can then looks something like this:: - - class MyNode(Node): - - useUi = True - uiClass = MyNodeGui - - ... - - @property - def myOption(self): - return self._myOption - - # the name in the decorator should match the name of the - # property to make sure communication goes well. - @myOption.setter - @updateOption('myOption') - def myOption(self, value): - # this could include validation, etc. - self._myOption = value - - ... - - -That is essentially all that is needed for the Node; only the process function that does something depending on the value of ``myOption`` is missing here. The UI class might then look like this:: - - class MyNodeGui(NodeWidget): - - def __init__(self, parent=None): - # this is a Qt requirement - super().__init__(parent) - - somehowSetUpWidget() - - self.optSetters = { - 'myOption' : self.setMyOption, - } - self.optGetters = { - 'myOption' : self.getMyOption, - } - - # often the trigger will be a valueChanged function or so, - # that returns a value. Since the signalOption function - # doesn't require one, we can use a lambda to bypass, if necessary. - self.somethingChanged.connect(lambda x: self.signalOption('myOption')) - - def setMyOption(self, value): - doSomething() - - def getMyOption(self): - return getInfoNeeded() - - -This node can then already be used, with the UI if desired, in a flowchart. diff --git a/doc/concepts/nodes.rst b/doc/concepts/nodes.rst deleted file mode 100644 index 1c53e79d..00000000 --- a/doc/concepts/nodes.rst +++ /dev/null @@ -1,4 +0,0 @@ -.. documentation for nodes and flowchart. - -Nodes and Flowcharts -==================== diff --git a/doc/conf.py b/doc/conf.py deleted file mode 100644 index 0ab4d645..00000000 --- a/doc/conf.py +++ /dev/null @@ -1,58 +0,0 @@ -# Configuration file for the Sphinx documentation builder. -# -# This file only contains a selection of the most common options. For a full -# list see the documentation: -# https://www.sphinx-doc.org/en/master/usage/configuration.html - -# -- Path setup -------------------------------------------------------------- - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# -# import os -# import sys -# sys.path.insert(0, os.path.abspath('.')) - - -# -- Project information ----------------------------------------------------- - -project = 'plottr' -copyright = '2019-2021, Wolfgang Pfaff' -author = 'Wolfgang Pfaff' - - -# -- General configuration --------------------------------------------------- - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.napoleon', - 'sphinx_autodoc_typehints', - 'sphinx.ext.todo', - 'sphinx.ext.mathjax', -] -napoleon_use_param = True - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This pattern also affects html_static_path and html_extra_path. -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] - - -# -- Options for HTML output ------------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# -html_theme = 'sphinx_rtd_theme' - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] \ No newline at end of file diff --git a/doc/examples.rst b/doc/examples.rst deleted file mode 100644 index 41b9db81..00000000 --- a/doc/examples.rst +++ /dev/null @@ -1,18 +0,0 @@ -Customization examples -====================== - -Here we want to show some ideas on how plottr could be customized. -can include other files (scripts, etc) if necessary. -Maybe this file should be moved into a subdirectory. - - -Creating a custom node ----------------------- - -to be done. - - -Creating a custom app ---------------------- - -to be done. diff --git a/doc/examples/Inferring grids.ipynb b/doc/examples/Inferring grids.ipynb deleted file mode 100644 index 0c8cc76c..00000000 --- a/doc/examples/Inferring grids.ipynb +++ /dev/null @@ -1,787 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "Collapsed": "false" - }, - "source": [ - "# The problem: inferring grids" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "Collapsed": "false" - }, - "source": [ - "When we acquire data we often do so point-by-point, or chunk-by-chunk. And in general it is not possible to know in advance what shape exactly the final data will have. For multi-dimensional data this means that we don't always know on what kind of of grid the data lies, if any. That information, however, is important for a variety of tasks we would like to perform, such as slicing our data, or plotting projections of it. And we want to do all of these already when we don't have the full data set yet, i.e., while a data acquisition is still running.\n", - "\n", - "Things would of course be much easier if a grid of the right shape was pre-allocated and then gradually filled. But most data saving is not done that way (like in qcodes, for example). For that reason, we look at a few ways on how to infer grids from tabular data, where data is saved row-by-row." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "Collapsed": "false" - }, - "source": [ - "# Setting up\n", - "\n", - "These are the important imports and some tool. Execute this first." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": { - "Collapsed": "false" - }, - "outputs": [], - "source": [ - "%matplotlib widget" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": { - "Collapsed": "false" - }, - "outputs": [], - "source": [ - "import numpy as np\n", - "from matplotlib import pyplot as plt\n", - "plt.close('all')\n", - "\n", - "from plottr.data import datadict as dd\n", - "from plottr.utils import num\n", - "from plottr.utils.num import _find_switches, is_invalid\n", - "from plottr.plot.mpl import ppcolormesh_from_meshgrid\n", - "\n", - "\n", - "def plot_image(x, y, z, ax=None, title=''):\n", - " \"\"\"Plot a grid as image. The arrays x, y, z need to be in meshgrid form.\"\"\"\n", - " \n", - " if ax is None:\n", - " fig, ax = plt.subplots(1, 1)\n", - " fig.canvas.layout.width = '500px'\n", - " fig.canvas.layout.height = '500px'\n", - " fig.subplots_adjust(top=0.9)\n", - " fig.suptitle(title)\n", - " \n", - " _x = x.flatten()\n", - " _y = y.flatten()\n", - " x0, x1 = _x[~np.isnan(_x)].min(), _x[~np.isnan(_x)].max()\n", - " y0, y1 = _y[~np.isnan(_y)].min(), _y[~np.isnan(_y)].max()\n", - " extent = [x0, x1, y0, y1]\n", - " z2 = z.copy()\n", - " z2 = z2 if x[0, 0] < x[1, 0] else z2[::-1, :]\n", - " z2 = z2 if y[0, 0] < y[0, 1] else z2[:, ::-1]\n", - " ax.imshow(z2.T, origin='lower', extent=extent, aspect='auto')\n", - "\n", - " \n", - "def plot_grid2d(x, y, z, title=''):\n", - " \"\"\"Plot a grid as image and pcolormesh side by side. x, y, z need to be meshgrids.\"\"\"\n", - " \n", - " fig, axes = plt.subplots(1, 2, sharex='all', sharey='all')\n", - " ax = axes[0]\n", - " plot_image(x, y, z, ax=ax)\n", - " \n", - " ax = axes[1]\n", - " ppcolormesh_from_meshgrid(ax, x, y, z)\n", - " \n", - " fig.tight_layout()\n", - " fig.canvas.layout.width = '800px'\n", - " fig.canvas.layout.height = '400px'\n", - " fig.suptitle(title)\n", - " fig.subplots_adjust(top=0.9)\n", - " \n", - "\n", - "def add_noise(grid2d, scale='auto'):\n", - " if scale == 'auto':\n", - " scale = grid2d.std() * 0.2\n", - " \n", - " for irow, row in enumerate(grid2d):\n", - " for ipt, pt in enumerate(row):\n", - " grid2d[irow, ipt] += np.random.normal(scale=scale)\n", - " \n", - " return grid2d\n", - " \n", - "\n", - "def gridpattern(x, y, noise=False, noise_scale='auto'):\n", - " xx, yy = np.meshgrid(x, y, indexing='ij')\n", - " \n", - " if noise:\n", - " xx = add_noise(xx, scale=noise_scale)\n", - " yy = add_noise(yy, scale=noise_scale)\n", - "\n", - " zz = np.sinc((xx**2 + yy**2)**.5)\n", - " for ix, _x in enumerate(x):\n", - " for iy, _y in enumerate(y):\n", - " if (ix%2 and iy%2) or (not ix%2 and not iy%2):\n", - " zz[ix, iy] -= -0.1\n", - " \n", - " return xx, yy, zz\n", - "\n", - "\n", - "def find_and_plot_switches(**arrs):\n", - " fig, axes = plt.subplots(len(arrs), 1, sharex='all')\n", - " if len(arrs) == 1:\n", - " axes = [axes]\n", - " \n", - " i = 0\n", - " for k, a in arrs.items():\n", - " switches = _find_switches(a)\n", - " axes[i].plot(a, drawstyle='steps-mid', color='b')\n", - " for s in switches:\n", - " axes[i].axvline(s, color='r')\n", - " axes[i].set_ylabel(k)\n", - " i+=1\n", - " axes[-1].set_xlabel('index')\n", - " \n", - " return switches, axes" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "Collapsed": "false" - }, - "source": [ - "# Inferring grids from sweep directions" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "Collapsed": "false" - }, - "source": [ - "The main method we'll be using to infer the grid is to look at systematics in the coordinates (the *independents*) of the data.\n", - "Since our main focus is to look at measurements, we look at the way the coordinates are swept or rastered -- this is by far the most common way how control parameters are changed in the lab.\n", - "\n", - "Very commonly, we sweep over our coordinates in nested loops, which then naturally form a grid. Coordinates then typically look something like this:" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": { - "Collapsed": "false" - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "(21, 15) (21, 15) (21, 15)\n" - ] - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "f265f63d61f14e2eb5d5ede914d01853", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Canvas(toolbar=Toolbar(toolitems=[('Home', 'Reset original view', 'home', 'home'), ('Back', 'Back to previous …" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# define two coordinate axes\n", - "x = np.linspace(-3, 3, 21)\n", - "y = np.linspace(-2, 2, 15)\n", - "\n", - "# make some fake data on a grid spanned by x and y\n", - "# internally, we use np.meshgrid(x, y, indexing='ij'), which produces a grid \n", - "# as if we had looped over x and y, x being the outer loop.\n", - "xx, yy, zz = gridpattern(x, y)\n", - "\n", - "# print the shapes\n", - "print(xx.shape, yy.shape, zz.shape)\n", - "\n", - "# plot the grid as image\n", - "plot_image(xx, yy, zz)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "Collapsed": "false" - }, - "source": [ - "When setting this up, we of course know exactly the shape, and we can do all operations like slicing, plotting, etc. right away, as shown above.\n", - "(**Note**: to visualize the grid, the data is overlayed with a checker board pattern)\n", - "\n", - "However, we might not have that information in the final data (or it could be that we didn't finish the sweep, and we only have parts of the grid). The only thing we can rely on in the end, is the data. And if it's stored in a tabular format, the shape information may be gone or incorrect.\n", - "\n", - "We will start with flattened data, like:" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": { - "Collapsed": "false" - }, - "outputs": [], - "source": [ - "x1d = xx.flatten()\n", - "y1d = yy.flatten()\n", - "z1d = zz.flatten()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "Collapsed": "false" - }, - "source": [ - "To reconstruct the grid, we can analyze the values of the coordinates that occur in the data. In general that can be tricky -- you would need to look at all occuring values and then sort the data accordingly onto the grid formed by all coordinates found.\n", - "\n", - "However, for the cases where grids are useful to start with, the experimenter will (hopefully!) have systematically swept over the coordinates (as we have done above, essentially).\n", - "If the sweeps are monotonous, we can reconstruct grids simply using `np.reshape`. The only thing we need to figure out is the shape of the grid we want to make.\n", - "\n", - "To do that, we can simply look at the evolution of the coordinates:" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": { - "Collapsed": "false" - }, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "b422f7c51bea44aab97eb672fa70e952", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Canvas(toolbar=Toolbar(toolitems=[('Home', 'Reset original view', 'home', 'home'), ('Back', 'Back to previous …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "(array([ 14, 29, 44, 59, 74, 89, 104, 119, 134, 149, 164, 179, 194,\n", - " 209, 224, 239, 254, 269, 284, 299]),\n", - " array([,\n", - " ],\n", - " dtype=object))" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "find_and_plot_switches(x=x1d, y=y1d)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "Collapsed": "false" - }, - "source": [ - "When we assume that sweep direction is monotonous, then we can simply count the number switches in direction (the period) to figure out how often an axis dimension is swept. The suspected switches are marked in red in the plot above.\n", - "\n", - "From these periods it is then easy to get the shape: Here we see that `y` is repeated 21 times -- that means 21 is the number of `x` values on the grid, and the total size divided by 21 is the number of `y` values.\n", - "\n", - "This basic principle is in a function that guesses the grid shape -- `plottr.utils.num.guess_grid_from_sweep_direction` --, which is automatically used in `plottr.data.datadict.datadict_to_meshgrid`." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": { - "Collapsed": "false" - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "['x', 'y'] (21, 15)\n" - ] - } - ], - "source": [ - "order, shape = num.guess_grid_from_sweep_direction(x=x1d, y=y1d)\n", - "print(order, shape)" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": { - "Collapsed": "false" - }, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "2a1ec645c48641ea9235e8f14fcdb1b2", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Canvas(toolbar=Toolbar(toolitems=[('Home', 'Reset original view', 'home', 'home'), ('Back', 'Back to previous …" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "plot_image(x1d.reshape(shape), y1d.reshape(shape), z1d.reshape(shape))" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "Collapsed": "false" - }, - "source": [ - "It's important to note that order of course matters. Consider this example:" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": { - "Collapsed": "false" - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "['not_really_y', 'not_really_x'] (21, 15)\n" - ] - } - ], - "source": [ - "order, shape = num.guess_grid_from_sweep_direction(not_really_x=y1d, not_really_y=x1d)\n", - "print(order, shape)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "Collapsed": "false" - }, - "source": [ - "The function that determines the grid shape gives us the correct answer -- but now the roles of x and y are swapped, because `not_really_y` is now the outer loop. This is important to keep in mind when doing things programatically." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "Collapsed": "false" - }, - "source": [ - "# Irregular grids" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "Collapsed": "false" - }, - "source": [ - "You might have wondered why were looking at sweep patterns rather than unique values, which might be easier to analyze.\n", - "The reason is that it's entirely possible to have a well-defined grid even when the coordinates in each row/column are not repeating exactly." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "Collapsed": "false" - }, - "source": [ - "## Noise\n", - "\n", - "One example is when the coordinate itself is subject to noise or other variations:" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": { - "Collapsed": "false" - }, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "360776dd48ba41809f80cb2c637fe0c5", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Canvas(toolbar=Toolbar(toolitems=[('Home', 'Reset original view', 'home', 'home'), ('Back', 'Back to previous …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "(array([ 8, 17, 26, 35]),\n", - " array([,\n", - " ],\n", - " dtype=object))" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "x = np.linspace(0, 2, 5)\n", - "y = np.linspace(-2, 2, 9)\n", - "\n", - "# steps are 0.5 on each coordinate -- add some noise on a scale that's somewhat smaller\n", - "xx, yy, zz = gridpattern(x, y, noise=True, noise_scale=0.2)\n", - "\n", - "# the data we'll get in practice is flattened again\n", - "x1d = xx.flatten()\n", - "y1d = yy.flatten()\n", - "z1d = zz.flatten()\n", - "\n", - "# plot coordinatates\n", - "find_and_plot_switches(x=x1d, y=y1d)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "Collapsed": "false" - }, - "source": [ - "It's obvious that this data would be hard to sort back onto a grid by looking at actual values. But because the noise is less than the (intentional) variation between the coordinates, we can still infer the grid shape by identifying large switches:" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": { - "Collapsed": "false" - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "['x', 'y'] (5, 9)\n" - ] - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "4660a4e8af22400ca253a4cba801f612", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Canvas(toolbar=Toolbar(toolitems=[('Home', 'Reset original view', 'home', 'home'), ('Back', 'Back to previous …" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "order, shape = num.guess_grid_from_sweep_direction(x=x1d, y=y1d)\n", - "print(order, shape)\n", - "\n", - "plot_grid2d(x1d.reshape(shape), y1d.reshape(shape), z1d.reshape(shape))" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "Collapsed": "false" - }, - "source": [ - "The left plot shows the grid plotted as image, whereas a the right is showing a more accurate representation where the coordinates are moved by the added noise." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "Collapsed": "false" - }, - "source": [ - "## Adaptive measurements\n", - "\n", - "Another example where irregular grids can occur is an adaptive sweep, where the coordinates in one dimension depend on the values of another. \n", - "A simple, artificial example, we again look at an image of the grid and also the 'real' representation." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": { - "Collapsed": "false" - }, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "c92b5e3000484d8fb8d6a69b9c1e0b90", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Canvas(toolbar=Toolbar(toolitems=[('Home', 'Reset original view', 'home', 'home'), ('Back', 'Back to previous …" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "x = np.linspace(-2, 2, 11)\n", - "y = np.linspace(-1, 1, 11)\n", - "xx, yy = np.meshgrid(x, y, indexing='ij')\n", - "\n", - "# we're stretching the grid a bit, depending on the value of x\n", - "for i in range(y.size):\n", - " yy[i,:] *= (2 * np.exp(-x[i]**2/2.))\n", - "\n", - "# mock data: a gaussian peak in 2D\n", - "zz = np.exp(-xx**2 - yy**2)\n", - "\n", - "plot_grid2d(xx, yy, zz)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "Collapsed": "false" - }, - "source": [ - "But of course, looking at switches still works. \n", - "\n", - "**Note:** When the distortion gets very bad, then it can become difficult to detect switches (when the magnitude of a switch is not much larger than the variations in the coordinate sweep). Then our method can fail." - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": { - "Collapsed": "false" - }, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "80d52793a21d4ffbbcd999443670c210", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Canvas(toolbar=Toolbar(toolitems=[('Home', 'Reset original view', 'home', 'home'), ('Back', 'Back to previous …" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "(array([ 10, 21, 32, 43, 54, 65, 76, 87, 98, 109]),\n", - " array([,\n", - " ],\n", - " dtype=object))" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "x1d = xx.flatten()\n", - "y1d = yy.flatten()\n", - "z1d = zz.flatten()\n", - "find_and_plot_switches(x=x1d, y=y1d)" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": { - "Collapsed": "false" - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "['x', 'y'] (11, 11)\n" - ] - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "655538921d654e6c80488b7f80ec3166", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Canvas(toolbar=Toolbar(toolitems=[('Home', 'Reset original view', 'home', 'home'), ('Back', 'Back to previous …" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "order, shape = num.guess_grid_from_sweep_direction(x=x1d, y=y1d)\n", - "print(order, shape)\n", - "\n", - "plot_grid2d(x1d.reshape(shape), y1d.reshape(shape), z1d.reshape(shape))" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "Collapsed": "false" - }, - "source": [ - "# Incomplete data" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "Collapsed": "false" - }, - "source": [ - "An important task is gridding of data where the target grid hasn't been fully filled.\n", - "This arises, for example, when a measurement is still ongoing, or has been aborted before finishing.\n", - "Then, the size of the data is generally not readily suited for reshaping.\n", - "\n", - "In this case we have implemented functionality to 'pad' the data such that a grid is possible again. \n", - "To do that we find the smallest possible grid that encloses the data, fill the data with NaN, and then reshape.\n", - "To make our life a bit easier, we use the DataDict format which has tools for this." - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": { - "Collapsed": "false" - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Full shapes: (21, 15) (21, 15) (21, 15)\n", - "Grid shape of the incomplete data: (15, 15)\n", - "DataDict shape: (15, 15)\n" - ] - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "cc55bf52a829458aabb5f23d569d483d", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Canvas(toolbar=Toolbar(toolitems=[('Home', 'Reset original view', 'home', 'home'), ('Back', 'Back to previous …" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# define two coordinate axes\n", - "x = np.linspace(-3, 3, 21)\n", - "y = np.linspace(-2, 2, 15)\n", - "\n", - "# make some fake data on a grid spanned by x and y\n", - "xx, yy, zz = gridpattern(x, y)\n", - "\n", - "# print the full shapes\n", - "print(\"Full shapes:\", xx.shape, yy.shape, zz.shape)\n", - "\n", - "# now make flattened data where some entries are missing at the end\n", - "nmissing = 100\n", - "x1d = xx.flatten()[:-nmissing]\n", - "y1d = yy.flatten()[:-nmissing]\n", - "z1d = zz.flatten()[:-nmissing]\n", - "\n", - "# note: we can still find the grid!\n", - "order, shape = num.guess_grid_from_sweep_direction(x=x1d, y=y1d)\n", - "print(\"Grid shape of the incomplete data:\", shape)\n", - "\n", - "# reconstruct the correct grid\n", - "# to do so we use the datadict format and its convenience tools:\n", - "data1d = dd.DataDict(\n", - " x = dict(values=x1d),\n", - " y = dict(values=y1d),\n", - " z = dict(values=z1d, axes=['x', 'y'])\n", - ")\n", - "\n", - "# guessing the grid, padding, and reshaping is all automatic here\n", - "data2d = dd.datadict_to_meshgrid(data1d)\n", - "print(\"DataDict shape:\", data2d.shape())\n", - "\n", - "# plot the grid\n", - "plot_image(data2d.data_vals('x'), data2d.data_vals('y'), data2d.data_vals('z'))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "Collapsed": "false" - }, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python [conda env:qcodes]", - "language": "python", - "name": "conda-env-qcodes-py" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.5" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/doc/examples/Live plotting qcodes data.ipynb b/doc/examples/Live plotting qcodes data.ipynb deleted file mode 100644 index 148f67dc..00000000 --- a/doc/examples/Live plotting qcodes data.ipynb +++ /dev/null @@ -1,509 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "Collapsed": "false" - }, - "source": [ - "# Introduction" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "Collapsed": "false" - }, - "source": [ - "This notebook illustrates the basics of how to use `plottr` -- in particular, the `inspectr` and `autoplot` tools -- to live plot data in a qcodes database." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "Collapsed": "false" - }, - "source": [ - "## Basic notebook setup" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "Collapsed": "false", - "ExecuteTime": { - "end_time": "2019-05-07T06:57:34.632640Z", - "start_time": "2019-05-07T06:57:34.606712Z" - } - }, - "outputs": [], - "source": [ - "DBPATH = './qcodes_liveplot_demo.db'\n", - "\n", - "import qcodes as qc\n", - "\n", - "qc.config.core.db_location = DBPATH\n", - "qc.initialise_database()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "Collapsed": "false" - }, - "source": [ - "# Launching inspectr" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "Collapsed": "false" - }, - "source": [ - "Next, we need to run the inspectr tool from the command line in a separate process. From within the plottr root directory, run \n", - "\n", - "``\n", - "$ python apps/inspectr.py --dbpath=./doc/examples/qcodes_liveplot_demo.db\n", - "``" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "Collapsed": "false" - }, - "source": [ - "We should now have two windows open; no data is yet shown if we started with a fresh .db file. \n", - "Now, before populating the database, let's enable automatic monitoring of the dataset. To do that, enter a refresh interval (given in seconds) in the inspectr window toolbar, and enable the auto-plot option." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "Collapsed": "false" - }, - "source": [ - "# Dummy experiments" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "Collapsed": "false" - }, - "source": [ - "Below are a few dummy qcodes experiments that should hopefully illustrate how the live plotter behaves. Run them while the inspectr is open, and monitoring is active (or not -- you can also refresh manually by pressing 'R'; this works for both inspectr and the autoplotter). " - ] - }, - { - "cell_type": "markdown", - "metadata": { - "Collapsed": "false", - "ExecuteTime": { - "end_time": "2018-12-31T12:47:55.343906Z", - "start_time": "2018-12-31T12:47:55.309855Z" - } - }, - "source": [ - "## Qcodes imports (and other relevant stuff)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "Collapsed": "false", - "ExecuteTime": { - "end_time": "2019-05-13T12:16:36.430546Z", - "start_time": "2019-05-13T12:16:26.671614Z" - } - }, - "outputs": [], - "source": [ - "import time\n", - "import numpy as np\n", - "from qcodes import load_or_create_experiment, Measurement, Parameter" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "Collapsed": "false" - }, - "source": [ - "## A very simple 1D sweep" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "Collapsed": "false" - }, - "source": [ - "This is the most basic measurement type we can imagine: sweep one independent parameter (`x`) and record data, point-by-point, as a function of that. \n", - "Here we have two dependents, `y` and `y2`.\n", - "\n", - "In plottr, you'll see a window with the the two line traces for the dependents." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "Collapsed": "false", - "ExecuteTime": { - "end_time": "2019-01-29T22:41:12.251621Z", - "start_time": "2019-01-29T22:41:12.246635Z" - } - }, - "outputs": [], - "source": [ - "xvals = np.linspace(0, 10, 101)\n", - "yvals = np.sin(xvals)\n", - "y2vals = np.cos(xvals)\n", - "\n", - "def simple_1d_sweep():\n", - " for x, y, y2 in zip(xvals, yvals, y2vals):\n", - " yield x, y, y2\n", - " \n", - "x = Parameter('x')\n", - "y = Parameter('y')\n", - "y2 = Parameter('y2')\n", - "\n", - "station = qc.Station(x, y, y2)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "Collapsed": "false", - "ExecuteTime": { - "end_time": "2019-01-29T22:42:08.163848Z", - "start_time": "2019-01-29T22:41:13.906794Z" - } - }, - "outputs": [], - "source": [ - "exp = load_or_create_experiment('very_simple_1d_sweep', sample_name='no sample')\n", - "\n", - "meas = Measurement(exp, station)\n", - "meas.register_parameter(x)\n", - "meas.register_parameter(y, setpoints=(x,))\n", - "meas.register_parameter(y2, setpoints=(x,))\n", - "meas.write_period = 2\n", - "\n", - "with meas.run() as datasaver:\n", - " for xval, yval, y2val in simple_1d_sweep():\n", - " datasaver.add_result(\n", - " (x, xval),\n", - " (y, yval),\n", - " (y2, y2val),\n", - " )\n", - " time.sleep(0.2)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "Collapsed": "false" - }, - "source": [ - "## A very simple 2D sweep" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "Collapsed": "false" - }, - "source": [ - "In exactly the same fashion, we can also take higher-dimensional data. \n", - "For 2D data, this means nested loops in the easiest case.\n", - "\n", - "We'll now see plottr slowly rastering the data as it gets saved." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "Collapsed": "false" - }, - "outputs": [], - "source": [ - "# set up the dummy data\n", - "xvals = np.linspace(-5, 5, 51)\n", - "yvals = np.linspace(-5, 5, 51)\n", - "xx, yy = np.meshgrid(xvals, yvals, indexing='ij')\n", - "zz = np.cos(xx) * np.cos(yy)\n", - "\n", - "def very_simple_2d_sweep():\n", - " for i, x in enumerate(xvals):\n", - " for j, y in enumerate(yvals):\n", - " yield x, y, zz[i, j]\n", - "\n", - "# configure the qcodes setup\n", - "x = Parameter('x')\n", - "y = Parameter('y')\n", - "z = Parameter('z')\n", - "station = qc.Station(x, y, z)\n", - "exp = load_or_create_experiment('very_simple_2d_sweep', sample_name='no sample')\n", - "\n", - "# set up the measurement\n", - "meas = Measurement(exp, station)\n", - "meas.register_parameter(x)\n", - "meas.register_parameter(y)\n", - "meas.register_parameter(z, setpoints=(x, y))\n", - "meas.write_period = 2\n", - "\n", - "# and start recording\n", - "with meas.run() as datasaver:\n", - " for xval, yval, zval in very_simple_2d_sweep():\n", - " datasaver.add_result(\n", - " (x, xval),\n", - " (y, yval),\n", - " (z, zval),\n", - " )\n", - " time.sleep(0.2)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "Collapsed": "false" - }, - "source": [ - "## A simple 2D sweep, with 1D in 'hardware'" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "Collapsed": "false" - }, - "source": [ - "Instead of sweeping point-by-point, it is also often the case that we get not single values, but whole arrays from a measurement call. \n", - "This makes data acquisition much faster, and is handled in essentially the same way.\n", - "The only difference in the example below is now that the 'measurement' returns arrays for `y` and `z` (e.g., the y-dependence of z could be something that is hardware-controlled in the lab), and that both have set `paramtype='array'` in the qcodes measurement and data objects." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "Collapsed": "false", - "ExecuteTime": { - "end_time": "2019-01-29T22:42:22.035442Z", - "start_time": "2019-01-29T22:42:22.029458Z" - } - }, - "outputs": [], - "source": [ - "# set up mock data\n", - "xvals = np.linspace(-5, 5, 51)\n", - "yvals = np.linspace(-5, 5, 51)\n", - "xx, yy = np.meshgrid(xvals, yvals, indexing='ij')\n", - "zz = np.cos(xx) * np.cos(yy)\n", - "\n", - "def simple_2d_sweep():\n", - " for i, x in enumerate(xvals):\n", - " yield x, yy[i, :], zz[i, :]\n", - "\n", - "# configure qcodes setup\n", - "x = Parameter('x')\n", - "y = Parameter('y')\n", - "z = Parameter('z')\n", - "station = qc.Station(x, y, z)\n", - "exp = load_or_create_experiment('simple_2d_sweep', sample_name='no sample')\n", - "\n", - "# set up measurement\n", - "meas = Measurement(exp, station)\n", - "meas.register_parameter(x)\n", - "meas.register_parameter(y, paramtype='array')\n", - "meas.register_parameter(z, setpoints=(x, y), paramtype='array')\n", - "meas.write_period = 2\n", - "\n", - "# start measuring\n", - "with meas.run() as datasaver:\n", - " for xval, yval, zval in simple_2d_sweep():\n", - " datasaver.add_result(\n", - " (x, xval),\n", - " (y, yval),\n", - " (z, zval),\n", - " )\n", - " time.sleep(0.2)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "Collapsed": "false" - }, - "source": [ - "## Complex data" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "Collapsed": "false" - }, - "source": [ - "Often, in particular when measuring in rf, our data is complex-valued. \n", - "This example shows that we can plot complex data as well, and can choose between real/imaginary and magnitude/phase representation in plottr.\n", - "The data here is mocking the noisy signal of resonator reflections (with slightly offset resonances and different line widths)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "Collapsed": "false" - }, - "outputs": [], - "source": [ - "# define frequency and complex signal\n", - "fvals = np.linspace(-5, 5, 101)\n", - "\n", - "# signal: three different traces with different resonances and linewidths\n", - "svals_1 = (2j * fvals - 1.0) / (2j * fvals + 1.0)\n", - "svals_2 = (2j * (fvals-0.5) - 2.0) / (2j * (fvals-0.5) + 2.0)\n", - "svals_3 = (2j * (fvals+0.5) - 0.5) / (2j * (fvals+0.5) + 0.5)\n", - "\n", - "# set up qcodes\n", - "frq = Parameter('detuning', unit='MHz')\n", - "sig1 = Parameter('reflection_1')\n", - "sig2 = Parameter('reflection_2')\n", - "sig3 = Parameter('reflection_3')\n", - "\n", - "station = qc.Station(frq, sig)\n", - "exp = load_or_create_experiment('mock_resonator_sweep', sample_name='no sample')\n", - "\n", - "# set up measurement\n", - "meas = Measurement(exp, station)\n", - "meas.register_parameter(frq, paramtype='array')\n", - "meas.register_custom_parameter('repetition')\n", - "meas.register_parameter(sig1, setpoints=('repetition', frq), paramtype='array')\n", - "meas.register_parameter(sig2, setpoints=('repetition', frq), paramtype='array')\n", - "meas.register_parameter(sig3, setpoints=('repetition', frq), paramtype='array')\n", - "meas.write_period = 2\n", - "\n", - "# start measuring\n", - "with meas.run() as datasaver:\n", - " for n in range(50):\n", - " datasaver.add_result(\n", - " (frq, fvals),\n", - " ('repetition', n),\n", - " (sig1, svals_1 + np.random.normal(size=fvals.size, scale=0.5) \n", - " + 1j*np.random.normal(size=fvals.size, scale=0.5)),\n", - " (sig2, svals_2 + np.random.normal(size=fvals.size, scale=0.5)\n", - " + 1j*np.random.normal(size=fvals.size, scale=0.5)),\n", - " (sig3, svals_3 + np.random.normal(size=fvals.size, scale=0.5)\n", - " + 1j*np.random.normal(size=fvals.size, scale=0.5)),\n", - " )" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "Collapsed": "false" - }, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "Collapsed": "false" - }, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "Collapsed": "false" - }, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "Collapsed": "false" - }, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python [conda env:qcodes]", - "language": "python", - "name": "conda-env-qcodes-py" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.5" - }, - "toc": { - "base_numbering": 1, - "nav_menu": {}, - "number_sections": true, - "sideBar": true, - "skip_h1_title": false, - "title_cell": "Table of Contents", - "title_sidebar": "Contents", - "toc_cell": false, - "toc_position": {}, - "toc_section_display": true, - "toc_window_display": true - }, - "varInspector": { - "cols": { - "lenName": 16, - "lenType": 16, - "lenVar": 40 - }, - "kernels_config": { - "python": { - "delete_cmd_postfix": "", - "delete_cmd_prefix": "del ", - "library": "var_list.py", - "varRefreshCmd": "print(var_dic_list())" - }, - "r": { - "delete_cmd_postfix": ") ", - "delete_cmd_prefix": "rm(", - "library": "var_list.r", - "varRefreshCmd": "cat(var_dic_list()) " - } - }, - "types_to_exclude": [ - "module", - "function", - "builtin_function_or_method", - "instance", - "_Feature" - ], - "window_display": false - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/doc/examples/Simple Live plotting example with DDH5.ipynb b/doc/examples/Simple Live plotting example with DDH5.ipynb deleted file mode 100644 index 19a861ef..00000000 --- a/doc/examples/Simple Live plotting example with DDH5.ipynb +++ /dev/null @@ -1,141 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2019-07-04T07:50:24.918740Z", - "start_time": "2019-07-04T07:50:24.482871Z" - } - }, - "outputs": [], - "source": [ - "import time\n", - "import os\n", - "import numpy as np\n", - "\n", - "from plottr.data import datadict as dd\n", - "from plottr.data import datadict_storage as dds" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The idea is pretty simple:\n", - "\n", - "We first define the structure of the datadict (you can also use a datadict that is already populated); this is equivalent to the idea of registering parameters in qcodes.\n", - "\n", - "You can then use the DDH5 writer to start saving data -- it'll determine file location automatically, within the base directory that is the first argument.\n", - "\n", - "To look at the data, you can use the `autoplot_ddh5` app. The easiest way might be to copy the file `apps/templates/autoplot_ddh5.bat` to some location of your choice, and edit the pathname variable to the correct folder in which `autoplot_ddh5.py` is located, such as:\n", - "\n", - "``\n", - " @set \"APPPATH=c:\\code\\plottr\\apps\"\n", - "``\n", - "\n", - "(note: this is the apps directory in the plottr base repository, not in the package). You can then associate opening `.ddh5` files with that batch file." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "ExecuteTime": { - "end_time": "2019-07-04T07:50:35.455377Z", - "start_time": "2019-07-04T07:50:26.221174Z" - } - }, - "outputs": [], - "source": [ - "data = dd.DataDict(\n", - " x = dict(unit='A'),\n", - " y = dict(unit='B'),\n", - " z = dict(axes=['x', 'y']),\n", - ")\n", - "data.validate()\n", - "\n", - "nrows = 100\n", - "\n", - "with dds.DDH5Writer(r\"d:\\data\", data) as writer:\n", - " for n in range(nrows):\n", - " writer.add_data(x=[n], \n", - " y=np.linspace(0,1,11).reshape(1,-1),\n", - " z=np.random.rand(11).reshape(1,-1)\n", - " )\n", - " time.sleep(1)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.7" - }, - "toc": { - "base_numbering": 1, - "nav_menu": {}, - "number_sections": true, - "sideBar": true, - "skip_h1_title": false, - "title_cell": "Table of Contents", - "title_sidebar": "Contents", - "toc_cell": false, - "toc_position": {}, - "toc_section_display": true, - "toc_window_display": false - }, - "varInspector": { - "cols": { - "lenName": 16, - "lenType": 16, - "lenVar": 40 - }, - "kernels_config": { - "python": { - "delete_cmd_postfix": "", - "delete_cmd_prefix": "del ", - "library": "var_list.py", - "varRefreshCmd": "print(var_dic_list())" - }, - "r": { - "delete_cmd_postfix": ") ", - "delete_cmd_prefix": "rm(", - "library": "var_list.r", - "varRefreshCmd": "cat(var_dic_list()) " - } - }, - "types_to_exclude": [ - "module", - "function", - "builtin_function_or_method", - "instance", - "_Feature" - ], - "window_display": false - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/doc/examples/autonode_app.py b/doc/examples/autonode_app.py deleted file mode 100644 index 4bd0160b..00000000 --- a/doc/examples/autonode_app.py +++ /dev/null @@ -1,90 +0,0 @@ -""" -Testing how to make a custom app with gui. -""" -import sys - -import numpy as np -import lmfit - -from plottr import QtWidgets -from plottr.data.datadict import DataDictBase -from plottr.data.qcodes_dataset import QCodesDSLoader -from plottr.node.tools import linearFlowchart -from plottr.node.data_selector import DataSelector -from plottr.node.grid import DataGridder -from plottr.node.dim_reducer import XYSelector -from plottr.node.autonode import autonode -from plottr.plot.base import PlotNode -from plottr.apps.autoplot import QCAutoPlotMainWindow - - -# specify the sine function we're fitting to -def sinefunc(x, amp, freq, phase): - return amp * np.sin(2 * np.pi * (freq * x + phase)) - -# this is the node. Using the autonode decorator, we only need -# to specify the processing function. -@autonode( - 'sineFitter', - confirm=True, - frequencyGuess={'initialValue': 1.0, 'type': float}, -) -def sinefit(self, dataIn: DataDictBase = None): - if dataIn is None: - return None - - # this is just a ghetto example: only support very simple datasets - naxes = len(dataIn.axes()) - ndeps = len(dataIn.dependents()) - if not (naxes == 1 and ndeps == 1): - return dict(dataOut=dataIn) - - # getting the data - axname = dataIn.axes()[0] - x = dataIn.data_vals(axname) - y = dataIn.data_vals(dataIn.dependents()[0]) - - # try to fit - sinemodel = lmfit.Model(sinefunc) - p0 = sinemodel.make_params(amp=1, freq=self.frequencyGuess, phase=0) - result = sinemodel.fit(y, p0, x=x) - - # if the fit works, add the fit result to the output - dataOut = dataIn.copy() - if result.success: - dataOut['fit'] = dict(values=result.best_fit, axes=[axname,]) - dataOut.add_meta('info', result.fit_report()) - else: - dataOut.add_meta('info', 'Could not fit sine.') - - return dict(dataOut=dataOut) - - -def main(pathAndId): - app = QtWidgets.QApplication([]) - - # flowchart and window - fc = linearFlowchart( - ('Dataset loader', QCodesDSLoader), - ('Data selection', DataSelector), - ('Grid', DataGridder), - ('Dimension assignment', XYSelector), - ('Sine fit', sinefit), - ('plot', PlotNode), - ) - - win = QCAutoPlotMainWindow(fc, pathAndId=pathAndId, - loaderName='Dataset loader') - win.show() - - return app.exec_() - - -if __name__ == '__main__': - if len(sys.argv) < 3: - print('need to specify .db path and run id.') - else: - - pathAndId = sys.argv[1], sys.argv[2] - print('try to open:', pathAndId) - main(pathAndId) diff --git a/doc/examples/node_with_dimension_selector_widget.py b/doc/examples/node_with_dimension_selector_widget.py deleted file mode 100644 index ab17b8c5..00000000 --- a/doc/examples/node_with_dimension_selector_widget.py +++ /dev/null @@ -1,105 +0,0 @@ -"""A simple script that illustrates how to use the :class:`.MultiDimensionSelector` widget -in a node to select axes in a dataset. - -This example does the following: -* create a flowchart with one node, that has a node widget. -* selected axes in the node widget will be deleted from the data when the - selection is changed, and the remaining data is printed to stdout. -""" - -from typing import List, Optional -from pprint import pprint - -import numpy as np - -from plottr import QtCore, QtWidgets, Signal, Slot -from plottr.data import DataDict -from plottr.node.node import Node, NodeWidget, updateOption, updateGuiQuietly -from plottr.node.tools import linearFlowchart -from plottr.gui.widgets import MultiDimensionSelector -from plottr.gui.tools import widgetDialog -from plottr.utils import testdata - - -class DummyNodeWidget(NodeWidget): - """Node widget for this dummy node""" - - def __init__(self, node: Node): - - super().__init__(embedWidgetClass=MultiDimensionSelector) - assert isinstance(self.widget, MultiDimensionSelector) # this is for mypy - - # allow selection of axis dimensions. See :class:`.MultiDimensionSelector`. - self.widget.dimensionType = 'axes' - - # specify the functions that link node property to GUI elements - self.optSetters = { - 'selectedAxes': self.setSelected, - } - self.optGetters = { - 'selectedAxes': self.getSelected, - } - - # make sure the widget is populated with the right dimensions - self.widget.connectNode(node) - - # when the user selects an option, notify the node - self.widget.dimensionSelectionMade.connect(lambda x: self.signalOption('selectedAxes')) - - @updateGuiQuietly - def setSelected(self, selected: List[str]) -> None: - self.widget.setSelected(selected) - - def getSelected(self) -> List[str]: - return self.widget.getSelected() - - -class DummyNode(Node): - useUi = True - uiClass = DummyNodeWidget - - def __init__(self, name: str): - super().__init__(name) - self._selectedAxes: List[str] = [] - - @property - def selectedAxes(self): - return self._selectedAxes - - @selectedAxes.setter - @updateOption('selectedAxes') - def selectedAxes(self, value: List[str]): - self._selectedAxes = value - - def process(self, dataIn = None) -> Dict[str, Optional[DataDict]]: - if super().process(dataIn) is None: - return None - data = dataIn.copy() - for k, v in data.items(): - for s in self.selectedAxes: - if s in v.get('axes', []): - idx = v['axes'].index(s) - v['axes'].pop(idx) - - for a in self.selectedAxes: - if a in data: - del data[a] - - pprint(data) - return dict(dataOut=data) - - -def main(): - fc = linearFlowchart(('dummy', DummyNode)) - node = fc.nodes()['dummy'] - dialog = widgetDialog(node.ui, title='dummy node') - data = testdata.get_2d_scalar_cos_data(2, 2, 1) - fc.setInput(dataIn=data) - return dialog, fc - - -if __name__ == '__main__': - app = QtWidgets.QApplication([]) - dialog, fc = main() - dialog.show() - app.exec_() diff --git a/doc/img/plot-node-system.png b/doc/img/plot-node-system.png deleted file mode 100644 index a380f08d..00000000 Binary files a/doc/img/plot-node-system.png and /dev/null differ diff --git a/doc/index.rst b/doc/index.rst deleted file mode 100644 index 9da74878..00000000 --- a/doc/index.rst +++ /dev/null @@ -1,31 +0,0 @@ -.. plottr documentation master file, created by - sphinx-quickstart on Sun Jan 6 11:38:09 2019. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - -Welcome to plottr's documentation! -================================== - -Todo: a quick description of what you can do with plottr, and a screenshot, or better a gif, -showing it in action. - -.. toctree:: - :maxdepth: 2 - :caption: Contents: - - intro - concepts/index - examples - -.. nodes/index -.. plotnode -.. api/index - - - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` \ No newline at end of file diff --git a/doc/intro.rst b/doc/intro.rst deleted file mode 100644 index 42bc34f8..00000000 --- a/doc/intro.rst +++ /dev/null @@ -1,7 +0,0 @@ -Basic usage -=========== - -Content: a quick tour of how to look at different types of data (qcodes, ddh5, -datadict directly using jupyter). - -TBD. \ No newline at end of file diff --git a/doc/make.bat b/doc/make.bat deleted file mode 100644 index 153be5e2..00000000 --- a/doc/make.bat +++ /dev/null @@ -1,35 +0,0 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=. -set BUILDDIR=_build - -if "%1" == "" goto help - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.https://www.sphinx-doc.org/ - exit /b 1 -) - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% - -:end -popd diff --git a/doc/nodes/index.rst b/doc/nodes/index.rst deleted file mode 100644 index 39b59b6f..00000000 --- a/doc/nodes/index.rst +++ /dev/null @@ -1,28 +0,0 @@ -Predefined nodes -================ - -Preparing data for plotting ---------------------------- - -Data Selection -^^^^^^^^^^^^^^ - -To be written. Describe data selector, idea of compatible data. - -Data Gridding -^^^^^^^^^^^^^ - -Converting data from a tabular format to a grid is done by the by the node class :class:`.DataGridder`: - - -.. autoclass:: plottr.node.grid.DataGridder - :members: grid - -.. autoclass:: plottr.node.grid.GridOption - :members: - - -Data slicing and reduction to 1D or 2D -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -To be written. diff --git a/doc/plotnode.rst b/doc/plotnode.rst deleted file mode 100644 index 309e127b..00000000 --- a/doc/plotnode.rst +++ /dev/null @@ -1,32 +0,0 @@ -Plotting -======== - -Plot Nodes ----------- -Plots have a somewhat special role in the node system: -We need a node to make plots aware of incoming data, but the node will (typically) not do anything to the data. -In the simplest case, :meth:`Node.process ` will just call a function that triggers plotting, using the just received data. -For many applications the base class :class:`PlotNode ` will do the job without any need to customize. - - -Plot Widgets ------------- -To make the plot node aware of the presence of a GUI, a suitable widget must be connected to it. -This can be done by instantiating :class:`PlotWidgetContainer `, and passing the instance to the node's :meth:`setPlotWidgetContainer ` method. -This will make sure that the container's :meth:`setData ` is called whenever the node receives data. -The container can then in turn host a :class:`PlotWidgetContainer `, which is connected by using :meth:`setPlotWidget `. -The reason why we don't connect the widget directly to the node is that the container may provide controls to change the widgets through user controls. - -.. image:: img/plot-node-system.png - -See the :ref:`API documentation` for more details. - - -Automatic plotting with Matplotlib ----------------------------------- -The most commonly used plot widget is based on matplotlib: :class:`AutoPlot `. -It determines automatically what an appropriate visualization of the received data is, and then plots that (at least if it can determine a good way to plot). -At the same time it gives the user a little bit of control over the appearance (partially through native matplotlib tools). -To separate plotting from the GUI elements we use :class:`FigureMaker `. - -See the :ref:`API documentation` for more details. \ No newline at end of file diff --git a/doc/requirements.txt b/doc/requirements.txt deleted file mode 100644 index 3821f846..00000000 --- a/doc/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -sphinx==8.1.3 -sphinx-autodoc-typehints>=1.10.3 -PyQt5>=5.9.0 diff --git a/plottr/__init__.py b/plottr/__init__.py index eddddc43..5280bf1d 100644 --- a/plottr/__init__.py +++ b/plottr/__init__.py @@ -6,14 +6,35 @@ import sys import signal + +# Logic here: for mypy to work, we need to import the Qt modules to have the correct types/stubs. +# For type checking we use PySide6, at runtime we use qtpy for backend abstraction. +PYSIDE6 = False +PYQT6 = False if TYPE_CHECKING: - from PyQt5 import QtCore, QtGui, QtWidgets - Signal = QtCore.pyqtSignal - Slot = QtCore.pyqtSlot + from PySide6 import QtCore, QtGui, QtWidgets + Signal = QtCore.Signal + Slot = QtCore.Slot + # In Qt6, QAction and QActionGroup moved from QtWidgets to QtGui + QAction = QtGui.QAction + QActionGroup = QtGui.QActionGroup + API_NAME = 'PySide6' else: - from qtpy import QtCore, QtGui, QtWidgets + import qtpy + from qtpy import QtCore, QtGui, QtWidgets, API_NAME + Signal = QtCore.Signal Slot = QtCore.Slot + API_NAME = qtpy.API_NAME + PYSIDE6 = qtpy.PYSIDE6 + PYQT6 = qtpy.PYQT6 + # In Qt6, QAction and QActionGroup moved from QtWidgets to QtGui + if PYSIDE6 or PYQT6: + QAction = QtGui.QAction + QActionGroup = QtGui.QActionGroup + else: + QAction = QtWidgets.QAction + QActionGroup = QtWidgets.QActionGroup from pyqtgraph.flowchart import Flowchart as pgFlowchart, Node as pgNode Flowchart = pgFlowchart @@ -32,7 +53,7 @@ def qtsleep(delay_sec: float) -> None: """sleep function that allows QT event processing in the background.""" loop = QtCore.QEventLoop() QtCore.QTimer.singleShot(int(delay_sec * 1000), loop.quit) - loop.exec_() + loop.exec() def qtapp() -> QtWidgets.QApplication: diff --git a/plottr/apps/appmanager.py b/plottr/apps/appmanager.py index a62a31ee..1abb7851 100644 --- a/plottr/apps/appmanager.py +++ b/plottr/apps/appmanager.py @@ -19,7 +19,7 @@ from typing import Dict, Union, Any, Callable, Tuple, Optional from traceback import print_exception -from plottr import QtCore, QtWidgets, QtGui, Flowchart, Signal, Slot, log, qtapp, qtsleep, plottrPath +from plottr import QtCore, QtWidgets, QtGui, Flowchart, Signal, Slot, log, qtapp, qtsleep, plottrPath, PYSIDE6 from plottr.gui.widgets import PlotWindow @@ -151,7 +151,7 @@ def __init__(self, setupFunc: AppType, port: int, parent: Optional[QtCore.QObjec self.serverThread.started.connect(self.server.run) self.serverThread.start() - @Slot(object) + @Slot(object) # type: ignore[arg-type] def onMessageReceived(self, message: Tuple[str, str, Any]) -> None: """ Handles message reception and reply to the app. Emits the signal replyReady with the reply. The signal is @@ -241,7 +241,7 @@ def __init__(self, parent: Optional[QtCore.QObject] = None): self.processes: Dict[IdType, QtCore.QProcess] = {} self.checking = True - @Slot(object, object) + @Slot(object, object) # type: ignore[arg-type] def onNewProcess(self, Id: IdType, process: QtCore.QProcess) -> None: """ Slot used to add a new process to the ProcessMonitor. @@ -268,10 +268,15 @@ def run(self) -> None: processesCopy = self.processes.copy() for Id, p in processesCopy.items(): state = p.state() - if state == 0: + if PYSIDE6 and state == QtCore.QProcess.ProcessState.NotRunning: + del self.processes[Id] + self.processTerminated.emit(Id) + elif not PYSIDE6 and state == QtCore.QProcess.NotRunning: del self.processes[Id] self.processTerminated.emit(Id) - qtsleep(0.01) + else: + continue + qtsleep(0.05) @Slot() def onReadyStandardOutput(self) -> None: @@ -329,7 +334,7 @@ def __init__(self, initialPort: int = 12345, parent: Optional[QtWidgets.QWidget] self.procmonThread: Optional[QtCore.QThread] = QtCore.QThread(parent=self) self.procmon.moveToThread(self.procmonThread) self.newProcess.connect(self.procmon.onNewProcess) - self.procmon.processTerminated.connect(self.onProcessEneded) + self.procmon.processTerminated.connect(self.onProcessEnded) self.procmonThread.started.connect(self.procmon.run) self.procmonThread.start() @@ -367,8 +372,8 @@ def launchApp(self, Id: IdType, module: str, func: str, *args: Any) -> bool: logger.warning(f'Id {Id} already exists') return False - @Slot(object) - def onProcessEneded(self, Id: IdType) -> None: + @Slot(object) # type: ignore[arg-type] + def onProcessEnded(self, Id: IdType) -> None: """ Gets triggered when the ProcessMonitor detects a process has been closed. Deletes the process from the internal dictionary. diff --git a/plottr/apps/apprunner.py b/plottr/apps/apprunner.py index 94bac3fb..6a2b4824 100644 --- a/plottr/apps/apprunner.py +++ b/plottr/apps/apprunner.py @@ -30,5 +30,5 @@ module = importlib.import_module(full_module) func = getattr(module, func_name) app = App(func, port, None, extra_arguments) - sys.exit(application.exec_()) + sys.exit(application.exec()) diff --git a/plottr/apps/autoplot.py b/plottr/apps/autoplot.py index 0bece004..ab9236a1 100644 --- a/plottr/apps/autoplot.py +++ b/plottr/apps/autoplot.py @@ -9,7 +9,7 @@ from typing import Union, Tuple, Optional, Type, List, Any, Type from packaging import version -from .. import QtCore, Flowchart, Signal, Slot, QtWidgets, QtGui +from .. import QtCore, Flowchart, Signal, Slot, QtWidgets, QtGui, QAction from .. import log as plottrlog from ..data.datadict import DataDictBase from ..data.datadict_storage import DDH5Loader @@ -159,7 +159,7 @@ def __init__(self, fc: Flowchart, self.fileMenu = self.menu.addMenu('&Data') if self.loaderNode is not None: - refreshAction = QtWidgets.QAction('&Refresh', self) + refreshAction = QAction('&Refresh', self) refreshAction.setShortcut('R') refreshAction.triggered.connect(self.refreshData) self.fileMenu.addAction(refreshAction) @@ -389,7 +389,7 @@ def main(f: str, g: str) -> int: app = QtWidgets.QApplication([]) fc, win = autoplotDDH5(f, g) - return app.exec_() + return app.exec() def script() -> None: diff --git a/plottr/apps/inspectr.py b/plottr/apps/inspectr.py index f6e9e7f6..9ceecc3f 100644 --- a/plottr/apps/inspectr.py +++ b/plottr/apps/inspectr.py @@ -24,7 +24,7 @@ from numpy import rint import pandas -from plottr import QtCore, QtWidgets, Signal, Slot, QtGui, Flowchart +from plottr import QtCore, QtWidgets, Signal, Slot, QtGui, Flowchart, QAction from .. import log as plottrlog from ..data.qcodes_dataset import (get_runs_from_db_as_dataframe, @@ -112,7 +112,7 @@ class SortableTreeWidgetItem(QtWidgets.QTreeWidgetItem): as numbers instead of sorting them alphabetically. """ def __init__(self, strings: Iterable[str]): - super().__init__(strings) + super().__init__(list(strings)) def __lt__(self, other: QtWidgets.QTreeWidgetItem) -> bool: col = self.treeWidget().sortColumn() @@ -158,16 +158,16 @@ def showContextMenu(self, position: QtCore.QPoint) -> None: copy_action = menu.addAction(copy_icon, "Copy") window = cast(QCodesDBInspector, self.window()) - starAction: QtWidgets.QAction = window.starAction # type: ignore[has-type] + starAction: QAction = window.starAction # type: ignore[has-type] starAction.setText('Star' if current_tag_char != self.tag_dict['star'] else 'Unstar') menu.addAction(starAction) - crossAction: QtWidgets.QAction = window.crossAction # type: ignore[has-type] + crossAction: QAction = window.crossAction # type: ignore[has-type] crossAction.setText('Cross' if current_tag_char != self.tag_dict['cross'] else 'Uncross') menu.addAction(crossAction) - action = menu.exec_(self.mapToGlobal(position)) + action = menu.exec(self.mapToGlobal(position)) if action == copy_action: QtWidgets.QApplication.clipboard().setText(item.text( model_index.column())) @@ -382,25 +382,25 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None, fileMenu = menu.addMenu('&File') # action: load db file - loadAction = QtWidgets.QAction('&Load', self) + loadAction = QAction('&Load', self) loadAction.setShortcut('Ctrl+L') loadAction.triggered.connect(self.loadDB) fileMenu.addAction(loadAction) # action: updates from the db file - refreshAction = QtWidgets.QAction('&Refresh', self) + refreshAction = QAction('&Refresh', self) refreshAction.setShortcut('R') refreshAction.triggered.connect(self.refreshDB) fileMenu.addAction(refreshAction) # action: star/unstar the selected run - self.starAction = QtWidgets.QAction() + self.starAction = QAction() self.starAction.setShortcut('Ctrl+Alt+S') self.starAction.triggered.connect(self.starSelectedRun) self.addAction(self.starAction) # action: cross/uncross the selected run - self.crossAction = QtWidgets.QAction() + self.crossAction = QAction() self.crossAction.setShortcut('Ctrl+Alt+X') self.crossAction.triggered.connect(self.crossSelectedRun) self.addAction(self.crossAction) @@ -665,7 +665,7 @@ def main(dbPath: Optional[str], log_level: Union[int, str] = logging.WARNING) -> if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): appinstance = QtWidgets.QApplication.instance() assert appinstance is not None - appinstance.exec_() + appinstance.exec() def script() -> None: diff --git a/plottr/apps/json_viewer.py b/plottr/apps/json_viewer.py index 8b16034a..c0e8737b 100644 --- a/plottr/apps/json_viewer.py +++ b/plottr/apps/json_viewer.py @@ -5,8 +5,12 @@ from typing import Any, List, Dict, Union, Optional from pathlib import Path -from qtpy.QtCore import QAbstractItemModel, QModelIndex, QObject, Qt -from qtpy.QtWidgets import QTreeView +from .. import API_NAME as __binding__, QtCore, QtWidgets +QAbstractItemModel = QtCore.QAbstractItemModel +QModelIndex = QtCore.QModelIndex +QObject = QtCore.QObject +Qt = QtCore.Qt +QTreeView = QtWidgets.QTreeView class TreeItem: @@ -182,7 +186,7 @@ def setData(self, index: QModelIndex, value: Any, role: Qt.ItemDataRole) -> bool item = index.internalPointer() item.value = str(value) - if __binding__ in ("PySide", "PyQt4"): # type: ignore[name-defined] + if __binding__ in ("PySide", "PyQt5"): self.dataChanged.emit(index, index) else: self.dataChanged.emit(index, index, [Qt.EditRole]) @@ -207,7 +211,7 @@ def headerData( #type: ignore[override] return None - def index(self, row: int, column: int, parent: QModelIndex = QModelIndex()) -> QModelIndex: + def index(self, row: int, column: int, parent: QModelIndex = QModelIndex()) -> QModelIndex: # type: ignore[override] """Override from QAbstractItemModel Return index according row, column and parent @@ -245,7 +249,7 @@ def parent(self, index: QModelIndex) -> QModelIndex: #type: ignore[override] return self.createIndex(parentItem.row(), 0, parentItem) - def rowCount(self, parent: QModelIndex = QModelIndex()) -> int: + def rowCount(self, parent: QModelIndex = QModelIndex()) -> int: # type: ignore[override] """Override from QAbstractItemModel Return row count from parent index @@ -260,14 +264,14 @@ def rowCount(self, parent: QModelIndex = QModelIndex()) -> int: return parentItem.childCount() - def columnCount(self, parent: QModelIndex = QModelIndex()) -> int: + def columnCount(self, parent: QModelIndex = QModelIndex()) -> int: # type: ignore[override] """Override from QAbstractItemModel Return column number. For the model, it always return 2 columns """ return 2 - def flags(self, index: QModelIndex) -> Qt.ItemFlags: + def flags(self, index: QModelIndex) -> Qt.ItemFlag: # type: ignore[override] """Override from QAbstractItemModel Return flags of index diff --git a/plottr/apps/monitr.py b/plottr/apps/monitr.py index 56baa7e8..c6cc1503 100644 --- a/plottr/apps/monitr.py +++ b/plottr/apps/monitr.py @@ -36,7 +36,7 @@ from watchdog.events import FileSystemEvent, FileSystemMovedEvent from .. import log as plottrlog -from .. import QtCore, QtWidgets, Signal, Slot, QtGui, plottrPath +from .. import QtCore, QtWidgets, Signal, Slot, QtGui, plottrPath, QAction, QActionGroup from .. import config_entry as getcfg from ..plot.mpl.autoplot import AutoPlot as MPLAutoPlot from ..plot.pyqtgraph.autoplot import AutoPlot as PGAutoPlot @@ -273,7 +273,7 @@ def add_file(self, path: Path) -> None: f" \n {path} was deleted " ) error_msg.setWindowTitle(f"Deleting __star__.tag") - error_msg.exec_() + error_msg.exec() return else: self.star = True @@ -289,7 +289,7 @@ def add_file(self, path: Path) -> None: f" \n {path} was deleted " ) error_msg.setWindowTitle(f"Deleting __trash__.tag") - error_msg.exec_() + error_msg.exec() return else: self.trash = True @@ -305,7 +305,7 @@ def add_file(self, path: Path) -> None: f"{path} was deleted." ) error_msg.setWindowTitle(f"Deleting __complete__.tag") - error_msg.exec_() + error_msg.exec() return else: self.complete = True @@ -321,7 +321,7 @@ def add_file(self, path: Path) -> None: f"{path} was deleted." ) error_msg.setWindowTitle(f"Deleting __interrupted__.tag") - error_msg.exec_() + error_msg.exec() return else: self.interrupted = True @@ -510,7 +510,7 @@ def on_renaming_file(self, item: Optional[Item] = None) -> None: item.setText(p.name) error_message = QtWidgets.QMessageBox() error_message.setText(f"{e}") - error_message.exec_() + error_message.exec() @Slot() def refresh_model(self) -> None: @@ -764,7 +764,7 @@ def _delete_all_children_from_main_dictionary(self, item: Item) -> None: self._delete_all_children_from_main_dictionary(child_item) del self.main_dictionary[child_item.path] - @Slot(FileSystemEvent) + @Slot(FileSystemEvent) # type: ignore[arg-type] def on_file_moved(self, event: FileSystemMovedEvent) -> None: """ Gets triggered every time a file is moved or the name of a file (including type) changes. @@ -1154,7 +1154,7 @@ def filter_requested( self.allowed_items = allowed_items self.trigger_filter() - def filterAcceptsRow( + def filterAcceptsRow( # type: ignore[override] self, source_row: int, source_parent: QtCore.QModelIndex ) -> bool: """ @@ -1247,14 +1247,14 @@ def __init__(self, proxy_model: SortFilterProxyModel, parent: Optional[Any] = No self.un_trash_text = "un-trash" self.context_menu = QtWidgets.QMenu(self) - self.copy_path_action = QtWidgets.QAction("copy path") - self.star_action = QtWidgets.QAction("star") - self.trash_action = QtWidgets.QAction("trash") - self.delete_action = QtWidgets.QAction("delete") - self.tag_actions: Dict[str, QtWidgets.QAction] = {} + self.copy_path_action = QAction("copy path") + self.star_action = QAction("star") + self.trash_action = QAction("trash") + self.delete_action = QAction("delete") + self.tag_actions: Dict[str, QAction] = {} for tag in self.model_.tags_dict.keys(): if tag not in self.tag_actions: - self.tag_actions[tag] = QtWidgets.QAction(str(tag)) + self.tag_actions[tag] = QAction(str(tag)) self.proxy_model.filter_incoming.connect(self.on_filter_incoming_event) self.proxy_model.filter_finished.connect(self.on_filter_ended_event) @@ -1296,7 +1296,7 @@ def set_all_tags(self) -> None: item = self.model_.item(i, 0) if item is not None and isinstance(item, Item): self._set_widget_for_item_and_children(item) - self.on_adjust_column_width() + self.on_adjust_column_width(None) def _set_widget_for_item_and_children(self, item: Item) -> None: """ @@ -1362,9 +1362,9 @@ def on_context_menu_requested(self, pos: QtCore.QPoint) -> None: # TODO: Implement the delete action in the model. # self.context_menu.addSeparator() # self.context_menu.addAction(self.delete_action) - self.context_menu.exec_(self.mapToGlobal(pos)) + self.context_menu.exec(self.mapToGlobal(pos)) - @Slot(object) + @Slot(object) # type: ignore[arg-type] def on_adjust_column_width(self, item: Optional[Item] = None) -> None: """ Gets called when the model changed the icon of an item. When changing an item icons that has the tag widget @@ -1380,15 +1380,15 @@ def on_adjust_column_width(self, item: Optional[Item] = None) -> None: @Slot(str) def on_add_tag_action(self, new_tag: str) -> None: if new_tag not in self.tag_actions: - self.tag_actions[new_tag] = QtWidgets.QAction() + self.tag_actions[new_tag] = QAction() @Slot(str) def on_delete_tag_action(self, deleted_tag: str) -> None: if deleted_tag in self.tag_actions: del self.tag_actions[deleted_tag] - @Slot(QtWidgets.QAction) - def on_context_action_triggered(self, action: QtWidgets.QAction) -> None: + @Slot(QAction) + def on_context_action_triggered(self, action: QAction) -> None: tag = action.text() if tag[0:3] == "un-": tag = tag[3:] @@ -1404,7 +1404,7 @@ def on_context_action_triggered(self, action: QtWidgets.QAction) -> None: self.model_.tag_action_triggered(item_index, tag) - def currentChanged( + def currentChanged( # type: ignore[override] self, current: QtCore.QModelIndex, previous: QtCore.QModelIndex ) -> None: """ @@ -2273,7 +2273,7 @@ def __init__( self.data = data # Popup menu. - self.plot_popup_action = QtWidgets.QAction("Plot") + self.plot_popup_action = QAction("Plot") self.popup_menu = QtWidgets.QMenu(self) self.plot_popup_action.triggered.connect(self.emit_plot_requested_signal) @@ -2332,7 +2332,7 @@ def on_context_menu_requested(self, pos: QtCore.QPoint) -> None: # Check that the item is in fact a top level item and open the popup menu if item is not None and parent_item is None: self.popup_menu.addAction(self.plot_popup_action) - self.popup_menu.exec_(self.mapToGlobal(pos)) + self.popup_menu.exec(self.mapToGlobal(pos)) self.popup_menu.removeAction(self.plot_popup_action) @Slot() @@ -2359,7 +2359,7 @@ def sizeHint(self) -> QtCore.QSize: rows += 1 index = self.indexFromItem(it.value()) height += self.rowHeight(index) - it += 1 # type: ignore[assignment, operator] # Taken from this example: + it += 1 # Taken from this example: # https://riverbankcomputing.com/pipermail/pyqt/2014-May/034315.html # calculating width: @@ -2537,7 +2537,7 @@ def save_activated(self) -> None: error_msg = QtWidgets.QMessageBox() error_msg.setText(f"{e}") error_msg.setWindowTitle(f"Error trying to save markdown edit.") - error_msg.exec_() + error_msg.exec() @Slot() def edit_activated(self) -> None: @@ -2649,14 +2649,14 @@ def create_md_file(self) -> None: f"File: {comment_path} already exists, please select a different file name." ) error_msg.setWindowTitle(f"Error trying to save comment.") - error_msg.exec_() + error_msg.exec() except Exception as e: # Show the error message error_msg = QtWidgets.QMessageBox() error_msg.setText(f"{e}") error_msg.setWindowTitle(f"Error trying to save comment.") - error_msg.exec_() + error_msg.exec() def resizeEvent(self, event: QtGui.QResizeEvent) -> None: """ @@ -2670,7 +2670,7 @@ def enterEvent(self, *args: Any, **kwargs: Any) -> None: self.save_button.show() def leaveEvent(self, *args: Any, **kwargs: Any) -> None: - super().enterEvent(*args, **kwargs) + super().leaveEvent(*args, **kwargs) self.save_button.hide() def size_change(self) -> None: @@ -2725,7 +2725,7 @@ def __init__(self, path_file: Path, *args: Any, **kwargs: Any): self.context_menu = QtWidgets.QMenu(self) # creating actions - self.copy_action = QtWidgets.QAction("copy") + self.copy_action = QAction("copy") self.copy_action.triggered.connect(self.on_copy_action) self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) @@ -2737,7 +2737,7 @@ def __init__(self, path_file: Path, *args: Any, **kwargs: Any): @Slot(QtCore.QPoint) def on_context_menu_requested(self, pos: QtCore.QPoint) -> None: - self.context_menu.exec_(self.mapToGlobal(pos)) + self.context_menu.exec(self.mapToGlobal(pos)) @Slot() def on_copy_action(self) -> None: @@ -2802,10 +2802,12 @@ def __init__(self, *args: Any, **kwargs: Any): self.verticalScrollBar().rangeChanged.connect(self.on_range_changed) def eventFilter(self, a0: QtCore.QObject, a1: QtCore.QEvent) -> bool: - self.setMinimumWidth(self.widget().minimumSizeHint().width()) + widget = self.widget() + if widget is not None: + self.setMinimumWidth(widget.minimumSizeHint().width()) return super().eventFilter(a0, a1) - @Slot(int) + @Slot(int) # type: ignore[arg-type] def on_range_changed(self) -> None: if self.first_scroll is True: bar = self.verticalScrollBar() @@ -3215,12 +3217,12 @@ def __init__( # Create menu bar menu_bar = self.menuBar() menu = menu_bar.addMenu("Backend") - self.backend_group = QtWidgets.QActionGroup(menu) + self.backend_group = QActionGroup(menu) for backend, plotWidgetClass in [ ("matplotlib", MPLAutoPlot), ("pyqtgraph", PGAutoPlot), ]: - action = QtWidgets.QAction(backend) + action = QAction(backend) action.setCheckable(True) action.setChecked(getcfg("main", "default-plotwidget") == plotWidgetClass) self.backend_group.addAction(action) @@ -3791,7 +3793,8 @@ def on_data_window_timer(self) -> None: False and calls on_update_data_widget. """ self.active_timer = False - self.on_update_data_widget(self.data_file_need_update) + if self.data_file_need_update is not None: + self.on_update_data_widget(self.data_file_need_update) def closeEvent(self, a0: QtGui.QCloseEvent) -> None: """ @@ -3822,4 +3825,4 @@ def script() -> int: app = QtWidgets.QApplication([]) win = Monitr(path) win.show() - return app.exec_() + return app.exec() diff --git a/plottr/apps/ui/monitr.py b/plottr/apps/ui/monitr.py index b252971a..40b651bc 100644 --- a/plottr/apps/ui/monitr.py +++ b/plottr/apps/ui/monitr.py @@ -41,7 +41,7 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None): self.plotAction.triggered.connect(self.onPlotActionTriggered) - @Slot(object) + @Slot(object) # type: ignore[arg-type] def setData(self, data: Dict[str, DataDict]) -> None: """Set the data to display.""" self.clear() diff --git a/plottr/data/datadict.py b/plottr/data/datadict.py index ac577878..98e83243 100644 --- a/plottr/data/datadict.py +++ b/plottr/data/datadict.py @@ -742,9 +742,13 @@ def __init__(self, parent: "DataDictBase") -> None: self._parent = parent def __getattribute__(self, __name: str) -> Any: - parent = super(DataDictBase._DataAccess, self).__getattribute__('_parent') + # this try/except block helps avoiding pickling/unpickling issues. + try: + parent = super(DataDictBase._DataAccess, self).__getattribute__('_parent') + except AttributeError: + parent = None - if __name in [k for k, _ in parent.data_items()]: + if parent is not None and __name in [k for k, _ in parent.data_items()]: return parent.data_vals(__name) else: return super(DataDictBase._DataAccess, self).__getattribute__(__name) diff --git a/plottr/data/datadict_storage.py b/plottr/data/datadict_storage.py index fbf612cb..63ee1a76 100644 --- a/plottr/data/datadict_storage.py +++ b/plottr/data/datadict_storage.py @@ -510,7 +510,7 @@ def process(self, dataIn: Optional[DataDictBase] = None) -> Optional[Dict[str, A self.loadingThread.start() return None - @Slot(object) + @Slot(object) # type: ignore[arg-type] def onThreadComplete(self, data: Optional[DataDict]) -> None: if data is None: return None diff --git a/plottr/gui/data_display.py b/plottr/gui/data_display.py index 3ff24813..8be6c004 100644 --- a/plottr/gui/data_display.py +++ b/plottr/gui/data_display.py @@ -5,7 +5,7 @@ from typing import Union, List, Tuple, Dict, Any, Optional -from .. import QtCore, QtWidgets, Signal, Slot +from .. import QtCore, QtWidgets, Signal, Slot, PYSIDE6 from ..data.datadict import DataDictBase @@ -27,7 +27,10 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None, self._dataShapes: Dict[str, Tuple[int, ...]] = {} self._readonly = readonly - self.setSelectionMode(self.MultiSelection) + if PYSIDE6: + self.setSelectionMode(self.SelectionMode.MultiSelection) + else: + self.setSelectionMode(self.MultiSelection) self.itemSelectionChanged.connect(self.emitSelection) def _makeItem(self, name: str) -> QtWidgets.QTreeWidgetItem: diff --git a/plottr/node/fitter.py b/plottr/node/fitter.py index 251efad5..5beba014 100644 --- a/plottr/node/fitter.py +++ b/plottr/node/fitter.py @@ -97,7 +97,7 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None, node: Optional[No self.input_options: Optional[FittingOptions] = None # fitting option in dataIn self.live_update = False self.dry_run = False - self.param_signals: List[QtCore.pyqtBoundSignal] = [] + self.param_signals: List[Any] = [] # Signal instances self.fitting_modules = INITIAL_MODULES self.my_layout = QtWidgets.QGridLayout() self.setLayout(self.my_layout) @@ -316,7 +316,7 @@ def addUpdateOptions(self) -> QtWidgets.QWidget: reloadInputOptButton = QtWidgets.QPushButton("Reload Input Option") grid.addWidget(reloadInputOptButton, 0, 3) - @Slot(QtCore.Qt.CheckState) + @Slot(QtCore.Qt.CheckState) # type: ignore[arg-type] def setLiveUpdate(live: QtCore.Qt.CheckState) -> None: ''' connect/disconnects the changing signal of each fitting option to signalAllOptions slot diff --git a/plottr/plot/pyqtgraph/autoplot.py b/plottr/plot/pyqtgraph/autoplot.py index 31b9e054..cc7aaea3 100644 --- a/plottr/plot/pyqtgraph/autoplot.py +++ b/plottr/plot/pyqtgraph/autoplot.py @@ -17,7 +17,7 @@ import numpy as np from pyqtgraph import mkPen -from plottr import QtWidgets, QtCore, Signal, Slot, \ +from plottr import QtWidgets, QtCore, Signal, Slot, QAction, QActionGroup, \ config_entry as getcfg from plottr.data.datadict import DataDictBase from .plots import Plot, PlotWithColorbar, PlotBase @@ -441,7 +441,7 @@ def __init__(self, options: FigureOptions, combineLinePlots.isChecked()) ) complexOptions = QtWidgets.QMenu(parent=self) - complexGroup = QtWidgets.QActionGroup(complexOptions) + complexGroup = QActionGroup(complexOptions) complexGroup.setExclusive(True) self._createComplexRepresentation() @@ -464,7 +464,7 @@ def _createComplexRepresentation(self) -> bool: #constructs/reconstructs the Complex Button with different viewing options based upon input data complexOptions = QtWidgets.QMenu(parent=self) - complexGroup = QtWidgets.QActionGroup(complexOptions) + complexGroup = QActionGroup(complexOptions) complexGroup.setExclusive(True) for k in ComplexRepresentation: @@ -473,7 +473,7 @@ def _createComplexRepresentation(self) -> bool: if not self.options.imagData and not k == ComplexRepresentation.real: continue if self.options.numAxes == 2 and k == ComplexRepresentation.log_MagAndPhase: continue - a = QtWidgets.QAction(k.label, complexOptions) + a = QAction(k.label, complexOptions) a.setCheckable(True) complexGroup.addAction(a) complexOptions.addAction(a) diff --git a/pyproject.toml b/pyproject.toml index 38be689a..58235d4d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,6 +52,7 @@ Tracker = "https://github.com/toolsforexperiments/plottr/issues" pyqt5 = ["PyQt5"] pyqt6 = ["PyQt6"] pyside2 = ["PySide2>=5.12"] +pyside6 = ["PySide6>=6.0"] qcodes = ["qcodes>=0.54.1"] [project.scripts] @@ -84,18 +85,26 @@ warn_unused_configs = true warn_redundant_casts = true no_implicit_optional = true disallow_untyped_defs = true -plugins = "numpy.typing.mypy_plugin" show_error_codes = true enable_error_code = "ignore-without-code" -# This module is auto generated so types -# are hard to fix +# This module is auto generated so types are hard to fix [[tool.mypy.overrides]] module = [ "plottr.apps.ui.Monitr_UI", ] ignore_errors = true +# Qt enum access patterns differ between PyQt5/qtpy and PySide6 type stubs. +# At runtime, qtpy provides compatibility, but mypy sees PySide6 nested enums. +# We disable attr-defined errors for plottr modules to allow old-style enum access +# (e.g., Qt.DisplayRole instead of Qt.ItemDataRole.DisplayRole) which works at runtime. +[[tool.mypy.overrides]] +module = [ + "plottr.*", +] +disable_error_code = ["attr-defined"] + [[tool.mypy.overrides]] module = [ "h5py", diff --git a/readthedocs.yml b/readthedocs.yml deleted file mode 100644 index efbe8c32..00000000 --- a/readthedocs.yml +++ /dev/null @@ -1,27 +0,0 @@ -# .readthedocs.yml -# Read the Docs configuration file -# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details - -# Required -version: 2 - -# Build documentation in the docs/ directory with Sphinx -sphinx: - configuration: doc/conf.py - -# Build documentation with MkDocs -#mkdocs: -# configuration: mkdocs.yml - -# Optionally build your docs in additional formats such as PDF and ePub -formats: all - -# Optionally set the version of Python and requirements required to build your docs -python: - version: 3.7 - install: - - requirements: doc/requirements.txt - - requirements: requirements.txt - - method: setuptools - path: . - system_packages: true diff --git a/test/apps/autoplot_app.py b/test/apps/autoplot_app.py deleted file mode 100644 index 5a90645b..00000000 --- a/test/apps/autoplot_app.py +++ /dev/null @@ -1,167 +0,0 @@ -"""Testing the autoplot app for live plotting and performance. - -To change what kind of data to plot, modify the data source in the main script at the bottom. - -Structure: -To be able to pass data in given intervals to the the autoplot window, we implement a DataSource object -that lives in a different thread from the GUI. -It's .data method should return a generator or other iterable. -In given intervals, the next data is then passed to the plotting app until the iterable is exhausted. -""" - -import logging -import sys -from time import time, sleep -from typing import Iterable - -import numpy as np - -from plottr import QtCore, QtWidgets, Signal -from plottr import log as plottrlog -from plottr.apps.autoplot import autoplot -from plottr.data.datadict import DataDictBase, DataDict -from plottr.plot.mpl.autoplot import AutoPlot as MPLAutoPlot -from plottr.plot.pyqtgraph.autoplot import AutoPlot as PGAutoPlot -from plottr.utils import testdata - -plottrlog.enableStreamHandler(True, level=logging.DEBUG) -logger = plottrlog.getLogger('plottr.test.autoplot_app') - - -class DataSource(QtCore.QObject): - """Abstract data source. For specific data, implement a child class.""" - dataready = Signal(object) - nomoredata = Signal() - initialdelay: float = 1.0 - delay: float = 0.0 - - def data(self) -> Iterable[DataDictBase]: - raise NotImplementedError - - def gimmesomedata(self) -> None: - _nsets = 0 - sleep(self.initialdelay) - - _t0 = time() - logger.info("DataSource: start producing data.") - for d in self.data(): - logger.info(f"DataSource: producing set {_nsets}") - self.dataready.emit(d) - _nsets += 1 - sleep(self.delay) - logger.info(f"DataSource: Finished production after {time() - _t0} s") - self.nomoredata.emit() - - -class LineDataMovie(DataSource): - """Produce a series of dummy line data (each rep reproduces the full set with different noise)""" - - def __init__(self, nreps: int = 1, nsets: int = 3, nsamples: int = 51): - super().__init__(None) - self.nreps = nreps - self.nsets = nsets - self.nsamples = nsamples - - def data(self) -> Iterable[DataDictBase]: - for i in range(self.nreps): - yield testdata.get_1d_scalar_cos_data(self.nsamples, self.nsets) - - -class ImageDataMovie(DataSource): - """Produce a series of dummy image data (each rep reproduces the full set with different noise)""" - - def __init__(self, nreps: int = 1, nsets: int = 2, nx: int = 21): - super().__init__(None) - self.nreps = nreps - self.nsets = nsets - self.nx = nx - - def data(self) -> Iterable[DataDictBase]: - for i in range(self.nreps): - data = testdata.get_2d_scalar_cos_data(self.nx, self.nx, self.nsets) - yield data - - -class ImageDataLiveAcquisition(DataSource): - """Produce a set of image data with a size that increases in chunks every interval.""" - - def __init__(self, nrows: int = 10, ncols=10, chunksize=10): - super().__init__(None) - self.nrows = nrows - self.ncols = ncols - self.chunksize = chunksize - - def data(self) -> Iterable[DataDictBase]: - fulldata = testdata.get_2d_scalar_cos_data(self.nrows, self.ncols) - idx = 0 - size = self.nrows * self.ncols - if size == 0: - raise ValueError('Data has size zero.') - - data = fulldata.structure(same_type=True) - assert isinstance(data, DataDictBase) - - while idx < size: - idx += self.chunksize - if idx >= size: - idx = size - for k, v in fulldata.data_items(): - data[k]['values'] = fulldata.data_vals(k)[:idx] - yield data - yield data - - -class ComplexImage(DataSource): - """Produce a complex image.""" - - def __init__(self, nx: int = 10, ny: int = 10): - super().__init__(None) - self.nx = nx - self.ny = ny - - def data(self) -> Iterable[DataDictBase]: - x = np.linspace(0, 10, self.nx) - y = np.linspace(0, 2 * np.pi, self.ny) - xx, yy = np.meshgrid(x, y, indexing='ij') - zz = np.exp(-1j * (0.5 * xx + yy)) - data = DataDict( - time=dict(values=xx.flatten()), - phase=dict(values=yy.flatten()), - data=dict(values=zz.flatten(), axes=['time', 'phase']), - conjugate=dict(values=zz.conj().flatten(), axes=['time', 'phase']) - ) - data.add_meta("title", "A complex data image (phasor vs time and phase)") - data.add_meta("info", "This is a test data set to test complex data display.") - yield data - - -def main(dataSrc): - plottrlog.LEVEL = logging.DEBUG - - app = QtWidgets.QApplication([]) - fc, win = autoplot(plotWidgetClass=plotWidgetClass) - - dataThread = QtCore.QThread() - dataSrc.moveToThread(dataThread) - dataSrc.dataready.connect(lambda d: win.setInput(data=d, resetDefaults=False)) - dataSrc.nomoredata.connect(dataThread.quit) - dataThread.started.connect(dataSrc.gimmesomedata) - win.windowClosed.connect(dataThread.quit) - - dataThread.start() - - if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): - QtWidgets.QApplication.instance().exec_() - - -# plotWidgetClass = MPLAutoPlot -plotWidgetClass = PGAutoPlot -# plotWidgetClass = None - -if __name__ == '__main__': - # src = LineDataMovie(20, 3, 31) - # src = ImageDataMovie(10, 2, 101) - src = ImageDataLiveAcquisition(101, 101, 67) - # src = ComplexImage(21, 21) - src.delay = 0.5 - main(src) diff --git a/test/apps/custom_app.py b/test/apps/custom_app.py deleted file mode 100644 index a6880a8c..00000000 --- a/test/apps/custom_app.py +++ /dev/null @@ -1,86 +0,0 @@ -""" -Testing how to make a custom app with gui. -""" -import numpy as np -import lmfit - -from plottr import QtWidgets -from plottr.data.datadict import DataDictBase, MeshgridDataDict -from plottr.gui.widgets import makeFlowchartWithPlotWindow -from plottr.node.dim_reducer import XYSelector -from plottr.node.autonode import autonode - - -def makeData(): - xvals = np.linspace(0, 10, 51) - reps = np.arange(20) - xx, rr = np.meshgrid(xvals, reps, indexing='ij') - data = sinefunc(xx, amp=0.8, freq=0.25, phase=0.1) - noise = np.random.normal(scale=0.2, size=data.shape) - data += noise - - dd = MeshgridDataDict( - x=dict(values=xx), - repetition=dict(values=rr), - sine=dict(values=data, axes=['x', 'repetition']), - ) - return dd - - -def sinefunc(x, amp, freq, phase): - return amp * np.sin(2 * np.pi * (freq * x + phase)) - - -@autonode( - 'sineFitter', - confirm=True, - frequencyGuess={'initialValue': 1.0, 'type': float}, -) -def sinefit(self, dataIn: DataDictBase = None): - if dataIn is None: - return None - - if len(dataIn.axes()) > 1 or len(dataIn.dependents()) > 1: - return dict(dataOut=dataIn) - - axname = dataIn.axes()[0] - x = dataIn.data_vals(axname) - y = dataIn.data_vals(dataIn.dependents()[0]) - - sinemodel = lmfit.Model(sinefunc) - p0 = sinemodel.make_params(amp=1, freq=self.frequencyGuess, phase=0) - result = sinemodel.fit(y, p0, x=x) - - dataOut = dataIn.copy() - if result.success: - dataOut['fit'] = dict(values=result.best_fit, axes=[axname,]) - dataOut.add_meta('info', result.fit_report()) - - return dict(dataOut=dataOut) - - -def makeNodeList(): - nodes = [ - ('Dimension selector', XYSelector), - ('Sine fitter', sinefit) - ] - return nodes - - -def main(): - app = QtWidgets.QApplication([]) - - # flowchart and window - nodes = makeNodeList() - win, fc = makeFlowchartWithPlotWindow(nodes) - win.show() - - # feed in data - data = makeData() - fc.setInput(dataIn=data) - - return app.exec_() - - -if __name__ == '__main__': - main() diff --git a/test/apps/fitter_test.py b/test/apps/fitter_test.py deleted file mode 100644 index 8f965168..00000000 --- a/test/apps/fitter_test.py +++ /dev/null @@ -1,68 +0,0 @@ -""" -Testing how to make a custom app with gui. -""" -import numpy as np -import lmfit - -from plottr import QtGui, QtWidgets -from plottr.data.datadict import DataDictBase, MeshgridDataDict -from plottr.gui.widgets import makeFlowchartWithPlotWindow -from plottr.node.dim_reducer import XYSelector -from plottr.node.fitter import FittingNode, FittingOptions -from plottr.analyzer.fitters.generic_functions import Cosine - -def makeData(): - xvals = np.linspace(0, 10, 51) - reps = np.arange(20) - xx, rr = np.meshgrid(xvals, reps, indexing='ij') - data = sinefunc(xx, amp=0.8, freq=0.25, phase=0.1) - noise = np.random.normal(scale=0.2, size=data.shape) - data += noise - - params = lmfit.Parameters() - for pn, pv in Cosine.guess(xvals, data[0]).items(): - params.add(pn, value=pv) - fitting_options = FittingOptions(Cosine, params) - - - - - dd = MeshgridDataDict( - x=dict(values=xx), - repetition=dict(values=rr), - sine=dict(values=data, axes=['x', 'repetition']), - __fitting_options__ = fitting_options - ) - return dd - -def sinefunc(x, amp, freq, phase): - return amp * np.sin(2 * np.pi * (freq * x + phase)) - - - - -def makeNodeList(): - nodes = [ - ('Dimension selector', XYSelector), - ('Fitter', FittingNode) - ] - return nodes - - -def main(): - app = QtWidgets.QApplication([]) - - # flowchart and window - nodes = makeNodeList() - win, fc = makeFlowchartWithPlotWindow(nodes) - win.show() - - # feed in data - data = makeData() - fc.setInput(dataIn=data) - - return app.exec_() - - -if __name__ == '__main__': - main() diff --git a/test/apps/test_histograms.py b/test/apps/test_histograms.py deleted file mode 100644 index 760329d2..00000000 --- a/test/apps/test_histograms.py +++ /dev/null @@ -1,169 +0,0 @@ -#%% magics to configure mainloop - -from IPython import get_ipython -ipy = get_ipython() -ipy.magic("load_ext autoreload") -ipy.magic("autoreload 2") -ipy.magic("gui qt") -ipy.magic("matplotlib qt") - - -#%% importing stuff and defining methods -from typing import Tuple - -import numpy as np -from matplotlib import pyplot as plt -from xhistogram.core import histogram - -from plottr.data.datadict import datadict_to_meshgrid -from plottr.node.tools import linearFlowchart -from plottr import Flowchart, QtCore -from plottr.data import DataDict -from plottr.apps.autoplot import AutoPlotMainWindow -from plottr.node.data_selector import DataSelector -from plottr.node.grid import DataGridder -from plottr.node.dim_reducer import XYSelector -from plottr.node.histogram import Histogrammer -from plottr.plot import makeFlowchartWithPlot - - -def testdata(n_reps=100, n_extra_axes=1): - reps = np.arange(n_reps) - axes = ['sample'] + [f'ax_{i}' for i in range(n_extra_axes)] - extra_axes_vals = [np.linspace(-1., 1., 10+i) for i in range(n_extra_axes)] - axes_vals = [reps] + extra_axes_vals - axes_vals_mesh = np.meshgrid(*axes_vals, indexing='ij') - - outcomes_mesh = np.random.normal( - loc=0, scale=1, size=axes_vals_mesh[0].shape, - ) - - for axvals in axes_vals_mesh[1:]: - outcomes_mesh = np.add(outcomes_mesh, axvals) - - dataset = DataDict( - result=dict(values=outcomes_mesh.flatten(), - axes=axes), - ) - for ax, axvals in zip(axes, axes_vals_mesh): - dataset[ax] = dict(values=axvals.flatten()) - - return dataset - - -def complex_testdata(n_samples=100, n_amps=21): - samples = np.arange(n_samples) - amps = np.arange(n_amps) - ss, aa = np.meshgrid(samples, amps, indexing='ij') - locs = aa * np.exp(-1j * 0.1 * np.pi) - values_real = np.random.normal(loc=locs.real, scale=0.5, size=ss.shape) - values_imag = np.random.normal(loc=locs.imag, scale=0.5, size=ss.shape) - vv = values_real + 1j*values_imag - - dataset = DataDict( - sample=dict(values=ss.flatten()), - amp=dict(values=aa.flatten()), - result=dict(values=vv.flatten(), - axes=['sample', 'amp']) - ) - - if dataset.validate(): - return dataset - - -def complex_testdata_many_independents(n_samples=100, n_amps=4, n_phases=8, n_widths=3): - samples = np.arange(n_samples) - amps = np.arange(n_amps)+1. - phases = np.linspace(0, 2*np.pi*(1.-1./n_phases), n_phases) - widths = (np.arange(n_widths)+1.)/5. - - ss, aa, pp, ww = np.meshgrid(samples, amps, phases, widths, indexing='ij') - locs = aa * np.exp(-1j*pp) - values_real = np.random.normal(loc=locs.real, scale=widths, size=ss.shape) - values_imag = np.random.normal(loc=locs.imag, scale=widths, size=ss.shape) - vv = values_real + 1j*values_imag - - dataset = DataDict( - sample=dict(values=ss.flatten()), - amp=dict(values=aa.flatten()), - phase=dict(values=pp.flatten()), - width=dict(values=ww.flatten()), - result=dict(values=vv.flatten(), - axes=['sample', 'amp', 'phase', 'width']) - ) - - if dataset.validate(): - return dataset - - -def plot() -> Tuple[Flowchart, AutoPlotMainWindow]: - - nodes = [ - ('Data selection', DataSelector), - ('Grid', DataGridder), - ('Histogram', Histogrammer), - ('Dimension assignment', XYSelector), - ] - fc = makeFlowchartWithPlot(nodes) - - widgetOptions = { - "Data selection": dict(visible=True, - dockArea=QtCore.Qt.TopDockWidgetArea), - "Dimension assignment": dict(visible=True, - dockArea=QtCore.Qt.TopDockWidgetArea), - } - win = AutoPlotMainWindow(fc, - loaderName=None, - widgetOptions=widgetOptions, - monitor=False) - win.show() - return fc, win - - -#%% verify testdata -# dataset = testdata() -# dataset_gridded = datadict_to_meshgrid(dataset) -# -# fig = plt.figure(constrained_layout=True) -# ax = fig.add_subplot(1, 2, 1) -# ax.imshow(dataset_gridded.data_vals('output'), aspect='auto') -# -# h, e = histogram(dataset_gridded.data_vals('output'), -# axis=0, bins=10) -# ax = fig.add_subplot(1,2,2) -# ax.imshow(h, aspect='auto') - -#%% testing the node stand-alone -# dataset = testdata() -# dataset_gridded = datadict_to_meshgrid(dataset) -# -# fc = linearFlowchart(('hist', Histogrammer)) -# fc.setInput(dataIn=dataset_gridded) -# -# hnode: Histogrammer = fc.nodes()['hist'] -# hnode.histogramAxis = 'repetition' -# hnode.nbins = 9 -# -# dataset_out = fc.outputValues()['dataOut'] -# -# fig = plt.figure(constrained_layout=True) -# ax = fig.add_subplot(1, 2, 1) -# ax.imshow(dataset_gridded.data_vals('output'), aspect='auto') -# -# ax = fig.add_subplot(1,2,2) -# ax.imshow(dataset_out.data_vals('output_count'), aspect='auto') - - -#%% launching an app and setting testdata -fc, win = plot() -win.show() - -hnode: Histogrammer = fc.nodes()['Histogram'] -dselnode: DataSelector = fc.nodes()['Data selection'] -dimnode: XYSelector = fc.nodes()['Dimension assignment'] - -# dataset = testdata(n_extra_axes=2) -dataset = complex_testdata(n_samples=100, n_amps=21) - -fc.setInput(dataIn=dataset) -dselnode.selectedData = ['result'] diff --git a/test/gui/__init__.py b/test/gui/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/test/gui/correct_offset.py b/test/gui/correct_offset.py deleted file mode 100644 index 013ba1b8..00000000 --- a/test/gui/correct_offset.py +++ /dev/null @@ -1,47 +0,0 @@ -import numpy as np - -from plottr import QtWidgets -from plottr.data.datadict import MeshgridDataDict -from plottr.gui.widgets import makeFlowchartWithPlotWindow -from plottr.node.filter.correct_offset import SubtractAverage - - -def subtractAverage(): - x = np.arange(11) - 5. - y = np.linspace(0, 10, 51) - xx, yy = np.meshgrid(x, y, indexing='ij') - zz = np.sin(yy) + xx - data = MeshgridDataDict( - x=dict(values=xx), - y=dict(values=yy), - z=dict(values=zz, axes=['x', 'y']) - ) - data.validate() - - x = np.arange(11) - 5. - y = np.linspace(0, 10, 51) - xx, yy = np.meshgrid(x, y, indexing='ij') - zz = np.sin(yy) + xx - data2 = MeshgridDataDict( - reps=dict(values=xx), - y=dict(values=yy), - z=dict(values=zz, axes=['reps', 'y']) - ) - data2.validate() - - # make app and gui, fc - app = QtWidgets.QApplication([]) - win, fc = makeFlowchartWithPlotWindow([ - ('sub', SubtractAverage) - ]) - win.show() - - # feed in data - fc.setInput(dataIn=data) - fc.setInput(dataIn=data2) - - return app.exec_() - - -if __name__ == '__main__': - subtractAverage() diff --git a/test/gui/data_display_widgets.py b/test/gui/data_display_widgets.py deleted file mode 100644 index 17bdab49..00000000 --- a/test/gui/data_display_widgets.py +++ /dev/null @@ -1,27 +0,0 @@ -"""data_display_widgets.py - -Testing scripts for GUI elements for data display. -""" - -from plottr import QtWidgets -from plottr.gui.tools import widgetDialog -from plottr.gui.data_display import DataSelectionWidget -from plottr.utils import testdata - - -def test_dataSelectionWidget(readonly: bool = False): - def selectionCb(selection): - print(selection) - - # app = QtWidgets.QApplication([]) - widget = DataSelectionWidget(readonly=readonly) - widget.dataSelectionMade.connect(selectionCb) - - # set up the UI, feed data in - data = testdata.three_incompatible_3d_sets(5, 5, 5) - dialog = widgetDialog(widget) - widget.setData(data.structure(), data.shapes()) - widget.clear() - widget.setData(data.structure(), data.shapes()) - return dialog - diff --git a/test/gui/data_selector.py b/test/gui/data_selector.py deleted file mode 100644 index 3665ecbf..00000000 --- a/test/gui/data_selector.py +++ /dev/null @@ -1,32 +0,0 @@ -from plottr import QtGui, QtWidgets -from plottr.gui.tools import widgetDialog -from plottr.node.data_selector import DataSelector -from plottr.node.tools import linearFlowchart -from plottr.utils import testdata - - -def test_data_selector(): - fc = linearFlowchart(('selector', DataSelector)) - selector = fc.nodes()['selector'] - dialog = widgetDialog(selector.ui, title='selector') - - data = testdata.three_incompatible_3d_sets(2, 2, 2) - fc.setInput(dataIn=data) - selector.selectedData = ['data'] - - # for testing purposes, insert differently structured data - data2 = testdata.two_compatible_noisy_2d_sets() - fc.setInput(dataIn=data2) - - # ... and go back. - fc.setInput(dataIn=data) - selector.selectedData = ['data'] - - return dialog, fc - - -if __name__ == '__main__': - app = QtWidgets.QApplication([]) - dialog, fc = test_data_selector() - dialog.show() - app.exec_() diff --git a/test/gui/ddh5_loader.py b/test/gui/ddh5_loader.py deleted file mode 100644 index add6e2ff..00000000 --- a/test/gui/ddh5_loader.py +++ /dev/null @@ -1,22 +0,0 @@ -from plottr import QtWidgets -from plottr.data import datadict_storage as dds -from plottr.node.tools import linearFlowchart -from plottr.gui.tools import widgetDialog - - -def loader_node(interactive=False): - def cb(*vals): - print(vals) - - if not interactive: - app = QtWidgets.QApplication([]) - - fc = linearFlowchart(('loader', dds.DDH5Loader)) - loader = fc.nodes()['loader'] - dialog = widgetDialog(loader.ui, 'loader') - - if not interactive: - loader.newDataStructure.connect(cb) - app.exec_() - else: - return dialog, fc diff --git a/test/gui/dimension_assignment.py b/test/gui/dimension_assignment.py deleted file mode 100644 index e3c0a2a0..00000000 --- a/test/gui/dimension_assignment.py +++ /dev/null @@ -1,68 +0,0 @@ -"""dimension_assignment.py - -Testing for axis settings / dimension-reduction widgets. -""" - -from plottr import QtWidgets -from plottr.data.datadict import datadict_to_meshgrid -from plottr.gui.tools import widgetDialog -from plottr.node.dim_reducer import XYSelectionWidget, DimensionReducer, XYSelector -from plottr.node.tools import linearFlowchart -from plottr.utils import testdata - - -def xySelectionWidget(): - def selectionCb(selection): - print(selection) - - app = QtWidgets.QApplication([]) - widget = XYSelectionWidget() - widget.rolesChanged.connect(selectionCb) - - # set up the UI, feed data in - data = datadict_to_meshgrid( - testdata.three_compatible_3d_sets(5, 5, 5) - ) - dialog = widgetDialog(widget) - widget.setData(data) - widget.clear() - widget.setData(data) - return app.exec_() - - -def dimReduction(interactive=False): - if not interactive: - app = QtWidgets.QApplication([]) - - fc = linearFlowchart(('reducer', DimensionReducer)) - reducer = fc.nodes()['reducer'] - dialog = widgetDialog(reducer.ui, 'reducer') - - data = datadict_to_meshgrid( - testdata.three_compatible_3d_sets(2, 2, 2) - ) - fc.setInput(dataIn=data) - - if not interactive: - app.exec_() - else: - return dialog, fc - - -def xySelection(interactive=False): - if not interactive: - app = QtWidgets.QApplication([]) - - fc = linearFlowchart(('xysel', XYSelector)) - selector = fc.nodes()['xysel'] - dialog = widgetDialog(selector.ui, 'xysel') - - data = datadict_to_meshgrid( - testdata.three_compatible_3d_sets(4, 4, 4) - ) - fc.setInput(dataIn=data) - - if not interactive: - app.exec_() - else: - return dialog, fc diff --git a/test/gui/dimension_selection_widgets.py b/test/gui/dimension_selection_widgets.py deleted file mode 100644 index b07dbe62..00000000 --- a/test/gui/dimension_selection_widgets.py +++ /dev/null @@ -1,33 +0,0 @@ -import argparse -from typing import Tuple - -from plottr import QtWidgets -from plottr.data.datadict import str2dd -from plottr.gui.widgets import AxisSelector, DependentSelector, DimensionSelector, \ - MultiDimensionSelector -from plottr.gui.tools import widgetDialog - - -def main(multi=False): - def cb(value): - print('Selection made:', value) - - data = str2dd("data1(x,y,z); data2(x,z);") - - if not multi: - w = DimensionSelector() - combo = w.combo - combo.setDimensions(data.axes()+data.dependents()) - combo.dimensionSelected.connect(cb) - else: - w = MultiDimensionSelector() - w.setDimensions(data.axes()+data.dependents()) - w.dimensionSelectionMade.connect(cb) - - return widgetDialog(w) - - -if __name__ == '__main__': - app = QtWidgets.QApplication([]) - dialog = main(multi=True) - app.exec_() diff --git a/test/gui/grid_options.py b/test/gui/grid_options.py deleted file mode 100644 index ee4e9428..00000000 --- a/test/gui/grid_options.py +++ /dev/null @@ -1,80 +0,0 @@ -"""grid_options.py - -Testing Widgets for the gridding node. -""" - -from plottr import QtGui -from plottr.data.datadict import datadict_to_meshgrid -from plottr.gui.tools import widgetDialog -from plottr.node.grid import GridOptionWidget, ShapeSpecificationWidget, DataGridder, GridOption -from plottr.utils import testdata -from plottr.node.tools import linearFlowchart - - -def test_shapeSpecWidget(): - def cb(val): - print(val) - - widget = ShapeSpecificationWidget() - widget.newShapeNotification.connect(cb) - - # set up the UI, feed data in - dialog = widgetDialog(widget) - widget.setAxes(['x', 'y', 'aVeryVeryVeryVeryLongAxisName']) - - widget.setShape({ - 'order': ['x', 'y', 'aVeryVeryVeryVeryLongAxisName'], - 'shape': (5,5,5), - }) - - widget.setShape({ - 'order': ['y', 'x', 'aVeryVeryVeryVeryLongAxisName'], - 'shape': (11, 4, -9), - }) - - return dialog - - -def test_gridOptionWidget(): - def cb(val): - print(val) - - widget = GridOptionWidget() - widget.optionSelected.connect(cb) - - # set up the UI, feed data in - data = datadict_to_meshgrid( - testdata.three_compatible_3d_sets(5, 5, 5) - ) - dialog = widgetDialog(widget) - widget.setAxes(data.axes()) - - widget.setShape({ - 'order': ['x', 'y', 'z'], - 'shape': (5,5,5), - }) - - return dialog - - -def test_GridNode(): - def cb(val): - print(val) - - fc = linearFlowchart(('grid', DataGridder)) - gridder = fc.nodes()['grid'] - dialog = widgetDialog(gridder.ui, 'gridder') - - data = testdata.three_compatible_3d_sets(2, 2, 2) - fc.setInput(dataIn=data) - - gridder.shapeDetermined.connect(cb) - - gridder.grid = GridOption.guessShape, {} - gridder.grid = GridOption.specifyShape, \ - dict(order=['x', 'y', 'z'], shape=(2,2,3)) - gridder.grid = GridOption.guessShape, {} - gridder.grid = GridOption.specifyShape, \ - dict(order=['x', 'y', 'z'], shape=(2,2,3)) - - return dialog, fc diff --git a/test/gui/mpl_figuremaker.py b/test/gui/mpl_figuremaker.py deleted file mode 100644 index e00934ee..00000000 --- a/test/gui/mpl_figuremaker.py +++ /dev/null @@ -1,72 +0,0 @@ -"""A set of simple tests of the MPL FigureMaker classes.""" - -import numpy as np - -from plottr import QtWidgets -from plottr.plot.base import ComplexRepresentation -from plottr.plot.mpl.autoplot import FigureMaker, PlotType -from plottr.plot.mpl.widgets import figureDialog - - -def test_multiple_line_plots(single_panel: bool = False): - """plot a few 1d traces.""" - fig, win = figureDialog() - - setpts = np.linspace(0, 10, 101) - data_1 = np.cos(setpts) - - with FigureMaker(fig) as fm: - fm.plotType = PlotType.multitraces if single_panel else PlotType.singletraces - - line_1 = fm.addData(setpts, data_1, labels=['x', r'$\cos(x)$']) - _ = fm.addData(setpts, data_1 ** 2, labels=['x', r'$\cos^2(x)$']) - _ = fm.addData(setpts, data_1 ** 3, labels=['x', r'$\cos^3(x)$']) - - return win - - -def test_complex_line_plots(single_panel: bool = False, - mag_and_phase_format: bool = False): - """Plot a couple of complex traces""" - fig, win = figureDialog() - - setpts = np.linspace(0, 10, 101) - data_1 = np.exp(-1j * setpts) - data_2 = np.conjugate(data_1) - - with FigureMaker(fig) as fm: - if mag_and_phase_format: - fm.complexRepresentation = ComplexRepresentation.magAndPhase - fm.plotType = PlotType.multitraces if single_panel else PlotType.singletraces - - line_1 = fm.addData(setpts, data_1, labels=['x', r'$\exp(-ix)$']) - _ = fm.addData(setpts, data_2, labels=['x', r'$\exp(ix)$']) - - return win - - -def main(): - app = QtWidgets.QApplication([]) - - wins = [] - - wins.append( - test_multiple_line_plots()) - wins.append( - test_multiple_line_plots(single_panel=True)) - # wins.append( - # test_complex_line_plots()) - # wins.append( - # test_complex_line_plots(single_panel=True)) - # wins.append( - # test_complex_line_plots(mag_and_phase_format=True)) - wins.append( - test_complex_line_plots(single_panel=True, mag_and_phase_format=True)) - - for w in wins: - w.show() - return app.exec_() - - -if __name__ == '__main__': - main() diff --git a/test/gui/pyqtgraph_figuremaker.py b/test/gui/pyqtgraph_figuremaker.py deleted file mode 100644 index 8024883e..00000000 --- a/test/gui/pyqtgraph_figuremaker.py +++ /dev/null @@ -1,114 +0,0 @@ -"""A set of simple tests of the pyqtgraph FigureMaker classes.""" - -import numpy as np - -from plottr import QtWidgets -from plottr.gui.tools import widgetDialog -from plottr.plot.base import PlotDataType, ComplexRepresentation -from plottr.plot.pyqtgraph.autoplot import FigureMaker - - -def test_basic_line_plot(): - x = np.linspace(0, 10, 51) - y = np.cos(x) - with FigureMaker() as fm: - line_1 = fm.addData(x, y, labels=['x', 'cos(x)'], - plotDataType=PlotDataType.line1d) - _ = fm.addData(x, y**2, labels=['x', 'cos^2(x)'], - join=line_1, - plotDataType=PlotDataType.scatter1d) - line_2 = fm.addData(x, np.abs(y), labels=['x', '|cos(x)|'], - plotDataType=PlotDataType.line1d) - return fm.widget - - -def test_images(): - x = np.linspace(0, 10, 51) - y = np.linspace(-4, 2, 51) - xx, yy = np.meshgrid(x, y, indexing='ij') - zz = np.cos(xx) * np.exp(-yy**2) - with FigureMaker() as fm: - img1 = fm.addData(xx, yy, zz, labels=['x', 'y', 'fake data'], - plotDataType=PlotDataType.grid2d) - img2 = fm.addData(xx, yy, zz[:, ::-1], labels=['x', 'y', 'fake data (mirror)'], - plotDataType=PlotDataType.grid2d) - return fm.widget - - -def test_scatter2d(): - x = np.linspace(0, 10, 21) - y = np.linspace(-4, 2, 21) - xx, yy = np.meshgrid(x, y, indexing='ij') - zz = np.cos(xx) * np.exp(-yy**2) - with FigureMaker() as fm: - s = fm.addData(xx.flatten(), yy.flatten(), zz.flatten(), labels=['x', 'y', 'fake data'], - plotDataType=PlotDataType.scatter2d) - return fm.widget - - -def test_complex_line_plots(single_panel: bool = False, - mag_and_phase_format: bool = False): - - setpts = np.linspace(0, 10, 101) - data_1 = np.exp(-1j * setpts) - data_2 = np.conjugate(data_1) - - with FigureMaker() as fm: - if mag_and_phase_format: - fm.complexRepresentation = ComplexRepresentation.magAndPhase - - line_1 = fm.addData(setpts, data_1, labels=['x', r'exp(-ix)']) - _ = fm.addData(setpts, data_2, labels=['x', r'exp(ix)'], - join=line_1 if single_panel else None) - - return fm.widget - - -def test_complex_images(mag_and_phase_format: bool = False): - x = np.linspace(0, 10, 51) - y = np.linspace(-4, 2, 51) - xx, yy = np.meshgrid(x, y, indexing='ij') - zz = np.exp(-1j*xx) * np.exp(-yy**2) - with FigureMaker() as fm: - if mag_and_phase_format: - fm.complexRepresentation = ComplexRepresentation.magAndPhase - - img1 = fm.addData(xx, yy, zz, labels=['x', 'y', 'fake data'], - plotDataType=PlotDataType.grid2d) - img2 = fm.addData(xx, yy, np.conjugate(zz), labels=['x', 'y', 'fake data (conjugate)'], - plotDataType=PlotDataType.grid2d) - return fm.widget - - -def main(): - app = QtWidgets.QApplication([]) - widgets = [] - - widgets.append( - test_basic_line_plot()) - # widgets.append( - # test_images()) - # widgets.append( - # test_scatter2d()) - # widgets.append( - # test_complex_line_plots()) - # widgets.append( - # test_complex_line_plots(single_panel=True)) - # widgets.append( - # test_complex_line_plots(mag_and_phase_format=True)) - # widgets.append( - # test_complex_line_plots(single_panel=True, mag_and_phase_format=True)) - # widgets.append( - # test_complex_images()) - # widgets.append( - # test_complex_images(mag_and_phase_format=True)) - - dgs = [] - for w in widgets: - dgs.append(widgetDialog(w)) - dgs[-1].show() - return app.exec_() - - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/test/gui/pyqtgraph_testing.py b/test/gui/pyqtgraph_testing.py deleted file mode 100644 index 2c7ccc0d..00000000 --- a/test/gui/pyqtgraph_testing.py +++ /dev/null @@ -1,94 +0,0 @@ -"""A simple script to play a bit with pyqtgraph plotting. -This has no direct connection to plottr but is just to explore. -""" - -import sys - -import numpy as np -import pyqtgraph as pg - -from plottr import QtWidgets, QtGui, QtCore -from plottr.gui.tools import widgetDialog - -pg.setConfigOption('background', 'w') -pg.setConfigOption('foreground', 'k') - -def image_test(): - app = QtWidgets.QApplication([]) - - # create data - x = np.linspace(0, 10, 51) - y = np.linspace(-4, 4, 51) - xx, yy = np.meshgrid(x, y, indexing='ij') - zz = np.cos(xx)*np.exp(-(yy-1.)**2) - - # layout widget - pgWidget = pg.GraphicsLayoutWidget() - - # main plot - imgPlot = pgWidget.addPlot(title='my image', row=0, col=0) - img = pg.ImageItem() - imgPlot.addItem(img) - - # histogram and colorbar - hist = pg.HistogramLUTItem() - hist.setImageItem(img) - pgWidget.addItem(hist) - hist.gradient.loadPreset('viridis') - - # cut elements - pgWidget2 = pg.GraphicsLayoutWidget() - - # plots for x and y cuts - xplot = pgWidget2.addPlot(row=1, col=0) - yplot = pgWidget2.addPlot(row=0, col=0) - - xplot.addLegend() - yplot.addLegend() - - # add crosshair to main plot - vline = pg.InfiniteLine(angle=90, movable=False, pen='r') - hline = pg.InfiniteLine(angle=0, movable=False, pen='b') - imgPlot.addItem(vline, ignoreBounds=True) - imgPlot.addItem(hline, ignoreBounds=True) - - def crossMoved(event): - pos = event[0].scenePos() - if imgPlot.sceneBoundingRect().contains(pos): - origin = imgPlot.vb.mapSceneToView(pos) - vline.setPos(origin.x()) - hline.setPos(origin.y()) - vidx = np.argmin(np.abs(origin.x()-x)) - hidx = np.argmin(np.abs(origin.y()-y)) - yplot.clear() - yplot.plot(zz[vidx, :], y, name='vertical cut', - pen=pg.mkPen('r', width=2), - symbol='o', symbolBrush='r', symbolPen=None) - xplot.clear() - xplot.plot(x, zz[:, hidx], name='horizontal cut', - pen=pg.mkPen('b', width=2), - symbol='o', symbolBrush='b', symbolPen=None) - - proxy = pg.SignalProxy(imgPlot.scene().sigMouseClicked, slot=crossMoved) - - dg = widgetDialog(pgWidget, title='pyqtgraph image test') - dg2 = widgetDialog(pgWidget2, title='line cuts') - - # setting the data - img.setImage(zz) - img.setRect(QtCore.QRectF(0, -4, 10, 8.)) - hist.setLevels(zz.min(), zz.max()) - - # formatting - imgPlot.setLabel('left', "Y", units='T') - imgPlot.setLabel('bottom', "X", units='A') - xplot.setLabel('left', 'Z') - xplot.setLabel('bottom', "X", units='A') - yplot.setLabel('left', "Y", units='T') - yplot.setLabel('bottom', "Z") - - if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): - QtWidgets.QApplication.instance().exec_() - -if __name__ == '__main__': - image_test() \ No newline at end of file diff --git a/test/gui/simple_2d_plot.py b/test/gui/simple_2d_plot.py deleted file mode 100644 index 5bf6158d..00000000 --- a/test/gui/simple_2d_plot.py +++ /dev/null @@ -1,75 +0,0 @@ -import logging -import time - -from plottr import QtWidgets -from plottr import log -from plottr.data.datadict import DataDict, datadict_to_meshgrid -from plottr.utils import testdata -from plottr.gui import PlotWindow -from plottr.plot.mpl import AutoPlot - - -def setup_logging(): - logger = log.getLogger() - log.enableStreamHandler(True) - log.LEVEL = logging.INFO - return logger - - -logger = setup_logging() - - -def simple_2d_plot(): - app = QtWidgets.QApplication([]) - win = PlotWindow() - plot = AutoPlot(parent=win) - win.plot.setPlotWidget(plot) - win.show() - - # plotting 1d traces - if False: - logger.info(f"1D trace") - t0 = time.perf_counter() - nsamples = 30 - for i in range(nsamples): - data = datadict_to_meshgrid( - testdata.get_1d_scalar_cos_data(201, 2) - ) - win.plot.setData(data) - t1 = time.perf_counter() - fps = nsamples/(t1-t0) - logger.info(f"Performance: {fps} FPS") - - # plotting images - if True: - logger.info(f"2D image") - t0 = time.perf_counter() - nsamples = 30 - for i in range(nsamples): - data = datadict_to_meshgrid( - testdata.get_2d_scalar_cos_data(201, 101, 1) - ) - win.plot.setData(data) - t1 = time.perf_counter() - fps = nsamples/(t1-t0) - logger.info(f"Performance: {fps} FPS") - - # plotting 2d scatter - if False: - logger.info(f"2D scatter") - t0 = time.perf_counter() - nsamples = 30 - for i in range(nsamples): - data = testdata.get_2d_scalar_cos_data(21, 21, 1) - win.plot.setData(data) - t1 = time.perf_counter() - fps = nsamples/(t1-t0) - logger.info(f"Performance: {fps} FPS") - - return app.exec_() - - -if __name__ == '__main__': - from plottr import plottrPath - print(plottrPath) - simple_2d_plot() diff --git a/test/prototyping/autoplot testing.ipynb b/test/prototyping/autoplot testing.ipynb deleted file mode 100644 index 3e83e2b4..00000000 --- a/test/prototyping/autoplot testing.ipynb +++ /dev/null @@ -1,109 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "id": "13fd24a7-5a8d-4045-a04f-245702201dce", - "metadata": {}, - "outputs": [], - "source": [ - "%gui qt\n", - "%load_ext autoreload\n", - "%autoreload 2" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "9f21e8b2-a442-40ae-8b3b-6243bdd71fdd", - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "ed548577-e45b-4819-9c10-31c474441e06", - "metadata": {}, - "outputs": [], - "source": [ - "from plottr.data.datadict import DataDict\n", - "from plottr.apps.autoplot import autoplot" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "700404b6-962f-4ae4-8826-322cf8d6605a", - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "aa0f36e4-f67e-4a38-bbfb-ab4bf640e54d", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "data = DataDict(\n", - " x = dict(values=np.arange(10)),\n", - " y = dict(values=np.arange(10)**2, axes=['x'])\n", - ")\n", - "data.validate()" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "e68d4a50-7929-4ffa-b584-131603c6b395", - "metadata": {}, - "outputs": [], - "source": [ - "fc, win = autoplot(data)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "53bd54e4-e1f3-4cb1-be29-c79a9ad1e3c1", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python [conda env:msmt-pyqt5]", - "language": "python", - "name": "conda-env-msmt-pyqt5-py" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.6" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/test/prototyping/new_data_methods.ipynb b/test/prototyping/new_data_methods.ipynb deleted file mode 100644 index 27f01f9c..00000000 --- a/test/prototyping/new_data_methods.ipynb +++ /dev/null @@ -1,176 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "%load_ext autoreload\n", - "%autoreload 2\n", - "%gui qt" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "from typing import Tuple\n", - "import numpy as np" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "from plottr.apps.autoplot import autoplot\n", - "from plottr.plot.pyqtgraph.autoplot import AutoPlot\n" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "from plottr.data.datadict import DataDict, MeshgridDataDict" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [], - "source": [ - "def oscillating_test_data(*specs: Tuple[float, float, int], amp=1, of=0):\n", - " axes = np.meshgrid(*[np.arange(n) for _, _, n in specs], indexing='ij')\n", - " data = amp * np.prod(np.array([np.cos(2*np.pi*(f*x+p)) \n", - " for x, (f, p, _) in zip(axes, specs)]), axis=0) \\\n", - " + np.random.normal(loc=0, scale=1, size=(axes[0].shape)) + of\n", - " dd = MeshgridDataDict()\n", - " for i, a in enumerate(axes):\n", - " dd[f'axis_{i}'] = dict(values=a)\n", - " dd['data'] = dict(\n", - " axes=[f'axis_{i}' for i in range(len(specs))],\n", - " values=data\n", - " )\n", - " dd.validate()\n", - " return dd\n" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "669.12" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "data = oscillating_test_data(\n", - " (0, 0, 10000),\n", - " (1/10, 0, 51),\n", - " (1/20, 0.25, 41),\n", - " amp=5,\n", - ")\n", - "data.nbytes()*1e-6" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "0.050184" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "data2 = data.mean('axis_0')\n", - "data2.nbytes()*1e-6" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [], - "source": [ - "fc, win = autoplot(plotWidgetClass=AutoPlot)\n", - "win.setInput(data=data)" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/wp/miniconda3/envs/msmt-pyqt5/lib/python3.11/site-packages/numpy/ma/core.py:467: RuntimeWarning: invalid value encountered in cast\n", - " fill_value = np.array(fill_value, copy=False, dtype=ndtype)\n" - ] - } - ], - "source": [ - "fc2, win2 = autoplot(plotWidgetClass=AutoPlot)\n", - "win2.setInput(data=data2)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "msmt-pyqt5", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.0" - }, - "orig_nbformat": 4, - "vscode": { - "interpreter": { - "hash": "6610d0d223300651404277538dfc70a7466493daba40fceb6aa864c596042666" - } - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/test/prototyping/plottrcfg_main.py b/test/prototyping/plottrcfg_main.py deleted file mode 100644 index ba05dd35..00000000 --- a/test/prototyping/plottrcfg_main.py +++ /dev/null @@ -1,5 +0,0 @@ -from plottr.plot.pyqtgraph.autoplot import AutoPlot as PGAutoPlot - -config = { - 'default-plotwidget': PGAutoPlot, -} diff --git a/test/prototyping/test_data.py b/test/prototyping/test_data.py deleted file mode 100644 index 76ea83d6..00000000 --- a/test/prototyping/test_data.py +++ /dev/null @@ -1,34 +0,0 @@ -from typing import Tuple -import numpy as np - - -from plottr.apps.autoplot import autoplot -from plottr.plot.pyqtgraph.autoplot import AutoPlot -from plottr.data.datadict import DataDict, MeshgridDataDict - - -def oscillating_test_data(*specs: Tuple[float, float, int], amp=1, of=0): - axes = np.meshgrid(*[np.arange(n) for _, _, n in specs], indexing='ij') - data = amp * np.prod(np.array([np.cos(2*np.pi*(f*x+p)) - for x, (f, p, _) in zip(axes, specs)]), axis=0) \ - + np.random.normal(loc=0, scale=1, size=(axes[0].shape)) + of - dd = MeshgridDataDict() - for i, a in enumerate(axes): - dd[f'axis_{i}'] = dict(values=a) - dd['data'] = dict( - axes=[f'axis_{i}' for i in range(len(specs))], - values=data - ) - dd.validate() - return dd - - -data = oscillating_test_data( - (0, 0, 10000), - (1/10, 0, 51), - (1/20, 0.25, 41), - amp=5, -) - -data2 = data.slice(axis_0=0) - diff --git a/test/pytest/pytest.ini b/test/pytest/pytest.ini index 814cca2e..1bde9d1d 100644 --- a/test/pytest/pytest.ini +++ b/test/pytest/pytest.ini @@ -1,2 +1,2 @@ [pytest] -qt_api=pyqt5 \ No newline at end of file +qt_api=pyside6 \ No newline at end of file diff --git a/test/pytest/test_app_manager.py b/test/pytest/test_app_manager.py index 8ee524cb..c505f81f 100644 --- a/test/pytest/test_app_manager.py +++ b/test/pytest/test_app_manager.py @@ -212,6 +212,8 @@ def test_pinging_app_from_outside_manager(qtbot, tmp_path): assert ret +# FIXME: This test is not working on my machine (MacBook Pro M2, MacOS Tahoe, Python 3.13) +# it works fine on CI, and in actual applications. So let's leave it for now. def test_getting_values(qtbot, tmp_path): datadict = _make_testdata() datadict_to_hdf5(datadict, str(tmp_path), 'data') diff --git a/test/pytest/test_ddh5.py b/test/pytest/test_ddh5.py index f916f067..4e99a33a 100644 --- a/test/pytest/test_ddh5.py +++ b/test/pytest/test_ddh5.py @@ -7,6 +7,7 @@ import numpy as np +from build.lib.plottr import qtsleep from plottr.data import datadict as dd from plottr.data import datadict_storage as dds from plottr.node.tools import linearFlowchart @@ -148,6 +149,10 @@ def test_loader_node(qtbot): with qtbot.waitSignal(node.loadingWorker.dataLoaded, timeout=1000) as blocker: node.filepath = str(FILEPATH) + + # wait a bit to make sure the output is set. + qtsleep(0.1) + out = fc.outputValues()['dataOut'].copy() out.pop('__title__') assert _clean_from_file(out) == data diff --git a/test/run_gui_test.py b/test/run_gui_test.py deleted file mode 100644 index 36986096..00000000 --- a/test/run_gui_test.py +++ /dev/null @@ -1,60 +0,0 @@ -import sys -import os -import argparse -import importlib -import inspect - -from plottr import QtWidgets, plottrPath - - -def run(func, **kw): - app = QtWidgets.QApplication([]) - _ = func(**kw) - return app.exec_() - - -def get_functions(): - testdir = os.path.join(plottrPath, '..', 'test') - testsdir = os.path.join(testdir, 'gui') - sys.path.append(testdir) - mods = [] - functions = {} - - for fn in os.listdir(testsdir): - try: - path = f"gui.{os.path.splitext(fn)[0]}" - if '__' not in path: - mod = importlib.import_module(path) - mods.append(mod) - except: - pass - - for mod in mods: - for name, fun in inspect.getmembers(mod): - if inspect.isfunction(fun) and 'test_' in name: - functions[f"{mod.__name__}.{name}"] = \ - dict(func=fun, signature=inspect.signature(fun)) - - return functions - - -if __name__ == '__main__': - funcs = get_functions() - names_help = "available test functions: " - for f, desc in funcs.items(): - names_help += f"\n - {f} {desc['signature']}" - - parser = argparse.ArgumentParser(description='Testing data display widgets', - formatter_class=argparse.RawTextHelpFormatter) - parser.add_argument('name', help=names_help, default=None, metavar='NAME', - choices=list(funcs.keys())) - parser.add_argument("--kwargs", help="keyword arguments for the function", - default={}) - - args = parser.parse_args() - - print(f'Running {args.name} with options: {args.kwargs}. \n') - kwargs = {} - if args.kwargs != {}: - kwargs = eval(args.kwargs) - run(funcs[args.name]['func'], **kwargs) diff --git a/test/scripts/h5py_concurrent_rw_lock.py b/test/scripts/h5py_concurrent_rw_lock.py deleted file mode 100644 index 3dbe01a8..00000000 --- a/test/scripts/h5py_concurrent_rw_lock.py +++ /dev/null @@ -1,155 +0,0 @@ -"""This is a test script for concurrent data write/read using a lock file. -""" - -from multiprocessing import Process -import time -from datetime import datetime -from pathlib import Path - -import h5py -import numpy as np - - -# which path to run this on. -# filepath = Path(r'Z:\swmr-testing\testdata.h5') -filepath = Path('./testdata.h5') - - -def mkdata(start, nrows, npts=1): - numbers = np.arange(start, start+nrows).reshape(nrows, -1) * np.ones(npts) # broadcasts to (nrows, npts) - return numbers - - -def info(sender, msg): - print(f'{datetime.now()} : {sender} : {msg}') - - -class FileOpener: - - def __init__(self, path, mode='r'): - self.path = path - self.mode = mode - - self.timeout = 10 - self.file = None - self.test_delay = 0.1 - - def __enter__(self): - self.file = self.open_when_unlocked() - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - self.file.close() - - def open_when_unlocked(self): - t0 = time.time() - while True: - try: - f = h5py.File(self.path, self.mode) - return f - except (OSError, PermissionError, RuntimeError): - info(f'file opener ({self.mode})', 'waiting for file to be unlocked') - - time.sleep(self.test_delay) # don't overwhelm the FS by very fast repeated calls. - - if time.time() - t0 > self.timeout: - raise RuntimeError('waiting for file unlock timed out') - - -class Writer(Process): - - ncols = 3 - nrows_per_rep = 1000 - nreps = 100 - delay = 0.01 - - def __init__(self): - super().__init__() - - def run(self): - data = mkdata(0, self.nrows_per_rep * self.nreps, self.ncols) - info('writer', 'starting') - info('writer', f"prepared data has shape {data.shape}") - - with FileOpener(str(filepath), 'w-') as fo: - g = fo.file.create_group('my_group') - - for i in range(self.nreps): - arr = data[i*self.nrows_per_rep:(i+1)*self.nrows_per_rep, ...] - - with FileOpener(str(filepath), 'a') as fo: - f = fo.file - g = f['my_group'] - if 'my_dataset' in g.keys(): - ds = g['my_dataset'] - shp = list(ds.shape) - shp[0] += arr.shape[0] - info('writer', f"Resizing to {tuple(shp)}") - ds.resize(tuple(shp)) - info('writer', f"Adding data") - ds[-arr.shape[0]:, ...] = arr - else: - info('writer', 'create dataset with first data') - ds = g.create_dataset('my_dataset', maxshape=tuple([None] + list(arr.shape)[1:]), data=arr) - - info('writer', f"... data written") - time.sleep(self.delay) - - -class Reader(Process): - - delay = 0.001 - maxruntime = None - - def run(self): - t0 = time.time() - info('reader', 'Starting') - - while True: - if not filepath.exists(): - continue - - with FileOpener(str(filepath), 'r') as fo: - f = fo.file - try: - ds = f['my_group/my_dataset'] - info('reader', f'shape {ds.shape}') - except KeyError: # happens when we want to start reading before the first data has arrived. - pass - - if self.delay is not None: - time.sleep(self.delay) - - if self.maxruntime is not None and time.time() - t0 > self.maxruntime: - break - - -if __name__ == '__main__': - filepath.unlink(missing_ok=True) - - writer = Writer() - writer.delay = 0.001 - writer.ncols = 1000 - - reader = Reader() - reader.delay = 1 - - writer.start() - time.sleep(1) - reader.start() - - writer.join() - reader.kill() - - refdata = mkdata(0, writer.nrows_per_rep*writer.nreps, writer.ncols) - - with h5py.File(filepath, 'r') as f: - ds = f['my_group/my_dataset'] - info('main', f'loaded data shape: {ds.shape}') - assert np.array_equal(refdata, ds[:]) - - - - - - diff --git a/test/scripts/h5py_concurrent_rw_swmr.py b/test/scripts/h5py_concurrent_rw_swmr.py deleted file mode 100644 index 514b49de..00000000 --- a/test/scripts/h5py_concurrent_rw_swmr.py +++ /dev/null @@ -1,120 +0,0 @@ -"""This is a test script for swmr data write/read. -While this complies with the HDF5 instructions, it causes issues on some Windows machines. -Also, it does seem to cause issues with network drives (this is documented by HDF5). -""" - -from multiprocessing import Process -import time -from datetime import datetime -from pathlib import Path - -import h5py -import numpy as np - - -# which path to run this on. -filepath = Path(r'Z:\swmr-testing\testdata.h5') -filepath = Path('./testdata.h5') - - -def mkdata(start, nrows, npts=1): - numbers = np.arange(start, start+nrows).reshape(nrows, -1) * np.ones(npts) # broadcasts to (nrows, npts) - return numbers - - -def info(sender, msg): - print(f'{datetime.now()} : {sender} : {msg}') - - -class Writer(Process): - - ncols = 3 - nrows_per_rep = 1000 - nreps = 100 - delay = 0.01 - - def __init__(self): - super().__init__() - - def run(self): - filepath.unlink(missing_ok=True) - arr = mkdata(0, self.nrows_per_rep, self.ncols) - info('writer', 'starting to write data') - - with h5py.File(str(filepath), 'a', libver='latest') as f: - g = f.create_group('my_group') - ds = g.create_dataset('my_dataset', maxshape=(None, self.ncols), data=arr) - f.swmr_mode = True - - for i in range(self.nreps): - shp = list(ds.shape) - arr = mkdata((i+1)*self.nrows_per_rep, self.nrows_per_rep, self.ncols) - shp[0] += arr.shape[0] - info('writer', f"Resizing to {tuple(shp)}") - ds.resize(tuple(shp)) - info('writer', f"Adding data") - ds[-arr.shape[0]:, ...] = arr - ds.flush() - info('writer', f"...Flushed") - time.sleep(self.delay) - - -class Reader(Process): - - delay = 0.001 - maxruntime = None - close_always = True - - def run(self): - t0 = time.time() - info('reader', 'starting to read data') - - if not self.close_always: - f = h5py.File(str(filepath), 'r', libver='latest', swmr=True) - assert f.swmr_mode - - while True: - if self.close_always: - with h5py.File(str(filepath), 'r', libver='latest', swmr=True) as f: - assert f.swmr_mode - ds = f['my_group/my_dataset'] - ds.refresh() - info('reader', f'shape {ds.shape}') - else: - ds = f['my_group/my_dataset'] - ds.refresh() - info('reader', f'shape {ds.shape}') - - if self.delay is not None: - time.sleep(self.delay) - - if self.maxruntime is not None and time.time() - t0 > self.maxruntime: - break - - if not self.close_always: - f.close() - - -if __name__ == '__main__': - writer = Writer() - reader = Reader() - reader.maxruntime = None - reader.delay = 0.01 - reader.close_always = True - - writer.start() - time.sleep(0.5) - reader.start() - - writer.join() - reader.kill() - - with h5py.File(filepath, 'r', libver='latest', swmr=True) as f: - ds = f['my_group/my_dataset'] - info('main', f'Retrieved shape {ds.shape}') - - - - - - diff --git a/test_requirements.txt b/test_requirements.txt index b1c3cc6f..851ae6d3 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -2,6 +2,6 @@ qcodes pytest pytest-qt mypy==1.13.0 -PyQt5-stubs==5.15.6.0 +PySide6-stubs pandas-stubs watchdog \ No newline at end of file