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 886f1166..ba1664eb 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -18,9 +18,12 @@ jobs: DISPLAY: ':99.0' steps: - - name: setup ubuntu-latest xvfb + # 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@v7 - name: Set up Python ${{ matrix.python-version }} diff --git a/README.md b/README.md index 2aa511b8..09943ac5 100644 --- a/README.md +++ b/README.md @@ -18,12 +18,33 @@ https://plottr.readthedocs.io (work in progress...) Plottr is installable from pypi with `pip install plottr` -Plottr requires either the PyQt5 or Pyside2 gui framework. -To install with PyQt5 or Pyside2 backend you can do -``pip install plottr[PyQt5]`` or ``pip install plottr[Pyside2]`` +### Qt bindings -Note that if you have installed ``pyqt`` from ``(Ana)Conda`` you should not use any of these -targets but do ``pip install plottr`` or install Plottr from conda forge: +Plottr is a Qt application. At runtime it talks to Qt through +[`qtpy`](https://github.com/spyder-ide/qtpy), so it works with any of the +supported Qt bindings, but **you must install one yourself** -- none is pulled +in automatically. Pick exactly one of: + +| Binding | pip extra | Notes | +|-----------|----------------------------|------------------------------------| +| PySide6 | `pip install plottr[pyside6]` | **Recommended** (and what testing/type-checking use by default) | +| PyQt5 | `pip install plottr[pyqt5]` | | +| PyQt6 | `pip install plottr[pyqt6]` | | +| PySide2 | `pip install plottr[pyside2]` | Older Qt5 binding | + +For example, the recommended install is: + +``` +pip install plottr[pyside6] +``` + +If you have **more than one** binding installed, select which one plottr (via +`qtpy`) should use by setting the `QT_API` environment variable before starting, +e.g. `QT_API=pyside6` (other valid values: `pyqt5`, `pyqt6`, `pyside2`). + +If you installed `pyqt` from `(Ana)Conda` you should **not** use any of the pip +extras above; instead do `pip install plottr` (Qt comes from conda) or install +plottr from conda-forge: ``` conda config --add channels conda-forge @@ -31,7 +52,8 @@ conda config --set channel_priority strict conda install plottr ``` -To install from source: clone the repo, and install using `pip install -e .` +To install from source: clone the repo, and install using +`pip install -e .[pyside6]` (or another binding extra of your choice). ## inspectr: QCoDeS dataset inspection and (live) plotting @@ -50,11 +72,12 @@ Note: this package is not compatible with the original `plottr` tool. You might want to install freshly if you still use the old version. ## Requirements: -* python >= 3.8 +* python >= 3.12 * the usual: numpy, mpl, ... * pandas >= 0.22 * xarray * pyqtgraph >= 0.12.1 +* one Qt binding (PySide6 recommended; see [Installation](#installation)) # Recent changes: 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 c00e6ced..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\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 21058ab5..00000000 --- a/doc/examples/Simple Live plotting example with DDH5.ipynb +++ /dev/null @@ -1,140 +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 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 b7e810bd..00000000 --- a/doc/examples/node_with_dimension_selector_widget.py +++ /dev/null @@ -1,104 +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 - - -from plottr import QtWidgets -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 85436cbd..00000000 --- a/doc/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -sphinx==9.1.0 -sphinx-autodoc-typehints>=3.12.0 -PyQt5>=5.15.11 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 e07ab8be..cadd5b7c 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, qtsleep, plottrPath +from plottr import QtCore, QtWidgets, QtGui, Flowchart, Signal, Slot, log, 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 d227fea1..99354e4a 100644 --- a/plottr/apps/autoplot.py +++ b/plottr/apps/autoplot.py @@ -8,7 +8,7 @@ import argparse from typing import Union, Tuple, Optional, Type, List, Any, Type, TYPE_CHECKING -from .. import QtCore, Flowchart, Signal, Slot, QtWidgets, QtGui +from .. import QtCore, Flowchart, Signal, Slot, QtWidgets, QtGui, QAction from ..data.datadict import DataDictBase from ..data.datadict_storage import DDH5Loader from ..data.qcodes_dataset import QCodesDSLoader @@ -200,7 +200,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) @@ -480,7 +480,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 add50761..10d81c18 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, @@ -165,7 +165,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() @@ -230,20 +230,20 @@ def showContextMenu(self, position: QtCore.QPoint) -> None: copy_action = menu.addAction(copy_icon, "Copy") window = cast(QCodesDBInspector, self.window()) - starAction: QtWidgets.QAction = window.starAction + starAction: QAction = window.starAction starAction.setText('Star' if current_tag_char != self.tag_dict['star'] else 'Unstar') menu.addAction(starAction) - crossAction: QtWidgets.QAction = window.crossAction + crossAction: QAction = window.crossAction 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())) @@ -600,25 +600,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) @@ -940,7 +940,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 3d746b4e..8969e49f 100644 --- a/plottr/apps/monitr.py +++ b/plottr/apps/monitr.py @@ -31,7 +31,7 @@ from watchdog.events import FileSystemEvent, FileSystemMovedEvent -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 @@ -267,7 +267,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 @@ -283,7 +283,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 @@ -299,7 +299,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 @@ -315,7 +315,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 @@ -504,7 +504,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: @@ -758,7 +758,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. @@ -1148,7 +1148,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: """ @@ -1241,14 +1241,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) @@ -1290,7 +1290,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: """ @@ -1356,9 +1356,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 @@ -1374,15 +1374,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:] @@ -1398,7 +1398,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: """ @@ -2178,7 +2178,7 @@ def on_create_path_list( if tpe == ContentType.data: self.path_list.append(str(path)) else: - item = self.model.itemFromIndex(item_index) # type: ignore[attr-defined] # Not sure why mypy is complaining about using itemFromIndex only here but not in other places. + item = self.model.itemFromIndex(item_index) assert isinstance(item, Item) for path, tpe in item.files.items(): if tpe == ContentType.data: @@ -2267,7 +2267,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) @@ -2326,7 +2326,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() @@ -2353,7 +2353,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: @@ -2540,7 +2540,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: @@ -2652,14 +2652,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: """ @@ -2673,7 +2673,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: @@ -2728,7 +2728,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) @@ -2740,7 +2740,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: @@ -2805,11 +2805,13 @@ 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) - def on_range_changed(self) -> None: + @Slot(int, int) + def on_range_changed(self, min_value: int, max_value: int) -> None: if self.first_scroll is True: bar = self.verticalScrollBar() if bar is not None: @@ -3218,21 +3220,21 @@ 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) menu.addAction(action) sort_menu = menu_bar.addMenu("Sort") - self.sort_group = QtWidgets.QActionGroup(sort_menu) + self.sort_group = QActionGroup(sort_menu) for label, default in [("Sort by type", True), ("Sort alphabetically", False)]: - action = QtWidgets.QAction(label) + action = QAction(label) action.setCheckable(True) action.setChecked(default) self.sort_group.addAction(action) @@ -3847,7 +3849,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: """ @@ -3878,4 +3881,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 d2d22898..8cbb5472 100644 --- a/plottr/apps/ui/monitr.py +++ b/plottr/apps/ui/monitr.py @@ -39,7 +39,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/apps/watchdog_classes.py b/plottr/apps/watchdog_classes.py index abf2df0e..012dd7a7 100644 --- a/plottr/apps/watchdog_classes.py +++ b/plottr/apps/watchdog_classes.py @@ -39,19 +39,19 @@ def __init__(self, closed_signal: Signal, self.modified_signal = modified_signal def on_closed(self, event: FileSystemEvent) -> None: - self.closed_signal.emit(event) # type: ignore[attr-defined] + self.closed_signal.emit(event) def on_deleted(self, event: FileSystemEvent) -> None: - self.deleted_signal.emit(event) # type: ignore[attr-defined] + self.deleted_signal.emit(event) def on_moved(self, event: FileSystemEvent) -> None: - self.moved_signal.emit(event) # type: ignore[attr-defined] + self.moved_signal.emit(event) def on_created(self, event: FileSystemEvent) -> None: - self.created_signal.emit(event) # type: ignore[attr-defined] + self.created_signal.emit(event) def on_modified(self, event: FileSystemEvent) -> None: - self.modified_signal.emit(event) # type: ignore[attr-defined] + self.modified_signal.emit(event) class WatcherClient(QtCore.QObject): diff --git a/plottr/data/datadict.py b/plottr/data/datadict.py index 0ab0e157..5a7b3733 100644 --- a/plottr/data/datadict.py +++ b/plottr/data/datadict.py @@ -797,9 +797,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 f0db481d..ad396023 100644 --- a/plottr/data/datadict_storage.py +++ b/plottr/data/datadict_storage.py @@ -509,7 +509,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 b5117896..0cc6b9d4 100644 --- a/plottr/gui/data_display.py +++ b/plottr/gui/data_display.py @@ -5,7 +5,7 @@ from typing import List, Tuple, Dict, Any, Optional -from .. import QtWidgets, Signal, Slot +from .. import QtWidgets, Signal, Slot, PYSIDE6 from ..data.datadict import DataDictBase @@ -28,7 +28,10 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None, self._readonly = readonly self._batchUpdate = False - self.setSelectionMode(self.MultiSelection) + if PYSIDE6: + self.setSelectionMode(self.SelectionMode.MultiSelection) + else: + self.setSelectionMode(self.MultiSelection) self.itemSelectionChanged.connect(self.emitSelection) def _ndims(self, name: str) -> int: diff --git a/plottr/gui/widgets.py b/plottr/gui/widgets.py index 5f9756d3..695d678f 100644 --- a/plottr/gui/widgets.py +++ b/plottr/gui/widgets.py @@ -6,7 +6,7 @@ from typing import Union, List, Tuple, Optional, Sequence, Dict, Any, Type, Generic, TypeVar from .tools import dictToTreeWidgetItems, dpiScalingFactor -from plottr import QtGui, Flowchart, QtWidgets, Signal, Slot +from plottr import QtGui, Flowchart, QtWidgets, Signal, Slot, PYSIDE6 from plottr.node import Node, linearFlowchart from plottr.node.node import updateGuiQuietly, emitGuiUpdate from ..plot import PlotNode, PlotWidgetContainer, PlotWidget @@ -456,7 +456,10 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None, self.node: Optional[Node] = None self.dimensionType = dimensionType - self.setSelectionMode(self.MultiSelection) + if PYSIDE6: + self.setSelectionMode(self.SelectionMode.MultiSelection) + else: + self.setSelectionMode(self.MultiSelection) self.itemSelectionChanged.connect(self.emitSelection) def setDimensions(self, dimensions: List[str]) -> None: diff --git a/plottr/node/fitter.py b/plottr/node/fitter.py index cff67c97..3fff6bed 100644 --- a/plottr/node/fitter.py +++ b/plottr/node/fitter.py @@ -93,7 +93,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) @@ -312,7 +312,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 2da22ce2..6ff14ae8 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 plottr.utils.latex import latex_to_html @@ -591,7 +591,7 @@ def __init__(self, options: FigureOptions, ) complexOptions = QtWidgets.QMenu(parent=self) - complexGroup = QtWidgets.QActionGroup(complexOptions) + complexGroup = QActionGroup(complexOptions) complexGroup.setExclusive(True) self._createComplexRepresentation() @@ -613,7 +613,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: @@ -622,7 +622,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/plottr/plot/pyqtgraph/plots.py b/plottr/plot/pyqtgraph/plots.py index 21dd889a..47f600f5 100644 --- a/plottr/plot/pyqtgraph/plots.py +++ b/plottr/plot/pyqtgraph/plots.py @@ -162,7 +162,7 @@ def setScatter2d(self, x: np.ndarray, y: np.ndarray, z: np.ndarray) -> None: def _colorScatterPoints(self, cbar: pg.ColorBarItem) -> None: if self.scatter is not None and self.scatterZVals is not None: z_norm = self._normalizeColors(self.scatterZVals, cbar.levels()) - colors = self.colorbar.cmap.mapToQColor(z_norm) + colors = self.colorbar.colorMap().mapToQColor(z_norm) self.scatter.setBrush(colors) def _normalizeColors(self, z: np.ndarray, levels: Tuple[float, float]) -> np.ndarray: diff --git a/pyproject.toml b/pyproject.toml index 12e65aa8..9c8124b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,6 +51,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] @@ -94,6 +95,16 @@ module = [ ] 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 6f54d6e9..00000000 --- a/test/apps/autoplot_app.py +++ /dev/null @@ -1,166 +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.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 6b7b7518..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 QtWidgets -from plottr.data.datadict import 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 39c6a7ab..00000000 --- a/test/apps/test_histograms.py +++ /dev/null @@ -1,165 +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 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 e635a7b9..00000000 --- a/test/gui/data_display_widgets.py +++ /dev/null @@ -1,26 +0,0 @@ -"""data_display_widgets.py - -Testing scripts for GUI elements for data display. -""" - -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 534e5b01..00000000 --- a/test/gui/data_selector.py +++ /dev/null @@ -1,32 +0,0 @@ -from plottr import 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 5a16d8eb..00000000 --- a/test/gui/dimension_selection_widgets.py +++ /dev/null @@ -1,31 +0,0 @@ - -from plottr import QtWidgets -from plottr.data.datadict import str2dd -from plottr.gui.widgets import 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 701d4412..00000000 --- a/test/gui/grid_options.py +++ /dev/null @@ -1,79 +0,0 @@ -"""grid_options.py - -Testing Widgets for the gridding node. -""" - -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 8ab504e7..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, 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 db9e0065..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_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 e43aa605..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 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 344f3809..00000000 --- a/test/prototyping/test_data.py +++ /dev/null @@ -1,32 +0,0 @@ -from typing import Tuple -import numpy as np - - -from plottr.data.datadict import 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 2bd8aa16..6f8b7d24 100644 --- a/test/pytest/test_app_manager.py +++ b/test/pytest/test_app_manager.py @@ -211,6 +211,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..edf480a7 100644 --- a/test/pytest/test_ddh5.py +++ b/test/pytest/test_ddh5.py @@ -148,6 +148,13 @@ def test_loader_node(qtbot): with qtbot.waitSignal(node.loadingWorker.dataLoaded, timeout=1000) as blocker: node.filepath = str(FILEPATH) + + # dataLoaded is followed by a queued onThreadComplete() slot that actually + # sets the flowchart output; wait until that has happened instead of + # sleeping a fixed amount (robust on slow CI / Windows). + qtbot.waitUntil(lambda: fc.outputValues()['dataOut'] is not None, + timeout=2000) + out = fc.outputValues()['dataOut'].copy() out.pop('__title__') assert _clean_from_file(out) == data @@ -249,8 +256,19 @@ def test_concurrent_write_and_read(): ref_dataset['__dataset.name__'] = '' writer.start() + + # The writer runs in a separate process; depending on the OS process start + # method (e.g. Windows uses 'spawn', not 'fork') it can take several seconds + # just to import plottr and create the file. Wait for the file to appear + # before reading, so the concurrent read below doesn't race against process + # startup. Concurrent access itself is coordinated by FileOpener's lock file. + while writer.is_alive() and not Path(writer.filepath).exists(): + time.sleep(0.1) + while writer.is_alive(): time.sleep(2) + if not Path(writer.filepath).exists(): + continue data_from_file = dds.datadict_from_hdf5(writer.filepath, structure_only=True) assert(data_from_file.structure(include_meta=False)) diff --git a/test/pytest/test_gui_figuremaker.py b/test/pytest/test_gui_figuremaker.py new file mode 100644 index 00000000..f68fabf3 --- /dev/null +++ b/test/pytest/test_gui_figuremaker.py @@ -0,0 +1,128 @@ +"""GUI smoke tests for the matplotlib and pyqtgraph FigureMaker classes. + +These verify that both plotting backends can build line plots, images and +scatter plots (including complex data) without raising under the active Qt +binding (PySide6 by default). + +They are the pytest-qt successors of the old interactive scripts that used to +live under ``test/gui`` (``mpl_figuremaker.py`` and +``pyqtgraph_figuremaker.py``). +""" + +import numpy as np + +from plottr.plot.base import ComplexRepresentation, PlotDataType +from plottr.plot.mpl.autoplot import FigureMaker as MPLFigureMaker, PlotType +from plottr.plot.mpl.widgets import figureDialog +from plottr.plot.pyqtgraph.autoplot import FigureMaker as PGFigureMaker + + +# -- matplotlib ---------------------------------------------------------------- + +def test_mpl_multiple_line_plots(qtbot): + """Several real 1d traces produce at least one axes.""" + fig, win = figureDialog() + qtbot.addWidget(win) + + setpts = np.linspace(0, 10, 101) + data = np.cos(setpts) + with MPLFigureMaker(fig) as fm: + fm.plotType = PlotType.singletraces + line = fm.addData(setpts, data, labels=['x', r'$\cos(x)$']) + fm.addData(setpts, data ** 2, labels=['x', r'$\cos^2(x)$']) + fm.addData(setpts, data ** 3, labels=['x', r'$\cos^3(x)$']) + assert line is not None + + assert len(fig.axes) > 0 + + +def test_mpl_complex_line_plots(qtbot): + """Complex traces in mag-and-phase format produce axes.""" + fig, win = figureDialog() + qtbot.addWidget(win) + + setpts = np.linspace(0, 10, 101) + data_1 = np.exp(-1j * setpts) + data_2 = np.conjugate(data_1) + with MPLFigureMaker(fig) as fm: + fm.complexRepresentation = ComplexRepresentation.magAndPhase + fm.plotType = PlotType.multitraces + fm.addData(setpts, data_1, labels=['x', r'$\exp(-ix)$']) + fm.addData(setpts, data_2, labels=['x', r'$\exp(ix)$']) + + assert len(fig.axes) > 0 + + +# -- pyqtgraph ----------------------------------------------------------------- + +def test_pyqtgraph_basic_line_plot(qtbot): + """Line + scatter 1d traces produce a widget.""" + x = np.linspace(0, 10, 51) + y = np.cos(x) + with PGFigureMaker() 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) + fm.addData(x, np.abs(y), labels=['x', '|cos(x)|'], + plotDataType=PlotDataType.line1d) + qtbot.addWidget(fm.widget) + assert fm.widget is not None + + +def test_pyqtgraph_images(qtbot): + """2d grid images produce a widget.""" + 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 PGFigureMaker() as fm: + fm.addData(xx, yy, zz, labels=['x', 'y', 'fake data'], + plotDataType=PlotDataType.grid2d) + fm.addData(xx, yy, zz[:, ::-1], labels=['x', 'y', 'fake data (mirror)'], + plotDataType=PlotDataType.grid2d) + qtbot.addWidget(fm.widget) + assert fm.widget is not None + + +def test_pyqtgraph_scatter2d(qtbot): + """2d scatter data produces a widget.""" + 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 PGFigureMaker() as fm: + fm.addData(xx.flatten(), yy.flatten(), zz.flatten(), + labels=['x', 'y', 'fake data'], + plotDataType=PlotDataType.scatter2d) + qtbot.addWidget(fm.widget) + assert fm.widget is not None + + +def test_pyqtgraph_complex_line_plots(qtbot): + """Complex 1d traces (single panel) produce a widget.""" + setpts = np.linspace(0, 10, 101) + data_1 = np.exp(-1j * setpts) + data_2 = np.conjugate(data_1) + with PGFigureMaker() as fm: + line_1 = fm.addData(setpts, data_1, labels=['x', 'exp(-ix)']) + fm.addData(setpts, data_2, labels=['x', 'exp(ix)'], join=line_1) + qtbot.addWidget(fm.widget) + assert fm.widget is not None + + +def test_pyqtgraph_complex_images(qtbot): + """Complex 2d images in mag-and-phase format produce a widget.""" + 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 PGFigureMaker() as fm: + fm.complexRepresentation = ComplexRepresentation.magAndPhase + fm.addData(xx, yy, zz, labels=['x', 'y', 'fake data'], + plotDataType=PlotDataType.grid2d) + fm.addData(xx, yy, np.conjugate(zz), + labels=['x', 'y', 'fake data (conjugate)'], + plotDataType=PlotDataType.grid2d) + qtbot.addWidget(fm.widget) + assert fm.widget is not None diff --git a/test/pytest/test_gui_nodes.py b/test/pytest/test_gui_nodes.py new file mode 100644 index 00000000..f9e22e04 --- /dev/null +++ b/test/pytest/test_gui_nodes.py @@ -0,0 +1,175 @@ +"""GUI smoke tests for plottr node widgets and plot windows. + +These tests verify that node UIs build inside a flowchart, accept data through +the flowchart, and that the plot windows can be constructed and fed data under +the active Qt binding (PySide6 by default). + +They are the pytest-qt successors of the old interactive scripts that used to +live under ``test/gui`` (``data_selector.py``, ``ddh5_loader.py``, +``dimension_assignment.py``, ``grid_options.py``, ``correct_offset.py`` and +``simple_2d_plot.py``). +""" + +import numpy as np +import pytest + +from plottr.data import datadict_storage as dds +from plottr.data.datadict_storage import DDH5LoaderWidget +from plottr.data.datadict import MeshgridDataDict, datadict_to_meshgrid +from plottr.gui import PlotWindow +from plottr.gui.widgets import makeFlowchartWithPlotWindow +from plottr.node.data_selector import DataSelector, DataDisplayWidget +from plottr.node.dim_reducer import ( + DimensionReducer, + DimensionReducerNodeWidget, + XYSelector, + XYSelectorNodeWidget, +) +from plottr.node.filter.correct_offset import SubtractAverage, SubtractAverageWidget +from plottr.node.grid import DataGridder, DataGridderNodeWidget, GridOption +from plottr.node.tools import linearFlowchart +from plottr.plot.mpl import AutoPlot +from plottr.utils import testdata + + +# Several other test modules set ``.useUi = False`` and even +# ``.uiClass = None`` (class attributes) and don't restore them, +# which would suppress UI creation here depending on test order. The widget +# classes themselves are unaffected by that pollution, so we reference them +# directly; the fixture below forces UI creation for the node classes exercised +# in this file and restores the previous values afterwards. +_NODE_UI_CLASSES = { + DataSelector: DataDisplayWidget, + dds.DDH5Loader: DDH5LoaderWidget, + DimensionReducer: DimensionReducerNodeWidget, + XYSelector: XYSelectorNodeWidget, + DataGridder: DataGridderNodeWidget, + SubtractAverage: SubtractAverageWidget, +} + + +@pytest.fixture +def node_ui_enabled(): + saved = {cls: (cls.useUi, cls.uiClass) for cls in _NODE_UI_CLASSES} + for cls, ui_class in _NODE_UI_CLASSES.items(): + cls.useUi = True + cls.uiClass = ui_class + yield + for cls, (use_ui, ui_class) in saved.items(): + cls.useUi = use_ui + cls.uiClass = ui_class + + +# -- node UIs in a flowchart --------------------------------------------------- + +def test_data_selector_node_ui(qtbot, node_ui_enabled): + """The DataSelector node builds its UI and accepts data.""" + fc = linearFlowchart(('selector', DataSelector)) + node = fc.nodes()['selector'] + assert node.ui is not None + qtbot.addWidget(node.ui) + + data = testdata.three_incompatible_3d_sets(2, 2, 2) + fc.setInput(dataIn=data) + node.selectedData = ['data'] + assert fc.outputValues()['dataOut'] is not None + + +def test_ddh5_loader_node_ui(qtbot, node_ui_enabled): + """The DDH5Loader node builds its UI.""" + fc = linearFlowchart(('loader', dds.DDH5Loader)) + node = fc.nodes()['loader'] + assert node.ui is not None + qtbot.addWidget(node.ui) + + +def test_dimension_reducer_node_ui(qtbot, node_ui_enabled): + """The DimensionReducer node builds its UI and passes meshgrid data through.""" + fc = linearFlowchart(('reducer', DimensionReducer)) + node = fc.nodes()['reducer'] + assert node.ui is not None + qtbot.addWidget(node.ui) + + data = datadict_to_meshgrid(testdata.three_compatible_3d_sets(5, 5, 5)) + fc.setInput(dataIn=data) + out = fc.outputValues()['dataOut'] + assert isinstance(out, MeshgridDataDict) + # the embedded selection widget gets one row per axis. + assert node.ui.widget.topLevelItemCount() == len(data.axes()) + + +def test_xy_selector_node_ui(qtbot, node_ui_enabled): + """The XYSelector node builds its UI and is populated from meshgrid data. + + Without explicit x/y role assignment the node output is ``None`` (it cannot + determine the plot axes yet); we therefore assert on the populated UI. + """ + fc = linearFlowchart(('xysel', XYSelector)) + node = fc.nodes()['xysel'] + assert node.ui is not None + qtbot.addWidget(node.ui) + + data = datadict_to_meshgrid(testdata.three_compatible_3d_sets(5, 5, 5)) + fc.setInput(dataIn=data) + assert node.ui.widget.topLevelItemCount() == len(data.axes()) + assert 'dataOut' in fc.outputValues() + + +def test_data_gridder_node_ui(qtbot, node_ui_enabled): + """The DataGridder node builds its UI and grids non-grid data on request.""" + fc = linearFlowchart(('grid', DataGridder)) + node = fc.nodes()['grid'] + assert node.ui is not None + qtbot.addWidget(node.ui) + + data = testdata.three_compatible_3d_sets(5, 5, 5) + fc.setInput(dataIn=data) + + node.grid = GridOption.guessShape, {} + + out = fc.outputValues()['dataOut'] + assert isinstance(out, MeshgridDataDict) + + +# -- plot windows -------------------------------------------------------------- + +def test_subtract_average_with_plot_window(qtbot, node_ui_enabled): + """A flowchart with a plot window accepts (and re-accepts) meshgrid data.""" + win, fc = makeFlowchartWithPlotWindow([('sub', SubtractAverage)]) + qtbot.addWidget(win) + + 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() + fc.setInput(dataIn=data) + + data2 = MeshgridDataDict( + reps=dict(values=xx), + y=dict(values=yy), + z=dict(values=zz, axes=['reps', 'y']), + ) + data2.validate() + fc.setInput(dataIn=data2) + + assert fc.outputValues()['dataOut'] is not None + + +def test_plot_window_with_mpl_autoplot(qtbot): + """A PlotWindow with an MPL AutoPlot accepts 1d and 2d data.""" + win = PlotWindow() + qtbot.addWidget(win) + plot = AutoPlot(parent=win) + win.plot.setPlotWidget(plot) + + data_1d = datadict_to_meshgrid(testdata.get_1d_scalar_cos_data(21, 1)) + win.plot.setData(data_1d) + + data_2d = datadict_to_meshgrid(testdata.get_2d_scalar_cos_data(21, 11, 1)) + win.plot.setData(data_2d) diff --git a/test/pytest/test_gui_widgets.py b/test/pytest/test_gui_widgets.py new file mode 100644 index 00000000..b573760d --- /dev/null +++ b/test/pytest/test_gui_widgets.py @@ -0,0 +1,151 @@ +"""GUI smoke tests for standalone plottr widgets. + +These tests verify that the data-display, dimension-selection and gridding +widgets can be constructed, populated with data, and emit their selection +signals without raising under the active Qt binding (PySide6 by default). + +They are the pytest-qt successors of the old interactive scripts that used to +live under ``test/gui`` (``data_display_widgets.py``, +``dimension_selection_widgets.py`` and ``grid_options.py``). +""" + +import numpy as np + +from plottr.data.datadict import str2dd, datadict_to_meshgrid +from plottr.gui.data_display import DataSelectionWidget +from plottr.gui.widgets import ( + AxisSelector, + DependentSelector, + DimensionSelector, + MultiDimensionSelector, +) +from plottr.node.grid import GridOption, GridOptionWidget, ShapeSpecificationWidget +from plottr.node.dim_reducer import XYSelectionWidget +from plottr.utils import testdata + + +# -- data display -------------------------------------------------------------- + +def test_data_selection_widget_set_and_clear(qtbot): + """The selection widget accepts data, can be cleared, and re-populated.""" + widget = DataSelectionWidget() + qtbot.addWidget(widget) + + data = testdata.three_incompatible_3d_sets(5, 5, 5) + widget.setData(data.structure(), data.shapes()) + assert set(widget.dataItems.keys()) == set(data.dependents()) + + widget.clear() + assert widget.dataItems == {} + + widget.setData(data.structure(), data.shapes()) + assert set(widget.dataItems.keys()) == set(data.dependents()) + + +def test_data_selection_widget_readonly(qtbot): + """A read-only selection widget still displays the data fields.""" + widget = DataSelectionWidget(readonly=True) + qtbot.addWidget(widget) + + data = testdata.three_incompatible_3d_sets(2, 2, 2) + widget.setData(data.structure(), data.shapes()) + assert len(widget.dataItems) == len(data.dependents()) + + +# -- dimension selection ------------------------------------------------------- + +def test_dimension_selector_emits(qtbot): + """Selecting a dimension in the combo emits ``dimensionSelected``.""" + data = str2dd("data1(x,y,z); data2(x,z);") + widget = DimensionSelector() + qtbot.addWidget(widget) + + dims = data.axes() + data.dependents() + widget.combo.setDimensions(dims) + # every available dimension is offered in the combo (plus the 'None' entry). + combo_entries = [widget.combo.itemText(i) for i in range(widget.combo.count())] + for d in dims: + assert d in combo_entries + + with qtbot.waitSignal(widget.combo.dimensionSelected, timeout=1000): + widget.combo.setCurrentText(dims[0]) + + +def test_axis_and_dependent_selectors(qtbot): + """Axis/dependent selectors populate their combo from the given dimensions.""" + data = str2dd("data1(x,y,z); data2(x,z);") + + axis_sel = AxisSelector() + qtbot.addWidget(axis_sel) + axis_sel.combo.setDimensions(data.axes()) + axis_entries = [axis_sel.combo.itemText(i) + for i in range(axis_sel.combo.count())] + for ax in data.axes(): + assert ax in axis_entries + + dep_sel = DependentSelector() + qtbot.addWidget(dep_sel) + dep_sel.combo.setDimensions(data.dependents()) + dep_entries = [dep_sel.combo.itemText(i) + for i in range(dep_sel.combo.count())] + for dep in data.dependents(): + assert dep in dep_entries + + +def test_multi_dimension_selector(qtbot): + """The multi-selector lists all dimensions and emits on selection.""" + data = str2dd("data1(x,y,z); data2(x,z);") + dims = data.axes() + data.dependents() + + widget = MultiDimensionSelector() + qtbot.addWidget(widget) + widget.setDimensions(dims) + assert widget.count() == len(dims) + + with qtbot.waitSignal(widget.dimensionSelectionMade, timeout=1000): + widget.setSelected([dims[0]]) + assert widget.getSelected() == [dims[0]] + + +# -- gridding ------------------------------------------------------------------ + +def test_shape_specification_widget(qtbot): + """ShapeSpecificationWidget takes axes and a shape and reports it back.""" + widget = ShapeSpecificationWidget() + qtbot.addWidget(widget) + + axes = ['x', 'y', 'aVeryVeryVeryVeryLongAxisName'] + widget.setAxes(axes) + widget.setShape({'order': axes, 'shape': (5, 5, 5)}) + + shape = widget.getShape() + assert list(shape['order']) == axes + assert tuple(shape['shape']) == (5, 5, 5) + + +def test_grid_option_widget_emits(qtbot): + """Selecting a grid option via its radio button emits ``optionSelected``.""" + data = datadict_to_meshgrid(testdata.three_compatible_3d_sets(5, 5, 5)) + widget = GridOptionWidget() + qtbot.addWidget(widget) + widget.setAxes(data.axes()) + + with qtbot.waitSignal(widget.optionSelected, timeout=1000): + widget.buttons[GridOption.guessShape].setChecked(True) + + +# -- xy selection (standalone widget) ------------------------------------------ + +def test_xy_selection_widget(qtbot): + """XYSelectionWidget populates one row per axis of meshgrid data.""" + data = datadict_to_meshgrid(testdata.three_compatible_3d_sets(4, 4, 4)) + widget = XYSelectionWidget() + qtbot.addWidget(widget) + + widget.setData(data.structure(), data.shapes(), type(data)) + assert widget.topLevelItemCount() == len(data.axes()) + + # clearing and re-setting data should be idempotent in row count. + widget.clear() + widget.setData(data.structure(), data.shapes(), type(data)) + assert widget.topLevelItemCount() == len(data.axes()) 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 a3329b04..fd992016 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -3,6 +3,6 @@ pytest pytest-qt hypothesis mypy==2.1.0 -PyQt5-stubs==5.15.6.0 +PySide6-stubs pandas-stubs watchdog \ No newline at end of file