diff --git a/README.rst b/README.rst index 0a636754364..89a55d6cd64 100644 --- a/README.rst +++ b/README.rst @@ -52,6 +52,19 @@ Go to the directory ``docs`` and This generate a webpage, index.html, in ``docs/_build/html`` with the rendered html. +QCoDeS Loop +=========== + +The modules ``qcodes.data``, ``qcodes.plots``, ``qcodes.actions``, +``qcodes.loops``, ``qcodes.measure``, ``qcodes.extensions.slack`` +and ``qcodes.utils.magic`` that were part of QCoDeS until version 0.37.0. +have been moved into an independent package called qcodes_loop. +Please see it's `repository `_ for more information. + +For the time being it is possible to automatically install the qcodes_loop +package when installing qcodes by executing ``pip install qcodes[loop]``. + Code of Conduct =============== diff --git a/docs/api/actions.rst b/docs/api/actions.rst deleted file mode 100644 index 61055b629bf..00000000000 --- a/docs/api/actions.rst +++ /dev/null @@ -1,5 +0,0 @@ -qcodes.actions -============== - -.. automodule:: qcodes.actions - :members: diff --git a/docs/api/data/data_array.rst b/docs/api/data/data_array.rst deleted file mode 100644 index 46289f96f8f..00000000000 --- a/docs/api/data/data_array.rst +++ /dev/null @@ -1,5 +0,0 @@ -qcodes.data.data_array ----------------------- - -.. automodule:: qcodes.data.data_array - :members: diff --git a/docs/api/data/data_set.rst b/docs/api/data/data_set.rst deleted file mode 100644 index d1341abadce..00000000000 --- a/docs/api/data/data_set.rst +++ /dev/null @@ -1,5 +0,0 @@ -qcodes.data.data_set --------------------- - -.. automodule:: qcodes.data.data_set - :members: diff --git a/docs/api/data/format.rst b/docs/api/data/format.rst deleted file mode 100644 index b73f5adf9df..00000000000 --- a/docs/api/data/format.rst +++ /dev/null @@ -1,5 +0,0 @@ -qcodes.data.format ------------------- - -.. automodule:: qcodes.data.format - :members: diff --git a/docs/api/data/gnuplot_format.rst b/docs/api/data/gnuplot_format.rst deleted file mode 100644 index 455504ef0cf..00000000000 --- a/docs/api/data/gnuplot_format.rst +++ /dev/null @@ -1,5 +0,0 @@ -qcodes.data.gnuplot_format --------------------------- - -.. automodule:: qcodes.data.gnuplot_format - :members: diff --git a/docs/api/data/hdf5_format.rst b/docs/api/data/hdf5_format.rst deleted file mode 100644 index 855925fc7ff..00000000000 --- a/docs/api/data/hdf5_format.rst +++ /dev/null @@ -1,5 +0,0 @@ -qcodes.data.hdf5_format ------------------------ - -.. automodule:: qcodes.data.hdf5_format - :members: diff --git a/docs/api/data/hdf5_format_hickle.rst b/docs/api/data/hdf5_format_hickle.rst deleted file mode 100644 index f2a0ce2bae1..00000000000 --- a/docs/api/data/hdf5_format_hickle.rst +++ /dev/null @@ -1,5 +0,0 @@ -qcodes.data.hdf5_format_hickle ------------------------------- - -.. automodule:: qcodes.data.hdf5_format_hickle - :members: diff --git a/docs/api/data/index.rst b/docs/api/data/index.rst deleted file mode 100644 index e5e2436f523..00000000000 --- a/docs/api/data/index.rst +++ /dev/null @@ -1,33 +0,0 @@ -.. _legacydata_api : - -qcodes.data -=========== - -.. autosummary:: - - qcodes.data - qcodes.data.data_array - qcodes.data.data_set - qcodes.data.format - qcodes.data.gnuplot_format - qcodes.data.hdf5_format - qcodes.data.hdf5_format_hickle - qcodes.data.io - qcodes.data.location - - -.. automodule:: qcodes.data - - -.. toctree:: - :maxdepth: 4 - :hidden: - - data_array - data_set - format - gnuplot_format - hdf5_format - hdf5_format_hickle - io - location diff --git a/docs/api/data/io.rst b/docs/api/data/io.rst deleted file mode 100644 index 1822267fa29..00000000000 --- a/docs/api/data/io.rst +++ /dev/null @@ -1,5 +0,0 @@ -qcodes.data.io --------------- - -.. automodule:: qcodes.data.io - :members: diff --git a/docs/api/data/location.rst b/docs/api/data/location.rst deleted file mode 100644 index 8de6c283ae2..00000000000 --- a/docs/api/data/location.rst +++ /dev/null @@ -1,5 +0,0 @@ -qcodes.data.location --------------------- - -.. automodule:: qcodes.data.location - :members: diff --git a/docs/api/extensions/slack.rst b/docs/api/extensions/slack.rst deleted file mode 100644 index 293478c64f6..00000000000 --- a/docs/api/extensions/slack.rst +++ /dev/null @@ -1,6 +0,0 @@ -qcodes.extensions.slack ------------------------ - -.. automodule:: qcodes.extensions.slack - :no-inherited-members: - :autosummary: diff --git a/docs/api/index.rst b/docs/api/index.rst index e9c68d175a6..e5f60b68a14 100644 --- a/docs/api/index.rst +++ b/docs/api/index.rst @@ -4,9 +4,6 @@ QCoDes API documentation ======================== This page documents the QCoDes API. This page contains documentation of all user facing modules. -A few of our modules are considered legacy and documented separately below. -This is split in roughly one page per QCoDeS submodule. A few of the submodules have been split for -additional clarity. QCoDeS API @@ -31,18 +28,3 @@ QCoDeS API station utils/index validators/index - -Legacy API ----------- - -.. toctree:: - :maxdepth: 2 - :includehidden: - - loops - actions - measure - data/index - extensions/slack - plots/index - utils/magic diff --git a/docs/api/loops.rst b/docs/api/loops.rst deleted file mode 100644 index 7522d1ad0aa..00000000000 --- a/docs/api/loops.rst +++ /dev/null @@ -1,7 +0,0 @@ -.. _loops_api : - -qcodes.loops -============ - -.. automodule:: qcodes.loops - :members: diff --git a/docs/api/measure.rst b/docs/api/measure.rst deleted file mode 100644 index 4f06b8f961f..00000000000 --- a/docs/api/measure.rst +++ /dev/null @@ -1,5 +0,0 @@ -qcodes.measure -============== - -.. automodule:: qcodes.measure - :members: diff --git a/docs/api/plots/base.rst b/docs/api/plots/base.rst deleted file mode 100644 index f62b35be0a8..00000000000 --- a/docs/api/plots/base.rst +++ /dev/null @@ -1,5 +0,0 @@ -qcodes.plots.base ------------------ - -.. automodule:: qcodes.plots.base - :members: diff --git a/docs/api/plots/colors.rst b/docs/api/plots/colors.rst deleted file mode 100644 index b6959d9f5ba..00000000000 --- a/docs/api/plots/colors.rst +++ /dev/null @@ -1,5 +0,0 @@ -qcodes.plots.colors -------------------- - -.. automodule:: qcodes.plots.colors - :members: diff --git a/docs/api/plots/index.rst b/docs/api/plots/index.rst deleted file mode 100644 index 0942b318c4e..00000000000 --- a/docs/api/plots/index.rst +++ /dev/null @@ -1,25 +0,0 @@ -.. _plots_api : - -qcodes.plots -============ - - -.. autosummary:: - - qcodes.plots - qcodes.plots.base - qcodes.plots.colors - qcodes.plots.pyqtgraph - qcodes.plots.qcmatplotlib - -.. automodule:: qcodes.plots - - -.. toctree:: - :maxdepth: 4 - :hidden: - - base - colors - pyqtgraph - qcmatplotlib diff --git a/docs/api/plots/pyqtgraph.rst b/docs/api/plots/pyqtgraph.rst deleted file mode 100644 index 6c44b0b5608..00000000000 --- a/docs/api/plots/pyqtgraph.rst +++ /dev/null @@ -1,5 +0,0 @@ -qcodes.plots.pyqtgraph ----------------------- - -.. automodule:: qcodes.plots.pyqtgraph - :members: diff --git a/docs/api/plots/qcmatplotlib.rst b/docs/api/plots/qcmatplotlib.rst deleted file mode 100644 index 4f8798b6c85..00000000000 --- a/docs/api/plots/qcmatplotlib.rst +++ /dev/null @@ -1,5 +0,0 @@ -qcodes.plots.qcmatplotlib -------------------------- - -.. automodule:: qcodes.plots.qcmatplotlib - :members: diff --git a/docs/api/utils/magic.rst b/docs/api/utils/magic.rst deleted file mode 100644 index 714ac192896..00000000000 --- a/docs/api/utils/magic.rst +++ /dev/null @@ -1,6 +0,0 @@ -qcodes.utils.magic ------------------- - -.. automodule:: qcodes.utils.magic - :no-inherited-members: - :autosummary: diff --git a/docs/changes/newsfragments/4971.breaking b/docs/changes/newsfragments/4971.breaking new file mode 100644 index 00000000000..fc91ec40800 --- /dev/null +++ b/docs/changes/newsfragments/4971.breaking @@ -0,0 +1,12 @@ +The modules ``qcodes_loop.data``, ``qcodes_loop.plots``, ``qcodes_loop.actions``, ``qcodes_loop.loops``, +``qcodes_loop.measure``, ``qcodes_loop.extensions.slack`` and ``qcodes_loop.utils.magic``, +``qcodes_loop.utils.qt_helpers`` have been split out into a separate package ``qcodes_loop``. +The respective modules in QCoDeS do still exist as deprecated aliases to the new modules in ``qcodes_loop`` +but will be removed in a future release. To use the aliases QCoDeS must be installed with the ``loop`` extra e.g. +you should install ``pip install qcodes[loop]``. If you make use of these modules we recommend updating imports +to use ``qcodes_loop`` as soon as possible. See the `readme `_ of +``qcodes_loop`` for more information. + +The functions ``qcodes.utils.helpers.tprint`` ``qcodes.utils.helpers.wait_secs`` and +``qcodes.utils.helpers.foreground_qt_window`` have been removed. +These helper functions are only used in ``qcodes_loop`` and were moved there. diff --git a/docs/community/objects.rst b/docs/community/objects.rst index 5982d693b31..ba9f7539db3 100644 --- a/docs/community/objects.rst +++ b/docs/community/objects.rst @@ -3,36 +3,6 @@ Object Hierarchy .. todo:: make sure it is updated and easy to read. -Rough linkages: ---------------- - -In **bold** the containing class creates this object. In *italics* the -container just holds this object (or class) as a default for derivatives -to use. Normal text shows the container includes and uses of this object. - -- Station -- Instrument: IPInstrument, VisaInstrument, MockInstrument - - - **Parameter** - - Validator: Anything, Strings, Numbers, Ints, Enum, MultiType - - **SweepValues**: SweepFixedValues, AdaptiveSweep - - Function - - Validator - -- **Monitor** -- *actions* -- DataManager -- **DataServer** -- :ref:`loops_api` -- actions: Parameter, Task, Wait, (Active)Loop -- **ActiveLoop** - - - **DataSet** - - **DataArray** - - **Formatter**: GNUPlotFormat - - **DiskIO** (may become subclass of IOManager?) - - **FormatLocation** (a location\_provider) - Station ------- diff --git a/docs/examples/index.rst b/docs/examples/index.rst index 917aeebb4f5..7d37cf4572a 100644 --- a/docs/examples/index.rst +++ b/docs/examples/index.rst @@ -68,14 +68,3 @@ Logging :glob: logging/* - - -Legacy examples ---------------- - -.. include:: legacy/readme.txt - -.. toctree:: - :glob: - - legacy/* diff --git a/docs/examples/legacy/Combined Parameters.ipynb b/docs/examples/legacy/Combined Parameters.ipynb deleted file mode 100644 index edf0f3a34eb..00000000000 --- a/docs/examples/legacy/Combined Parameters.ipynb +++ /dev/null @@ -1,452 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Combined Parameters" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": { - "scrolled": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Logging hadn't been started.\n", - "Activating auto-logging. Current session state plus future input saved.\n", - "Filename : C:\\Users\\a-halakh\\.qcodes\\logs\\command_history.log\n", - "Mode : append\n", - "Output logging : True\n", - "Raw input log : False\n", - "Timestamping : True\n", - "State : active\n", - "Qcodes Logfile : C:\\Users\\a-halakh\\.qcodes\\logs\\200324-28996-qcodes.log\n", - "False\n" - ] - } - ], - "source": [ - "import numpy as np\n", - "\n", - "import qcodes as qc\n", - "from qcodes.loops import Loop\n", - "from qcodes.parameters import ManualParameter\n", - "from qcodes.validators import Numbers" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "If you want to sweep multiple parameters at once qcodes offers the combine function.\n", - "You can combine any number of any kind paramter. \n", - "We'll use a ManualParameter for this example." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "p1 = ManualParameter('p1', vals=Numbers(-10, 10))\n", - "p2 = ManualParameter('p2', vals=Numbers(-10, 10))\n", - "p3 = ManualParameter('p3', vals=Numbers(-10, 10))\n", - "p4 = ManualParameter('p4', vals=Numbers(-10, 10))\n", - "# set to -1 so we get some data out\n", - "p4.set(-1)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Simple combined parameters " - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "combined = qc.combine(p1, p2, p3, name='combined')\n", - "\n", - "sweep_vals = np.array([[1, 1,1], [1, 1,1]])" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "# 2d loop with a inner loop over a combined parameter\n", - "loop = Loop(p1.sweep(0,10,1)).loop(combined.sweep(sweep_vals), delay=0.001).each(p4)" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [], - "source": [ - "data = loop.get_data_set(name='testsweep')" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Started at 2020-03-24 18:35:07\n", - "DataSet:\n", - " location = 'data/2020-03-24/#004_testsweep_18-35-07'\n", - " | | | \n", - " Setpoint | p1_set | p1 | (11,)\n", - " Setpoint | combined_set | combined | (11, 2)\n", - " Measured | p4 | p4 | (11, 2)\n", - " Measured | p1 | p1 | (11, 2)\n", - " Measured | p2 | p2 | (11, 2)\n", - " Measured | p3 | p3 | (11, 2)\n", - "Finished at 2020-03-24 18:35:07\n" - ] - } - ], - "source": [ - "data = loop.run()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The combined_set just stores the indices " - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "DataArray[11,2]: combined_set\n", - "array([[0., 1.],\n", - " [0., 1.],\n", - " [0., 1.],\n", - " [0., 1.],\n", - " [0., 1.],\n", - " [0., 1.],\n", - " [0., 1.],\n", - " [0., 1.],\n", - " [0., 1.],\n", - " [0., 1.],\n", - " [0., 1.]])\n" - ] - } - ], - "source": [ - "print(data.combined_set)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "But the acutal set values are saved, but labeled as \"measured\"" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "DataArray[11,2]: p3\n", - "array([[1., 1.],\n", - " [1., 1.],\n", - " [1., 1.],\n", - " [1., 1.],\n", - " [1., 1.],\n", - " [1., 1.],\n", - " [1., 1.],\n", - " [1., 1.],\n", - " [1., 1.],\n", - " [1., 1.],\n", - " [1., 1.]])" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "data.p3" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Combine and aggregate parameters\n", - "\n", - "If an aggregator function is given, the aggregated values are saved instead of the indices." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [], - "source": [ - "# define an aggregator function that takes as arguments the parameters you whish to aggegate\n", - "def linear(x,y,z):\n", - " return x+y+z" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [], - "source": [ - "combined = qc.combine(p1, p2, p3, name='combined', label=\"Sum\", unit=\"a.u\", aggregator=linear)\n", - "\n", - "x_vals = np.linspace(1, 2, 2)\n", - "y_vals = np.linspace(1, 2, 2)\n", - "z_vals = np.linspace(1, 2, 2)" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [], - "source": [ - "# 2d loop with a inner loop over a combined parameter\n", - "loop = Loop(p1.sweep(0,10,1)).loop(combined.sweep(x_vals, y_vals, z_vals), delay=0.001).each(p4)" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [], - "source": [ - "data = loop.get_data_set(name='testsweep')" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Started at 2020-03-24 18:35:07\n", - "DataSet:\n", - " location = 'data/2020-03-24/#005_testsweep_18-35-07'\n", - " | | | \n", - " Setpoint | p1_set | p1 | (11,)\n", - " Setpoint | combined_set | combined | (11, 2)\n", - " Measured | p4 | p4 | (11, 2)\n", - " Measured | p1 | p1 | (11, 2)\n", - " Measured | p2 | p2 | (11, 2)\n", - " Measured | p3 | p3 | (11, 2)\n", - "Finished at 2020-03-24 18:35:07\n" - ] - } - ], - "source": [ - "data = loop.run()" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "collapsed": true - }, - "source": [ - "the combined_set now stores the aggregated values" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "DataArray[11,2]: combined_set\n", - "array([[3., 6.],\n", - " [3., 6.],\n", - " [3., 6.],\n", - " [3., 6.],\n", - " [3., 6.],\n", - " [3., 6.],\n", - " [3., 6.],\n", - " [3., 6.],\n", - " [3., 6.],\n", - " [3., 6.],\n", - " [3., 6.]])\n" - ] - } - ], - "source": [ - "print(data.combined_set)" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "OrderedDict([('__class__', 'qcodes.instrument.parameter.CombinedParameter'),\n", - " ('unit', 'a.u'),\n", - " ('label', 'Sum'),\n", - " ('full_name', 'combined'),\n", - " ('aggregator', ''),\n", - " ('p1',\n", - " {'value': 2.0,\n", - " 'raw_value': 2.0,\n", - " 'ts': '2020-03-24 18:35:07',\n", - " '__class__': 'qcodes.instrument.parameter.ManualParameter',\n", - " 'full_name': 'p1',\n", - " 'post_delay': 0,\n", - " 'label': 'p1',\n", - " 'inter_delay': 0,\n", - " 'vals': '',\n", - " 'unit': '',\n", - " 'name': 'p1'}),\n", - " ('p2',\n", - " {'value': 2.0,\n", - " 'raw_value': 2.0,\n", - " 'ts': '2020-03-24 18:35:07',\n", - " '__class__': 'qcodes.instrument.parameter.ManualParameter',\n", - " 'full_name': 'p2',\n", - " 'post_delay': 0,\n", - " 'label': 'p2',\n", - " 'inter_delay': 0,\n", - " 'vals': '',\n", - " 'unit': '',\n", - " 'name': 'p2'}),\n", - " ('p3',\n", - " {'value': 2.0,\n", - " 'raw_value': 2.0,\n", - " 'ts': '2020-03-24 18:35:07',\n", - " '__class__': 'qcodes.instrument.parameter.ManualParameter',\n", - " 'full_name': 'p3',\n", - " 'post_delay': 0,\n", - " 'label': 'p3',\n", - " 'inter_delay': 0,\n", - " 'vals': '',\n", - " 'unit': '',\n", - " 'name': 'p3'})])" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# snapshot of the combined parameter\n", - "combined.snapshot()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "anaconda-cloud": {}, - "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.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": 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": 1 -} diff --git a/docs/examples/legacy/Comprehensive Plotting How-To.ipynb b/docs/examples/legacy/Comprehensive Plotting How-To.ipynb deleted file mode 100644 index 7d24808c5ff..00000000000 --- a/docs/examples/legacy/Comprehensive Plotting How-To.ipynb +++ /dev/null @@ -1,756 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Comprehensive Plotting How-To" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Logging hadn't been started.\n", - "Activating auto-logging. Current session state plus future input saved.\n", - "Filename : C:\\Users\\a-halakh\\.qcodes\\logs\\command_history.log\n", - "Mode : append\n", - "Output logging : True\n", - "Raw input log : False\n", - "Timestamping : True\n", - "State : active\n", - "Qcodes Logfile : C:\\Users\\a-halakh\\.qcodes\\logs\\200324-17004-qcodes.log\n", - "False\n" - ] - } - ], - "source": [ - "import qcodes as qc\n", - "from qcodes.loops import Loop\n", - "from qcodes.plots.qcmatplotlib import MatPlot" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Plotting data in QCoDeS can be done using either MatPlot or QTPlot, with matplotlib and pyqtgraph as backends, respectively. \n", - "MatPlot and QTPlot tailor these plotting backends to QCoDeS, providing many features.\n", - "For example, when plotting a DataArray in a DataSet, the corresponding ticks, labels, etc. are automatically added to the plot.\n", - "Both MatPlot and QTPlot support live plotting while a measurement is running.\n", - "\n", - "One of the main differences between the two backends is that matplotlib is more strongly integrated with Jupyter Notebook, while pyqtgraph uses the PyQT GUI.\n", - "For matplotlib, this has the advantage that plots can be displayed within a notebook (though it also has a gui).\n", - "The advantage of pyqtgraph is that it can be easily embedded in PyQT GUI's.\n", - "\n", - "This guide aims to provide a detailed guide on how to use each of the two plotting tools." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "loc_provider = qc.data.location.FormatLocation(fmt='data/{date}/#{counter}_{name}_{time}')\n", - "qc.data.data_set.DataSet.location_provider = loc_provider" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## MatPlot" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The QCoDeS MatPlot relies on the matplotlib package, which is quite similar to Matlab's plotting tools.\n", - "It integrates nicely with Jupyter notebook, and as a result, interactive plots can be displayed within a notebook using the following command:" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "%matplotlib inline" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Simple 1D sweep" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "As a first example, we perform a simple 1D sweep.\n", - "We create two trivial parameters, one for measuring a value, and the other for sweeping the value of the measured parameter." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "p_measure = qc.ManualParameter(name='measured_val')\n", - "p_sweep = qc.Parameter(name='sweep_val', set_cmd=p_measure.set)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Next we perform a measurement, and attach the `update` method of the `plot` object to the loop, resulting in live plotting.\n", - "Note that the resulting plot automatically has the correct x values and labels." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Started at 2020-03-24 18:39:35\n", - "DataSet:\n", - " location = 'data/2020-03-24/#006_test_plotting_1D_18-39-35'\n", - " | | | \n", - " Setpoint | sweep_val_set | sweep_val | (21,)\n", - " Measured | measured_val | measured_val | (21,)\n", - "Finished at 2020-03-24 18:39:37\n" - ] - }, - { - "data": { - "text/plain": [ - "DataSet:\n", - " location = 'data/2020-03-24/#006_test_plotting_1D_18-39-35'\n", - " | | | \n", - " Setpoint | sweep_val_set | sweep_val | (21,)\n", - " Measured | measured_val | measured_val | (21,)" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "loop = Loop(\n", - " p_sweep.sweep(0, 20, step=1), delay=0.05).each(\n", - " p_measure)\n", - "data = loop.get_data_set(name='test_plotting_1D')\n", - "\n", - "# Create plot for measured data\n", - "plot = MatPlot(data.measured_val)\n", - "# Attach updating of plot to loop\n", - "loop.with_bg_task(plot.update)\n", - "\n", - "loop.run()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Subplots" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In a measurement, there is often more than a single parameter that is measured.\n", - "MatPlot supports multiple subplots, and upon initialization it will create a subplot for each of the arguments it receives.\n", - "\n", - "Let us create a second parameter that, when measured, always returns the value 10." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [], - "source": [ - "p_measure2 = qc.ManualParameter(name='measured_val_2', initial_value=10)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In the example below, three arguments are provided, resulting in three subplots.\n", - "By default, subplots will be placed as columns on a single row, up to three columns.\n", - "After this, a new row will be created (can be overridden in `MatPlot.max_subplot_columns`).\n", - "\n", - "Multiple DataArrays can also be plotted in a single subplot by passing them as a list in a single arg.\n", - "As an example, notice how the first subplot shows multiple values." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Started at 2020-03-24 18:39:38\n", - "DataSet:\n", - " location = 'data/2020-03-24/#007_test_plotting_1D_2_18-39-38'\n", - " | | | \n", - " Setpoint | sweep_val_set | sweep_val | (21,)\n", - " Measured | measured_val | measured_val | (21,)\n", - " Measured | measured_val_2 | measured_val_2 | (21,)\n", - "Finished at 2020-03-24 18:39:41\n" - ] - }, - { - "data": { - "text/plain": [ - "DataSet:\n", - " location = 'data/2020-03-24/#007_test_plotting_1D_2_18-39-38'\n", - " | | | \n", - " Setpoint | sweep_val_set | sweep_val | (21,)\n", - " Measured | measured_val | measured_val | (21,)\n", - " Measured | measured_val_2 | measured_val_2 | (21,)" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "loop = Loop(\n", - " p_sweep.sweep(0, 20, step=1), delay=0.05).each(\n", - " p_measure,\n", - " p_measure2)\n", - "data = loop.get_data_set(name='test_plotting_1D_2')\n", - "\n", - "# Create plot for measured data\n", - "plot = MatPlot([data.measured_val, data.measured_val_2], data.measured_val, data.measured_val_2)\n", - "# Attach updating of plot to loop\n", - "loop.with_bg_task(plot.update)\n", - "\n", - "loop.run()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The data arrays don't all have to be passed along during initialization of the MatPlot instance.\n", - "We can access the subplots of the plot object as if the plot was a list (e.g. `plot[0]` would give you the first subplot).\n", - "To illustrate this, the example below results in the same plot as above." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Started at 2020-03-24 18:39:42\n", - "DataSet:\n", - " location = 'data/2020-03-24/#008_test_plotting_1D_3_18-39-41'\n", - " | | | \n", - " Setpoint | sweep_val_set | sweep_val | (21,)\n", - " Measured | measured_val | measured_val | (21,)\n", - " Measured | measured_val_2 | measured_val_2 | (21,)\n", - "Finished at 2020-03-24 18:39:45\n" - ] - }, - { - "data": { - "text/plain": [ - "DataSet:\n", - " location = 'data/2020-03-24/#008_test_plotting_1D_3_18-39-41'\n", - " | | | \n", - " Setpoint | sweep_val_set | sweep_val | (21,)\n", - " Measured | measured_val | measured_val | (21,)\n", - " Measured | measured_val_2 | measured_val_2 | (21,)" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "loop = Loop(\n", - " p_sweep.sweep(0, 20, step=1), delay=0.05).each(\n", - " p_measure,\n", - " p_measure2)\n", - "data = loop.get_data_set(name='test_plotting_1D_3')\n", - "\n", - "# Create plot for measured data\n", - "plot = MatPlot(subplots=3)\n", - "plot[0].add(data.measured_val)\n", - "plot[0].add(data.measured_val_2)\n", - "plot[1].add(data.measured_val)\n", - "plot[2].add(data.measured_val_2)\n", - "\n", - "# Attach updating of plot to loop\n", - "loop.with_bg_task(plot.update)\n", - "\n", - "loop.run()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Note that we passed the kwarg `subplots=3` to specify that we need 3 subplots.\n", - "The `subplots` kwarg can be either an int or a tuple.\n", - "If it is an int, it will segment the value such that there are at most three columns.\n", - "If a tuple is provided, its first element indicates the number of rows, and the second the number of columns.\n", - "\n", - "Furthermore, the size of the figure is automatically computed based on the number of subplots.\n", - "This can be overridden by passing the kwarg `figsize=(x_length, y_length)` upon initialization.\n", - "Additionally, `MatPlot.default_figsize` can be overridden to change the default computed figsize for a given subplot dimensionality." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 2D Plots" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "As illustrated below, MatPlot can also plot two-dimensional data arrays.\n", - "MatPlot automatically handles setting the appropriate x- and y-axes, and also adds a colorbar by default.\n", - "Note that we can also plot the individual traces of a 2D array, as shown in the first subplot below.\n", - "This is done by passing all the elements (=rows) of the 2D array as a single argument using the splat (*) operator." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [], - "source": [ - "p_sweep2 = qc.Parameter(name='sweep_val_2', set_cmd=p_measure2.set)" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Started at 2020-03-24 18:39:47\n", - "DataSet:\n", - " location = 'data/2020-03-24/#009_test_plotting_2D_18-39-46'\n", - " | | | \n", - " Setpoint | sweep_val_set | sweep_val | (21,)\n", - " Setpoint | sweep_val_2_set | sweep_val_2 | (21, 11)\n", - " Measured | measured_val | measured_val | (21, 11)\n", - "Finished at 2020-03-24 18:39:55\n" - ] - }, - { - "data": { - "text/plain": [ - "DataSet:\n", - " location = 'data/2020-03-24/#009_test_plotting_2D_18-39-46'\n", - " | | | \n", - " Setpoint | sweep_val_set | sweep_val | (21,)\n", - " Setpoint | sweep_val_2_set | sweep_val_2 | (21, 11)\n", - " Measured | measured_val | measured_val | (21, 11)" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "loop = Loop(\n", - " p_sweep.sweep(0, 20, step=1), delay=0.05).loop(\n", - " p_sweep2.sweep(0, 10, step=1), delay=0.01).each(\n", - " p_measure)\n", - "data = loop.get_data_set(name='test_plotting_2D')\n", - "\n", - "# Create plot for measured data\n", - "plot = MatPlot([*data.measured_val], data.measured_val)\n", - "# Attach updating of plot to loop\n", - "loop.with_bg_task(plot.update)\n", - "\n", - "loop.run()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In the example above, the colorbar can be accessed via `plot[1].qcodes_colorbar`.\n", - "This can be useful when you want to modify the colorbar (e.g. change the color limits `clim`).\n", - "\n", - "Note that the above plot was updated every time an inner loop was completed. \n", - "This is because the update method was attached to the outer loop.\n", - "If you instead want it to update within an outer loop, you have to attach it to an inner loop: `loop[0].with_bg_task(plot.update)` (`loop[0]` is the first action of the outer loop, which is the inner loop)." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Interfacing with Matplotlib\n", - "As Matplot is built directly on top of Matplotlib, you can use standard Matplotlib functions which are readily available online in Matplotlib documentation as well as StackOverflow and similar sites. Here, we first perform the same measurement and obtain the corresponding figure:" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Started at 2020-03-24 18:39:57\n", - "DataSet:\n", - " location = 'data/2020-03-24/#010_test_plotting_2D_2_18-39-56'\n", - " | | | \n", - " Setpoint | sweep_val_set | sweep_val | (21,)\n", - " Setpoint | sweep_val_2_set | sweep_val_2 | (21, 11)\n", - " Measured | measured_val | measured_val | (21, 11)\n", - "Finished at 2020-03-24 18:40:05\n" - ] - }, - { - "data": { - "text/plain": [ - "DataSet:\n", - " location = 'data/2020-03-24/#010_test_plotting_2D_2_18-39-56'\n", - " | | | \n", - " Setpoint | sweep_val_set | sweep_val | (21,)\n", - " Setpoint | sweep_val_2_set | sweep_val_2 | (21, 11)\n", - " Measured | measured_val | measured_val | (21, 11)" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "loop = Loop(\n", - " p_sweep.sweep(0, 20, step=1), delay=0.05).loop(\n", - " p_sweep2.sweep(0, 10, step=1), delay=0.01).each(\n", - " p_measure)\n", - "data = loop.get_data_set(name='test_plotting_2D_2')\n", - "\n", - "# Create plot for measured data\n", - "plot = MatPlot([*data.measured_val], data.measured_val)\n", - "# Attach updating of plot to loop\n", - "loop.with_bg_task(plot.update)\n", - "\n", - "loop.run()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "To use the matplotlib api, we need access to the matplotlib Figure and Axis objects. \n", - "Each subplot has its correspond Axis object, which are grouped together into a single Figure object.\n", - "A subplot Axis can be accessed via its index. As an example, we will modify the title of the first axis:" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [], - "source": [ - "ax = plot[0] # shorthand for plot.subplots[0]\n", - "ax.set_title(\"My left subplot title\");" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Note that this returns the actual matplotlib Axis object.\n", - "It does have the additional QCoDeS method `Axis.add()`, which allows easily adding of a QCoDeS DataArray. See http://matplotlib.org/api/axes_api.html for documentation of the Matplotlib Axes class.\n", - "\n", - "The Matplotlib Figure object can be accessed via the fig attribute on the QCoDeS Matplot object:" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [], - "source": [ - "fig = plot.fig\n", - "fig.tight_layout();" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "See http://matplotlib.org/api/figure_api.html for documentation of the Matplotlib Figure class.\n", - "\n", - "Matplotlib also offers a second way to modify plots, namely pyplot.\n", - "This can be imported via:" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [], - "source": [ - "from matplotlib import pyplot as plt" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In pyplot, there is always an active axis and figure, similar to Matlab plotting.\n", - "Every time a new plot is created, it will update the active axis and figure.\n", - "The active Figure and Axis can be changed via `plt.scf(fig)` and `plt.sca(ax)`, respectively.\n", - "\n", - "As an example, the following code will change the title of the last-created plot (the right subplot of the previous figure):" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "plt.title('My right subplot title');" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "See https://matplotlib.org/users/pyplot_tutorial.html for documentation on Pyplot" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Event handling" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Since matplotlib is an interactive plotting tool, one can program actions that are dependent on events.\n", - "There are many events, such as clicking on a plot, pressing a key, etc.\n", - "\n", - "As an example, we can attach a trivial function to occur when the plot object is closed. You can replace this with other functionality, such as stopping the loop." - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "def handle_close(event):\n", - " print('Plot closed')\n", - " \n", - "plot = MatPlot()\n", - "plot.fig.canvas.mpl_connect('close_event', handle_close);" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "On a related note, matplotlib also has widgets that can be added to plots, allowing additional interactivity with the dataset.\n", - "An example would be adding a slider to show 2D plots of a 3D dataset (e.g. https://matplotlib.org/examples/widgets/slider_demo.html)." - ] - } - ], - "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.7.5" - }, - "nbsphinx": { - "timeout": 600 - }, - "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": "block", - "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/docs/examples/legacy/Datasaving examples.ipynb b/docs/examples/legacy/Datasaving examples.ipynb deleted file mode 100644 index 1bdb8bd67a2..00000000000 --- a/docs/examples/legacy/Datasaving examples.ipynb +++ /dev/null @@ -1,283 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Datasaving Examples" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Logging hadn't been started.\n", - "Activating auto-logging. Current session state plus future input saved.\n", - "Filename : C:\\Users\\Jens-Work\\.qcodes\\logs\\command_history.log\n", - "Mode : append\n", - "Output logging : True\n", - "Raw input log : False\n", - "Timestamping : True\n", - "State : active\n", - "Qcodes Logfile : C:\\Users\\Jens-Work\\.qcodes\\logs\\201004-29284-qcodes.log\n" - ] - } - ], - "source": [ - "%matplotlib nbagg\n", - "from importlib import reload\n", - "\n", - "import numpy as np\n", - "\n", - "import qcodes as qc\n", - "from qcodes.loops import Loop" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "import logging\n", - "import sys\n", - "\n", - "# Create logger\n", - "logger = logging.getLogger()\n", - "logger.setLevel(logging.DEBUG)\n", - "\n", - "# Create STDERR handler\n", - "handler = logging.StreamHandler(sys.stderr)\n", - "# ch.setLevel(logging.DEBUG)\n", - "\n", - "# Create formatter and add it to the handler\n", - "formatter = logging.Formatter('%(name)s - %(levelname)s - %(message)s')\n", - "handler.setFormatter(formatter)\n", - "\n", - "# Set STDERR handler as the only handler \n", - "logger.handlers = [handler]" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "from qcodes.data import hdf5_format\n", - "\n", - "reload(hdf5_format)\n", - "h5fmt = hdf5_format.HDF5Format()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Start a loop and generate data from dummy instruments " - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "station = qc.station.Station()" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "qcodes.instrument.base - DEBUG - [MockParabola(MockParabola)] Error getting or interpreting *IDN?: ''\n" - ] - }, - { - "data": { - "text/plain": [ - "'MockParabola'" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from qcodes.tests.instrument_mocks import MockParabola\n", - "\n", - "station.add_component(MockParabola(name='MockParabola'))" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "qcodes.data.data_set - DEBUG - Attempting to write\n", - "qcodes.data.gnuplot_format - DEBUG - Attempting to write the following group: MockParabola_x_set\n", - "qcodes.data.gnuplot_format - DEBUG - Cannot match save range, skipping this group.\n", - "qcodes.data.data_set - DEBUG - Finalising the DataSet. Writing.\n", - "qcodes.data.gnuplot_format - DEBUG - Attempting to write the following group: MockParabola_x_set\n", - "qcodes.data.gnuplot_format - DEBUG - Wrote header to file\n", - "qcodes.data.gnuplot_format - DEBUG - Wrote to file from 0 to 10\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Started at 2020-10-04 11:50:36\n", - "DataSet:\n", - " location = 'data/2020-10-04/#001_MockParabola_run_11-50-36'\n", - " | | | \n", - " Setpoint | MockParabola_x_set | x | (10,)\n", - " Measured | MockParabola_skewed_parabola | skewed_parabola | (10,)\n", - "Finished at 2020-10-04 11:50:36\n" - ] - } - ], - "source": [ - "loop = Loop(station.MockParabola.x[-100:100:20]).each(station.MockParabola.skewed_parabola)\n", - "data_l = loop.run(name='MockParabola_run', formatter=qc.data.gnuplot_format.GNUPlotFormat())" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "qcodes.data.data_set - DEBUG - Attempting to write\n", - "qcodes.data.data_set - DEBUG - Finalising the DataSet. Writing.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Started at 2020-10-04 11:50:36\n", - "DataSet:\n", - " location = 'data/2020-10-04/#002_MockParabola_run_11-50-36'\n", - " | | | \n", - " Setpoint | MockParabola_x_set | x | (10,)\n", - " Setpoint | MockParabola_y_set | y | (10, 15)\n", - " Measured | MockParabola_skewed_parabola | skewed_parabola | (10, 15)\n", - "Finished at 2020-10-04 11:50:36\n" - ] - } - ], - "source": [ - "reload(hdf5_format)\n", - "h5fmt = hdf5_format.HDF5Format()\n", - "loop = Loop(station.MockParabola.x[-100:100:20]).loop(\n", - " station.MockParabola.y[-100:50:10]).each(station.MockParabola.skewed_parabola)\n", - "data_l = loop.run(name='MockParabola_run', formatter=h5fmt)" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [], - "source": [ - "from importlib import reload\n", - "\n", - "from qcodes.data import hdf5_format\n", - "\n", - "reload(hdf5_format)\n", - "h5fmt = hdf5_format.HDF5Format()\n", - "data2 = qc.data.data_set.DataSet(location=data_l.location, formatter=h5fmt)\n", - "data2.read()" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "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.8.13" - }, - "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 - }, - "widgets": { - "application/vnd.jupyter.widget-state+json": { - "state": {}, - "version_major": 2, - "version_minor": 0 - } - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/docs/examples/legacy/Legacy.ipynb b/docs/examples/legacy/Legacy.ipynb deleted file mode 100644 index 1482cd468c7..00000000000 --- a/docs/examples/legacy/Legacy.ipynb +++ /dev/null @@ -1,133 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Legacy features\n", - "This section describes legacy features from early implementations of QCoDeS. As new features often use similar (or identical) names and achieve similar goals, these older implementations have been laid to rest in these example notebooks. The intent of preserving this data is twofold: first to preserve documentation for users still relient on these systems, and second to better understand these older implementations should their code be revisted. \n", - "\n", - "In addition to the information here, we aim to provide links to sections of documentation where updated information can be found, this is intended to assist early users in transitioning to newer features.\n", - "\n", - "## Overview\n", - "A QCoDeS experiment typically consists of a Loop that sweeps over one or more Parameters of one or more Instruments, measures other Parameters at each sweep point, and stores all of the results into a DataSet.\n", - "\n", - "While the simple case is quite straightforward, it is possible to create a very general experiment by defining richer Parameters and by performing additional Loop actions at each sweep point. The overview on this page provides a high-level picture of the general capabilities; consult the detailed API references and the samples to see some of the complex procedures that can be described and run.\n", - "\n", - "\n", - "## Loop\n", - "A Loop is the QCoDeS way to acquire one or more arrays of data. Every Loop that’s executed consists of a settable Parameter to be varied, some collection of values to set it to, some actions to do at each setpoint, and some conditions by which to run the Loop.\n", - "\n", - "An action can be:\n", - "- A gettable Parameter (something to measure). Each such Parameter will generate one (or more, if the Parameter itself creates multiple outputs).\n", - "- A Task to do (for example you measure once, then have a Task to change a gate voltage, then you measure again, and finally a Task to put the gate voltage back where it was).\n", - "- Wait, a specialized task that just delays execution (but may do other things like monitoring the system in that time)\n", - "- BreakIf, a callable test for whether to quit (this level of) the Loop.\n", - "\n", - "Another Loop nested inside the first, with all its own setpoints and actions. Measurements within a nested loop will produce a higher-dimension output array.\n", - "\n", - "The key loop running conditions are:\n", - "- background or not: A background Loop runs in its own separate process, so that you can be doing things in the main process like live plotting, analysis on the data as it arrives, preparing for the next measurement, or even unrelated tasks, while the Loop is running. The disadvantage is complexity, in that you can only use RemoteInstruments, and debugging gets much harder.\n", - "- use threads: If true, we will group measurement actions and try to execute them concurrently across several threads. This can dramatically speed up slow measurements involving several instruments, or all instruments are local.\n", - "- data manager: If not False, we create another extra process whose job it is to offload data storage, and sync data back to the main process on demand, so that the Loop process can run with as little overhead as possible.\n", - "\n", - "\n", - "### Responsibilities:\n", - "\n", - "- creating the dataset that will be needed to store its data\n", - "- where and how to save the data to disk\n", - "- generating all the metadata for the DataSet. \n", - "\n", - "> Metadata is intended to describe the system and software configuration to give it context, help reproduce and troubleshoot the experiment, and to aid searching and datamining later. The Loop generates its own metadata, regarding when and how it was run and the Parameters and other actions involved, as well as asking all the Instruments, via a qcodes.station if possible, for their own metadata and including it.\n", - "\n", - "- sequencing actions: the Loop should have the highest priority and the least overhead of extra responsibilities so that setpoints and actions occur with as fast and reliable timing as possible.\n", - "\n", - "Before the Loop is run, it holds the setpoint and action definitions you are building up. You can actually keep a loop at any level of definition and reuse it later. Loop methods chain by creating entirely new objects, so that you can hold onto the Loop at any stage of definition and reuse just what has been defined up to that point.\n", - "\n", - "After the Loop is run, it returns a dataset and the executed loop itself, along with the process it starts if it’s a background Loop, only hold state (such as the current indices within the potentially nested Loops) while it is running.\n", - "\n", - "### Loops can fail:\n", - "If you try to use a (parameter of a) local instrument in a background loop\n", - "\n", - "## Measure\n", - "If you want to create a dataset without running a loop - for example, from a single Parameter.get() that returns one or more whole arrays - you can use Measure. Measure works very similarly to Loop, accepting all the same action types. The API for running a Measure is also very similar to Loop, with the difference that Measure does not allow background acquisition.\n", - "\n", - "If any of the actions return scalars, these will be entered in the DataSet as 1D length-1 arrays, along with a similar length-1 setpoint array.\n", - "\n", - "Just like a Loop, you can hold a Measure object, with its list of actions to execute, and reuse it multiple times.\n", - "\n", - "## DataSet\n", - "\n", - "A DataSet is a way to group arrays of data together, describe the meaning of each and their relationships to each other, and record metadata.\n", - "\n", - "Typically a DataSet is the result of running a single Loop, and contains all the data generated by the Loop as well as all the metadata necessary to understand and repeat it.\n", - "\n", - "The data in a DataSet is stored in one or more DataArray objects, each of which is a single numpy ndarray (wrapped with some extra functionality and attributes). The metadata is stored in a JSON-compatible dictionary structure.\n", - "\n", - "A DataArray with N dimensions should list N setpoint arrays, each of which is also a DataArray in the same DataSet. The first setpoint array should have 1 dimension, the second 2 dimensions, etc. This follows the procedure of most experimental loops, where the outer loop parameter only changes when you increment the outer loop.\n", - "\n", - "If your loop does not work this way, and the setpoint of the first index changes with the second index, you should either use an array of integers as the outer setpoints, and treat your varying indices as a separate measured array, or you may prefer to store all of the setpoints and measurements as 1D arrays, where each index represents one condition across all arrays, akin to an SQL table (where each array would represent one column of the table).\n", - "\n", - "One DataArray can only be part of at most one DataSet. This ensures that we don’t generate irreversible situations by saving an array in multiple places and reloading them separately, or conflicts if we try to sync (or reload) several DataSets with inconsistent data in the multiply-referenced arrays, and that we can always refer from the DataArray to a single DataSet, which is important for live plotting.\n", - "\n", - "The DataSet also specifies where and how it is to be stored on disk. Storage is specified by an io_manager (the physical device / protocol, and base location in the normal case of disk storage), a location (string, relative path within the io manager), and formatter (specifies the file type and how to read to and write from a DataSet).\n", - "\n", - "### Responsibilities:\n", - "\n", - "Accepting incremental pieces of data (setpoints and measurements as they become available)\n", - "\n", - "Either holding that data locally (within its DataArrays), or pushing it to another copy of itself that stores it\n", - "\n", - "If it’s a copy that holds data, each DataArray maintains a record of the range of indices that have changed since the last save to storage, the last index that has been saved, and (if it’s in PULL_FROM_SERVER mode) the last index that has been synced from the server. This implicitly assumes that the DataArrays are filled in order of the raveled indices, ie looping over the inner index first.\n", - "\n", - "It’s up to the Formatter to look at each of these DataArrays, decide what parts of the changes in each to save to storage, and then tell each DataArray what it saved (expressed as a greatest raveled index). With that information the DataArray updates its record of what still needs saving. This is done so that a Formatter can choose to combine several DataArrays into one table, which may require writing only the values at array positions which have been finished in all of these arrays.\n", - "\n", - "Each DataSet holds:\n", - "\n", - "- Its own metadata (JSON-compatible dict, i.e., everything that the custom JSON encoder class qcodes.utils.NumpyJSONEncoder supports.)\n", - "- Its mode (PUSH_TO_SERVER, PULL_FROM_SERVER, LOCAL)\n", - "- A dict of DataArrays, each with attributes: name (which is also its dictionary key in DataSet.arrays), label, units, setpoints. If the DataSet is in PUSH_TO_SERVER mode, these DataArrays do not hold any data. Otherwise, these DataArrays contain numpy arrays of data, as well as records (as described above) of what parts of that array have been changed, saved, and synced.\n", - "- location, formatter, and io manager\n", - "\n", - "### DataSets can fail:\n", - "\n", - "If somehow the data in storage does not match the record in memory of what it has saved, for example if you change the stored file during acquisition. The consequences depend on the formatter (this could be completely destructive for GNUPlotFormat or other text-based formats, probably less so for HDF5) but in general the DataSet has no way of independently checking that the existing data on disk is still what it thinks it is. A safe but slow way around this is to rewrite the stored files completely\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "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.8.13" - }, - "widgets": { - "application/vnd.jupyter.widget-state+json": { - "state": {}, - "version_major": 2, - "version_minor": 0 - } - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/docs/examples/legacy/Measure without a Loop.ipynb b/docs/examples/legacy/Measure without a Loop.ipynb deleted file mode 100644 index 2ea822a5ac8..00000000000 --- a/docs/examples/legacy/Measure without a Loop.ipynb +++ /dev/null @@ -1,172 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Measure without a Loop\n", - "\n", - "If you have a parameter that returns a whole array at once, often you want to measure it directly into a DataSet.\n", - "\n", - "This shows how that works in QCoDeS" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "2020-03-24 18:45:32,769 ¦ qcodes.instrument.base ¦ WARNING ¦ base ¦ snapshot_base ¦ 214 ¦ [dac2_ChanA(DummyChannel)] Snapshot: Could not update parameter: dummy_sp_axis\n", - "2020-03-24 18:45:32,798 ¦ qcodes.instrument.base ¦ WARNING ¦ base ¦ snapshot_base ¦ 214 ¦ [dac2_ChanB(DummyChannel)] Snapshot: Could not update parameter: dummy_sp_axis\n", - "2020-03-24 18:45:32,804 ¦ qcodes.instrument.base ¦ WARNING ¦ base ¦ snapshot_base ¦ 214 ¦ [dac2_ChanC(DummyChannel)] Snapshot: Could not update parameter: dummy_sp_axis\n", - "2020-03-24 18:45:32,807 ¦ qcodes.instrument.base ¦ WARNING ¦ base ¦ snapshot_base ¦ 214 ¦ [dac2_ChanD(DummyChannel)] Snapshot: Could not update parameter: dummy_sp_axis\n", - "2020-03-24 18:45:32,819 ¦ qcodes.instrument.base ¦ WARNING ¦ base ¦ snapshot_base ¦ 214 ¦ [dac2_ChanE(DummyChannel)] Snapshot: Could not update parameter: dummy_sp_axis\n", - "2020-03-24 18:45:32,838 ¦ qcodes.instrument.base ¦ WARNING ¦ base ¦ snapshot_base ¦ 214 ¦ [dac2_ChanF(DummyChannel)] Snapshot: Could not update parameter: dummy_sp_axis\n" - ] - } - ], - "source": [ - "%matplotlib nbagg\n", - "import numpy as np\n", - "\n", - "import qcodes as qc\n", - "from qcodes.actions import Task\n", - "from qcodes.measure import Measure\n", - "\n", - "# import dummy driver for the tutorial\n", - "from qcodes.tests.instrument_mocks import DummyChannelInstrument, DummyInstrument\n", - "\n", - "dac1 = DummyInstrument(name=\"dac\")\n", - "dac2 = DummyChannelInstrument(name=\"dac2\")\n", - "\n", - "\n", - "# the default dummy instrument returns always a constant value, in the following line we make it random \n", - "# just for the looks 💅\n", - "dac2.A.dummy_array_parameter.get = lambda: np.random.randint(0, 100, size=5)\n", - "\n", - "# The station is a container for all instruments that makes it easy \n", - "# to log meta-data\n", - "station = qc.Station(dac1, dac2)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Instantiates all the instruments needed for the demo\n", - "\n", - "For this tutorial we're going to use the regular parameters (c0, c1, c2, vsd) and ArrayGetter, which is just a way to construct a parameter that returns a whole array at once out of simple parameters, as well as AverageAndRaw, which returns a scalar *and* an array together." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Only array output\n", - "The arguments to Measure are all the same actions you use in a Loop.\n", - "If they return only arrays, you will see exactly those arrays (with their setpoints) in the output DataSet" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "DataSet:\n", - " location = 'data/2020-03-24/#013_{name}_18-45-41'\n", - " | | | \n", - " Measured | dac2_ChanA_dummy_array_parameter_1 | dummy_array_parameter | (5,)\n", - " Measured | dac2_ChanA_dummy_array_parameter_3 | dummy_array_parameter | (5,)\n", - "acquired at 2020-03-24 18:45:41\n" - ] - } - ], - "source": [ - "data = Measure(\n", - " Task(dac1.dac1.set, 0),\n", - " dac2.A.dummy_array_parameter,\n", - " Task(dac1.dac1.set, 2),\n", - " dac2.A.dummy_array_parameter,\n", - ").run()" - ] - }, - { - "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.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": 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": 1 -} diff --git a/docs/examples/legacy/The Location Formatter.ipynb b/docs/examples/legacy/The Location Formatter.ipynb deleted file mode 100644 index 8b5dd6f1651..00000000000 --- a/docs/examples/legacy/The Location Formatter.ipynb +++ /dev/null @@ -1,237 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# The Location Formatter\n", - "\n", - "The Location Formatter controls the format of the location to which data are saved.\n", - "\n", - "This notebook shows some examples of setting different location formats." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Logging hadn't been started.\n", - "Activating auto-logging. Current session state plus future input saved.\n", - "Filename : C:\\Users\\a-halakh\\.qcodes\\logs\\command_history.log\n", - "Mode : append\n", - "Output logging : True\n", - "Raw input log : False\n", - "Timestamping : True\n", - "State : active\n", - "Qcodes Logfile : C:\\Users\\a-halakh\\.qcodes\\logs\\200325-3652-qcodes.log\n" - ] - } - ], - "source": [ - "%matplotlib nbagg\n", - "import time\n", - "\n", - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "\n", - "import qcodes as qc\n", - "from qcodes.data.location import FormatLocation\n", - "from qcodes.loops import Loop" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "# First we set up some mock experiment\n", - "from qcodes.tests.instrument_mocks import DummyInstrument\n", - "\n", - "gates = DummyInstrument('some_gates', gates=['plunger', 'left', 'topo'])\n", - "meter = DummyInstrument('meter', gates=['voltage', 'current'])\n", - "\n", - "station = qc.Station(gates, meter)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## The formatter in action\n", - "\n", - "Now let's run some loops to get datasets and see where they end up.\n", - "\n", - "When writing the location format, some fields are automatically filled out.\n", - "\n", - "That is the fields '{date}', '{time}', and '{counter}'.\n", - "All other fields must have their values provided via the record dict." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Started at 2020-03-25 11:51:27\n", - "DataSet:\n", - " location = '2020-03-25/#003_unicorn_2020-03-25_11-51-27'\n", - " | | | \n", - " Setpoint | some_gates_plunger_set | plunger | (25,)\n", - " Measured | meter_voltage | voltage | (25,)\n", - "Finished at 2020-03-25 11:51:27\n" - ] - } - ], - "source": [ - "loc_fmt='{date}/#{counter}_{name}_{date}_{time}' # set the desired location format\n", - "rcd={'name': 'unicorn'} # provide a value for 'name'\n", - "loc_provider = FormatLocation(fmt=loc_fmt, record=rcd) # create a location provider using that format\n", - "\n", - "loop = Loop(gates.plunger.sweep(0, 1, num=25), 0).each(meter.voltage)\n", - "data2 = loop.run(location=loc_provider)" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Started at 2020-03-25 11:51:27\n", - "DataSet:\n", - " location = 'my_custom_folder/#004_randomnumber_7_2020-03-25_11-51-27'\n", - " | | | \n", - " Setpoint | some_gates_plunger_set | plunger | (25,)\n", - " Measured | meter_voltage | voltage | (25,)\n", - "Finished at 2020-03-25 11:51:27\n" - ] - } - ], - "source": [ - "# Now let's do that a few times with different formats\n", - "\n", - "import numpy as np\n", - "\n", - "loc_fmt='my_custom_folder/#{counter}_randomnumber_{name}_{date}_{time}'\n", - "rcd = {'name': str(np.random.randint(1, 100))}\n", - "loc_provider = FormatLocation(fmt=loc_fmt, record=rcd)\n", - "\n", - "loop = Loop(gates.plunger.sweep(0, 1, num=25), 0).each(meter.voltage)\n", - "data2 = loop.run(location=loc_provider)" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Started at 2020-03-25 11:51:27\n", - "DataSet:\n", - " location = '2020-03-25/#004_{name}_2020-03-25_hammer_time'\n", - " | | | \n", - " Setpoint | some_gates_plunger_set | plunger | (25,)\n", - " Measured | meter_voltage | voltage | (25,)\n", - "Finished at 2020-03-25 11:51:28\n" - ] - } - ], - "source": [ - "# You can also overwrite the custom fields\n", - "\n", - "loc_fmt='{date}/#{counter}_{name}_{date}_{time}'\n", - "rcd = {'time': 'hammer_time'}\n", - "loc_provider = FormatLocation(fmt=loc_fmt, record=rcd)\n", - "\n", - "loop = Loop(gates.plunger.sweep(0, 1, num=25), 0).each(meter.voltage)\n", - "data2 = loop.run(location=loc_provider)" - ] - }, - { - "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.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": 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": 1 -} diff --git a/docs/examples/legacy/The Snapshot.ipynb b/docs/examples/legacy/The Snapshot.ipynb deleted file mode 100644 index 5fcda601fc0..00000000000 --- a/docs/examples/legacy/The Snapshot.ipynb +++ /dev/null @@ -1,562 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# The Snapshot\n", - "\n", - "This notebook sheds some light on the snapshot of instruments.\n", - "\n", - "__NOTE__: this notebook uses a depreated `Loop` construct for some of its examples. Please, instead, refer to [__Working with snapshots__ notebook from `docs/examples/DataSet`](DataSet/Working%20with%20snapshots.ipynb)." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Logging hadn't been started.\n", - "Activating auto-logging. Current session state plus future input saved.\n", - "Filename : C:\\Users\\a-halakh\\.qcodes\\logs\\command_history.log\n", - "Mode : append\n", - "Output logging : True\n", - "Raw input log : False\n", - "Timestamping : True\n", - "State : active\n", - "Qcodes Logfile : C:\\Users\\a-halakh\\.qcodes\\logs\\200324-30512-qcodes.log\n", - "False\n" - ] - } - ], - "source": [ - "import json\n", - "import os\n", - "from pprint import pprint\n", - "\n", - "import qcodes as qc\n", - "from qcodes.loops import Loop\n", - "from qcodes.tests.instrument_mocks import DummyInstrument\n", - "\n", - "# For this tutorial, we initialise our favourite pair of mock instruments,\n", - "# a DMM and a DAC\n", - "\n", - "dmm = DummyInstrument('dmm', gates=['v1', 'v2'])\n", - "dac = DummyInstrument('dac', gates=['ch1', 'ch2'])\n", - "\n", - "station = qc.Station(dmm, dac)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The main point of having a `Station` is that it *snapshots* the state of all added instruments. But what does that mean? Recall that an instrument is, loosely speaking, a collection of `Parameters`." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Parameter snapshot" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'__class__': 'qcodes.instrument.parameter.Parameter',\n", - " 'full_name': 'dmm_v1',\n", - " 'instrument': 'qcodes.tests.instrument_mocks.DummyInstrument',\n", - " 'instrument_name': 'dmm',\n", - " 'inter_delay': 0,\n", - " 'label': 'Gate v1',\n", - " 'name': 'v1',\n", - " 'post_delay': 0,\n", - " 'raw_value': 0,\n", - " 'ts': '2020-03-24 18:47:43',\n", - " 'unit': 'V',\n", - " 'vals': '',\n", - " 'value': 0}\n" - ] - } - ], - "source": [ - "# Each parameter has a snapshot, containing information about its current value,\n", - "# when that value was set, what the allowed values are, etc.\n", - "pprint(dmm.v1.snapshot())" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Instrument snapshot" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'__class__': 'qcodes.tests.instrument_mocks.DummyInstrument',\n", - " 'functions': {},\n", - " 'name': 'dmm',\n", - " 'parameters': {'IDN': {'__class__': 'qcodes.instrument.parameter.Parameter',\n", - " 'full_name': 'dmm_IDN',\n", - " 'instrument': 'qcodes.tests.instrument_mocks.DummyInstrument',\n", - " 'instrument_name': 'dmm',\n", - " 'inter_delay': 0,\n", - " 'label': 'IDN',\n", - " 'name': 'IDN',\n", - " 'post_delay': 0,\n", - " 'raw_value': {'firmware': None,\n", - " 'model': 'dmm',\n", - " 'serial': None,\n", - " 'vendor': None},\n", - " 'ts': '2020-03-24 18:47:43',\n", - " 'unit': '',\n", - " 'vals': '',\n", - " 'value': {'firmware': None,\n", - " 'model': 'dmm',\n", - " 'serial': None,\n", - " 'vendor': None}},\n", - " 'v1': {'__class__': 'qcodes.instrument.parameter.Parameter',\n", - " 'full_name': 'dmm_v1',\n", - " 'instrument': 'qcodes.tests.instrument_mocks.DummyInstrument',\n", - " 'instrument_name': 'dmm',\n", - " 'inter_delay': 0,\n", - " 'label': 'Gate v1',\n", - " 'name': 'v1',\n", - " 'post_delay': 0,\n", - " 'raw_value': 0,\n", - " 'ts': '2020-03-24 18:47:43',\n", - " 'unit': 'V',\n", - " 'vals': '',\n", - " 'value': 0},\n", - " 'v2': {'__class__': 'qcodes.instrument.parameter.Parameter',\n", - " 'full_name': 'dmm_v2',\n", - " 'instrument': 'qcodes.tests.instrument_mocks.DummyInstrument',\n", - " 'instrument_name': 'dmm',\n", - " 'inter_delay': 0,\n", - " 'label': 'Gate v2',\n", - " 'name': 'v2',\n", - " 'post_delay': 0,\n", - " 'raw_value': 0,\n", - " 'ts': '2020-03-24 18:47:43',\n", - " 'unit': 'V',\n", - " 'vals': '',\n", - " 'value': 0}},\n", - " 'submodules': {}}\n" - ] - } - ], - "source": [ - "# Each instrument has a snapshot that is basically the snapshots of all the parameters\n", - "pprint(dmm.snapshot())" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Sweep snapshot" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'parameter': {'__class__': 'qcodes.instrument.parameter.Parameter',\n", - " 'full_name': 'dac_ch1',\n", - " 'instrument': 'qcodes.tests.instrument_mocks.DummyInstrument',\n", - " 'instrument_name': 'dac',\n", - " 'inter_delay': 0,\n", - " 'label': 'Gate ch1',\n", - " 'name': 'ch1',\n", - " 'post_delay': 0,\n", - " 'raw_value': 0,\n", - " 'ts': '2020-03-24 18:47:43',\n", - " 'unit': 'V',\n", - " 'vals': '',\n", - " 'value': 0},\n", - " 'values': [{'first': 0.0, 'last': 10.0, 'num': 25, 'type': 'linear'}]}\n" - ] - } - ], - "source": [ - "# When running QCoDeS loops, something is being swept. This is controlled with the `sweep` of a parameter.\n", - "# Sweeps also have snapshots\n", - "a_sweep = dac.ch1.sweep(0, 10, num=25)\n", - "pprint(a_sweep.snapshot())" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Loop/Measurement snapshot" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "{'__class__': 'qcodes.loops.ActiveLoop',\n", - " 'actions': [{'__class__': 'qcodes.instrument.parameter.Parameter',\n", - " 'full_name': 'dmm_v1',\n", - " 'instrument': 'qcodes.tests.instrument_mocks.DummyInstrument',\n", - " 'instrument_name': 'dmm',\n", - " 'inter_delay': 0,\n", - " 'label': 'Gate v1',\n", - " 'name': 'v1',\n", - " 'post_delay': 0,\n", - " 'raw_value': 0,\n", - " 'ts': '2020-03-24 18:47:43',\n", - " 'unit': 'V',\n", - " 'vals': '',\n", - " 'value': 0}],\n", - " 'delay': 0,\n", - " 'sweep_values': {'parameter': {'__class__': 'qcodes.instrument.parameter.Parameter',\n", - " 'full_name': 'dac_ch1',\n", - " 'instrument': 'qcodes.tests.instrument_mocks.DummyInstrument',\n", - " 'instrument_name': 'dac',\n", - " 'inter_delay': 0,\n", - " 'label': 'Gate ch1',\n", - " 'name': 'ch1',\n", - " 'post_delay': 0,\n", - " 'raw_value': 0,\n", - " 'ts': '2020-03-24 18:47:43',\n", - " 'unit': 'V',\n", - " 'vals': '',\n", - " 'value': 0},\n", - " 'values': [{'first': 0.0,\n", - " 'last': 1.0,\n", - " 'num': 10,\n", - " 'type': 'linear'}]},\n", - " 'then_actions': []}\n" - ] - } - ], - "source": [ - "# All this is of course nice since a snapshot is saved every time a measurement is \n", - "# performed. Let's see this in action with a Loop.\n", - "\n", - "# This is a qc.loop, sweeping a dac gate and reading a dmm voltage\n", - "lp = Loop(dac.ch1.sweep(0, 1, num=10), 0).each(dmm.v1)\n", - "\n", - "# before the loop runs, the snapshot is quite modest; it contains the snapshots of\n", - "# the two involved parameters and the sweep\n", - "pprint(lp.snapshot())" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": { - "scrolled": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Started at 2020-03-24 18:47:43\n", - "DataSet:\n", - " location = 'data/2020-03-24/#014_{name}_18-47-43'\n", - " | | | \n", - " Setpoint | dac_ch1_set | ch1 | (10,)\n", - " Measured | dmm_v1 | v1 | (10,)\n", - "Finished at 2020-03-24 18:47:43\n", - "{'__class__': 'qcodes.data.data_set.DataSet',\n", - " 'arrays': {'dac_ch1_set': {'__class__': 'qcodes.data.data_array.DataArray',\n", - " 'action_indices': (),\n", - " 'array_id': 'dac_ch1_set',\n", - " 'full_name': 'dac_ch1',\n", - " 'instrument': 'qcodes.tests.instrument_mocks.DummyInstrument',\n", - " 'instrument_name': 'dac',\n", - " 'inter_delay': 0,\n", - " 'is_setpoint': True,\n", - " 'label': 'Gate ch1',\n", - " 'name': 'ch1',\n", - " 'post_delay': 0,\n", - " 'raw_value': 0,\n", - " 'shape': (10,),\n", - " 'unit': 'V',\n", - " 'vals': ''},\n", - " 'dmm_v1': {'__class__': 'qcodes.data.data_array.DataArray',\n", - " 'action_indices': (0,),\n", - " 'array_id': 'dmm_v1',\n", - " 'full_name': 'dmm_v1',\n", - " 'instrument': 'qcodes.tests.instrument_mocks.DummyInstrument',\n", - " 'instrument_name': 'dmm',\n", - " 'inter_delay': 0,\n", - " 'is_setpoint': False,\n", - " 'label': 'Gate v1',\n", - " 'name': 'v1',\n", - " 'post_delay': 0,\n", - " 'raw_value': 0,\n", - " 'shape': (10,),\n", - " 'unit': 'V',\n", - " 'vals': ''}},\n", - " 'formatter': 'qcodes.data.gnuplot_format.GNUPlotFormat',\n", - " 'io': '\",\n", - " 'location': 'data/2020-03-24/#014_{name}_18-47-43',\n", - " 'loop': {'__class__': 'qcodes.loops.ActiveLoop',\n", - " 'actions': [{'__class__': 'qcodes.instrument.parameter.Parameter',\n", - " 'full_name': 'dmm_v1',\n", - " 'instrument': 'qcodes.tests.instrument_mocks.DummyInstrument',\n", - " 'instrument_name': 'dmm',\n", - " 'inter_delay': 0,\n", - " 'label': 'Gate v1',\n", - " 'name': 'v1',\n", - " 'post_delay': 0,\n", - " 'raw_value': 0,\n", - " 'ts': '2020-03-24 18:47:43',\n", - " 'unit': 'V',\n", - " 'vals': '',\n", - " 'value': 0}],\n", - " 'delay': 0,\n", - " 'sweep_values': {'parameter': {'__class__': 'qcodes.instrument.parameter.Parameter',\n", - " 'full_name': 'dac_ch1',\n", - " 'instrument': 'qcodes.tests.instrument_mocks.DummyInstrument',\n", - " 'instrument_name': 'dac',\n", - " 'inter_delay': 0,\n", - " 'label': 'Gate ch1',\n", - " 'name': 'ch1',\n", - " 'post_delay': 0,\n", - " 'raw_value': 0,\n", - " 'ts': '2020-03-24 18:47:43',\n", - " 'unit': 'V',\n", - " 'vals': '',\n", - " 'value': 0},\n", - " 'values': [{'first': 0.0,\n", - " 'last': 1.0,\n", - " 'num': 10,\n", - " 'type': 'linear'}]},\n", - " 'then_actions': [],\n", - " 'ts_end': '2020-03-24 18:47:43',\n", - " 'ts_start': '2020-03-24 18:47:43',\n", - " 'use_threads': 'data/dataset'},\n", - " 'station': {'components': {},\n", - " 'config': None,\n", - " 'default_measurement': [],\n", - " 'instruments': {'dac': {'__class__': 'qcodes.tests.instrument_mocks.DummyInstrument',\n", - " 'functions': {},\n", - " 'name': 'dac',\n", - " 'parameters': {'IDN': {'__class__': 'qcodes.instrument.parameter.Parameter',\n", - " 'full_name': 'dac_IDN',\n", - " 'instrument': 'qcodes.tests.instrument_mocks.DummyInstrument',\n", - " 'instrument_name': 'dac',\n", - " 'inter_delay': 0,\n", - " 'label': 'IDN',\n", - " 'name': 'IDN',\n", - " 'post_delay': 0,\n", - " 'raw_value': {'firmware': None,\n", - " 'model': 'dac',\n", - " 'serial': None,\n", - " 'vendor': None},\n", - " 'ts': '2020-03-24 '\n", - " '18:47:43',\n", - " 'unit': '',\n", - " 'vals': '',\n", - " 'value': {'firmware': None,\n", - " 'model': 'dac',\n", - " 'serial': None,\n", - " 'vendor': None}},\n", - " 'ch1': {'__class__': 'qcodes.instrument.parameter.Parameter',\n", - " 'full_name': 'dac_ch1',\n", - " 'instrument': 'qcodes.tests.instrument_mocks.DummyInstrument',\n", - " 'instrument_name': 'dac',\n", - " 'inter_delay': 0,\n", - " 'label': 'Gate ch1',\n", - " 'name': 'ch1',\n", - " 'post_delay': 0,\n", - " 'raw_value': 0,\n", - " 'ts': '2020-03-24 '\n", - " '18:47:43',\n", - " 'unit': 'V',\n", - " 'vals': '',\n", - " 'value': 0},\n", - " 'ch2': {'__class__': 'qcodes.instrument.parameter.Parameter',\n", - " 'full_name': 'dac_ch2',\n", - " 'instrument': 'qcodes.tests.instrument_mocks.DummyInstrument',\n", - " 'instrument_name': 'dac',\n", - " 'inter_delay': 0,\n", - " 'label': 'Gate ch2',\n", - " 'name': 'ch2',\n", - " 'post_delay': 0,\n", - " 'raw_value': 0,\n", - " 'ts': '2020-03-24 '\n", - " '18:47:43',\n", - " 'unit': 'V',\n", - " 'vals': '',\n", - " 'value': 0}},\n", - " 'submodules': {}},\n", - " 'dmm': {'__class__': 'qcodes.tests.instrument_mocks.DummyInstrument',\n", - " 'functions': {},\n", - " 'name': 'dmm',\n", - " 'parameters': {'IDN': {'__class__': 'qcodes.instrument.parameter.Parameter',\n", - " 'full_name': 'dmm_IDN',\n", - " 'instrument': 'qcodes.tests.instrument_mocks.DummyInstrument',\n", - " 'instrument_name': 'dmm',\n", - " 'inter_delay': 0,\n", - " 'label': 'IDN',\n", - " 'name': 'IDN',\n", - " 'post_delay': 0,\n", - " 'raw_value': {'firmware': None,\n", - " 'model': 'dmm',\n", - " 'serial': None,\n", - " 'vendor': None},\n", - " 'ts': '2020-03-24 '\n", - " '18:47:43',\n", - " 'unit': '',\n", - " 'vals': '',\n", - " 'value': {'firmware': None,\n", - " 'model': 'dmm',\n", - " 'serial': None,\n", - " 'vendor': None}},\n", - " 'v1': {'__class__': 'qcodes.instrument.parameter.Parameter',\n", - " 'full_name': 'dmm_v1',\n", - " 'instrument': 'qcodes.tests.instrument_mocks.DummyInstrument',\n", - " 'instrument_name': 'dmm',\n", - " 'inter_delay': 0,\n", - " 'label': 'Gate v1',\n", - " 'name': 'v1',\n", - " 'post_delay': 0,\n", - " 'raw_value': 0,\n", - " 'ts': '2020-03-24 '\n", - " '18:47:43',\n", - " 'unit': 'V',\n", - " 'vals': '',\n", - " 'value': 0},\n", - " 'v2': {'__class__': 'qcodes.instrument.parameter.Parameter',\n", - " 'full_name': 'dmm_v2',\n", - " 'instrument': 'qcodes.tests.instrument_mocks.DummyInstrument',\n", - " 'instrument_name': 'dmm',\n", - " 'inter_delay': 0,\n", - " 'label': 'Gate v2',\n", - " 'name': 'v2',\n", - " 'post_delay': 0,\n", - " 'raw_value': 0,\n", - " 'ts': '2020-03-24 '\n", - " '18:47:43',\n", - " 'unit': 'V',\n", - " 'vals': '',\n", - " 'value': 0}},\n", - " 'submodules': {}}},\n", - " 'parameters': {}}}\n" - ] - } - ], - "source": [ - "# After the loop has run, the dataset contains more information, in particular the \n", - "# snapshots for ALL parameters off ALL instruments in the station\n", - "data = lp.run('data/dataset')\n", - "pprint(data.snapshot())\n", - "\n", - "# This is the snapshot that get's saved to disk alongside your data. \n", - "# It's worthwhile familiarising yourself with it, so that you may retrieve\n", - "# valuable information down the line!" - ] - }, - { - "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.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": 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": 1 -} diff --git a/docs/examples/legacy/Tutorial.ipynb b/docs/examples/legacy/Tutorial.ipynb deleted file mode 100644 index 588631cf077..00000000000 --- a/docs/examples/legacy/Tutorial.ipynb +++ /dev/null @@ -1,652 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# QCoDeS tutorial \n", - "Basic overview of QCoDeS\n", - "\n", - "## Table of Contents \n", - " * [Workflow](#workflow)\n", - " * [Basic instrument interaction](#inst_io)\n", - " * [Measuring](#measurement)\n", - " * [The loop: 1D example](#loop_1D)\n", - " * [The loop: 2D example](#loop_2D)\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Typical QCodes workflow \n", - "(back to [ToC](#toc))\n", - "\n", - "1. Start up an interactive python session (e.g. using jupyter) \n", - "2. import desired modules \n", - "3. instantiate required instruments \n", - "4. experiment! " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Importing" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": { - "scrolled": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Logging hadn't been started.\n", - "Activating auto-logging. Current session state plus future input saved.\n", - "Filename : C:\\Users\\a-halakh\\.qcodes\\logs\\command_history.log\n", - "Mode : append\n", - "Output logging : True\n", - "Raw input log : False\n", - "Timestamping : True\n", - "State : active\n", - "Qcodes Logfile : C:\\Users\\a-halakh\\.qcodes\\logs\\200325-16084-qcodes.log\n" - ] - } - ], - "source": [ - "# usually, one imports QCoDeS and some instruments\n", - "import qcodes as qc\n", - "from qcodes.data.data_set import load_data\n", - "from qcodes.loops import Loop\n", - "from qcodes.plots.pyqtgraph import QtPlot\n", - "from qcodes.plots.qcmatplotlib import MatPlot\n", - "\n", - "# In this tutorial, we import the dummy instrument\n", - "from qcodes.tests.instrument_mocks import DummyInstrument\n", - "\n", - "# real instruments are imported in a similar way, e.g.\n", - "# from qcodes.instrument_drivers.Keysight.KeysightAgilent_33XXX import WaveformGenerator_33XXX" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Instantiation of instruments" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "# It is not enough to import the instruments, they must also be instantiated\n", - "# Note that this can only be done once. If you try to re-instantiate an existing instrument, QCoDeS will \n", - "# complain that 'Another instrument has the name'.\n", - "\n", - "\n", - "# In this turotial, we consider a simple situation: A single DAC outputting voltages to a Digital Multi Meter\n", - "\n", - "dac = DummyInstrument(name=\"dac\", gates=['ch1', 'ch2']) # The DAC voltage source\n", - "dmm = DummyInstrument(name=\"dmm\", gates=['voltage']) # The DMM voltage reader\n", - "\n", - "# the default dummy instrument returns always a constant value, in the following line we make it random \n", - "# just for the looks 💅\n", - "import random\n", - "\n", - "dmm.voltage.get = lambda: random.randint(0, 100)\n", - "\n", - "# Finally, the instruments should be bound to a Station. Only instruments bound to the Station get recorded in the\n", - "# measurement metadata, so your metadata is blind to any instrument not in the Station.\n", - "\n", - "station = qc.Station(dac, dmm)" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "# For the tutorial, we add a parameter that loudly prints what it is being set to\n", - "# (It is used below)\n", - "\n", - "chX = 0\n", - "\n", - "def myget():\n", - " return chX\n", - " \n", - "def myset(x):\n", - " global chX\n", - " chX = x\n", - " print(f'Setting to {x}')\n", - " return None\n", - " \n", - "dac.add_parameter('verbose_channel',\n", - " label='Verbose Channel',\n", - " unit='V',\n", - " get_cmd=myget,\n", - " set_cmd=myset)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### The location provider can be set globally " - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "loc_provider = qc.data.location.FormatLocation(fmt='data/{date}/#{counter}_{name}_{time}')\n", - "qc.data.data_set.DataSet.location_provider=loc_provider" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We are now ready to play with the instruments!" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Basic instrument interaction \n", - "(back to [ToC](#toc))\n", - "\n", - "The interaction with instruments mainly consists of `setting` and `getting` the instruments' `parameters`. A parameter can be anything from the frequency of a signal generator over the output impedance of an AWG to the traces from a lock-in amplifier. In this tutorial we --for didactical reasons-- only consider scalar parameters. " - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "8" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# The voltages output by the dac can be set like so\n", - "dac.ch1.set(8) \n", - "# Now the output is 8 V. We can read this value back\n", - "dac.ch1.get()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Setting IMMEDIATELY changes a value. For voltages, that is sometimes undesired. The value can instead be ramped by stepping and waiting." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": { - "scrolled": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Setting to 0\n", - "Setting to 9\n", - "Setting to 8.9\n", - "Setting to 8.8\n", - "Setting to 8.7\n", - "Setting to 8.6\n", - "Setting to 8.5\n", - "Setting to 8.4\n", - "Setting to 8.3\n", - "Setting to 8.2\n", - "Setting to 8.1\n", - "Setting to 8.0\n", - "Setting to 7.9\n", - "Setting to 7.8\n", - "Setting to 7.7\n", - "Setting to 7.6\n", - "Setting to 7.5\n", - "Setting to 7.4\n", - "Setting to 7.3\n", - "Setting to 7.2\n", - "Setting to 7.1\n", - "Setting to 7.0\n", - "Setting to 6.9\n", - "Setting to 6.8\n", - "Setting to 6.699999999999999\n", - "Setting to 6.6\n", - "Setting to 6.5\n", - "Setting to 6.4\n", - "Setting to 6.3\n", - "Setting to 6.199999999999999\n", - "Setting to 6.1\n", - "Setting to 6.0\n", - "Setting to 5.9\n", - "Setting to 5.8\n", - "Setting to 5.699999999999999\n", - "Setting to 5.6\n", - "Setting to 5.5\n", - "Setting to 5.4\n", - "Setting to 5.3\n", - "Setting to 5.199999999999999\n", - "Setting to 5.1\n", - "Setting to 5\n" - ] - } - ], - "source": [ - "dac.verbose_channel.set(0) \n", - "dac.verbose_channel.set(9) # immediate voltage jump of 9 Volts (!)\n", - "\n", - "# first set a step size\n", - "dac.verbose_channel.step = 0.1\n", - "# and a wait time\n", - "dac.verbose_channel.inter_delay = 0.01 # in seconds\n", - "\n", - "# now a \"staircase ramp\" is performed by setting\n", - "dac.verbose_channel.set(5)\n", - "\n", - "# after such a ramp, it is a good idea to reset the step and delay\n", - "dac.verbose_channel.step = 0\n", - "# and a wait time\n", - "dac.verbose_channel.inter_delay = 0 " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**NOTE**: that is ramp is blocking and has a low resolution since each `set` on a real instrument has a latency on the order of ms. Some instrument drivers support native-resolution asynchronous ramping. Always refer to your instrument driver if you need high performance of an instrument." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Measuring \n", - "(back to [ToC](#toc))\n", - "\n", - "### 1D Loop example\n", - "\n", - "#### Defining the `Loop` and actions\n", - "\n", - "Before you run a measurement loop you do two things:\n", - "1. You describe what parameter(s) to vary and how. This is the creation of a `Loop` object: `loop = Loop(sweep_values, ...)`\n", - "2. You describe what to do at each step in the loop. This is `loop.each(*actions)` \n", - " - measurements (any object with a `.get` method will be interpreted as a measurement)\n", - " - `Task`: some callable (which can have arguments with it) to be executed each time through the loop. Does not generate data.\n", - " - `Wait`: a specialized `Task` just to wait a certain time.\n", - " - `BreakIf`: some condition that, if it returns truthy, breaks (this level of) the loop" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [], - "source": [ - "# For instance, sweep a dac voltage and record with the dmm\n", - "\n", - "loop = Loop(dac.ch1.sweep(0, 20, 0.1), delay=0.001).each(dmm.voltage)\n", - "data = loop.get_data_set(name='testsweep')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "plot_1d = QtPlot() # create a plot\n", - "plot_1d.add(data.dmm_voltage) # add a graph to the plot\n", - "_ = loop.with_bg_task(plot_1d.update, plot_1d.save).run() # run the loop" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# The plot may be recalled easily\n", - "plot_1d" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Output of the loop\n", - "\n", - "* A loop returns a dataset.\n", - "* The representation of the dataset shows what arrays it contains and where it is saved.\n", - "* The dataset initially starts out empty (filled with NAN's) and get's filled while the Loop get's executed." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Once the measurement is done, take a look at the file in finder/explorer (the dataset.location should give you the relative path). \n", - "Note also the snapshot that captures the settings of all instruments at the start of the Loop. \n", - "This metadata is also accesible from the dataset and captures a snapshot of each instrument listed in the station. " - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'functions': {},\n", - " 'submodules': {},\n", - " '__class__': 'qcodes.tests.instrument_mocks.DummyInstrument',\n", - " 'parameters': {'IDN': {'value': {'vendor': None,\n", - " 'model': 'dac',\n", - " 'serial': None,\n", - " 'firmware': None},\n", - " 'raw_value': {'vendor': None,\n", - " 'model': 'dac',\n", - " 'serial': None,\n", - " 'firmware': None},\n", - " 'ts': '2020-03-25 12:33:24',\n", - " '__class__': 'qcodes.instrument.parameter.Parameter',\n", - " 'full_name': 'dac_IDN',\n", - " 'unit': '',\n", - " 'label': 'IDN',\n", - " 'vals': '',\n", - " 'post_delay': 0,\n", - " 'name': 'IDN',\n", - " 'instrument': 'qcodes.tests.instrument_mocks.DummyInstrument',\n", - " 'instrument_name': 'dac',\n", - " 'inter_delay': 0},\n", - " 'ch1': {'value': 20.0,\n", - " 'raw_value': 20.0,\n", - " 'ts': '2020-03-25 12:33:44',\n", - " '__class__': 'qcodes.instrument.parameter.Parameter',\n", - " 'full_name': 'dac_ch1',\n", - " 'unit': 'V',\n", - " 'label': 'Gate ch1',\n", - " 'vals': '',\n", - " 'post_delay': 0,\n", - " 'name': 'ch1',\n", - " 'instrument': 'qcodes.tests.instrument_mocks.DummyInstrument',\n", - " 'instrument_name': 'dac',\n", - " 'inter_delay': 0},\n", - " 'ch2': {'value': 0,\n", - " 'raw_value': 0,\n", - " 'ts': '2020-03-25 12:33:24',\n", - " '__class__': 'qcodes.instrument.parameter.Parameter',\n", - " 'full_name': 'dac_ch2',\n", - " 'unit': 'V',\n", - " 'label': 'Gate ch2',\n", - " 'vals': '',\n", - " 'post_delay': 0,\n", - " 'name': 'ch2',\n", - " 'instrument': 'qcodes.tests.instrument_mocks.DummyInstrument',\n", - " 'instrument_name': 'dac',\n", - " 'inter_delay': 0},\n", - " 'verbose_channel': {'value': 5,\n", - " 'raw_value': 5,\n", - " 'ts': '2020-03-25 12:33:24',\n", - " '__class__': 'qcodes.instrument.parameter.Parameter',\n", - " 'full_name': 'dac_verbose_channel',\n", - " 'unit': 'V',\n", - " 'label': 'Verbose Channel',\n", - " 'post_delay': 0,\n", - " 'name': 'verbose_channel',\n", - " 'instrument': 'qcodes.tests.instrument_mocks.DummyInstrument',\n", - " 'instrument_name': 'dac',\n", - " 'inter_delay': 0}},\n", - " 'name': 'dac'}" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "dac.snapshot()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "There is also a more human-readable version of the essential information." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "dac:\n", - "\tparameter value\n", - "--------------------------------------------------------------------------------\n", - "IDN :\t{'vendor': None, 'model': 'dac', 'serial': None, 'firmware'...\n", - "ch1 :\t20 (V)\n", - "ch2 :\t0 (V)\n", - "verbose_channel :\t5 (V)\n" - ] - } - ], - "source": [ - "dac.print_readable_snapshot()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Loading data" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The dataset knows its own location, which we may use to load data." - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": { - "scrolled": false - }, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], - "source": [ - "location = data.location\n", - "loaded_data = load_data(location)\n", - "plot = MatPlot(loaded_data.dmm_voltage)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "## Example: multiple 2D measurements with live plotting" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [], - "source": [ - "# Loops can be nested, so that a new sweep runs for each point in the outer loop\n", - "\n", - "loop = Loop(dac.ch1.sweep(0, 5, 1), 0.1).loop(dac.ch2.sweep(0, 5, 1), 0.1).each(\n", - " dmm.voltage\n", - " )\n", - "data = loop.get_data_set(name='2D_test')" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Started at 2020-03-25 12:33:46\n", - "DataSet:\n", - " location = 'data/2020-03-25/#014_2D_test_12-33-46'\n", - " | | | \n", - " Setpoint | dac_ch1_set | ch1 | (6,)\n", - " Setpoint | dac_ch2_set | ch2 | (6, 6)\n", - " Measured | dmm_voltage | voltage | (6, 6)\n", - "Finished at 2020-03-25 12:33:51\n" - ] - } - ], - "source": [ - "plot = QtPlot()\n", - "plot.add(data.dmm_voltage, figsize=(1200, 500))\n", - "_ = loop.with_bg_task(plot.update, plot.save).run()" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "plot" - ] - } - ], - "metadata": { - "anaconda-cloud": {}, - "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.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": 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": 1 -} diff --git a/docs/examples/legacy/readme.txt b/docs/examples/legacy/readme.txt deleted file mode 100644 index 5cb63b36929..00000000000 --- a/docs/examples/legacy/readme.txt +++ /dev/null @@ -1,3 +0,0 @@ -This folder contains examples of the legacy QCoDeS dataset -and plotting utilities. They are not recommended for new users -but may be useful as reference for existing users. diff --git a/pyproject.toml b/pyproject.toml index 33a43c78352..69449173475 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -74,6 +74,7 @@ Changelog = "https://qcodes.github.io/Qcodes/changes/index.html" qtplot = ["pyqtgraph>=0.11.0"] slack = ["slack-sdk>=3.4.2"] zurichinstruments = ["zhinst-qcodes>=0.3"] +loop = ["qcodes_loop>=0.1.1"] test = [ "coverage[toml]>=6.0.0", "deepdiff>=5.0.2", @@ -98,7 +99,8 @@ test = [ "types-setuptools>=57.0.0", "types-tabulate>=0.1.0", "types-tqdm>=4.64.6", - "types_pywin32>=305.0.0.7" + "types_pywin32>=305.0.0.7", + "qcodes_loop>=0.1.1", ] docs = [ "autodocsumm>=0.2.9", @@ -115,6 +117,7 @@ docs = [ "towncrier>=22.8.0", "scipy>=1.7.0", # examples using scipy "slack-sdk>=3.4.2", # slack example notebook + "qcodes_loop>=0.1.1", # legacy dataset import examples ] [project.scripts] diff --git a/qcodes/__init__.py b/qcodes/__init__.py index 13b4fae2883..24fc70937f7 100644 --- a/qcodes/__init__.py +++ b/qcodes/__init__.py @@ -85,26 +85,6 @@ QCoDeSDeprecationWarning, ) - -try: - _register_magic = config.core.get('register_magic', False) - if _register_magic is not False: - # get_ipython is part of the public api but IPython does - # not use __all__ to mark this - from IPython import get_ipython # type: ignore[attr-defined] - - # Check if we are in IPython - ip = get_ipython() - if ip is not None: - from qcodes.utils.magic import register_magic_class - - register_magic_class(magic_commands=_register_magic) -except ImportError: - pass -except RuntimeError as e: - print(e) - - def test(**kwargs: Any) -> int: """ Run QCoDeS tests. This requires the test requirements given diff --git a/qcodes/actions.py b/qcodes/actions.py index 7f7f4dc6bc6..ac38a74838d 100644 --- a/qcodes/actions.py +++ b/qcodes/actions.py @@ -1,223 +1,25 @@ """Actions, mainly to be executed in measurement Loops.""" import time -from qcodes.utils import is_function, thread_map - -_NO_SNAPSHOT = {'type': None, 'description': 'Action without snapshot'} - - -# exception when threading is attempted used to simultaneously -# query the same instrument for several values -class UnsafeThreadingException(Exception): - pass - - -def _actions_snapshot(actions, update): - """Make a list of snapshots from a list of actions.""" - snapshot = [] - for action in actions: - if hasattr(action, 'snapshot'): - snapshot.append(action.snapshot(update=update)) - else: - snapshot.append(_NO_SNAPSHOT) - return snapshot - - -class Task: - """ - A predefined task to be executed within a measurement Loop. - - The first argument should be a callable, to which any subsequent - args and kwargs (which are evaluated before the loop starts) are passed. - - The args and kwargs are first evaluated if they are found to be callable. - - Keyword Args passed when the Task is called are ignored, - but are accepted for compatibility with other things happening in a Loop. - - Args: - func (Callable): Function to executed - *args: pass to func, after evaluation if callable - **kwargs: pass to func, after evaluation if callable - - """ - def __init__(self, func, *args, **kwargs): - self.func = func - self.args = args - self.kwargs = kwargs - - def __call__(self, **ignore_kwargs): - # If any of the arguments are callable, evaluate them first - eval_args = [arg() if callable(arg) else arg for arg in self.args] - eval_kwargs = {k: (v() if callable(v) else v) for k, v in self.kwargs.items()} - - self.func(*eval_args, **eval_kwargs) - - def snapshot(self, update=False): - """ - Snapshots task - Args: - update (bool): TODO not in use - - Returns: - dict: snapshot - """ - return {'type': 'Task', 'func': repr(self.func)} - - -class Wait: - """ - A simple class to tell a Loop to wait seconds. - - This is transformed into a Task within the Loop, such that - it can do other things (monitor, check for halt) during the delay. - - But for use outside of a Loop, it is also callable (then it just sleeps) - - Args: - delay: seconds to delay - - Raises: - ValueError: if delay is negative - """ - def __init__(self, delay): - if not delay >= 0: - raise ValueError(f"delay must be > 0, not {repr(delay)}") - self.delay = delay - - def __call__(self): - if self.delay: - time.sleep(self.delay) - - def snapshot(self, update=False): - """ - Snapshots delay - Args: - update (bool): TODO not in use - - Returns: - dict: snapshot - """ - return {'type': 'Wait', 'delay': self.delay} - - -class _Measure: - """ - A callable collection of parameters to measure. - - This should not be constructed manually, only by an ActiveLoop. - """ - def __init__(self, params_indices, data_set, use_threads): - self.use_threads = use_threads and len(params_indices) > 1 - # the applicable DataSet.store function - self.store = data_set.store - - # for performance, pre-calculate which params return data for - # multiple arrays, and the name mappings - self.getters = [] - self.param_ids = [] - self.composite = [] - paramcheck = [] # list to check if parameters are unique - for param, action_indices in params_indices: - self.getters.append(param.get) - - if param._instrument: - paramcheck.append((param, param._instrument)) - - if hasattr(param, 'names'): - part_ids = [] - for i in range(len(param.names)): - param_id = data_set.action_id_map[action_indices + (i,)] - part_ids.append(param_id) - self.param_ids.append(None) - self.composite.append(part_ids) - else: - param_id = data_set.action_id_map[action_indices] - self.param_ids.append(param_id) - self.composite.append(False) - - if self.use_threads: - insts = [p[1] for p in paramcheck] - if (len(set(insts)) != len(insts)): - duplicates = [p for p in paramcheck if insts.count(p[1]) > 1] - raise UnsafeThreadingException('Can not use threading to ' - 'read ' - 'several things from the same ' - 'instrument. Specifically, you ' - 'asked for' - ' {}.'.format(duplicates)) - - def __call__(self, loop_indices, **ignore_kwargs): - out_dict = {} - if self.use_threads: - out = thread_map(self.getters) - else: - out = [g() for g in self.getters] - - for param_out, param_id, composite in zip(out, self.param_ids, - self.composite): - if composite: - for val, part_id in zip(param_out, composite): - out_dict[part_id] = val - else: - out_dict[param_id] = param_out - - self.store(loop_indices, out_dict) - - -class _Nest: - - """ - Wrapper to make a callable nested ActiveLoop. - - This should not be constructed manually, only by an ActiveLoop. - """ - - def __init__(self, inner_loop, action_indices): - self.inner_loop = inner_loop - self.action_indices = action_indices - - def __call__(self, **kwargs): - self.inner_loop._run_loop(action_indices=self.action_indices, **kwargs) - - -class BreakIf: - - """ - Loop action that breaks out of the loop if a condition is truthy. - - Args: - condition (Callable): a callable taking no arguments. - Can be a simple function that returns truthy when it's time to quit - Raises: - TypeError: if condition is not a callable with no aguments. - - Examples: - >>> BreakIf(lambda: gates.chan1.get() >= 3) - """ - - def __init__(self, condition): - if not is_function(condition, 0): - raise TypeError('BreakIf condition must be a callable with ' - 'no arguments') - self.condition = condition - - def __call__(self, **ignore_kwargs): - if self.condition(): - raise _QcodesBreak - - def snapshot(self, update=False): - """ - Snapshots breakif action - Args: - update (bool): TODO not in use - - Returns: - dict: snapshot - - """ - return {'type': 'BreakIf', 'condition': repr(self.condition)} - - -class _QcodesBreak(Exception): - pass +from qcodes.utils import is_function, issue_deprecation_warning, thread_map + +try: + from qcodes_loop.actions import ( + BreakIf, + Task, + UnsafeThreadingException, + Wait, + _actions_snapshot, + _Measure, + _Nest, + _QcodesBreak, + ) + +except ImportError as e: + raise ImportError( + "qcodes.actions is deprecated and has moved to " + "the package `qcodes_loop`. Please install qcodes_loop directly or " + "with `pip install qcodes[loop]" + ) from e + +issue_deprecation_warning("qcodes.actions module", alternative="qcodes_loop.actions") diff --git a/qcodes/data/__init__.py b/qcodes/data/__init__.py index 569f62c93ee..cfcea4c744f 100644 --- a/qcodes/data/__init__.py +++ b/qcodes/data/__init__.py @@ -1,6 +1,20 @@ -from .data_array import DataArray -from .format import Formatter -from .gnuplot_format import GNUPlotFormat -from .hdf5_format import HDF5Format -from .io import DiskIO -from .location import FormatLocation +""" +This module has moved to the qcodes_loop package. +""" +from qcodes.utils import issue_deprecation_warning + +try: + from qcodes_loop.data.data_array import DataArray + from qcodes_loop.data.format import Formatter + from qcodes_loop.data.gnuplot_format import GNUPlotFormat + from qcodes_loop.data.hdf5_format import HDF5Format + from qcodes_loop.data.io import DiskIO + from qcodes_loop.data.location import FormatLocation +except ImportError as e: + raise ImportError( + "qcodes.data is deprecated and has moved to " + "the package `qcodes_loop`. Please install qcodes_loop directly or " + "with `pip install qcodes[loop]" + ) from e + +issue_deprecation_warning("qcodes.data module", alternative="qcodes_loop.data") diff --git a/qcodes/data/data_array.py b/qcodes/data/data_array.py index 685ccf20949..a096575998b 100644 --- a/qcodes/data/data_array.py +++ b/qcodes/data/data_array.py @@ -8,622 +8,23 @@ import logging -from qcodes.utils import DelegateAttributes, full_class +from qcodes.utils import DelegateAttributes, full_class, issue_deprecation_warning _LOG = logging.getLogger(__name__) -class DataArray(DelegateAttributes): - - """ - A container for one parameter in a measurement loop. - - If this is a measured parameter, This object doesn't contain - the data of the setpoints it was measured at, but it references - the DataArray objects of these parameters. Those objects only have - the dimensionality at which they were set - ie the inner loop setpoint - the same dimensionality as the measured parameter, but the outer - loop setpoint(s) have lower dimensionality - - When it's first created, a DataArray has no dimensionality, you must call - .nest for each dimension. - - If preset_data is provided it is used to initialize the data, and the array - can still be nested around it (making many copies of the data). - Otherwise it is an error to nest an array that already has data. - - Once the array is initialized, a DataArray acts a lot like a numpy array, - because we delegate attributes through to the numpy array - - Args: - parameter (Optional[Parameter]): The parameter whose values will - populate this array, if any. Will copy ``name``, ``full_name``, - ``label``, ``unit``, and ``snapshot`` from here unless you - provide them explicitly. - - name (Optional[str]): The short name of this array. - TODO: use full_name as name, and get rid of short name - - full_name (Optional[str]): The complete name of this array. If the - array is based on a parameter linked to an instrument, this is - typically '_' - - label (Optional[str]): A description of the values in this array to - use for axis and colorbar labels on plots. - - snapshot (Optional[dict]): Metadata snapshot to save with this array. - - array_id (Optional[str]): A name for this array that's unique within - its ``DataSet``. Typically the full_name, but when the ``DataSet`` - is constructed we will append '_' (``i`` is an integer starting - from 1) if necessary to differentiate arrays with the same id. - TODO: this only happens for arrays provided to the DataSet - constructor, not those added with add_array. Fix this! - Also, do we really need array_id *and* full_name (let alone name - but I've already said we should remove this)? - - set_arrays (Optional[Tuple[DataArray]]): If this array is being - created with shape already, you can provide one setpoint array - per dimension. The first should have one dimension, the second - two dimensions, etc. - - shape (Optional[Tuple[int]]): The shape (as in numpy) of the array. - Will be prepended with new dimensions by any calls to ``nest``. - - action_indices (Optional[Tuple[int]]): If used within a ``Loop``, - these are the indices at each level of nesting within the - ``Loop`` of the loop action that's populating this array. - TODO: this shouldn't be in DataArray at all, the loop should - handle converting this to array_id internally (maybe it - already does?) - - unit (Optional[str]): The unit of the values stored in this array. - - units (Optional[str]): DEPRECATED, redirects to ``unit``. - - is_setpoint (bool): True if this is a setpoint array, False if it - is measured. Default False. - - preset_data (Optional[Union[numpy.ndarray, Sequence]]): Contents of the - array, if already known (for example if this is a setpoint - array). ``shape`` will be inferred from this array instead of - from the ``shape`` argument. - """ - - # attributes of self to include in the snapshot - SNAP_ATTRS = ( - 'array_id', - 'name', - 'shape', - 'unit', - 'label', - 'action_indices', - 'is_setpoint') - - # attributes of the parameter (or keys in the incoming snapshot) - # to copy to DataArray attributes, if they aren't set some other way - COPY_ATTRS_FROM_INPUT = ( - 'name', - 'label', - 'unit') - - # keys in the parameter snapshot to omit from our snapshot - SNAP_OMIT_KEYS = ( - 'ts', - 'value', - '__class__', - 'set_arrays', - 'shape', - 'array_id', - 'action_indices') - - def __init__(self, parameter=None, name=None, full_name=None, label=None, - snapshot=None, array_id=None, set_arrays=(), shape=None, - action_indices=(), unit=None, units=None, is_setpoint=False, - preset_data=None): - self.name = name - self.full_name = full_name or name - self.label = label - self.shape = shape - if units is not None: - _LOG.warning( - f"`units` is deprecated for the " - f"`DataArray` class, use `unit` instead. {self!r}" - ) - if unit is None: - unit = units - self.unit = unit - self.array_id = array_id - self.is_setpoint = is_setpoint - self.action_indices = action_indices - self.set_arrays = set_arrays - - self._preset = False - - # store a reference up to the containing DataSet - # this also lets us make sure a DataArray is only in one DataSet - self._data_set = None - - self.last_saved_index = None - self.modified_range = None - - self.ndarray = None - if snapshot is None: - snapshot = {} - self._snapshot_input = {} - - if parameter is not None: - param_full_name = getattr(parameter, 'full_name', None) - if param_full_name and not full_name: - self.full_name = parameter.full_name - - if hasattr(parameter, 'snapshot') and not snapshot: - snapshot = parameter.snapshot() - else: - # TODO: why is this in an else clause? - for attr in self.COPY_ATTRS_FROM_INPUT: - if (hasattr(parameter, attr) and - not getattr(self, attr, None)): - setattr(self, attr, getattr(parameter, attr)) - - for key, value in snapshot.items(): - if key not in self.SNAP_OMIT_KEYS: - self._snapshot_input[key] = value - - if (key in self.COPY_ATTRS_FROM_INPUT and - not getattr(self, key, None)): - setattr(self, key, value) - - if not self.label: - self.label = self.name - - if preset_data is not None: - self.init_data(preset_data) - elif shape is None: - self.shape = () - - @property - def data_set(self): - """ - The DataSet this array belongs to. - - A DataArray can belong to at most one DataSet. - TODO: make this a weakref - """ - return self._data_set - - @data_set.setter - def data_set(self, new_data_set): - if (self._data_set is not None and - new_data_set is not None and - self._data_set != new_data_set): - raise RuntimeError('A DataArray can only be part of one DataSet') - self._data_set = new_data_set - - def nest(self, size, action_index=None, set_array=None): - """ - Nest this array inside a new outer loop. - - You cannot call ``nest`` after ``init_data`` unless this is a - setpoint array. - TODO: is this restriction really useful? And should we maintain - a distinction between _preset and is_setpoint, or can wejust use - is_setpoint? - - Args: - size (int): Length of the new loop. - - action_index (Optional[int]): Within the outer loop at this - nesting level, which action does this array derive from? - - set_array (Optional[DataArray]): The setpoints of the new outer - loop. If this DataArray *is* a setpoint array, you should - omit both ``action_index`` and ``set_array``, and it will - reference itself as the inner setpoint array. - - Returns: - DataArray: self, in case you want to construct the array with - chained method calls. - """ - if self.ndarray is not None and not self._preset: - raise RuntimeError('Only preset arrays can be nested after data ' - 'is initialized! {}'.format(self)) - - if set_array is None: - if self.set_arrays: - raise TypeError('a setpoint array must be its own inner loop') - set_array = self - - self.shape = (size, ) + self.shape - - if action_index is not None: - self.action_indices = (action_index, ) + self.action_indices - - self.set_arrays = (set_array, ) + self.set_arrays - - if self._preset: - inner_data = self.ndarray - self.ndarray = np.ndarray(self.shape) - # existing preset array copied to every index of the nested array. - for i in range(size): - self.ndarray[i] = inner_data - - # update modified_range so the entire array still looks modified - self.modified_range = (0, self.ndarray.size - 1) - - self._set_index_bounds() - - return self - - def init_data(self, data=None): - """ - Create the actual numpy array to hold data. - - The array will be sized based on either ``self.shape`` or - data provided here. - - Idempotent: will do nothing if the array already exists. - - If data is provided, this array is marked as a preset - meaning it can still be nested around this data. - TODO: per above, perhaps remove this distinction entirely? - - Args: - data (Optional[Union[numpy.ndarray, Sequence]]): If provided, - we fill the array with this data. Otherwise the new - array will be filled with NaN. - - Raises: - ValueError: if ``self.shape`` does not match ``data.shape`` - ValueError: if the array was already initialized with a - different shape than we're about to create - """ - if data is not None: - if not isinstance(data, np.ndarray): - if isinstance(data, collections.abc.Iterator): - # faster than np.array(tuple(data)) (or via list) - # but requires us to assume float - data = np.fromiter(data, float) - else: - data = np.array(data) - - if self.shape is None: - self.shape = data.shape - elif data.shape != self.shape: - raise ValueError('preset data must be a sequence ' - 'with shape matching the array shape', - data.shape, self.shape) - self.ndarray = data - self._preset = True - - # mark the entire array as modified - self.modified_range = (0, data.size - 1) - - elif self.ndarray is not None: - if self.ndarray.shape != self.shape: - raise ValueError('data has already been initialized, ' - 'but its shape doesn\'t match self.shape') - return - else: - self.ndarray = np.ndarray(self.shape) - self.clear() - self._set_index_bounds() - - def _set_index_bounds(self): - self._min_indices = [0 for d in self.shape] - self._max_indices = [d - 1 for d in self.shape] - - def clear(self): - """Fill the (already existing) data array with nan.""" - # only floats can hold nan values. I guess we could - # also raise an error in this case? But generally float is - # what people want anyway. - if self.ndarray.dtype != float: - self.ndarray = self.ndarray.astype(float) - self.ndarray.fill(float('nan')) - - def __setitem__(self, loop_indices, value): - """ - Set data values. - - Follows numpy syntax, allowing indices of lower dimensionality than - the array, if value makes up the extra dimension(s) - - Also update the record of modifications to the array. If you don't - want this overhead, you can access ``self.ndarray`` directly. - """ - if isinstance(loop_indices, collections.abc.Iterable): - min_indices = list(loop_indices) - max_indices = list(loop_indices) - else: - min_indices = [loop_indices] - max_indices = [loop_indices] - - for i, index in enumerate(min_indices): - if isinstance(index, slice): - start, stop, step = index.indices(self.shape[i]) - min_indices[i] = start - max_indices[i] = start + ( - ((stop - start - 1)//step) * step) - - min_li = self.flat_index(min_indices, self._min_indices) - max_li = self.flat_index(max_indices, self._max_indices) - self._update_modified_range(min_li, max_li) - - self.ndarray.__setitem__(loop_indices, value) - - def __getitem__(self, loop_indices): - return self.ndarray[loop_indices] - - delegate_attr_objects = ['ndarray'] - - def __len__(self): - """ - Array length. - - Must be explicitly delegated, because len() will look for this - attribute to already exist. - """ - return len(self.ndarray) - - def flat_index(self, indices, index_fill=None): - """ - Generate the raveled index for the given indices. - - This is the index you would have if the array is reshaped to 1D, - looping over the indices from inner to outer. - - Args: - indices (Sequence): indices of an element or slice of this array. - - index_fill (Optional[Sequence]): extra indices to use if - ``indices`` has less dimensions than the array, ie it points - to a slice rather than a single element. Use zeros to get the - beginning of this slice, and [d - 1 for d in shape] to get the - end of the slice. - - Returns: - int: the resulting flat index. - """ - if len(indices) < len(self.shape): - indices = indices + index_fill[len(indices):] - return np.ravel_multi_index(tuple(zip(indices)), self.shape)[0] - - def _update_modified_range(self, low, high): - if self.modified_range: - self.modified_range = (min(self.modified_range[0], low), - max(self.modified_range[1], high)) - else: - self.modified_range = (low, high) - - def mark_saved(self, last_saved_index): - """ - Mark certain outstanding modifications as saved. - - Args: - last_saved_index (int): The flat index of the last point - saved. If ``modified_range`` extends beyond this, the - data past ``last_saved_index`` will still be marked - modified, otherwise ``modified_range`` is cleared - entirely. - """ - if self.modified_range: - if last_saved_index >= self.modified_range[1]: - self.modified_range = None - else: - self.modified_range = (max(self.modified_range[0], - last_saved_index + 1), - self.modified_range[1]) - self.last_saved_index = last_saved_index - - def clear_save(self): - """ - Make previously saved parts of this array look unsaved (modified). - - This can be used to force overwrite or rewrite, like if we're - moving or copying the ``DataSet``. - """ - if self.last_saved_index is not None: - self._update_modified_range(0, self.last_saved_index) - - self.last_saved_index = None - - def get_synced_index(self): - """ - Get the last index which has been synced from the server. - - Will also initialize the array if this hasn't happened already. - TODO: seems hacky to init_data here. - - Returns: - int: the last flat index which has been synced from the server, - or -1 if no data has been synced. - """ - if not hasattr(self, 'synced_index'): - self.init_data() - self.synced_index = -1 - - return self.synced_index - - def get_changes(self, synced_index): - """ - Find changes since the last sync of this array. - - Args: - synced_index (int): The last flat index which has already - been synced. - - Returns: - Union[dict, None]: None if there is no new data. If there is, - returns a dict with keys: - start (int): the flat index of the first returned value. - stop (int): the flat index of the last returned value. - vals (List[float]): the new values - """ - latest_index = self.last_saved_index - if latest_index is None: - latest_index = -1 - if self.modified_range: - latest_index = max(latest_index, self.modified_range[1]) - - vals = [ - self.ndarray[np.unravel_index(i, self.ndarray.shape)] - for i in range(synced_index + 1, latest_index + 1) - ] - - if vals: - return { - 'start': synced_index + 1, - 'stop': latest_index, - 'vals': vals - } - - def apply_changes(self, start, stop, vals): - """ - Insert new synced values into the array. - - To be be called in a ``PULL_FROM_SERVER`` ``DataSet`` using results - returned by ``get_changes`` from the ``DataServer``. - - TODO: check that vals has the right length? - - Args: - start (int): the flat index of the first new value. - stop (int): the flat index of the last new value. - vals (List[float]): the new values - """ - for i, val in enumerate(vals): - index = np.unravel_index(i + start, self.ndarray.shape) - self.ndarray[index] = val - self.synced_index = stop - - def __repr__(self): - array_id_or_none = f' {self.array_id}' if self.array_id else '' - return '{}[{}]:{}\n{}'.format(self.__class__.__name__, - ','.join(map(str, self.shape)), - array_id_or_none, repr(self.ndarray)) - - def snapshot(self, update=False): - """JSON representation of this DataArray.""" - snap = {'__class__': full_class(self)} - - snap.update(self._snapshot_input) - - for attr in self.SNAP_ATTRS: - snap[attr] = getattr(self, attr) - - return snap - - def fraction_complete(self): - """ - Get the fraction of this array which has data in it. - - Or more specifically, the fraction of the latest point in the array - where we have touched it. - - Returns: - float: fraction of array which is complete, from 0.0 to 1.0 - """ - if self.ndarray is None: - return 0.0 - - last_index = -1 - if self.last_saved_index is not None: - last_index = max(last_index, self.last_saved_index) - if self.modified_range is not None: - last_index = max(last_index, self.modified_range[1]) - if getattr(self, 'synced_index', None) is not None: - last_index = max(last_index, self.synced_index) - - return (last_index + 1) / self.ndarray.size - - def to_xarray(self) -> "xr.DataArray": - """ Return this DataArray as an xarray dataarray - - Returns: - DataArray in xarray format - """ - import xarray as xr - xarray_dictionary = data_array_to_xarray_dictionary(self) - xarray_dataarray = xr.DataArray.from_dict(xarray_dictionary) - return xarray_dataarray - - @classmethod - def from_xarray( - cls, xarray_dataarray: "xr.DataArray", array_id: Optional[str] = None - ) -> "DataArray": - """Create a DataArray from an xarray DataArray - - Args: - array_id: Array id for the new DataArray. If None, then use the first data variable from the argument - Returns: - Created xarray DataArray - """ - xarray_dict = xarray_dataarray.to_dict() - if array_id is None: - array_id = list(xarray_dict['dims'])[0] - data_array = xarray_data_array_dictionary_to_data_array(array_id, xarray_dict, is_setpoint=False) - return data_array - - -def data_array_to_xarray_dictionary(data_array: DataArray) -> Dict[str, Any]: - """Convert DataArray to a dictionary in xarray format. - - Args: - data_array: The DataArray to convert. - - Returns: - dict: A dictionary containing the data in xarray format. - """ - key_mapping = {"unit": "units", "name": "name", "label": "long_name"} - - data_dictionary: Dict[str, Any] = {"name": data_array.array_id} - data_dictionary["attrs"] = { - target_key: getattr(data_array, key) for key, target_key in key_mapping.items() - } - if data_array.is_setpoint: - data_dictionary["dims"] = tuple([data_array.array_id]) - data_dictionary["depends_on"] = data_dictionary["dims"] - data = data_array.ndarray - # flatten data, assumes setpoint is uniform as for a normal gridded dataset - while len(data.shape) > 1: - data = data[0, ..., :] - data_dictionary["data"] = data - else: - if data_array.set_arrays: - data_dictionary["dims"] = tuple(a.array_id for a in data_array.set_arrays) - data_dictionary["depends_on"] = data_dictionary["dims"] - data_dictionary["data"] = data_array.ndarray - - return data_dictionary - - -def xarray_data_array_dictionary_to_data_array( - array_id: str, array_dictionary: Dict[str, Any], is_setpoint: bool = False, preset_data=None): - """Convert xarray dictionary to a DataArray - - This conversion is for bith the data array and the the internal xarray structure, e.g. the datavars and coords. - Args: - array_id: Create the new DataArray with this id - array_dictionary: Data to convert - is_setpoint: Passed to the DataArray constructor - preset_data: If None use the data from the dictionary, otherwise use the specified data. - - Returns: - dict: A dictionary containing the data in xarray format. - """ - if preset_data is None: - preset_data = np.array(array_dictionary["data"]) - array_name = array_dictionary.get("name", array_id) - - array_full_name = array_dictionary["attrs"].get("long_name", array_name) - data_array = DataArray( - name=array_name, - full_name=array_full_name, - label=array_dictionary["attrs"].get("long_name", ""), - unit=array_dictionary["attrs"].get("units", None), - is_setpoint=is_setpoint, - shape=preset_data.shape, - array_id=array_id, - preset_data=preset_data, +try: + from qcodes_loop.data.data_array import ( + DataArray, + data_array_to_xarray_dictionary, + xarray_data_array_dictionary_to_data_array, ) - return data_array +except ImportError as e: + raise ImportError( + "qcodes.data.data_array is deprecated and has moved to " + "the package `qcodes_loop`. Please install qcodes_loop directly or " + "with `pip install qcodes[loop]" + ) from e + +issue_deprecation_warning( + "qcodes.data.data_array module", alternative="qcodes_loop.data.data_array" +) diff --git a/qcodes/data/data_set.py b/qcodes/data/data_set.py index 14e926e1132..ed03c34c8d1 100644 --- a/qcodes/data/data_set.py +++ b/qcodes/data/data_set.py @@ -10,820 +10,39 @@ import numpy as np -if TYPE_CHECKING: - import xarray as xr - -from qcodes.data.data_array import ( - DataArray, - data_array_to_xarray_dictionary, - xarray_data_array_dictionary_to_data_array, +from qcodes.utils import ( + DelegateAttributes, + deep_update, + full_class, + issue_deprecation_warning, ) -from qcodes.utils import DelegateAttributes, deep_update, full_class - -from .gnuplot_format import GNUPlotFormat -from .io import DiskIO -from .location import FormatLocation - -log = logging.getLogger(__name__) - - -def new_data(location=None, loc_record=None, name=None, overwrite=False, - io=None, **kwargs): - """ - Create a new DataSet. - - Args: - location (Optional[Union[str,Callable, Bool]]): If you provide a string, - it must be an unused location in the io manager. Can also be: - - - a Callable ``location provider`` with one required parameter - (the io manager), and one optional (``record`` dict), - which returns a location string when called - - ``False`` - denotes an only-in-memory temporary DataSet. - - Note that the full path to or physical location of the data is a - combination of io + location. the default ``DiskIO`` sets the base - directory, which this location is a relative path inside. - Default ``DataSet.location_provider`` which is initially - ``FormatLocation()`` - - loc_record (Optional[dict]): If location is a callable, this will be - passed to it as ``record`` - - name (Optional[str]): overrides the ``name`` key in the ``loc_record``. - - overwrite (bool): Are we allowed to overwrite an existing location? - Default False. - - io (Optional[io_manager]): base physical location of the ``DataSet``. - Default ``DataSet.default_io`` is initially ``DiskIO('.')`` which - says the root data directory is the current working directory, ie - where you started the python session. - - arrays (Optional[List[qcodes.data.data_array.DataArray]): arrays to add - to the DataSet. Can be added later with ``self.add_array(array)``. - - formatter (Optional[Formatter]): sets the file format/structure to - write (and read) with. Default ``DataSet.default_formatter`` which - is initially ``GNUPlotFormat()``. - - write_period (Optional[float]): seconds - between saves to disk. - Returns: - A new ``DataSet`` object ready for storing new data in. - """ - if io is None: - io = DataSet.default_io - - if name is not None: - if not loc_record: - loc_record = {} - loc_record['name'] = name - - if location is None: - location = DataSet.location_provider - - if callable(location): - location = location(io, record=loc_record) - - if location and (not overwrite) and io.list(location): - raise FileExistsError('"' + location + '" already has data') - - return DataSet(location=location, io=io, **kwargs) - - -def load_data(location=None, formatter=None, io=None): - """ - Load an existing DataSet. - - Args: - location (Optional[str]): the location to load from. Default is the - current live DataSet. - Note that the full path to or physical location of the data is a - combination of io + location. the default ``DiskIO`` sets the base - directory, which this location is a relative path inside. - - formatter (Optional[Formatter]): sets the file format/structure to - read with. Default ``DataSet.default_formatter`` which - is initially ``GNUPlotFormat()``. - - io (Optional[io_manager]): base physical location of the ``DataSet``. - Default ``DataSet.default_io`` is initially ``DiskIO('.')`` which - says the root data directory is the current working directory, ie - where you started the python session. - - Returns: - A new ``DataSet`` object loaded with pre-existing data. - """ - if location is False: - raise ValueError('location=False means a temporary DataSet, ' - 'which is incompatible with load_data') - - data = DataSet(location=location, formatter=formatter, io=io) - data.read_metadata() - data.read() - return data - - -class DataSet(DelegateAttributes): - - """ - A container for one complete measurement loop. - - May contain many individual arrays with potentially different - sizes and dimensionalities. - - Normally a DataSet should not be instantiated directly, but through - ``new_data`` or ``load_data``. - - Args: - location (Union[str,bool]): A location in the io manager, or ``False`` - for an only-in-memory temporary DataSet. - Note that the full path to or physical location of the data is a - combination of io + location. the default ``DiskIO`` sets the base - directory, which this location is a relative path inside. - - io (Optional[io_manager]): base physical location of the ``DataSet``. - Default ``DataSet.default_io`` is initially ``DiskIO('.')`` which - says the root data directory is the current working directory, ie - where you started the python session. - - arrays (Optional[List[qcodes.data.data_array.DataArray]): arrays to add - to the DataSet. Can be added later with ``self.add_array(array)``. - - formatter (Optional[Formatter]): sets the file format/structure to - write (and read) with. Default ``DataSet.default_formatter`` which - is initially ``GNUPlotFormat()``. - - write_period (Optional[float]): Only if ``mode=LOCAL``, seconds - between saves to disk. If not ``LOCAL``, the ``DataServer`` handles - this and generally writes more often. Use None to disable writing - from calls to ``self.store``. Default 5. - """ - - # ie data_set.arrays['vsd'] === data_set.vsd - delegate_attr_dicts = ['arrays'] - - default_io = DiskIO('.') - default_formatter = GNUPlotFormat() - location_provider = FormatLocation() - - background_functions: dict[str, Callable[..., Any]] = OrderedDict() - """ - The value ``fn`` is a callable accepting no - arguments, and ``key`` is a name to identify the function and help - you attach and remove it. - - In ``DataSet.complete`` we call each of these periodically, in the - order that they were attached. - - Note that because this is a class attribute, the functions will - apply to every DataSet. If you want specific functions for one - DataSet you can override this with an instance attribute. - """ - - def __init__(self, location=None, arrays=None, formatter=None, io=None, - write_period=5): - if location is False or isinstance(location, str): - self.location = location - else: - raise ValueError('unrecognized location ' + repr(location)) - - # TODO: when you change formatter or io (and there's data present) - # make it all look unsaved - self.formatter = formatter or self.default_formatter - self.io = io or self.default_io - - self.write_period = write_period - self.last_write = 0 - self.last_store = -1 - - self.metadata = {} - - self.arrays = _PrettyPrintDict() - if arrays: - self.action_id_map = self._clean_array_ids(arrays) - for array in arrays: - self.add_array(array) - - if self.arrays: - for array in self.arrays.values(): - array.init_data() - - def sync(self): - """ - Synchronize this DataSet with the DataServer or storage. - - If this DataSet is on the server, asks the server for changes. - If not, reads the entire DataSet from disk. - - Returns: - bool: True if this DataSet is live on the server - """ - # TODO: sync implies bidirectional... and it could be! - # we should keep track of last sync timestamp and last modification - # so we can tell whether this one, the other one, or both copies have - # changed (and I guess throw an error if both did? Would be cool if we - # could find a robust and intuitive way to make modifications to the - # version on the DataServer from the main copy) - - # LOCAL DataSet - no need to sync just use local data - return False - - def fraction_complete(self): - """ - Get the fraction of this DataSet which has data in it. - - Returns: - float: the average of all measured (not setpoint) arrays' - ``fraction_complete()`` values, independent of the individual - array sizes. If there are no measured arrays, returns zero. - """ - array_count, total = 0, 0 - - for array in self.arrays.values(): - if not array.is_setpoint: - array_count += 1 - total += array.fraction_complete() - - return total / (array_count or 1) - - def complete(self, delay=1.5): - """ - Periodically sync the DataSet and display percent complete status. - - Also, each period, execute functions stored in (class attribute) - ``self.background_functions``. If a function fails, we log its - traceback and continue on. If any one function fails twice in - a row, it gets removed. - - Args: - delay (float): seconds between iterations. Default 1.5 - """ - log.info( - f'waiting for DataSet <{self.location}> to complete') - - failing = {key: False for key in self.background_functions} - - completed = False - while True: - log.info('DataSet: {:.0f}% complete'.format( - self.fraction_complete() * 100)) - - # first check if we're done - if self.sync() is False: - completed = True - - # then even if we *are* done, execute the background functions - # because we want things like live plotting to get the final data - for key, fn in list(self.background_functions.items()): - try: - log.debug(f"calling {key}: {repr(fn)}") - fn() - failing[key] = False - except Exception: - log.info(format_exc()) - if failing[key]: - log.warning( - 'background function {} failed twice in a row, ' - 'removing it'.format(key)) - del self.background_functions[key] - failing[key] = True - - if completed: - break - - # but only sleep if we're not already finished - time.sleep(delay) - - log.info(f'DataSet <{self.location}> is complete') - - def get_changes(self, synced_indices): - """ - Find changes since the last sync of this DataSet. - - Args: - synced_indices (dict): ``{array_id: synced_index}`` where - synced_index is the last flat index which has already - been synced, for any (usually all) arrays in the DataSet. - - Returns: - Dict[dict]: keys are ``array_id`` for each array with changes, - values are dicts as returned by ``DataArray.get_changes`` - and required as kwargs to ``DataArray.apply_changes``. - Note that not all arrays in ``synced_indices`` need be - present in the return, only those with changes. - """ - changes = {} - - for array_id, synced_index in synced_indices.items(): - array_changes = self.arrays[array_id].get_changes(synced_index) - if array_changes: - changes[array_id] = array_changes - - return changes - - def add_array(self, data_array): - """ - Add one DataArray to this DataSet, and mark it as part of this DataSet. - - Note: DO NOT just set ``data_set.arrays[id] = data_array``, because - this will not check if we are overwriting another array, nor set the - reference back to this DataSet, nor that the ``array_id`` in the array - matches how you're storing it here. - - Args: - data_array (DataArray): the new array to add - - Raises: - ValueError: if there is already an array with this id here. - """ - # TODO: mask self.arrays so you *can't* set it directly? - - if data_array.array_id in self.arrays: - raise ValueError('array_id {} already exists in this ' - 'DataSet'.format(data_array.array_id)) - self.arrays[data_array.array_id] = data_array - - # back-reference to the DataSet - data_array.data_set = self - - def remove_array(self, array_id): - """ Remove an array from a dataset - - Throws an exception when the array specified is refereced by other - arrays in the dataset. - - Args: - array_id (str): array_id of array to be removed - """ - for a in self.arrays: - sa = self.arrays[a].set_arrays - if array_id in [a.array_id for a in sa]: - raise Exception( - 'cannot remove array %s as it is referenced by a' % array_id) - _ = self.arrays.pop(array_id) - self.action_id_map = self._clean_array_ids(self.arrays.values()) - - def _clean_array_ids(self, arrays): - """ - replace action_indices tuple with compact string array_ids - stripping off as much extraneous info as possible - """ - action_indices = [array.action_indices for array in arrays] - for array in arrays: - name = array.full_name - if array.is_setpoint and name and not name.endswith('_set'): - name += '_set' - - array.array_id = name - array_ids = {array.array_id for array in arrays} - for name in array_ids: - param_arrays = [array for array in arrays - if array.array_id == name] - self._clean_param_ids(param_arrays, name) - - array_ids = [array.array_id for array in arrays] - - return dict(zip(action_indices, array_ids)) - def _clean_param_ids(self, arrays, name): - # strip off as many leading equal indices as possible - # and append the rest to the back of the name with underscores - param_action_indices = [list(array.action_indices) for array in arrays] - while all(len(ai) for ai in param_action_indices): - if len({ai[0] for ai in param_action_indices}) == 1: - for ai in param_action_indices: - ai[:1] = [] - else: - break - for array, ai in zip(arrays, param_action_indices): - array.array_id = name + ''.join('_' + str(i) for i in ai) - - def store(self, loop_indices, ids_values): - """ - Insert data into one or more of our DataArrays. - - Args: - loop_indices (tuple): the indices within whatever loops we are - inside. May have fewer dimensions than some of the arrays - we are inserting into, if the corresponding value makes up - the remaining dimensionality. - values (Dict[Union[float, Sequence]]): a dict whose keys are - array_ids, and values are single numbers or entire slices - to insert into that array. - """ - for array_id, value in ids_values.items(): - self.arrays[array_id][loop_indices] = value - self.last_store = time.time() - if (self.write_period is not None and - time.time() > self.last_write + self.write_period): - log.debug('Attempting to write') - self.write() - self.last_write = time.time() - # The below could be useful but as it writes at every single - # step of the loop its too verbose even at debug - # else: - # log.debug('.store method: This is not the right time to write') - - def default_parameter_name(self, paramname: str | None = None) -> str | None: - """Return name of default parameter for plotting - - The default parameter is determined by looking into - metdata['default_parameter_name']. If this variable is not present, - then the closest match to the argument paramname is tried. - - Args: - paramname: Name to match to parameter name - - Returns: - Name of the default parameter, or None if no parameter is found - """ - arraynames = self.arrays.keys() - - # overrule parameter name from the metadata - if self.metadata.get('default_parameter_name', False): - paramname = self.metadata['default_parameter_name'] - - # try to return the exact name - if paramname in arraynames: - return paramname - - # try find something similar - if paramname is not None: - vv = [v for v in arraynames if v.endswith(paramname)] - if len(vv) > 0: - return vv[0] - vv = [v for v in arraynames if v.startswith(paramname)] - if len(vv) > 0: - return vv[0] - - # try to get the first non-setpoint array - vv = [v for v in arraynames if not self.arrays[v].is_setpoint] - if (len(vv) > 0): - return sorted(vv)[0] - - # fallback: any array found - try: - name = sorted(list(arraynames))[0] - return name - except IndexError: - pass - return None - - def default_parameter_array(self, paramname='amplitude'): - """ Return default parameter array - - Args: - paramname (str): Name to match to parameter name. - Defaults to 'amplitude' - - Returns: - DataArray: array corresponding to the default parameter - - See also: - default_parameter_name - - """ - paramname = self.default_parameter_name(paramname=paramname) - return getattr(self, paramname, None) - - def read(self): - """Read the whole DataSet from storage, overwriting the local data.""" - if self.location is False: - return - self.formatter.read(self) - - def read_metadata(self): - """Read the metadata from storage, overwriting the local data.""" - if self.location is False: - return - self.formatter.read_metadata(self) - - def write(self, write_metadata=False, only_complete=True, filename=None): - """ - Writes updates to the DataSet to storage. - N.B. it is recommended to call data_set.finalize() when a DataSet is - no longer expected to change to ensure files get closed - - Args: - write_metadata (bool): write the metadata to disk - only_complete (bool): passed on to the match_save_range inside - self.formatter.write. Used to ensure that all new data gets - saved even when some columns are strange. - filename (Optional[str]): The filename (minus extension) to use. - The file gets saved in the usual location. - """ - if self.location is False: - return - - # Only the gnuplot formatter has a "filename" kwarg - if isinstance(self.formatter, GNUPlotFormat): - self.formatter.write(self, - self.io, - self.location, - write_metadata=write_metadata, - only_complete=only_complete, - filename=filename) - else: - self.formatter.write(self, - self.io, - self.location, - write_metadata=write_metadata, - only_complete=only_complete) - - def write_copy(self, path=None, io_manager=None, location=None): - """ - Write a new complete copy of this DataSet to storage. - - Args: - path (Optional[str]): An absolute path on this system to write to. - If you specify this, you may not include either ``io_manager`` - or ``location``. - - io_manager (Optional[io_manager]): A new ``io_manager`` to use with - either the ``DataSet``'s same or a new ``location``. - - location (Optional[str]): A new ``location`` to write to, using - either this ``DataSet``'s same or a new ``io_manager``. - """ - if io_manager is not None or location is not None: - if path is not None: - raise TypeError('If you provide io_manager or location ' - 'to write_copy, you may not provide path.') - if io_manager is None: - io_manager = self.io - elif location is None: - location = self.location - elif path is not None: - io_manager = DiskIO(None) - location = path - else: - raise TypeError('You must provide at least one argument ' - 'to write_copy') - - if location is False: - raise ValueError('write_copy needs a location, not False') - - lsi_cache = {} - mr_cache = {} - for array_id, array in self.arrays.items(): - lsi_cache[array_id] = array.last_saved_index - mr_cache[array_id] = array.modified_range - # array.clear_save() is not enough, we _need_ to set modified_range - # TODO - identify *when* clear_save is not enough, and fix it - # so we *can* use it. That said, maybe we will *still* want to - # use the full array here no matter what, or strip trailing NaNs - # separately, either here or in formatter.write? - array.last_saved_index = None - array.modified_range = (0, array.ndarray.size - 1) - - try: - self.formatter.write(self, io_manager, location, force_write=True) - self.snapshot() - self.formatter.write_metadata(self, io_manager, location, - read_first=False) - finally: - for array_id, array in self.arrays.items(): - array.last_saved_index = lsi_cache[array_id] - array.modified_range = mr_cache[array_id] - - def add_metadata(self, new_metadata): - """ - Update DataSet.metadata with additional data. - - Args: - new_metadata (dict): new data to be deep updated into - the existing metadata - """ - deep_update(self.metadata, new_metadata) - - def save_metadata(self): - """Evaluate and save the DataSet's metadata.""" - if self.location is not False: - self.snapshot() - self.formatter.write_metadata(self, self.io, self.location) - - def finalize(self, filename=None, write_metadata=True): - """ - Mark the DataSet complete and write any remaining modifications. - - Also closes the data file(s), if the ``Formatter`` we're using - supports that. - - Args: - filename (Optional[str]): The file name (minus extension) to - write to. The location of the file is the usual one. - write_metadata (bool): Whether to save a snapshot. For e.g. dumping - raw data inside a loop, a snapshot is not wanted. - """ - log.debug('Finalising the DataSet. Writing.') - # write all new data, not only (to?) complete columns - self.write(only_complete=False, filename=filename) - - if hasattr(self.formatter, 'close_file'): - self.formatter.close_file(self) - - if write_metadata: - self.save_metadata() - - def snapshot(self, update=False): - """JSON state of the DataSet.""" - array_snaps = {} - for array_id, array in self.arrays.items(): - array_snaps[array_id] = array.snapshot(update=update) - - self.metadata.update({ - '__class__': full_class(self), - 'location': self.location, - 'arrays': array_snaps, - 'formatter': full_class(self.formatter), - 'io': repr(self.io) - }) - return deepcopy(self.metadata) - - def get_array_metadata(self, array_id): - """ - Get the metadata for a single contained DataArray. - - Args: - array_id (str): the array to get metadata for. - - Returns: - dict: metadata for this array. - """ - try: - return self.metadata['arrays'][array_id] - except (AttributeError, KeyError): - return None - - def __repr__(self): - """Rich information about the DataSet and contained arrays.""" - out = type(self).__name__ + ':' - - attrs = [['location', repr(self.location)]] - attr_template = '\n {:8} = {}' - for var, val in attrs: - out += attr_template.format(var, val) - - arr_info = [['', '', '', '']] - - if hasattr(self, 'action_id_map'): - id_items = [ - item for index, item in sorted(self.action_id_map.items())] - else: - id_items = self.arrays.keys() - - for array_id in id_items: - array = self.arrays[array_id] - setp = 'Setpoint' if array.is_setpoint else 'Measured' - name = array.name or 'None' - array_id = array_id or 'None' - arr_info.append([setp, array_id, name, repr(array.shape)]) - - column_lengths = [max(len(row[i]) for row in arr_info) - for i in range(len(arr_info[0]))] - out_template = ('\n ' - '{info[0]:{lens[0]}} | {info[1]:{lens[1]}} | ' - '{info[2]:{lens[2]}} | {info[3]}') - - for arr_info_i in arr_info: - out += out_template.format(info=arr_info_i, lens=column_lengths) - - return out - - def to_xarray(self) -> xr.Dataset: - """ Convert the dataset to an xarray Dataset """ - return qcodes_dataset_to_xarray_dataset(self) - - @classmethod - def from_xarray(cls, xarray_dataset: xr.Dataset) -> DataSet: - """ Convert the dataset to an xarray DataSet """ - return xarray_dataset_to_qcodes_dataset(xarray_dataset) - - -class _PrettyPrintDict(Dict[Any, Any]): - """ - simple wrapper for a dict to repr its items on separate lines - with a bit of indentation - """ - - def __repr__(self): - body = "\n ".join( - repr(k) + ": " + self._indent(repr(v)) for k, v in self.items() - ) - return "{\n " + body + "\n}" - - def _indent(self, s): - lines = s.split('\n') - return '\n '.join(lines) - - -def dataset_to_xarray_dictionary( - data_set: DataSet, include_metadata: bool = True -) -> dict[str, Any]: - """Convert QcodesDataSet to dictionary. - - Args: - data_set: The data to convert. - include_data: If True then include the ndarray field. - include_metadata: If True then include the metadata. - - Returns: - Dictionary containing the serialized data. - """ - data_dictionary: dict[str, Any] = { - "dims": {}, - "attrs": {}, - "coords": {}, - "data_vars": {}, - } - - pa = data_set.default_parameter_array() - dimensions = [(a.array_id, a.size) for a in pa.set_arrays] - data_dictionary["dims"] = dict(dimensions) - - for array_id in [item[0] for item in dimensions]: - data_array = data_set.arrays[array_id] - data_dictionary["coords"][array_id] = data_array_to_xarray_dictionary( - data_array - ) - - for array_id, data_array in data_set.arrays.items(): - if not data_array.is_setpoint: - data_dictionary["data_vars"][array_id] = data_array_to_xarray_dictionary( - data_array - ) - - if include_metadata: - data_dictionary["attrs"]["metadata"] = data_set.metadata - data_dictionary["attrs"]["qcodes_location"] = data_set.location - - return data_dictionary - - -def qcodes_dataset_to_xarray_dataset( - data_set: DataSet, -) -> xr.Dataset: - """ Convert QCoDeS gridded dataset to xarray dataset """ - import xarray as xr - - xarray_dictionary = dataset_to_xarray_dictionary(data_set) - xarray_dataset = xr.Dataset.from_dict(xarray_dictionary) - return xarray_dataset - - -def xarray_dictionary_to_dataset( - xarray_dictionary: dict[str, Any], -) -> DataSet: - """Convert xarray dictionary to Qcodes DataSet. - - Args: - xarray_dictionary: data to convert - - Returns: - QCoDeS dataSet with converted data. - """ - dataset = new_data() - dataset.metadata.update(xarray_dictionary["attrs"]) - - grid_coords: list[Any] = [] - set_array_names = [] - - coordinate_names = list(xarray_dictionary["data_vars"].values())[0]["dims"] - - assert set(coordinate_names) == set(xarray_dictionary["coords"]), ( - "conversion to qcodes requires all coordinates" - + " to match data variable coordinates" +try: + from qcodes_loop.data.data_array import ( + DataArray, + data_array_to_xarray_dictionary, + xarray_data_array_dictionary_to_data_array, ) - - for array_key in coordinate_names: - coord_dictionary = xarray_dictionary["coords"][array_key] - preset_data = np.array(coord_dictionary["data"]) - - tiled_preset_data = np.tile(preset_data, [g.size for g in grid_coords] + [1]) - grid_coords.append(preset_data) - - data_array = xarray_data_array_dictionary_to_data_array( - array_key, coord_dictionary, True, preset_data=tiled_preset_data - ) - dataset.add_array(data_array) - set_array_names.append(array_key) - for array_key, datavar_dictionary in xarray_dictionary["data_vars"].items(): - set_arrays = tuple(dataset.arrays[name] for name in set_array_names) - - data_array = xarray_data_array_dictionary_to_data_array( - array_key, datavar_dictionary, False - ) - data_array.set_arrays = set_arrays - dataset.add_array(data_array) - - return dataset - - -def xarray_dataset_to_qcodes_dataset(xarray_data_set: xr.Dataset) -> DataSet: - """ Convert QCoDeS gridded dataset to xarray dataset """ - xarray_dictionary = xarray_data_set.to_dict() - qcodes_dataset = xarray_dictionary_to_dataset(xarray_dictionary) - - return qcodes_dataset + from qcodes_loop.data.data_set import ( + DataSet, + _PrettyPrintDict, + dataset_to_xarray_dictionary, + load_data, + new_data, + qcodes_dataset_to_xarray_dataset, + xarray_dataset_to_qcodes_dataset, + xarray_dictionary_to_dataset, + ) + from qcodes_loop.data.gnuplot_format import GNUPlotFormat + from qcodes_loop.data.io import DiskIO + from qcodes_loop.data.location import FormatLocation +except ImportError as e: + raise ImportError( + "qcodes.data.data_set is deprecated and has moved to " + "the package `qcodes_loop`. Please install qcodes_loop directly or " + "with `pip install qcodes[loop]" + ) from e + +issue_deprecation_warning( + "qcodes.data.data_set module", alternative="qcodes_loop.data.data_set" +) diff --git a/qcodes/data/format.py b/qcodes/data/format.py index 770a8e58a4a..5fd252554ff 100644 --- a/qcodes/data/format.py +++ b/qcodes/data/format.py @@ -2,353 +2,20 @@ from collections import namedtuple from operator import attrgetter from traceback import format_exc -from typing import TYPE_CHECKING, Set - -if TYPE_CHECKING: - import qcodes.data.data_set +from typing import Set +from qcodes.utils import issue_deprecation_warning log = logging.getLogger(__name__) -ArrayGroup = namedtuple("ArrayGroup", "shape set_arrays data name") - - -class Formatter: - """ - Data file formatters - - Formatters translate between DataSets and data files. - - Each Formatter is expected to implement writing methods: - - - ``write``: to write the ``DataArrays`` - - ``write_metadata``: to write the metadata structure - - Optionally, if this Formatter keeps the data file(s) open - between write calls, it may implement: - - - ``close_file``: to perform any final cleanup and release the - file and any other resources. - - and reading methods: - - - ``read`` or ``read_one_file`` to reconstruct the ``DataArrays``, either - all at once (``read``) or one file at a time, supplied by the base class - ``read`` method that loops over all data files at the correct location. - - - ``read_metadata``: to reload saved metadata. If a subclass overrides - ``read``, this method should call ``read_metadata``, but keep it also - as a separate method because it occasionally gets called independently. - - All of these methods accept a ``data_set`` argument, which should be a - ``DataSet`` object. Even if you are loading a new data set from disk, this - object should already have attributes: - - - io: an IO manager (see qcodes.data.io) - location: a string, like a file path, that identifies the DataSet and - tells the IO manager where to store it - - arrays: a dict of ``{array_id:DataArray}`` to read into. - - - read will create entries that don't yet exist. - - write will write ALL DataArrays in the DataSet, using - last_saved_index and modified_range, as well as whether or not - it found the specified file, to determine how much to write. - """ - - ArrayGroup = ArrayGroup - - def write( - self, - data_set: "qcodes.data.data_set.DataSet", - io_manager, - location, - write_metadata=True, - force_write=False, - only_complete=True, - ): - """ - Write the DataSet to storage. - - Subclasses must override this method. - - It is up to the Formatter to decide when to overwrite completely, - and when to just append or otherwise update the file(s). - - Args: - data_set: the data we are writing. - io_manager (io_manager): base physical location to write to. - location (str): the file location within the io_manager. - write_metadata (bool): if True, then the metadata is written to disk - force_write (bool): if True, then the data is written to disk - only_complete (bool): Used only by the gnuplot formatter's - overridden version of this method - """ - raise NotImplementedError - - def read(self, data_set: "qcodes.data.data_set.DataSet") -> None: - """ - Read the entire ``DataSet``. - - Find all files matching ``data_set.location`` (using io_manager.list) - and call ``read_one_file`` on each. Subclasses may either override - this method (if they use only one file or want to do their own - searching) or override ``read_one_file`` to use the search and - initialization functionality defined here. - - Args: - data_set: the data to read into. Should already have - attributes ``io`` (an io manager), ``location`` (string), - and ``arrays`` (dict of ``{array_id: array}``, can be empty - or can already have some or all of the arrays present, they - expect to be overwritten) - """ - io_manager = data_set.io - location = data_set.location - - data_files = io_manager.list(location) - if not data_files: - raise OSError('no data found at ' + location) - - # in case the DataArrays exist but haven't been initialized - for array in data_set.arrays.values(): - if array.ndarray is None: - array.init_data() - - self.read_metadata(data_set) - - ids_read: Set[str] = set() - for fn in data_files: - with io_manager.open(fn, 'r') as f: - try: - self.read_one_file(data_set, f, ids_read) - except ValueError: - log.warning('error reading file ' + fn) - log.warning(format_exc()) - - def write_metadata( - self, - data_set: "qcodes.data.data_set.DataSet", - io_manager, - location, - read_first=True, - **kwargs - ): - """ - Write the metadata for this DataSet to storage. - - Subclasses must override this method. - - Args: - data_set: the data we are writing. - io_manager (io_manager): base physical location to write to. - location (str): the file location within the io_manager. - read_first (Optional[bool]): whether to first look for previously - saved metadata that may contain more information than the local - copy. - """ - raise NotImplementedError - - def read_metadata(self, data_set: "qcodes.data.data_set.DataSet"): - """ - Read the metadata from this DataSet from storage. - - Subclasses must override this method. - - Args: - data_set: the data to read metadata into - """ - raise NotImplementedError - - def read_one_file(self, data_set: "qcodes.data.data_set.DataSet", f, ids_read): - """ - Read data from a single file into a ``DataSet``. - - Formatter subclasses that break a DataSet into multiple data files may - choose to override either this method, which handles one file at a - time, or ``read`` which finds matching files on its own. - - Args: - data_set: the data we are reading into. - - f: a file-like object to read from, as provided by - ``io_manager.open``. - - ids_read (set): ``array_ids`` that we have already read. - When you read an array, check that it's not in this set (except - setpoints, which can be in several files with different inner - loops) then add it to the set so other files know it should not - be read again. - - Raises: - ValueError: if a duplicate array_id of measured data is found - """ - raise NotImplementedError - - def match_save_range(self, group, file_exists, only_complete=True): - """ - Find the save range that will joins all changes in an array group. - - Matches all full-sized arrays: the data arrays plus the inner loop - setpoint array. - - Note: if an outer loop has changed values (without the inner - loop or measured data changing) we won't notice it here. We assume - that before an iteration of the inner loop starts, the outer loop - setpoint gets set and then does not change later. - - Args: - group (Formatter.ArrayGroup): a ``namedtuple`` containing the - arrays that go together in one file, as tuple ``group.data``. - - file_exists (bool): Does this file already exist? If True, and - all arrays in the group agree on ``last_saved_index``, we - assume the file has been written up to this index and we can - append to it. Otherwise we will set the returned range to start - from zero (so if the file does exist, it gets completely - overwritten). - - only_complete (bool): Should we write all available new data, - or only complete rows? If True, we write only the range of - array indices which all arrays in the group list as modified, - so that future writes will be able to do a clean append to - the data file as more data arrives. - Default True. - - Returns: - Tuple(int, int): the first and last raveled indices that should - be saved. Returns None if: - - * no data is present - * no new data can be found - """ - inner_setpoint = group.set_arrays[-1] - full_dim_data = (inner_setpoint, ) + group.data - - # always return None if there are no modifications, - # even if there are last_saved_index inconsistencies - # so we don't do extra writing just to reshape the file - for array in full_dim_data: - if array.modified_range: - break - else: - return None - - last_saved_index = inner_setpoint.last_saved_index - - if last_saved_index is None or not file_exists: - if last_saved_index is None and file_exists: - log.warning("Inconsistent file information. " - "last_save_index is None but file exists. " - "Will overwrite") - if last_saved_index is not None and not file_exists: - log.warning("Inconsistent file information. " - "last_save_index is not None but file does not " - "exist. Will rewrite from scratch") - return self._match_save_range_whole_file( - full_dim_data, only_complete) - - # force overwrite if inconsistent last_saved_index - for array in group.data: - if array.last_saved_index != last_saved_index: - return self._match_save_range_whole_file( - full_dim_data, only_complete) - - return self._match_save_range_incremental( - full_dim_data, last_saved_index, only_complete) - - @staticmethod - def _match_save_range_whole_file(arrays, only_complete): - max_save = None - agg = (min if only_complete else max) - for array in arrays: - array_max = array.last_saved_index - if array_max is None: - array_max = -1 - mr = array.modified_range - if mr: - array_max = max(array_max, mr[1]) - max_save = (array_max if max_save is None else - agg(max_save, array_max)) - - if max_save >= 0: - return (0, max_save) - else: - return None - - @staticmethod - def _match_save_range_incremental(arrays, last_saved_index, only_complete): - mod_ranges = [] - for array in arrays: - mr = array.modified_range - if not mr: - if only_complete: - return None - else: - continue - mod_ranges.append(mr) - - mod_range = mod_ranges[0] - agg = (min if only_complete else max) - for mr in mod_ranges[1:]: - mod_range = (min(mod_range[0], mr[0]), - agg(mod_range[1], mr[1])) - - if last_saved_index >= mod_range[1]: - return (0, last_saved_index) - elif last_saved_index >= mod_range[0]: - return (0, mod_range[1]) - else: - return (last_saved_index + 1, mod_range[1]) - - def group_arrays(self, arrays): - """ - Find the sets of arrays which share all the same setpoint arrays. - - Some Formatters use this grouping to determine which arrays to save - together in one file. - - Args: - arrays (Dict[DataArray]): all the arrays in a DataSet - - Returns: - List[Formatter.ArrayGroup]: namedtuples giving: - - - shape (Tuple[int]): dimensions as in numpy - - set_arrays (Tuple[DataArray]): the setpoints of this group - - data (Tuple[DataArray]): measured arrays in this group - - name (str): a unique name of this group, obtained by joining - the setpoint array ids. - """ - - set_array_sets = tuple({array.set_arrays - for array in arrays.values()}) - all_set_arrays = set() - for set_array_set in set_array_sets: - all_set_arrays.update(set_array_set) - - grouped_data = [[] for _ in set_array_sets] - - for array in arrays.values(): - i = set_array_sets.index(array.set_arrays) - if array not in all_set_arrays: # array.set_arrays[-1] != array: - # don't include the setpoint array itself in the data - grouped_data[i].append(array) - - out = [] - id_getter = attrgetter('array_id') - for set_arrays, data in zip(set_array_sets, grouped_data): - leni = len(set_arrays) - if not data and any(1 for other_set_arrays in set_array_sets if - len(other_set_arrays) > leni and - other_set_arrays[:leni] == set_arrays): - # this is an outer loop that doesn't have any data of its own, - # so skip it. - # Inner-loop setpoints with no data is weird (we set values - # but didn't measure anything there?) but we should keep it. - continue - group_name = '_'.join(sai.array_id for sai in set_arrays) - out.append(self.ArrayGroup(shape=set_arrays[-1].shape, - set_arrays=set_arrays, - data=tuple(sorted(data, key=id_getter)), - name=group_name)) - return out +try: + from qcodes_loop.data.format import ArrayGroup, Formatter +except ImportError as e: + raise ImportError( + "qcodes.data.format is deprecated and has moved to " + "the package `qcodes_loop`. Please install qcodes_loop directly or " + "with `pip install qcodes[loop]" + ) from e +issue_deprecation_warning( + "qcodes.data.format module", alternative="qcodes_loop.data.format" +) diff --git a/qcodes/data/gnuplot_format.py b/qcodes/data/gnuplot_format.py index 503ba13eb06..f54c6a2104b 100644 --- a/qcodes/data/gnuplot_format.py +++ b/qcodes/data/gnuplot_format.py @@ -6,426 +6,18 @@ import numpy as np -from qcodes.utils import NumpyJSONEncoder, deep_update - -from .data_array import DataArray -from .format import Formatter - -if TYPE_CHECKING: - import qcodes.data.data_set - - -log = logging.getLogger(__name__) - - -class GNUPlotFormat(Formatter): - """ - Saves data in one or more gnuplot-format files. We make one file for - each set of matching dependent variables in the loop. - - Args: - - extension (str): file extension for data files. Defaults to - 'dat' - - terminator (str): newline character(s) to use on write - not used for reading, we will read any combination of '\\\\r' - and '\\\\n'. Defaults to '\\\\n' - - separator (str): field (column) separator, must be whitespace. - Only used for writing, we will read with any whitespace separation. - Defaults to '\\\\t'. - - comment (str): lines starting with this are not data - Comments are written with this full string, and identified on read - by just the string after stripping whitespace. Defaults to '# '. - - number_format (str): from the format mini-language, how to - format numeric data into a string. Defaults to 'g'. - - always_nest (bool): whether to always make a folder for files - or just make a single data file if all data has the same setpoints. - Defaults to bool. - - These files are basically tab-separated values, but any quantity of - any whitespace characters is accepted. - - Each row represents one setting of the setpoint variable(s) - the setpoint variable(s) are in the first column(s) - measured variable(s) come after. - - The data is preceded by comment lines (starting with #). - We use three: - - - one for the variable name - - the (longer) axis label, in quotes so a label can contain whitespace. - - for each dependent var, the (max) number of points in that dimension - (this also tells us how many dependent vars we have in this file) - - :: - - # id1\tid2\tid3... - # "label1"\t"label2"\t"label3"... - # 100\t250 - 1\t2\t3... - 2\t3\t4... - - For data of 2 dependent variables, gnuplot puts each inner loop into one - block, then increments the outer loop in the next block, separated by a - blank line. - - We extend this to an arbitrary quantity of dependent variables by using - one blank line for each loop level that resets. (gnuplot *does* seem to - use 2 blank lines sometimes, to denote a whole new dataset, which sort - of corresponds to our situation.) - """ - - def __init__(self, extension='dat', terminator='\n', separator='\t', - comment='# ', number_format='.15g', metadata_file=None): - self.metadata_file = metadata_file or 'snapshot.json' - # file extension: accept either with or without leading dot - self.extension = '.' + extension.lstrip('.') - - # line terminator (only used for writing; will read any \r\n combo) - if terminator not in ('\r', '\n', '\r\n'): - raise ValueError( - r'GNUPlotFormat terminator must be \r, \n, or \r\n') - self.terminator = terminator - - # field separator (only used for writing; will read any whitespace) - if not re.fullmatch(r'\s+', separator): - raise ValueError('GNUPlotFormat separator must be whitespace') - self.separator = separator - - # beginning of a comment line. (when reading, just checks the - # non-whitespace character(s) of comment - self.comment = comment - self.comment_chars = comment.rstrip() - if not self.comment_chars: - raise ValueError('comment must have some non-whitespace') - self.comment_len = len(self.comment_chars) - - # number format (only used for writing; will read any number) - self.number_format = '{:' + number_format + '}' - - def read_one_file(self, data_set, f, ids_read): - """ - Called by Formatter.read to bring one data file into - a DataSet. Setpoint data may be duplicated across multiple files, - but each measured DataArray must only map to one file. - - args: - data_set: the DataSet we are reading into - f: a file-like object to read from - ids_read: a `set` of array_ids that we have already read. - when you read an array, check that it's not in this set (except - setpoints, which can be in several files with different inner loop) - then add it to the set so other files know not to read it again - """ - if not f.name.endswith(self.extension): - return - - arrays = data_set.arrays - ids = self._read_comment_line(f).split() - labels = self._get_labels(self._read_comment_line(f)) - shape = tuple(map(int, self._read_comment_line(f).split())) - ndim = len(shape) - - set_arrays = () - data_arrays = [] - indexed_ids = list(enumerate(ids)) - - for i, array_id in indexed_ids[:ndim]: - snap = data_set.get_array_metadata(array_id) - - # setpoint arrays - set_shape = shape[: i + 1] - if array_id in arrays: - set_array = arrays[array_id] - if set_array.shape != set_shape: - raise ValueError( - 'shapes do not match for set array: ' + array_id) - if array_id not in ids_read: - # it's OK for setpoints to be duplicated across - # multiple files, but we should only empty the - # array out the first time we see it, so subsequent - # reads can check for consistency - set_array.clear() - else: - set_array = DataArray(label=labels[i], array_id=array_id, - set_arrays=set_arrays, shape=set_shape, - is_setpoint=True, snapshot=snap) - set_array.init_data() - data_set.add_array(set_array) - - set_arrays = set_arrays + (set_array, ) - ids_read.add(array_id) - - for i, array_id in indexed_ids[ndim:]: - snap = data_set.get_array_metadata(array_id) - - # data arrays - if array_id in ids_read: - raise ValueError('duplicate data id found: ' + array_id) - - if array_id in arrays: - data_array = arrays[array_id] - data_array.clear() - else: - data_array = DataArray(label=labels[i], array_id=array_id, - set_arrays=set_arrays, shape=shape, - snapshot=snap) - data_array.init_data() - data_set.add_array(data_array) - data_arrays.append(data_array) - ids_read.add(array_id) - - indices = [0] * ndim - first_point = True - resetting = 0 - for line in f: - if self._is_comment(line): - continue - - # ignore leading or trailing whitespace (including in blank lines) - line = line.strip() - - if not line: - # each consecutive blank line implies one more loop to reset - # when we read the next data point. Don't depend on the number - # of setpoints that change, as there could be weird cases, like - # bidirectional sweeps, or highly diagonal sweeps, where this - # is incorrect. Anyway this really only matters for >2D sweeps. - if not first_point: - resetting += 1 - continue - - values = tuple(map(float, line.split())) - - if resetting: - indices[-resetting - 1] += 1 - indices[-resetting:] = [0] * resetting - resetting = 0 - - for value, set_array in zip(values[:ndim], set_arrays): - nparray = set_array.ndarray - myindices = tuple(indices[:nparray.ndim]) - stored_value = nparray[myindices] - if math.isnan(stored_value): - nparray[myindices] = value - elif stored_value != value: - raise ValueError('inconsistent setpoint values', - stored_value, value, set_array.name, - myindices, indices) - - for value, data_array in zip(values[ndim:], data_arrays): - # set .ndarray directly to avoid the overhead of __setitem__ - # which updates modified_range on every call - data_array.ndarray[tuple(indices)] = value - - indices[-1] += 1 - first_point = False - - # Since we skipped __setitem__, back up to the last read point and - # mark it as saved that far. - # Using mark_saved is better than directly setting last_saved_index - # because it also ensures modified_range is set correctly. - indices[-1] -= 1 - for array in set_arrays + tuple(data_arrays): - array.mark_saved(array.flat_index(indices[:array.ndim])) - - def _is_comment(self, line): - return line[:self.comment_len] == self.comment_chars - - def _read_comment_line(self, f): - s = f.readline() - if not self._is_comment(s): - raise ValueError('expected a comment line, found:\n' + s) - return s[self.comment_len:] - - def _get_labels(self, labelstr): - labelstr = labelstr.strip() - if labelstr[0] != '"' or labelstr[-1] != '"': - # fields are *not* quoted - return labelstr.split() - else: - # fields *are* quoted (and escaped) - parts = re.split('"\\s+"', labelstr[1:-1]) - return [l.replace('\\"', '"').replace('\\\\', '\\') for l in parts] - - # this signature is unfortunatly incompatible with the super class - # so we have to ignore type errors - def write( # type: ignore[override] - self, - data_set: "qcodes.data.data_set.DataSet", - io_manager, - location, - force_write=False, - write_metadata=True, - only_complete=True, - filename=None, - ): - """ - Write updates in this DataSet to storage. - - Will choose append if possible, overwrite if not. - - Args: - data_set: the data we're storing - io_manager (io_manager): the base location to write to - location (str): the file location within io_manager - only_complete (bool): passed to match_save_range, answers the - following question: Should we write all available new data, - or only complete rows? Is used to make sure that everything - gets written when the DataSet is finalised, even if some - dataarrays are strange (like, full of nans) - filename (Optional[str]): Filename to save to. Will override - the usual naming scheme and possibly overwrite files, so - use with care. The file will be saved in the normal location. - """ - arrays = data_set.arrays - - # puts everything with same dimensions together - groups = self.group_arrays(arrays) - existing_files = set(io_manager.list(location)) - written_files = set() - - # Every group gets its own datafile - for group in groups: - log.debug('Attempting to write the following ' - 'group: {}'.format(group.name)) - # it might be useful to output the whole group as below but it is - # very verbose - #log.debug('containing {}'.format(group)) - - if filename: - fn = io_manager.join(location, filename + self.extension) - else: - fn = io_manager.join(location, group.name + self.extension) - - written_files.add(fn) - - # fn may or may not be an absolute path depending on the location manager - # used however, io_manager always returns relative paths so make sure both are - # relative by calling to_location - file_exists = io_manager.to_location(fn) in existing_files - save_range = self.match_save_range(group, file_exists, - only_complete=only_complete) - - if save_range is None: - log.debug('Cannot match save range, skipping this group.') - continue - - overwrite = save_range[0] == 0 or force_write - open_mode = 'w' if overwrite else 'a' - shape = group.set_arrays[-1].shape - - with io_manager.open(fn, open_mode) as f: - if overwrite: - f.write(self._make_header(group)) - log.debug('Wrote header to file') - - for i in range(save_range[0], save_range[1] + 1): - indices = np.unravel_index(i, shape) - - # insert a blank line for each loop that reset (to index 0) - # note that if *all* indices are zero (the first point) - # we won't put any blanks - for j, index in enumerate(reversed(indices)): - if index != 0: - if j: - f.write(self.terminator * j) - break - - one_point = self._data_point(group, indices) - f.write(self.separator.join(one_point) + self.terminator) - log.debug('Wrote to file from ' - '{} to {}'.format(save_range[0], save_range[1]+1)) - # now that we've saved the data, mark it as such in the data. - # we mark the data arrays and the inner setpoint array. Outer - # setpoint arrays have different dimension (so would need a - # different unraveled index) but more importantly could have - # a different saved range anyway depending on whether there - # is outer data taken before or after the inner loop. Anyway we - # never look at the outer setpoint last_saved_index or - # modified_range, we just assume it's got the values we need. - for array in group.data + (group.set_arrays[-1],): - array.mark_saved(save_range[1]) - - if write_metadata: - self.write_metadata( - data_set, io_manager=io_manager, location=location) - - def write_metadata( - self, - data_set: "qcodes.data.data_set.DataSet", - io_manager, - location, - read_first=True, - **kwargs - ): - """ - Write all metadata in this DataSet to storage. - - Args: - data_set: the data we're storing - - io_manager (io_manager): the base location to write to - - location (str): the file location within io_manager - - read_first (Optional[bool]): read previously saved metadata before - writing? The current metadata will still be the used if - there are changes, but if the saved metadata has information - not present in the current metadata, it will be retained. - Default True. - """ - if read_first: - # In case the saved file has more metadata than we have here, - # read it in first. But any changes to the in-memory copy should - # override the saved file data. - memory_metadata = data_set.metadata - data_set.metadata = {} - self.read_metadata(data_set) - deep_update(data_set.metadata, memory_metadata) - - fn = io_manager.join(location, self.metadata_file) - with io_manager.open(fn, 'w', encoding='utf8') as snap_file: - json.dump(data_set.metadata, snap_file, sort_keys=False, - indent=4, ensure_ascii=False, cls=NumpyJSONEncoder) - - def read_metadata(self, data_set): - io_manager = data_set.io - location = data_set.location - fn = io_manager.join(location, self.metadata_file) - if io_manager.list(fn): - with io_manager.open(fn, 'r') as snap_file: - metadata = json.load(snap_file) - data_set.metadata.update(metadata) - - def _make_header(self, group): - ids, labels = [], [] - for array in group.set_arrays + group.data: - ids.append(array.array_id) - label = getattr(array, 'label', array.array_id) - label = label.replace('\\', '\\\\').replace('"', '\\"') - labels.append('"' + label + '"') - - shape = [str(size) for size in group.set_arrays[-1].shape] - if len(shape) != len(group.set_arrays): - raise ValueError('array dimensionality does not match setpoints') - - out = (self._comment_line(ids) + self._comment_line(labels) + - self._comment_line(shape)) - - return out - - def _comment_line(self, items): - return self.comment + self.separator.join(items) + self.terminator - - def _data_point(self, group, indices): - for array in group.set_arrays: - yield self.number_format.format(array[indices[:array.ndim]]) - - for array in group.data: - yield self.number_format.format(array[indices]) +from qcodes.utils import NumpyJSONEncoder, deep_update, issue_deprecation_warning + +try: + from qcodes_loop.data.data_array import DataArray + from qcodes_loop.data.format import Formatter + from qcodes_loop.data.gnuplot_format import GNUPlotFormat +except ImportError as e: + raise ImportError( + "qcodes.data.gunplot_format is deprecated and has moved to " + "the package `qcodes_loop`. Please install qcodes_loop directly or " + "with `pip install qcodes[loop]" + ) from e +issue_deprecation_warning( + "qcodes.data.gunplot_format module", alternative="qcodes_loop.data.gunplot_format" +) diff --git a/qcodes/data/hdf5_format.py b/qcodes/data/hdf5_format.py index 4dfc320bf49..2415e8fa502 100644 --- a/qcodes/data/hdf5_format.py +++ b/qcodes/data/hdf5_format.py @@ -3,569 +3,26 @@ import os from typing import TYPE_CHECKING -import lazy_loader # type: ignore[import] - -h5py = lazy_loader.load("h5py") import numpy as np import qcodes as qc -from qcodes.utils import NumpyJSONEncoder, deep_update - -from .data_array import DataArray -from .format import Formatter - -if TYPE_CHECKING: - import qcodes.data.data_set - -class HDF5Format(Formatter): - """ - HDF5 formatter for saving qcodes datasets. - - Capable of storing (write) and recovering (read) qcodes datasets. - - """ - - _format_tag = 'hdf5' - - def close_file(self, data_set: "qcodes.data.data_set.DataSet"): - """ - Closes the hdf5 file open in the dataset. - - Args: - data_set: DataSet object - """ - if hasattr(data_set, '_h5_base_group'): - data_set._h5_base_group.close() - # Removes reference to closed file - del data_set._h5_base_group - else: - logging.warning( - 'Cannot close file, data_set has no open hdf5 file') - - def _create_file(self, filepath): - """ - creates a hdf5 file (data_object) at a location specified by - filepath - """ - folder, _filename = os.path.split(filepath) - if not os.path.isdir(folder): - os.makedirs(folder) - file = h5py.File(filepath, 'a') - return file - - def _open_file(self, data_set, location=None): - if location is None: - location = data_set.location - filepath = self._filepath_from_location(location, - io_manager=data_set.io) - data_set._h5_base_group = h5py.File(filepath, 'r+') - - def read(self, data_set: "qcodes.data.data_set.DataSet", location=None): - """ - Reads an hdf5 file specified by location into a data_set object. - If no data_set is provided will create an empty data_set to read into. - - - Args: - data_set: the data to read into. Should already have - attributes ``io`` (an io manager), ``location`` (string), - and ``arrays`` (dict of ``{array_id: array}``, can be empty - or can already have some or all of the arrays present, they - expect to be overwritten) - location (None or str): Location to write the data. If no location - is provided will use the location specified in the dataset. - """ - def decode_bytes_if_needed(s): - """ - h5py 2 stores strings encoded as bytestrings - h5py 3 fixes this and stores them as regular utf8 strings - - This is a simple wrapper to always convert to regular strings - """ - try: - s = s.decode() - except AttributeError: - pass - return s - - self._open_file(data_set, location) - - if '__format_tag' in data_set._h5_base_group.attrs: - format_tag = data_set._h5_base_group.attrs['__format_tag'] - if format_tag != self._format_tag: - raise Exception('format tag %s does not match tag %s of file %s' % - (format_tag, self._format_tag, location)) - - for i, array_id in enumerate( - data_set._h5_base_group['Data Arrays'].keys()): - # Decoding string is needed because of h5py/issues/379 - name = array_id # will be overwritten if not in file - dat_arr = data_set._h5_base_group['Data Arrays'][array_id] - - # write ensures these attributes always exist - name = decode_bytes_if_needed(dat_arr.attrs['name']) - label = decode_bytes_if_needed(dat_arr.attrs['label']) - - # get unit from units if no unit field, for backward compatibility - if 'unit' in dat_arr.attrs: - unit = decode_bytes_if_needed(dat_arr.attrs['unit']) - else: - unit = decode_bytes_if_needed(dat_arr.attrs['units']) - - is_setpoint_str = decode_bytes_if_needed(dat_arr.attrs['is_setpoint']) - is_setpoint = str_to_bool(is_setpoint_str) - # if not is_setpoint: - set_arrays = dat_arr.attrs['set_arrays'] - set_arrays = [decode_bytes_if_needed(s) for s in set_arrays] - # else: - # set_arrays = () - vals = dat_arr[:, 0] - if 'shape' in dat_arr.attrs.keys(): - # extend with NaN if needed - esize = np.prod(dat_arr.attrs['shape']) - vals = np.append(vals, [np.nan] * (esize - vals.size)) - vals = vals.reshape(dat_arr.attrs['shape']) - if array_id not in data_set.arrays.keys(): # create new array - d_array = DataArray( - name=name, array_id=array_id, label=label, parameter=None, - unit=unit, - is_setpoint=is_setpoint, set_arrays=(), - preset_data=vals) - data_set.add_array(d_array) - else: # update existing array with extracted values - d_array = data_set.arrays[array_id] - d_array.name = name - d_array.label = label - d_array.unit = unit - d_array.is_setpoint = is_setpoint - d_array.ndarray = vals - d_array.shape = dat_arr.attrs['shape'] - # needed because I cannot add set_arrays at this point - data_set.arrays[array_id]._sa_array_ids = set_arrays - - # Add copy/ref of setarrays (not array id only) - # Note, this is not pretty but a result of how the dataset works - for array_id, d_array in data_set.arrays.items(): - for sa_id in d_array._sa_array_ids: - d_array.set_arrays += (data_set.arrays[sa_id], ) - data_set = self.read_metadata(data_set) - return data_set - - def _filepath_from_location(self, location, io_manager): - filename = os.path.split(location)[-1] - filepath = io_manager.to_path(location + - f'/{filename}.hdf5') - return filepath - - def _create_data_object(self, data_set, io_manager=None, - location=None): - # Create the file if it is not there yet - if io_manager is None: - io_manager = data_set.io - if location is None: - location = data_set.location - filepath = self._filepath_from_location(location, io_manager) - # note that this creates an hdf5 file in a folder with the same - # name. This is useful for saving e.g. images in the same folder - # I think this is a sane default (MAR). - data_set._h5_base_group = self._create_file(filepath) - data_set._h5_base_group.attrs["__qcodes_version"] = qc.__version__ - data_set._h5_base_group.attrs["__format_tag"] = self._format_tag - - return data_set._h5_base_group - - def write(self, data_set, io_manager=None, location=None, - force_write=False, flush=True, write_metadata=True, - only_complete=False): - """ - Writes a data_set to an hdf5 file. - - Args: - data_set: qcodes data_set to write to hdf5 file - io_manager: io_manger used for providing path - location: location can be used to specify custom location - force_write (bool): if True creates a new file to write to - flush (bool) : whether to flush after writing, can be disabled - for testing or performance reasons - write_metadata (bool): If True write the dataset metadata to disk - only_complete (bool): Not used by this formatter, but must be - included in the call signature to avoid an "unexpected - keyword argument" TypeError. - - N.B. It is recommended to close the file after writing, this can be - done by calling ``HDF5Format.close_file(data_set)`` or - ``data_set.finalize()`` if the data_set formatter is set to an - hdf5 formatter. Note that this is not required if the dataset - is created from a Loop as this includes a data_set.finalize() - statement. - - The write function consists of two parts, writing DataArrays and - writing metadata. - - - The main part of write consists of writing and resizing arrays, - the resizing providing support for incremental writes. - - - write_metadata is called at the end of write and dumps a - dictionary to an hdf5 file. If there already is metadata it will - delete this and overwrite it with current metadata. - - """ - if not hasattr(data_set, '_h5_base_group') or force_write: - data_set._h5_base_group = self._create_data_object( - data_set, io_manager, location) - - data_name = 'Data Arrays' - - if data_name not in data_set._h5_base_group.keys(): - arr_group = data_set._h5_base_group.create_group(data_name) - else: - arr_group = data_set._h5_base_group[data_name] - - for array_id in data_set.arrays.keys(): - if array_id not in arr_group.keys() or force_write: - self._create_dataarray_dset(array=data_set.arrays[array_id], - group=arr_group) - dset = arr_group[array_id] - # Resize the dataset and add the new values - - # dataset refers to the hdf5 dataset here - datasetshape = dset.shape - old_dlen = datasetshape[0] - x = data_set.arrays[array_id] - try: - # get latest NaN element - new_dlen = (~np.isnan(x)).flatten().nonzero()[0][-1] + 1 - except IndexError: - new_dlen = old_dlen - - new_datasetshape = (new_dlen, - datasetshape[1]) - dset.resize(new_datasetshape) - new_data_shape = (new_dlen - old_dlen, datasetshape[1]) - dset[old_dlen:new_dlen] = x.flat[old_dlen:new_dlen].reshape(new_data_shape) - # allow resizing extracted data, here so it gets written for - # incremental writes aswell - dset.attrs['shape'] = x.shape - if write_metadata: - self.write_metadata( - data_set, io_manager=io_manager, location=location) - - # flush ensures buffers are written to disk - # (useful for ensuring openable by other files) - if flush: - data_set._h5_base_group.file.flush() - - def _create_dataarray_dset(self, array, group): - """ - input arguments - array: Dataset data array - group: group in the hdf5 file where the dset will be created - - creates a hdf5 datasaset that represents the data array. - """ - # Check for empty meta attributes, use array_id if name and/or label - # is not specified - if array.label is not None: - label = array.label - else: - label = array.array_id - - if array.name is not None: - name = array.name - else: - name = array.array_id - - # Create the hdf5 dataset - dset = group.create_dataset( - array.array_id, (0, 1), - maxshape=(None, 1)) - dset.attrs['label'] = _encode_to_utf8(str(label)) - dset.attrs['name'] = _encode_to_utf8(str(name)) - dset.attrs['unit'] = _encode_to_utf8(str(array.unit or '')) - dset.attrs['is_setpoint'] = _encode_to_utf8(str(array.is_setpoint)) - - set_arrays = [] - # list will remain empty if array does not have set_array - for i in range(len(array.set_arrays)): - set_arrays += [_encode_to_utf8( - str(array.set_arrays[i].array_id))] - dset.attrs['set_arrays'] = set_arrays - - return dset - - def write_metadata(self, data_set, io_manager=None, location=None, read_first=True, **kwargs): - """ - Writes metadata of dataset to file using write_dict_to_hdf5 method - - Note that io and location are arguments that are only here because - of backwards compatibility with the loop. - This formatter uses io and location as specified for the main - dataset. - The read_first argument is ignored. - """ - if not hasattr(data_set, '_h5_base_group'): - # added here because loop writes metadata before data itself - data_set._h5_base_group = self._create_data_object(data_set) - if 'metadata' in data_set._h5_base_group.keys(): - del data_set._h5_base_group['metadata'] - metadata_group = data_set._h5_base_group.create_group('metadata') - self.write_dict_to_hdf5(data_set.metadata, metadata_group) - - # flush ensures buffers are written to disk - # (useful for ensuring openable by other files) - data_set._h5_base_group.file.flush() - - def _read_list_group(self, entry_point, list_type): - d = {} - self.read_dict_from_hdf5(data_dict=d, - h5_group=entry_point[list_type]) - - if list_type == 'tuple': - item = tuple(d[k] for k in sorted(d.keys())) - elif list_type == 'list': - item = [d[k] for k in sorted(d.keys())] - else: - raise Exception('type %s not supported' % list_type) - - return item - - def _write_list_group(self, key, item, entry_point, list_type): - entry_point.create_group(key) - group_attrs = entry_point[key].attrs - group_attrs['list_type'] = list_type - - if list_type == 'tuple' or list_type == 'list': - item = {str(v[0]): v[1] for v in enumerate(item)} - else: - raise Exception('type %s not supported' % type(item)) - - entry_point[key].create_group(list_type) - self.write_dict_to_hdf5( - data_dict=item, - entry_point=entry_point[key][list_type]) - - def write_dict_to_hdf5(self, data_dict, entry_point): - """ Write a (nested) dictionary to HDF5 - - Args: - data_dict (dict): Dicionary to be written - entry_point (object): Object to write to - """ - for key, item in data_dict.items(): - if isinstance(key, (float, int)): - key = '__' + str(type(key)) + '__' + str(key) - - if isinstance(item, (str, bool, float, int)): - entry_point.attrs[key] = item - elif isinstance(item, np.ndarray): - entry_point.create_dataset(key, data=item) - elif isinstance(item, (np.int32, np.int64)): - entry_point.attrs[key] = int(item) - elif item is None: - # as h5py does not support saving None as attribute - # I create special string, note that this can create - # unexpected behaviour if someone saves a string with this name - entry_point.attrs[key] = 'NoneType:__None__' - elif isinstance(item, dict): - entry_point.create_group(key) - self.write_dict_to_hdf5(data_dict=item, - entry_point=entry_point[key]) - elif isinstance(item, tuple): - self._write_list_group(key, item, entry_point, 'tuple') - elif isinstance(item, list): - if len(item) > 0: - elt_type = type(item[0]) - if all(isinstance(x, elt_type) for x in item): - if isinstance(item[0], (int, float, - np.int32, np.int64)): - - entry_point.create_dataset(key, - data=np.array(item)) - entry_point[key].attrs['list_type'] = 'array' - elif isinstance(item[0], str): - dt = h5py.special_dtype(vlen=str) - data = np.array(item) - data = data.reshape((-1, 1)) - ds = entry_point.create_dataset( - key, (len(data), 1), dtype=dt) - ds[:] = data - elif isinstance(item[0], dict): - entry_point.create_group(key) - group_attrs = entry_point[key].attrs - group_attrs['list_type'] = 'dict' - base_list_key = 'list_idx_{}' - group_attrs['base_list_key'] = base_list_key - group_attrs['list_length'] = len(item) - for i, list_item in enumerate(item): - list_item_grp = entry_point[key].create_group( - base_list_key.format(i)) - self.write_dict_to_hdf5( - data_dict=list_item, - entry_point=list_item_grp) - else: - logging.warning( - 'List of type "{}" for "{}":"{}" not ' - 'supported, storing as string'.format( - elt_type, key, item)) - entry_point.attrs[key] = str(item) - else: - self._write_list_group(key, item, entry_point, 'list') - else: - # as h5py does not support saving None as attribute - entry_point.attrs[key] = 'NoneType:__emptylist__' - - else: - logging.warning( - 'Type "{}" for "{}":"{}" not supported, ' - 'storing as string'.format(type(item), key, item)) - entry_point.attrs[key] = str(item) - - def read_metadata(self, data_set: "qcodes.data.data_set.DataSet"): - """ - Reads in the metadata, this is also called at the end of a read - statement so there should be no need to call this explicitly. - - Args: - data_set: Dataset object to read the metadata into - """ - # checks if there is an open file in the dataset as load_data does - # reading of metadata before reading the complete dataset - if not hasattr(self, '_h5_base_group'): - self._open_file(data_set) - if 'metadata' in data_set._h5_base_group.keys(): - metadata_group = data_set._h5_base_group['metadata'] - self.read_dict_from_hdf5(data_set.metadata, metadata_group) - return data_set - - def read_dict_from_hdf5(self, data_dict, h5_group): - """ Read a dictionary from HDF5 - - Args: - data_dict (dict): Dataset to read from - h5_group (object): HDF5 object to read from - """ - - if 'list_type' not in h5_group.attrs: - for key, item in h5_group.items(): - if isinstance(item, h5py.Group): - data_dict[key] = {} - data_dict[key] = self.read_dict_from_hdf5(data_dict[key], - item) - else: # item either a group or a dataset - if 'list_type' not in item.attrs: - data_dict[key] = item[...] - else: - data_dict[key] = list(item[...]) - for key, item in h5_group.attrs.items(): - if type(item) is str: - # Extracts "None" as an exception as h5py does not support - # storing None, nested if statement to avoid elementwise - # comparison warning - if item == 'NoneType:__None__': - item = None - elif item == 'NoneType:__emptylist__': - item = [] - else: - pass - data_dict[key] = item - elif h5_group.attrs['list_type'] == 'tuple': - data_dict = self._read_list_group(h5_group, 'tuple') - elif h5_group.attrs['list_type'] == 'list': - data_dict = self._read_list_group(h5_group, 'list') - elif h5_group.attrs['list_type'] == 'dict': - # preallocate empty list - list_to_be_filled = [None] * h5_group.attrs['list_length'] - base_list_key = h5_group.attrs['base_list_key'] - for i in range(h5_group.attrs['list_length']): - list_to_be_filled[i] = {} - self.read_dict_from_hdf5( - data_dict=list_to_be_filled[i], - h5_group=h5_group[base_list_key.format(i)]) - - # THe error is here!, extract correctly but not adding to - # data dict correctly - data_dict = list_to_be_filled - else: - raise NotImplementedError('cannot read "list_type":"{}"'.format( - h5_group.attrs['list_type'])) - return data_dict - - -def _encode_to_utf8(s): - """ - Required because h5py does not support python3 strings - converts byte type to string - """ - return s.encode('utf-8') - - -def str_to_bool(s): - if s == 'True': - return True - elif s == 'False': - return False - else: - raise ValueError(f"Cannot covert {s} to a bool") - - -class HDF5FormatMetadata(HDF5Format): - - _format_tag = 'hdf5-json' - metadata_file = 'snapshot.json' - - def write_metadata( - self, - data_set: "qcodes.data.data_set.DataSet", - io_manager=None, - location=None, - read_first=False, - **kwargs, - ): - """ - Write all metadata in this DataSet to storage. - - Args: - data_set: the data we're storing - - io_manager (io_manager): the base location to write to - - location (str): the file location within io_manager - - read_first (Optional[bool]): read previously saved metadata before - writing? The current metadata will still be the used if - there are changes, but if the saved metadata has information - not present in the current metadata, it will be retained. - Default True. - kwargs (dict): From the dicionary the key sort_keys is extracted (default value: False). If True, then the - keys of the metadata will be stored sorted in the json file. Note: sorting is only possible if - the keys of the metadata dictionary can be compared. - - """ - sort_keys = kwargs.get('sort_keys', False) - - # this statement is here to make the linter happy - if io_manager is None or location is None: - raise Exception('please set io_manager and location arguments ') - - if read_first: - # In case the saved file has more metadata than we have here, - # read it in first. But any changes to the in-memory copy should - # override the saved file data. - memory_metadata = data_set.metadata - data_set.metadata = {} - self.read_metadata(data_set) - deep_update(data_set.metadata, memory_metadata) - - fn = io_manager.join(location, self.metadata_file) - with io_manager.open(fn, 'w', encoding='utf8') as snap_file: - json.dump(data_set.metadata, snap_file, sort_keys=sort_keys, - indent=4, ensure_ascii=False, cls=NumpyJSONEncoder) - - def read_metadata(self, data_set): - io_manager = data_set.io - location = data_set.location - fn = io_manager.join(location, self.metadata_file) - if io_manager.list(fn): - with io_manager.open(fn, 'r') as snap_file: - metadata = json.load(snap_file) - data_set.metadata.update(metadata) +from qcodes.utils import NumpyJSONEncoder, deep_update, issue_deprecation_warning + +try: + from qcodes_loop.data.data_array import DataArray + from qcodes_loop.data.format import Formatter + from qcodes_loop.data.hdf5_format import ( + HDF5Format, + HDF5FormatMetadata, + _encode_to_utf8, + str_to_bool, + ) +except ImportError as e: + raise ImportError( + "qcodes.data.hdf5_format is deprecated and has moved to " + "the package `qcodes_loop`. Please install qcodes_loop directly or " + "with `pip install qcodes[loop]" + ) from e +issue_deprecation_warning( + "qcodes.data.hdf5_format module", alternative="qcodes_loop.data.hdf5_format" +) diff --git a/qcodes/data/hdf5_format_hickle.py b/qcodes/data/hdf5_format_hickle.py index 4faaf5d87c5..83f32bb1835 100644 --- a/qcodes/data/hdf5_format_hickle.py +++ b/qcodes/data/hdf5_format_hickle.py @@ -1,80 +1,18 @@ import logging from typing import TYPE_CHECKING -import hickle - -from qcodes.utils import deep_update - -from .hdf5_format import HDF5Format - -if TYPE_CHECKING: - import qcodes.data.data_set - - -#%% - -log = logging.getLogger(__name__) - - -class HDF5FormatHickle(HDF5Format): - - _metadata_file = 'snapshot.hickle' - _format_tag = 'hdf5-hickle' - - def write_metadata( - self, - data_set: "qcodes.data.data_set.DataSet", - io_manager=None, - location=None, - read_first=False, - **kwargs - ): - """ - Write all metadata in this DataSet to storage. - - Args: - data_set: the data we're storing - - io_manager (io_manager): the base location to write to - - location (str): the file location within io_manager - - read_first (Optional[bool]): read previously saved metadata before - writing? The current metadata will still be the used if - there are changes, but if the saved metadata has information - not present in the current metadata, it will be retained. - Default True. - """ - - # this statement is here to make the linter happy - if io_manager is None or location is None: - raise Exception('please set io_manager and location arguments ') - - if read_first: - # In case the saved file has more metadata than we have here, - # read it in first. But any changes to the in-memory copy should - # override the saved file data. - memory_metadata = data_set.metadata - data_set.metadata = {} - self.read_metadata(data_set) - deep_update(data_set.metadata, memory_metadata) - - log.info('writing metadata to file %s' % self._metadata_file) - fn = io_manager.join(location, self._metadata_file) - with io_manager.open(fn, 'w', encoding='utf8') as snap_file: - hickle.dump(data_set.metadata, snap_file) - - def read_metadata(self, data_set: "qcodes.data.data_set.DataSet"): - """Reads in the metadata - - Args: - data_set: Dataset object to read the metadata into - """ - io_manager = data_set.io - location = data_set.location - fn = io_manager.join(location, self._metadata_file) - if io_manager.list(fn): - log.info('reading metadata from file %s' % self._metadata_file) - with io_manager.open(fn, 'r') as snap_file: - metadata = hickle.load(snap_file) - data_set.metadata.update(metadata) +from qcodes.utils import deep_update, issue_deprecation_warning + +try: + from qcodes_loop.data.hdf5_format import HDF5Format + from qcodes_loop.data.hdf5_format_hickle import HDF5FormatHickle +except ImportError as e: + raise ImportError( + "qcodes.data.hdf5_format_hickle is deprecated and has moved to " + "the package `qcodes_loop`. Please install qcodes_loop directly or " + "with `pip install qcodes[loop]" + ) from e +issue_deprecation_warning( + "qcodes.data.hdf5_format_hickle module", + alternative="qcodes_loop.data.hdf5_format_hickle", +) diff --git a/qcodes/data/io.py b/qcodes/data/io.py index a8cec4eedb4..5de8e567943 100644 --- a/qcodes/data/io.py +++ b/qcodes/data/io.py @@ -1,241 +1,17 @@ -""" -IO managers for QCodes. - -IO managers wrap whatever physical storage layer the user wants to use -in an interface mimicking the built-in context manager, with -some restrictions to minimize the overhead in creating new IO managers. - -The main thing these managers need to implement is the open context manager: - -- Only the context manager needs to be implemented, not separate - open function and close methods. - -- open takes the standard parameters: - - - filename: (string) - - mode: (string) only 'r' (read), 'w' (write), and 'a' (append) are - expected to be implemented. As with normal file objects, the only - difference between write and append is that write empties the file - before adding new data, and append leaves the existing contents in - place but starts writing at the end. - - encoding: If a special output encoding is desired. i.e. 'utf8 - -- the file-like object returned should implement a minimal set of operations. - - In read mode: - - read([size]): read to the end or at most size bytes into a string - - readline([size]): read until a newline or up to size bytes, into a string - - iter(): usually return self, but can be any iterator over lines - - next(): assuming iter() returns self, this yields the next line. - - In write or append mode: - - write(s): add string s to the end of the file. - - writelines(seq): add a sequence of strings - -IO managers should also implement: - -- a join method, ala ``os.path.join(*args)``. -- a list method, that returns all objects matching location -- a remove method, ala os.remove(path) except that it will remove directories - as well as files, since we're allowing "locations" to be directories - or files. -""" - import os import re import shutil from contextlib import contextmanager from fnmatch import fnmatch -ALLOWED_OPEN_MODES = ('r', 'w', 'a') - - -class DiskIO: - - """ - Simple IO object to wrap disk operations with a custom base location. - - Also accepts both forward and backward slashes at any point, and - normalizes both to the OS we are currently on. - - Args: - base_location (str): a path to the root data folder. - Converted to an absolute path immediately, so even if you supply a - relative path, later changes to the OS working directory will not - affect data paths. - """ - - def __init__(self, base_location): - if base_location is None: - self.base_location = None - else: - base_location = self._normalize_slashes(base_location) - self.base_location = os.path.abspath(base_location) - - @contextmanager - def open(self, filename, mode, encoding=None): - """ - Mimic the interface of the built in open context manager. - - Args: - filename (str): path relative to base_location. - - mode (str): 'r' (read), 'w' (write), or 'a' (append). - Other open modes are not supported because we don't want - to force all IO managers to support others. - - Returns: - context manager yielding the open file - """ - if mode not in ALLOWED_OPEN_MODES: - raise ValueError(f'mode {mode} not allowed in IO managers') - - filepath = self.to_path(filename) - - # make directories if needed - dirpath = os.path.dirname(filepath) - if not os.path.exists(dirpath): - os.makedirs(dirpath) - - # normally we'd construct this context manager with try/finally, but - # here we already have a context manager for open so we just wrap it - with open(filepath, mode, encoding=encoding) as f: - yield f - - def _normalize_slashes(self, location): - # note that this is NOT os.path.join - the difference is os.path.join - # discards empty strings, so if you use it on a re.split absolute - # path you will get a relative path! - return os.sep.join(re.split('[\\\\/]', location)) - - def to_path(self, location): - """ - Convert a location string into a path on the local file system. - - For DiskIO this just fixes slashes and prepends the base location, - doing nothing active with the file. But for other io managers that - refer to remote storage, this method may actually fetch the file and - put it at a temporary local path. - - Args: - location (str): A location string for a complete dataset or - a file within it. - - Returns: - str: The path on disk to which this location maps. - """ - location = self._normalize_slashes(location) - if self.base_location: - return os.path.join(self.base_location, location) - else: - return location - - def to_location(self, path): - """ - Convert a local filesystem path into a location string. - - Args: - path (str): a path on the local file system. - - Returns: - str: the location string corresponding to this path. - """ - if self.base_location: - return os.path.join(self.base_location, path) - else: - return path - - def __repr__(self): - """Show the base location in the repr.""" - return f"" - - def join(self, *args): - """Context-dependent os.path.join for this io manager.""" - return os.path.join(*list(map(self._normalize_slashes, args))) - - def isfile(self, location): - """Check whether this location matches a file.""" - path = self.to_path(location) - return os.path.isfile(path) - - def list(self, location, maxdepth=1, include_dirs=False): - """ - Return all files that match location. - - This is either files whose names match up to an arbitrary extension, - or any files within an exactly matching directory name. - - Args: - location (str): the location to match. - May contain the usual path wildcards * and ? - - maxdepth (Optional[int]): maximum levels of directory nesting to - recurse into looking for files. Default 1. - - include_dirs (Optional[bool]): whether to allow directories in - the results or just files. Default False. - - Returns: - A list of matching files and/or directories, as locations - relative to our base_location. - """ - location = self._normalize_slashes(location) - search_dir, pattern = os.path.split(location) - path = self.to_path(search_dir) - - if not os.path.isdir(path): - return [] - - matches = [fn for fn in os.listdir(path) if fnmatch(fn, pattern + '*')] - out = [] - - for match in matches: - matchpath = self.join(path, match) - if os.path.isdir(matchpath) and fnmatch(match, pattern): - if maxdepth > 0: - # exact directory match - walk down to maxdepth - for root, dirs, files in os.walk(matchpath, topdown=True): - depth = root[len(path):].count(os.path.sep) - if depth == maxdepth: - dirs[:] = [] # don't recurse any further - - for fn in files + (dirs if include_dirs else []): - out.append(self.to_location(self.join(root, fn))) - - elif include_dirs: - out.append(self.join(search_dir, match)) - - elif (os.path.isfile(matchpath) and - (fnmatch(match, pattern) or - fnmatch(os.path.splitext(match)[0], pattern))): - # exact filename match, or match up to an extension - # note that we need fnmatch(match, pattern) in addition to the - # splitext test to cover the case of the base filename itself - # containing a dot. - out.append(self.join(search_dir, match)) - - return out - - def remove(self, filename): - """Delete a file or folder and prune the directory tree.""" - path = self.to_path(filename) - if os.path.isdir(path): - shutil.rmtree(path) - else: - os.remove(path) - - filepath = os.path.split(path)[0] - try: - os.removedirs(filepath) - except OSError: - # directory was not empty - good that we're not removing it! - pass - - def remove_all(self, location): - """ - Delete all files/directories in the dataset at this location. - - Afterward prunes the directory tree. - """ - for fn in self.list(location): - self.remove(fn) +from qcodes.utils import issue_deprecation_warning + +try: + from qcodes_loop.data.io import ALLOWED_OPEN_MODES, DiskIO +except ImportError as e: + raise ImportError( + "qcodes.data.io is deprecated and has moved to " + "the package `qcodes_loop`. Please install qcodes_loop directly or " + "with `pip install qcodes[loop]" + ) from e +issue_deprecation_warning("qcodes.data.io module", alternative="qcodes_loop.data.io") diff --git a/qcodes/data/location.py b/qcodes/data/location.py index f3a154a2076..a0bd7dd26cc 100644 --- a/qcodes/data/location.py +++ b/qcodes/data/location.py @@ -5,170 +5,16 @@ from typing import cast import qcodes - - -class SafeFormatter(string.Formatter): - - """Modified string formatter that doesn't complain about missing keys.""" - - def get_value(self, key, args, kwargs): - """Missing keys just get left as they were: '{key}'.""" - try: - return super().get_value(key, args, kwargs) - except: - return f'{{{key}}}' - - -class FormatLocation: - - """ - This is the default DataSet Location provider. - - It provides a callable that returns a new (not used by another DataSet) - location string, based on a format string ``fmt`` and a dict ``record`` of - information to pass to ``fmt``. - - Default record items are ``date``, ``time``, and ``counter`` - Record item priority from lowest to highest (double items will be - overwritten): - - - current ``date``, and ``time`` - - record dict from ``__init__`` - - record dict from ``__call__`` - - automatic ``counter`` - - For example if any record dict contains a `date` keyword, it will no longer - be auto-generated. - - Uses ``io.list`` to search for existing data at a matching location. - - ``counter`` must NOT be provided in the record. If ``fmt`` contains - '{counter}', we look for existing files matching everything BEFORE this, - then find the highest counter (integer) among those files and use the next - value. - - If the format string does not contain ``{counter}`` but the location we - would return is occupied, we add ``'_{counter}'`` to the end. - - Usage:: - - loc_provider = FormatLocation( - fmt='{date}/#{counter}_{time}_{name}_{label}') - loc = loc_provider(DiskIO('.'), - record={'name': 'Rainbow', 'label': 'test'}) - loc - > '2016-04-30/#001_13-28-15_Rainbow_test' - - Args: - fmt (Optional[str]): a format string that all the other info will be - inserted into. Default '{date}/{time}', or '{date}/{time}_{name}' - if there is a ``name`` in the record. - - fmt_date (Optional[str]): a ``datetime.strftime`` format string, - should only use the date part. The result will be inserted in - '{date}' in ``fmt``. Default '%Y-%m-%d'. - - fmt_time (Optional[str]): a ``datetime.strftime`` format string, - should only use the time part. The result will be inserted in - '{time}' in ``fmt``. Default '%H-%M-%S'. - - fmt_counter (Optional[str]): a format string for the counter (integer) - which is automatically generated from existing DataSets that the - io manager can see. Default '{03}'. - - record (Optional[dict]): A dict of default values to provide when - calling the location_provider. Values provided later will - override these values. - - Note: - Do not include date/time or number formatting in ``fmt`` itself, such - as '{date:%Y-%m-%d}' or '{counter:03}' - """ - - default_fmt = qcodes.config['core']['default_fmt'] - default_fmt = cast(str, default_fmt) - - def __init__(self, fmt=None, fmt_date=None, fmt_time=None, - fmt_counter=None, record=None): - # TODO(giulioungaretti) this should be - # FormatLocation.default_fmt - self.fmt = fmt or self.default_fmt - self.fmt_date = fmt_date or '%Y-%m-%d' - self.fmt_time = fmt_time or '%H-%M-%S' - self.fmt_counter = fmt_counter or '{:03}' - self.base_record = record - self.formatter = SafeFormatter() - - self.counter = 0 - for testval in (1, 23, 456, 7890): - if self._findint(self.fmt_counter.format(testval)) != testval: - raise ValueError('fmt_counter must produce a correct integer ' - 'representation of its argument (eg "{:03}")', - fmt_counter) - - def _findint(self, s): - try: - return int(re.findall(r'\d+', s)[0]) - except: - return 0 - - def __call__(self, io, record=None): - """ - Call the location provider to get a new location. - - Args: - io (io_manager): where we intend to put the new DataSet. - - record (Optional[dict]): information to insert in the format string - Any key provided here will override the default record - """ - loc_fmt = self.fmt - - time_now = datetime.now() - date = time_now.strftime(self.fmt_date) - time = time_now.strftime(self.fmt_time) - format_record = {'date': date, 'time': time} - - if self.base_record: - format_record.update(self.base_record) - if record: - format_record.update(record) - - if 'counter' in format_record: - raise KeyError('you must not provide a counter in your record.', - format_record) - - if ('name' in format_record) and ('{name}' not in loc_fmt): - loc_fmt += '_{name}' - - if '{counter}' not in loc_fmt: - location = self.formatter.format(loc_fmt, **format_record) - if io.list(location): - loc_fmt += '_{counter}' - # redirect to the counter block below, but starting from 2 - # because the already existing file counts like 1 - existing_count = 1 - else: - return location - else: - # if counter is already in loc_fmt, start from 1 - existing_count = 0 - - # now search existing files for the next allowed counter - - head_fmt = loc_fmt.split('{counter}', 1)[0] - # io.join will normalize slashes in head to match the locations - # returned by io.list - head = io.join(self.formatter.format(head_fmt, **format_record)) - - file_list = io.list(head + '*', maxdepth=0, include_dirs=True) - - for f in file_list: - cnt = self._findint(f[len(head):]) - existing_count = max(existing_count, cnt) - - self.counter = existing_count + 1 - format_record['counter'] = self.fmt_counter.format(self.counter) - location = self.formatter.format(loc_fmt, **format_record) - - return location +from qcodes.utils import issue_deprecation_warning + +try: + from qcodes_loop.data.location import FormatLocation, SafeFormatter +except ImportError as e: + raise ImportError( + "qcodes.data.location is deprecated and has moved to " + "the package `qcodes_loop`. Please install qcodes_loop directly or " + "with `pip install qcodes[loop]" + ) from e +issue_deprecation_warning( + "qcodes.data.location module", alternative="qcodes_loop.data.location" +) diff --git a/qcodes/dataset/legacy_import.py b/qcodes/dataset/legacy_import.py index e006d1fd5b4..8051b09f8d8 100644 --- a/qcodes/dataset/legacy_import.py +++ b/qcodes/dataset/legacy_import.py @@ -2,15 +2,17 @@ import json from pathlib import Path +from typing import TYPE_CHECKING import numpy as np -from qcodes.data.data_array import DataArray -from qcodes.data.data_set import DataSet as OldDataSet -from qcodes.data.data_set import load_data from qcodes.dataset.experiment_container import Experiment from qcodes.dataset.measurements import DataSaver, Measurement +if TYPE_CHECKING: + from qcodes_loop.data.data_array import DataArray + from qcodes_loop.data.data_set import DataSet as OldDataSet + def setup_measurement( dataset: OldDataSet, exp: Experiment | None = None @@ -95,6 +97,12 @@ def import_dat_file(location: str | Path, exp: Experiment | None = None) -> list If None the default one is used. See the docs of :class:`qcodes.dataset.Measurement` for more details. """ + try: + from qcodes_loop.data.data_set import load_data + except ImportError as e: + raise ImportError( + "The legacy importer requires qcodes_loop to be installed." + ) from e loaded_data = load_data(str(location)) diff --git a/qcodes/extensions/slack.py b/qcodes/extensions/slack.py index 16b19c2fadd..8e80ae50d72 100644 --- a/qcodes/extensions/slack.py +++ b/qcodes/extensions/slack.py @@ -1,47 +1,3 @@ -""" -Slack bot is used to send information about qcodes via Slack IMs. -Some default commands are provided, and custom commands/tasks can be -attached (see below). - -To setup the Slack bot, a bot first has to be created via Slack -by clicking 'Create New App' on https://api.slack.com/apps. -Once created, the bot will have a name and unique token. -These and other settings have to be saved in a config dict (see init( or -Parameters) in :class:`Slack`). - -The App containing your bot needs to have the following bot token scopes to -perform all methods successfully: -- channels:history -- channels:read -- chat:write -- files:write -- users:read -These can be set after clicking OAuth & Permissions in the left menubar after -selecting your bot at https://api.slack.com/apps (or during creation). - -Communication with the Slack bot is performed via instant messaging. -When an IM is sent to the Slack bot, it will be processed during the next -`update()` call (provided the username is registered in the config). -Standard commands provided to the Slack bot are: - -- plot: Upload latest qcodes plot. -- msmt/measurement: Print information about latest measurement. -- notify finished: Send message once measurement is finished. - -Custom commands can be added as (cmd, func) key-value pairs to -`self.commands`. When `cmd` is sent to the bot, `func` is evaluated. - -Custom tasks can be added as well. These are functions that are performed -every time an update is called. The function must return a boolean that -indicates if the task should be removed from the list of tasks. -A custom task can be added as a (cmd, func) key-value pair to -`self.task_commands`. -They can then be called through Slack IM via: - -``notify/task {cmd} *args:`` register task with name `cmd` that is -performed every time `update()` is called. -""" - import inspect import logging import os @@ -57,403 +13,23 @@ from urllib3.exceptions import ReadTimeoutError from qcodes import config as qc_config -from qcodes.loops import active_data_set, active_loop from qcodes.parameters import ParameterBase -from qcodes.plots.base import BasePlot - - -class SlackTimeoutWarning(UserWarning): - pass - - -def convert_command(text): - def try_convert_str(string): - try: - val = int(string) - return val - except ValueError: - pass - try: - val = float(string) - return val - except ValueError: - pass - - return string - - # Format text to lowercase, and remove trailing whitespaces - text = text.lower().rstrip(" ") - command, *args_str = text.split(" ") - - # Convert string args to floats/kwargs - args = [] - kwargs = {} - for arg in args_str: - if "=" in arg: - # arg is a kwarg - key, val = arg.split("=") - # Try to convert into a float - val = try_convert_str(val) - kwargs[key] = val - else: - # arg is not a kwarg - # Try to convert into a float - val = try_convert_str(arg) - args.append(val) - return command, args, kwargs - - -class Slack(threading.Thread): - def __init__(self, interval=3, config=None, auto_start=True, **commands): - """ - Initializes Slack bot, including auto-updating widget if in notebook - and using multiprocessing. - - Args: - interval (int): Update interval for widget (must be over 1s). - config (Optional[dict]): Config dict - If not given, uses qc.config['user']['slack'] - The config dict must contain the following keys: - - - 'bot_name': Name of the bot - - 'bot_token': Token from bot (obtained from slack website) - - 'names': Usernames to periodically check for IM messages - - auto_start (bool): Defaults to True. - - """ - if config is not None: - self.config = config - else: - self.config = qc_config.user.slack - - self.slack = WebClient(token=self.config["token"]) - self.users = self.get_users(self.config["names"]) - self.get_im_ids(self.users) - - self.commands = { - "plot": self.upload_latest_plot, - "msmt": self.print_measurement_information, - "measurement": self.print_measurement_information, - "notify": self.add_task, - "help": self.help_message, - "task": self.add_task, - **commands, - } - self.task_commands = {"finished": self.check_msmt_finished} - - self.interval = interval - self.tasks = [] - - # Flag that exits loop when set to True (called via self.exit()) - self._exit = False - - # Flag that enables actions to be performed in the event loop - # Enabled via self.start(), disabled via self.stop() - self._is_active = False - - # Call Thread init - super().__init__() - - if auto_start: - self.start() - - def start(self): - self._is_active = True - try: - # Start thread, can only be called once - super().start() - except RuntimeError: - # Thread already started, ignoring - pass - - def run(self): - """ - Thread event loop that periodically checks for updates. - Can be stopped via :meth:`stop` , after which the Thread is stopped. - Returns: - None. - """ - while not self._exit: - # Continue event loop - if self._is_active: - # check for updates - self.update() - sleep(self.interval) - - def stop(self): - """ - Stop checking for updates. Can be started again via :meth:`start`. - Returns: - None. - """ - self._is_active = False - - def exit(self): - """ - Exit event loop, stop Thread. - Returns: - None - """ - self._stop = True - - def user_from_id(self, user_id): - """ - Retrieve user from user id. - Args: - user_id: Id from which to retrieve user information. - - Returns: - dict: User information. - """ - return self.slack.users_info(user=user_id)["user"] - - def get_users(self, usernames): - """ - Extracts user information for users. - Args: - usernames: Slack usernames of users. - - Returns: - dict: {username: user} - """ - users = {} - response = self.slack.users_list() - for member in response["members"]: - if member["name"] in usernames: - users[member["name"]] = member - if len(users) != len(usernames): - remaining_names = [name for name in usernames if name not in users] - raise RuntimeError(f"Could not find names {remaining_names}") - return users - - def get_im_ids(self, users): - """ - Adds IM ids of users to users dict. - Also adds `last_ts` to the latest IM message - Args: - users (dict): {username: user} - - Returns: - None. - """ - response = self.slack.conversations_list(types="im") - user_ids = {username: user["id"] for username, user in users.items()} - im_ids = {chan["user"]: chan["id"] for chan in response["channels"]} - for username, user_id in user_ids.items(): - if user_id in im_ids.keys(): - users[username]["im_id"] = im_ids[user_id] - # update last ts - messages = self.get_im_messages(username=username, limit=1) - if messages: - users[username]["last_ts"] = float(messages[0]["ts"]) - else: - users[username]["last_ts"] = None - - def get_im_messages(self, username, **kwargs): - """ - Retrieves IM messages from username. - Args: - username: Name of user. - **kwargs: Additional kwargs for retrieving IM messages. - - Returns: - List of IM messages. - """ - # provide backward compatibility with 'count' keyword. It still works, - # but is undocumented. 'count' likely does the same as 'limit', but - # 'limit' takes precedence - if "limit" not in kwargs.keys(): - kwargs["limit"] = kwargs.pop("count", None) - - channel = self.users[username].get("im_id", None) - if channel is None: - return [] - else: - response = self.slack.conversations_history(channel=channel, **kwargs) - return response["messages"] - - def get_new_im_messages(self): - """ - Retrieves new IM messages for each user in self.users. - Updates user['last_ts'] to ts of newest message. - Returns: - im_messages (Dict): {username: [messages list]} newer than last_ts. - """ - im_messages = {} - for username, user in self.users.items(): - last_ts = user.get("last_ts", None) - new_messages = self.get_im_messages(username=username, oldest=last_ts) - # Kwarg 'oldest' sometimes also returns message with ts==last_ts - new_messages = [m for m in new_messages if float(m["ts"]) != last_ts] - im_messages[username] = new_messages - if new_messages: - self.users[username]["last_ts"] = float(new_messages[0]["ts"]) - return im_messages - - def update(self): - """ - Performs tasks, and checks for new messages. - Periodically called from widget update. - Returns: - None. - """ - new_tasks = [] - for task in self.tasks: - task_finished = task() - if not task_finished: - new_tasks.append(task) - self.tasks = new_tasks - - new_messages = {} - try: - new_messages = self.get_new_im_messages() - except (ReadTimeout, HTTPError, ConnectTimeout, ReadTimeoutError) as e: - # catch any timeouts caused by network delays - warnings.warn("error retrieving slack messages", SlackTimeoutWarning) - logging.info(e) - self.handle_messages(new_messages) - - def help_message(self): - """Return simple help message""" - cc = ", ".join("`" + str(k) + "`" for k in self.commands.keys()) - return "\nAvailable commands: %s" % cc - - def handle_messages(self, messages): - """ - Performs commands depending on messages. - This includes adding tasks to be performed during each update. - """ - for user, user_messages in messages.items(): - for message in user_messages: - if message.get("user", None) != self.users[user]["id"]: - # Filter out bot messages - continue - channel = self.users[user]["im_id"] - # Extract command (first word) and possible args - command, args, kwargs = convert_command(message["text"]) - if command in self.commands: - msg = f"Executing {command}" - if args: - msg += f" {args}" - if kwargs: - msg += f" {kwargs}" - self.slack.chat_postMessage(text=msg, channel=channel) - - func = self.commands[command] - try: - if isinstance(func, ParameterBase): - results = func(*args, **kwargs) - else: - # Only add channel and Slack if they are explicit - # kwargs - func_sig = inspect.signature(func) - if "channel" in func_sig.parameters: - kwargs["channel"] = channel - if "slack" in func_sig.parameters: - kwargs["slack"] = self - results = func(*args, **kwargs) - - if results is not None: - self.slack.chat_postMessage( - text=f"Results: {results}", channel=channel - ) - - except Exception: - self.slack.chat_postMessage( - text=f"Error: {traceback.format_exc()}", channel=channel - ) - else: - self.slack.chat_postMessage( - text=f"Command {command} not understood. Try `help`", - channel=channel, - ) - - def add_task(self, command, *args, channel, **kwargs): - """ - Add a task to self.tasks, which will be executed during each update - Args: - command: Task command. - *args: Additional args for command. - channel: Slack channel (can also be IM channel). - **kwargs: Additional kwargs for particular. - - Returns: - None. - """ - if command in self.task_commands: - self.slack.chat_postMessage(text=f'Added task "{command}"', channel=channel) - func = self.task_commands[command] - self.tasks.append(partial(func, *args, channel=channel, **kwargs)) - else: - self.slack.chat_postMessage( - text=f"Task command {command} not understood", channel=channel - ) - - def upload_latest_plot(self, channel, **kwargs): - """ - Uploads latest plot (if any) to slack channel. - The latest plot is retrieved from - :class:`qcodes.plots.base.BasePlot`, which is updated - every time a new qcodes plot is instantiated. - Args: - channel: Slack channel (can also be IM channel). - **kwargs: Not used. - - Returns: - None. - """ - - # Create temporary filename - temp_filename = tempfile.mktemp(suffix=".jpg") - # Retrieve latest plot - latest_plot = BasePlot.latest_plot - if latest_plot is not None: - # Saves latest plot to filename - latest_plot.save(filename=temp_filename) - # Upload plot to slack - self.slack.files_upload(file=temp_filename, channels=channel) - os.remove(temp_filename) - else: - self.slack.chat_postMessage(text="No latest plot", channel=channel) +from qcodes.utils import issue_deprecation_warning - def print_measurement_information(self, channel, **kwargs): - """ - Prints information about the current measurement. - Information printed is percentage complete, and dataset representation. - Dataset is retrieved from DataSet.latest_dataset, which updates itself - every time a new dataset is created - Args: - channel: Slack channel (can also be IM channel). - **kwargs: Not used. +try: + from qcodes_loop.extensions.slack import Slack, SlackTimeoutWarning, convert_command + from qcodes_loop.loops import active_data_set, active_loop + from qcodes_loop.plots.base import BasePlot +except ImportError as e: + raise ImportError( + "qcodes.utils.slack is deprecated and has moved to " + "the package `qcodes_loop`. Please install qcodes_loop directly or " + "with `pip install qcodes[loop]" + ) from e - Returns: - None. - """ - dataset = active_data_set() - if dataset is not None: - self.slack.chat_postMessage( - text="Measurement is {:.0f}% complete".format( - 100 * dataset.fraction_complete() - ), - channel=channel, - ) - self.slack.chat_postMessage(text=repr(dataset), channel=channel) - else: - self.slack.chat_postMessage(text="No latest dataset found", channel=channel) - def check_msmt_finished(self, channel, **kwargs): - """ - Checks if the latest measurement is completed. - Args: - channel: Slack channel (can also be IM channel). - **kwargs: Not used. - Returns: - bool: True if measurement is finished, False otherwise. - """ - if active_loop() is None: - self.slack.chat_postMessage(text="Measurement complete", channel=channel) - return True - else: - return False +__all__ = ["Slack", "SlackTimeoutWarning", "convert_command"] +issue_deprecation_warning( + "qcodes.utils.slack module", alternative="qcodes_loop.extensions.slack" +) diff --git a/qcodes/instrument_drivers/Keithley/_Keithley_2600.py b/qcodes/instrument_drivers/Keithley/_Keithley_2600.py index e7a9dde915c..2f1e20bf36f 100644 --- a/qcodes/instrument_drivers/Keithley/_Keithley_2600.py +++ b/qcodes/instrument_drivers/Keithley/_Keithley_2600.py @@ -1,16 +1,16 @@ +from __future__ import annotations + import logging import struct import sys import warnings from enum import Enum -from typing import Any, Dict, List, Literal, Optional, Sequence, Tuple +from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Sequence, Tuple import numpy as np import qcodes.validators as vals -from qcodes.data.data_set import DataSet from qcodes.instrument import Instrument, InstrumentChannel, VisaInstrument -from qcodes.measure import Measure from qcodes.parameters import ( ArrayParameter, Parameter, @@ -19,6 +19,10 @@ create_on_off_val_mapping, ) +if TYPE_CHECKING: + from qcodes_loop.data.data_set import DataSet + + if sys.version_info >= (3, 11): from enum import StrEnum else: @@ -237,14 +241,14 @@ class _ParameterWithStatus(Parameter): def __init__(self, *args: Any, **kwargs: Any): super().__init__(*args, **kwargs) - self._measurement_status: Optional[Keithley2600MeasurementStatus] = None + self._measurement_status: Keithley2600MeasurementStatus | None = None @property - def measurement_status(self) -> Optional[Keithley2600MeasurementStatus]: + def measurement_status(self) -> Keithley2600MeasurementStatus | None: return self._measurement_status @staticmethod - def _parse_response(data: str) -> Tuple[float, Keithley2600MeasurementStatus]: + def _parse_response(data: str) -> tuple[float, Keithley2600MeasurementStatus]: value, meas_status = data.split("\t") status_bits = [ @@ -260,9 +264,9 @@ def _parse_response(data: str) -> Tuple[float, Keithley2600MeasurementStatus]: def snapshot_base( self, - update: Optional[bool] = True, - params_to_skip_update: Optional[Sequence[str]] = None, - ) -> Dict[Any, Any]: + update: bool | None = True, + params_to_skip_update: Sequence[str] | None = None, + ) -> dict[Any, Any]: snapshot = super().snapshot_base( update=update, params_to_skip_update=params_to_skip_update ) @@ -626,6 +630,13 @@ def doFastSweep(self, start: float, stop: float, steps: int, mode: str) -> DataS 'VI' (current sweep two probe setup) or 'VIfourprobe' (current sweep four probe setup) """ + try: + from qcodes_loop.measure import Measure + except ImportError as e: + raise ImportError( + "The doFastSweep method requires the " + "qcodes_loop package to be installed." + ) # prepare setpoints, units, name self.fastsweep.prepareSweep(start, stop, steps, mode) @@ -704,7 +715,7 @@ def _fast_sweep( return self._execute_lua(script, steps) - def _execute_lua(self, _script: List[str], steps: int) -> np.ndarray: + def _execute_lua(self, _script: list[str], steps: int) -> np.ndarray: """ This is the function that sends the Lua script to be executed and returns the corresponding data from the buffer. @@ -891,7 +902,7 @@ def __init__(self, name: str, address: str, **kwargs: Any) -> None: "2636B": [100e-12, 1.5], } # Add the channel to the instrument - self.channels: List[Keithley2600Channel] = [] + self.channels: list[Keithley2600Channel] = [] for ch in ["a", "b"]: ch_name = f"smu{ch}" channel = Keithley2600Channel(self, ch_name, ch_name) @@ -908,12 +919,12 @@ def __init__(self, name: str, address: str, **kwargs: Any) -> None: def _display_settext(self, text: str) -> None: self.visa_handle.write(f'display.settext("{text}")') - def get_idn(self) -> Dict[str, Optional[str]]: + def get_idn(self) -> dict[str, str | None]: IDNstr = self.ask_raw("*IDN?") vendor, model, serial, firmware = map(str.strip, IDNstr.split(",")) model = model[6:] - IDN: Dict[str, Optional[str]] = { + IDN: dict[str, str | None] = { "vendor": vendor, "model": model, "serial": serial, @@ -958,7 +969,7 @@ def ask(self, cmd: str) -> str: return super().ask(f"print({cmd:s})") @staticmethod - def _scriptwrapper(program: List[str], debug: bool = False) -> str: + def _scriptwrapper(program: list[str], debug: bool = False) -> str: """ wraps a program so that the output can be put into visa_handle.write and run. diff --git a/qcodes/loops.py b/qcodes/loops.py index da07c07a2d5..0481036235a 100644 --- a/qcodes/loops.py +++ b/qcodes/loops.py @@ -1,43 +1,5 @@ """ -Data acquisition loops. - -The general scheme is: - -1. create a (potentially nested) Loop, which defines the sweep setpoints and -delays - -2. activate the loop (which changes it to an ActiveLoop object), - -3. run it with the .run method, which creates a DataSet to hold the data, -and defines how and where to save the data. - -Some examples: - -- 1D sweep, using the default measurement set - ->>> Loop(sweep_values, delay).run() - -- 2D sweep, using the default measurement set sv1 is the outer loop, sv2 is the - inner. - ->>> Loop(sv1, delay1).loop(sv2, delay2).run() - -- 1D sweep with specific measurements to take at each point - ->>> Loop(sv, delay).each(param4, param5).run() - -- Multidimensional sweep: 1D measurement of param6 on the outer loop, and another - measurement in an inner loop. - ->>> Loop(sv1, delay).each(param6, Loop(sv2, delay).each(sv3, delay)).run() - -Supported commands to .each are: - - - Parameter: anything with a .get method and .name or .names see - parameter.py for options - - ActiveLoop - - Task: any callable that does not generate data - - Wait: a delay +Deprecated """ import logging import time @@ -46,890 +8,34 @@ import numpy as np -from qcodes.data.data_array import DataArray -from qcodes.data.data_set import new_data from qcodes.metadatable import Metadatable from qcodes.station import Station -from qcodes.utils import full_class - -from .actions import ( - BreakIf, - Task, - Wait, - _actions_snapshot, - _Measure, - _Nest, - _QcodesBreak, -) - -log = logging.getLogger(__name__) - -_tprint_times: Dict[str, float] = {} - - -def wait_secs(finish_clock: float) -> float: - """ - Calculate the number of seconds until a given clock time. - The clock time should be the result of ``time.perf_counter()``. - Does NOT wait for this time. - """ - delay = finish_clock - time.perf_counter() - if delay < 0: - logging.warning(f"negative delay {delay:.6f} sec") - return 0 - return delay - - -def tprint(string: str, dt: int = 1, tag: str = "default") -> None: - """Print progress of a loop every ``dt`` seconds.""" - ptime = _tprint_times.get(tag, 0) - if (time.time() - ptime) > dt: - print(string) - _tprint_times[tag] = time.time() - - -def active_loop(): - return ActiveLoop.active_loop - - -def active_data_set(): - loop = active_loop() - if loop is not None and loop.data_set is not None: - return loop.data_set - else: - return None - - -class Loop(Metadatable): - """ - The entry point for creating measurement loops - - Args: - sweep_values: a SweepValues or compatible object describing what - parameter to set in the loop and over what values - delay: a number of seconds to wait after setting a value before - continuing. 0 (default) means no waiting and no warnings. > 0 - means to wait, potentially filling the delay time with monitoring, - and give an error if you wait longer than expected. - progress_interval: should progress of the loop every x seconds. Default - is None (no output) - - After creating a Loop, you attach one or more ``actions`` to it, making an - ``ActiveLoop`` - - ``actions`` is a sequence of things to do at each ``Loop`` step: that can be - a ``Parameter`` to measure, a ``Task`` to do (any callable that does not - yield data), ``Wait`` times, or another ``ActiveLoop`` or ``Loop`` to nest - inside this one. - """ - def __init__(self, sweep_values, delay=0, station=None, - progress_interval=None): - super().__init__() - if delay < 0: - raise ValueError(f"delay must be > 0, not {repr(delay)}") - - self.sweep_values = sweep_values - self.delay = delay - self.station = station - self.nested_loop = None - self.actions = None - self.then_actions = () - self.bg_task = None - self.bg_final_task = None - self.bg_min_delay = None - self.progress_interval = progress_interval - - def __getitem__(self, item): - """ - Retrieves action with index `item` - Args: - item: actions index - - Returns: - loop.actions[item] - """ - return self.actions[item] - - def loop(self, sweep_values, delay=0): - """ - Nest another loop inside this one. - - Args: - sweep_values: - delay (int): - - Examples: - >>> Loop(sv1, d1).loop(sv2, d2).each(*a) - - is equivalent to: - - >>> Loop(sv1, d1).each(Loop(sv2, d2).each(*a)) - - Returns: a new Loop object - the original is untouched - """ - out = self._copy() - - if out.nested_loop: - # nest this new loop inside the deepest level - out.nested_loop = out.nested_loop.loop(sweep_values, delay) - else: - out.nested_loop = Loop(sweep_values, delay) - - return out - - def _copy(self): - out = Loop(self.sweep_values, self.delay, - progress_interval=self.progress_interval) - out.nested_loop = self.nested_loop - out.then_actions = self.then_actions - out.station = self.station - return out - - def each(self, *actions): - """ - Perform a set of actions at each setting of this loop. - TODO(setting vs setpoints) ? better be verbose. - - Args: - *actions (Any): actions to perform at each setting of the loop - - Each action can be: - - - a Parameter to measure - - a Task to execute - - a Wait - - another Loop or ActiveLoop - - """ - actions = list(actions) - - self.validate_actions(*actions) - - if self.nested_loop: - # recurse into the innermost loop and apply these actions there - actions = [self.nested_loop.each(*actions)] - - return ActiveLoop(self.sweep_values, self.delay, *actions, - then_actions=self.then_actions, station=self.station, - progress_interval=self.progress_interval, - bg_task=self.bg_task, bg_final_task=self.bg_final_task, bg_min_delay=self.bg_min_delay) - - def with_bg_task(self, task, bg_final_task=None, min_delay=0.01): - """ - Attaches a background task to this loop. - - Args: - task: A callable object with no parameters. This object will be - invoked periodically during the measurement loop. - - bg_final_task: A callable object with no parameters. This object will be - invoked to clean up after or otherwise finish the background - task work. - - min_delay (int, float): The minimum number of seconds to wait - between task invocations. Defaults to 0.01 s. - Note that if a task is doing a lot of processing it is recommended - to increase min_delay. - Note that the actual time between task invocations may be much - longer than this, as the task is only run between passes - through the loop. - """ - return _attach_bg_task(self, task, bg_final_task, min_delay) - - @staticmethod - def validate_actions(*actions): - """ - Whitelist acceptable actions, so we can give nice error messages - if an action is not recognized - """ - for action in actions: - if isinstance(action, (Task, Wait, BreakIf, ActiveLoop)): - continue - if hasattr(action, 'get') and (hasattr(action, 'name') or - hasattr(action, 'names')): - continue - raise TypeError('Unrecognized action:', action, - 'Allowed actions are: objects (parameters) with ' - 'a `get` method and `name` or `names` attribute, ' - 'and `Task`, `Wait`, `BreakIf`, and `ActiveLoop` ' - 'objects. `Loop` objects are OK too, except in ' - 'Station default measurements.') - - - def then(self, *actions, overwrite=False): - """ - Attach actions to be performed after the loop completes. - - These can only be ``Task`` and ``Wait`` actions, as they may not generate - any data. - - returns a new Loop object - the original is untouched - - This is more naturally done to an ActiveLoop (ie after .each()) - and can also be done there, but it's allowed at this stage too so that - you can define final actions and share them among several ``Loops`` that - have different loop actions, or attach final actions to a Loop run - - TODO: - examples of this ? with default actions. - - Args: - *actions: ``Task`` and ``Wait`` objects to execute in order - - overwrite: (default False) whether subsequent .then() calls (including - calls in an ActiveLoop after .then() has already been called on - the Loop) will add to each other or overwrite the earlier ones. - Returns: - a new Loop object - the original is untouched - """ - return _attach_then_actions(self._copy(), actions, overwrite) - - def snapshot_base(self, update: Optional[bool] = False, - params_to_skip_update: Optional[Sequence[str]] = None): - """ - State of the loop as a JSON-compatible dict (everything that - the custom JSON encoder class :class:'.NumpyJSONEncoder' - supports). - - Args: - update: If True, update the state by querying the underlying - sweep_values and actions. If None only update state if known - to be invalid. If False, just use the latest values - in memory. - params_to_skip_update: Unused in this implementation. - - Returns: - dict: base snapshot - """ - return { - '__class__': full_class(self), - 'sweep_values': self.sweep_values.snapshot(update=update), - 'delay': self.delay, - 'then_actions': _actions_snapshot(self.then_actions, update) - } - - -def _attach_then_actions(loop, actions, overwrite): - """Inner code for both Loop.then and ActiveLoop.then.""" - for action in actions: - if not isinstance(action, (Task, Wait)): - raise TypeError('Unrecognized action:', action, - '.then() allows only `Task` and `Wait` ' - 'actions.') - - if overwrite: - loop.then_actions = actions - else: - loop.then_actions = loop.then_actions + actions - - return loop - - -def _attach_bg_task(loop, task, bg_final_task, min_delay): - """Inner code for both Loop and ActiveLoop.bg_task""" - if loop.bg_task is None: - loop.bg_task = task - loop.bg_min_delay = min_delay - else: - raise RuntimeError('Only one background task is allowed per loop') - - if bg_final_task: - loop.bg_final_task = bg_final_task - - return loop - - -class ActiveLoop(Metadatable): - """ - Created by attaching ``actions`` to a ``Loop``, this is the object that - actually runs a measurement loop. An ``ActiveLoop`` can no longer be nested, - only run, or used as an action inside another ``Loop`` which will run the - whole thing. - - The ``ActiveLoop`` determines what ``DataArrays`` it will need to hold the - data it collects, and it creates a ``DataSet`` holding these ``DataArrays`` - """ - - # Currently active loop, is set when calling loop.run(set_active=True) - # is reset to None when active measurement is finished - active_loop = None - - def __init__(self, sweep_values, delay, *actions, then_actions=(), - station=None, progress_interval=None, bg_task=None, - bg_final_task=None, bg_min_delay=None): - super().__init__() - self.sweep_values = sweep_values - self.delay = delay - self.actions = list(actions) - self.progress_interval = progress_interval - self.then_actions = then_actions - self.station = station - self.bg_task = bg_task - self.bg_final_task = bg_final_task - self.bg_min_delay = bg_min_delay - self.data_set = None - - # if the first action is another loop, it changes how delays - # happen - the outer delay happens *after* the inner var gets - # set to its initial value - self._nest_first = hasattr(actions[0], 'containers') - - def __getitem__(self, item): - """ - Retrieves action with index `item` - Args: - item: actions index - - Returns: - loop.actions[item] - """ - return self.actions[item] - - def then(self, *actions, overwrite=False): - """ - Attach actions to be performed after the loop completes. - - These can only be ``Task`` and ``Wait`` actions, as they may not - generate any data. - - returns a new ActiveLoop object - the original is untouched - - - - Args: - *actions: ``Task`` and ``Wait`` objects to execute in order - - overwrite: (default False) whether subsequent .then() calls (including - calls in an ActiveLoop after .then() has already been called on - the Loop) will add to each other or overwrite the earlier ones. - """ - loop = ActiveLoop(self.sweep_values, self.delay, *self.actions, - then_actions=self.then_actions, station=self.station) - return _attach_then_actions(loop, actions, overwrite) - - def with_bg_task(self, task, bg_final_task=None, min_delay=0.01): - """ - Attaches a background task to this loop. - - Args: - task: A callable object with no parameters. This object will be - invoked periodically during the measurement loop. - - bg_final_task: A callable object with no parameters. This object will be - invoked to clean up after or otherwise finish the background - task work. - - min_delay (int, float): The minimum number of seconds to wait - between task invocations. Note that the actual time between - task invocations may be much longer than this, as the task is - only run between passes through the loop. Defaults to 0.01 s. - """ - return _attach_bg_task(self, task, bg_final_task, min_delay) - - def snapshot_base(self, update=False, - params_to_skip_update: Optional[Sequence[str]] = None): - """Snapshot of this ActiveLoop's definition.""" - return { - '__class__': full_class(self), - 'sweep_values': self.sweep_values.snapshot(update=update), - 'delay': self.delay, - 'actions': _actions_snapshot(self.actions, update), - 'then_actions': _actions_snapshot(self.then_actions, update) - } - - def containers(self): - """ - Finds the data arrays that will be created by the actions in this - loop, and nests them inside this level of the loop. - - Recursively calls `.containers` on any enclosed actions. - """ - loop_size = len(self.sweep_values) - data_arrays = [] - loop_array = DataArray(parameter=self.sweep_values.parameter, - is_setpoint=True) - loop_array.nest(size=loop_size) - - data_arrays = [loop_array] - # hack set_data into actions - new_actions = self.actions[:] - if hasattr(self.sweep_values, "parameters"): # combined parameter - for parameter in self.sweep_values.parameters: - new_actions.append(parameter) - - for i, action in enumerate(new_actions): - if hasattr(action, 'containers'): - action_arrays = action.containers() - - elif hasattr(action, 'get'): - # this action is a parameter to measure - # note that this supports lists (separate output arrays) - # and arrays (nested in one/each output array) of return values - action_arrays = self._parameter_arrays(action) - - else: - # this *is* covered but the report misses it because Python - # optimizes it away. See: - # https://bitbucket.org/ned/coveragepy/issues/198 - continue # pragma: no cover - - for array in action_arrays: - array.nest(size=loop_size, action_index=i, - set_array=loop_array) - data_arrays.extend(action_arrays) - - return data_arrays - - def _parameter_arrays(self, action): - out = [] - - # first massage all the input parameters to the general multi-name form - if hasattr(action, 'names'): - names = action.names - full_names = action.full_names - labels = getattr(action, 'labels', names) - if len(labels) != len(names): - raise ValueError('must have equal number of names and labels') - action_indices = tuple((i,) for i in range(len(names))) - elif hasattr(action, 'name'): - names = (action.name,) - full_names = (action.full_name,) - labels = (getattr(action, 'label', action.name),) - action_indices = ((),) - else: - raise ValueError('a gettable parameter must have .name or .names') - if hasattr(action, 'names') and hasattr(action, 'units'): - units = action.units - elif hasattr(action, 'unit'): - units = (action.unit,) - else: - units = tuple(['']*len(names)) - num_arrays = len(names) - shapes = getattr(action, 'shapes', None) - sp_vals = getattr(action, 'setpoints', None) - sp_names = getattr(action, 'setpoint_names', None) - sp_labels = getattr(action, 'setpoint_labels', None) - sp_units = getattr(action, 'setpoint_units', None) - - if shapes is None: - shapes = (getattr(action, 'shape', ()),) * num_arrays - sp_vals = (sp_vals,) * num_arrays - sp_names = (sp_names,) * num_arrays - sp_labels = (sp_labels,) * num_arrays - sp_units = (sp_units,) * num_arrays - else: - sp_blank = (None,) * num_arrays - # _fill_blank both supplies defaults and tests length - # if values are supplied (for shapes it ONLY tests length) - shapes = self._fill_blank(shapes, sp_blank) - sp_vals = self._fill_blank(sp_vals, sp_blank) - sp_names = self._fill_blank(sp_names, sp_blank) - sp_labels = self._fill_blank(sp_labels, sp_blank) - sp_units = self._fill_blank(sp_units, sp_blank) - - # now loop through these all, to make the DataArrays - # record which setpoint arrays we've made, so we don't duplicate - all_setpoints = {} - for name, full_name, label, unit, shape, i, sp_vi, sp_ni, sp_li, sp_ui in zip( - names, full_names, labels, units, shapes, action_indices, - sp_vals, sp_names, sp_labels, sp_units): - - if shape is None or shape == (): - shape, sp_vi, sp_ni, sp_li, sp_ui= (), (), (), (), () - else: - sp_blank = (None,) * len(shape) - sp_vi = self._fill_blank(sp_vi, sp_blank) - sp_ni = self._fill_blank(sp_ni, sp_blank) - sp_li = self._fill_blank(sp_li, sp_blank) - sp_ui = self._fill_blank(sp_ui, sp_blank) - - setpoints = () - # loop through dimensions of shape to make the setpoint arrays - for j, (vij, nij, lij, uij) in enumerate(zip(sp_vi, sp_ni, sp_li, sp_ui)): - sp_def = (shape[: 1 + j], j, setpoints, vij, nij, lij, uij) - if sp_def not in all_setpoints: - all_setpoints[sp_def] = self._make_setpoint_array(*sp_def) - out.append(all_setpoints[sp_def]) - setpoints = setpoints + (all_setpoints[sp_def],) - - # finally, make the output data array with these setpoints - out.append(DataArray(name=name, full_name=full_name, label=label, - shape=shape, action_indices=i, unit=unit, - set_arrays=setpoints, parameter=action)) - - return out - - def _fill_blank(self, inputs, blanks): - if inputs is None: - return blanks - elif len(inputs) == len(blanks): - return inputs - else: - raise ValueError('Wrong number of inputs supplied') - - def _make_setpoint_array(self, shape, i, prev_setpoints, vals, name, - label, unit): - if vals is None: - vals = self._default_setpoints(shape) - elif isinstance(vals, DataArray): - # can't simply use the DataArray, even though that's - # what we're going to return here, because it will - # get nested (don't want to alter the original) - # DataArrays do have the advantage though of already including - # name and label, so take these if they exist - if vals.name is not None: - name = vals.name - if vals.label is not None: - label = vals.label - - # extract a copy of the numpy array - vals = np.array(vals.ndarray) - else: - # turn any sequence into a (new) numpy array - vals = np.array(vals) - - if vals.shape != shape: - raise ValueError('nth setpoint array should have shape matching ' - 'the first n dimensions of shape.') - - if name is None: - name = f'index{i}' - - return DataArray(name=name, label=label, set_arrays=prev_setpoints, - shape=shape, preset_data=vals, unit=unit, is_setpoint=True) - - def _default_setpoints(self, shape): - if len(shape) == 1: - return np.arange(0, shape[0], 1) - - sp = np.ndarray(shape) - sp_inner = self._default_setpoints(shape[1:]) - for i in range(len(sp)): - sp[i] = sp_inner - - return sp - - def set_common_attrs(self, data_set, use_threads): - """ - set a couple of common attributes that the main and nested loops - all need to have: - - the DataSet collecting all our measurements - - a queue for communicating with the main process - """ - self.data_set = data_set - self.use_threads = use_threads - for action in self.actions: - if hasattr(action, 'set_common_attrs'): - action.set_common_attrs(data_set, use_threads) - - def get_data_set(self, *args, **kwargs): - """ - Return the data set for this loop. - - If no data set has been created yet, a new one will be created and - returned. Note that all arguments can only be provided when the - `DataSet` is first created; giving these during `run` when - `get_data_set` has already been called on its own is an error. - - Args: - data_manager: a DataManager instance (omit to use default, - False to store locally) - - kwargs are passed along to data_set.new_data. The key ones are: - - Args: - location: the location of the DataSet, a string whose meaning - depends on formatter and io, or False to only keep in memory. - May be a callable to provide automatic locations. If omitted, will - use the default DataSet.location_provider - name: if location is default or another provider function, name is - a string to add to location to make it more readable/meaningful - to users - formatter: knows how to read and write the file format - default can be set in DataSet.default_formatter - io: knows how to connect to the storage (disk vs cloud etc) - write_period: how often to save to storage during the loop. - default 5 sec, use None to write only at the end - - returns: - a DataSet object that we can use to plot - """ - if self.data_set is None: - data_set = new_data(arrays=self.containers(), *args, **kwargs) - self.data_set = data_set - - else: - has_args = len(kwargs) or len(args) - if has_args: - raise RuntimeError( - 'The DataSet for this loop already exists. ' - 'You can only provide DataSet attributes, such as ' - 'data_manager, location, name, formatter, io, ' - 'write_period, when the DataSet is first created.') - - return self.data_set - - def run_temp(self, **kwargs): - """ - wrapper to run this loop in the foreground as a temporary data set, - especially for use in composite parameters that need to run a Loop - as part of their get method - """ - return self.run(quiet=True, location=False, **kwargs) - - def run(self, use_threads=False, quiet=False, station=None, - progress_interval=False, set_active=True, *args, **kwargs): - """ - Execute this loop. - - Args: - use_threads: (default False): whenever there are multiple `get` calls - back-to-back, execute them in separate threads so they run in - parallel (as long as they don't block each other) - quiet: (default False): set True to not print anything except errors - station: a Station instance for snapshots (omit to use a previously - provided Station, or the default Station) - progress_interval (int, float): show progress of the loop every x - seconds. If provided here, will override any interval provided - with the Loop definition. Defaults to None - - kwargs are passed along to data_set.new_data. These can only be - provided when the `DataSet` is first created; giving these during `run` - when `get_data_set` has already been called on its own is an error. - The key ones are: - - Args: - location: the location of the DataSet, a string whose meaning - depends on formatter and io, or False to only keep in memory. - May be a callable to provide automatic locations. If omitted, will - use the default DataSet.location_provider - name: if location is default or another provider function, name is - a string to add to location to make it more readable/meaningful - to users - formatter: knows how to read and write the file format - default can be set in DataSet.default_formatter - io: knows how to connect to the storage (disk vs cloud etc) - write_period: how often to save to storage during the loop. - default 5 sec, use None to write only at the end - - - returns: - a DataSet object that we can use to plot - """ - if progress_interval is not False: - self.progress_interval = progress_interval - - data_set = self.get_data_set(*args, **kwargs) - - self.set_common_attrs(data_set=data_set, use_threads=use_threads) - - station = station or self.station or Station.default - if station: - data_set.add_metadata({'station': station.snapshot()}) - - # information about the loop definition is in its snapshot - data_set.add_metadata({'loop': self.snapshot()}) - # then add information about how and when it was run - ts = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - data_set.add_metadata({'loop': { - 'ts_start': ts, - 'use_threads': use_threads, - }}) - - data_set.save_metadata() - - if set_active: - ActiveLoop.active_loop = self - - try: - if not quiet: - print(datetime.now().strftime('Started at %Y-%m-%d %H:%M:%S')) - self._run_wrapper() - ds = self.data_set - finally: - if not quiet: - print(repr(self.data_set)) - print(datetime.now().strftime('Finished at %Y-%m-%d %H:%M:%S')) - - # After normal loop execution we clear the data_set so we can run - # again. But also if something went wrong during the loop execution - # we want to clear the data_set attribute so we don't try to reuse - # this one later. - self.data_set = None - if set_active: - ActiveLoop.active_loop = None - - return ds - - def _compile_actions(self, actions, action_indices=()): - callables = [] - measurement_group = [] - for i, action in enumerate(actions): - new_action_indices = action_indices + (i,) - if hasattr(action, 'get'): - measurement_group.append((action, new_action_indices)) - continue - elif measurement_group: - callables.append(_Measure(measurement_group, self.data_set, - self.use_threads)) - measurement_group[:] = [] - - callables.append(self._compile_one(action, new_action_indices)) - - if measurement_group: - callables.append(_Measure(measurement_group, self.data_set, - self.use_threads)) - measurement_group[:] = [] - - return callables - - def _compile_one(self, action, new_action_indices): - if isinstance(action, Wait): - return Task(self._wait, action.delay) - elif isinstance(action, ActiveLoop): - return _Nest(action, new_action_indices) - else: - return action - - def _run_wrapper(self, *args, **kwargs): - try: - self._run_loop(*args, **kwargs) - finally: - if hasattr(self, 'data_set'): - # TODO (giulioungaretti) WTF? - # somehow this does not show up in the data_set returned by - # run(), but it is saved to the metadata - ts = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - self.data_set.add_metadata({'loop': {'ts_end': ts}}) - self.data_set.finalize() - - def _run_loop(self, first_delay=0, action_indices=(), - loop_indices=(), current_values=(), - **ignore_kwargs): - """ - the routine that actually executes the loop, and can be called - from one loop to execute a nested loop - - first_delay: any delay carried over from an outer loop - action_indices: where we are in any outer loop action arrays - loop_indices: setpoint indices in any outer loops - current_values: setpoint values in any outer loops - signal_queue: queue to communicate with main process directly - ignore_kwargs: for compatibility with other loop tasks - """ - - # at the beginning of the loop, the time to wait after setting - # the loop parameter may be increased if an outer loop requested longer - delay = max(self.delay, first_delay) - - callables = self._compile_actions(self.actions, action_indices) - n_callables = 0 - for item in callables: - if hasattr(item, 'param_ids'): - n_callables += len(item.param_ids) - else: - n_callables += 1 - t0 = time.time() - last_task = t0 - imax = len(self.sweep_values) - - self.last_task_failed = False - - for i, value in enumerate(self.sweep_values): - if self.progress_interval is not None: - tprint('loop %s: %d/%d (%.1f [s])' % ( - self.sweep_values.name, i, imax, time.time() - t0), - dt=self.progress_interval, tag='outerloop') - if i: - tprint("Estimated finish time: %s" % ( - time.asctime(time.localtime(t0 + ((time.time() - t0) * imax / i)))), - dt=self.progress_interval, tag="finish") - - set_val = self.sweep_values.set(value) - - new_indices = loop_indices + (i,) - new_values = current_values + (value,) - data_to_store = {} - - if hasattr(self.sweep_values, "parameters"): # combined parameter - set_name = self.data_set.action_id_map[action_indices] - if hasattr(self.sweep_values, 'aggregate'): - value = self.sweep_values.aggregate(*set_val) - # below is useful but too verbose even at debug - # log.debug('Calling .store method of DataSet because ' - # 'sweep_values.parameters exist') - self.data_set.store(new_indices, {set_name: value}) - # set_val list of values to set [param1_setpoint, param2_setpoint ..] - for j, val in enumerate(set_val): - set_index = action_indices + (j+n_callables, ) - set_name = (self.data_set.action_id_map[set_index]) - data_to_store[set_name] = val - else: - set_name = self.data_set.action_id_map[action_indices] - data_to_store[set_name] = value - # below is useful but too verbose even at debug - # log.debug('Calling .store method of DataSet because a sweep step' - # ' was taken') - self.data_set.store(new_indices, data_to_store) - - if not self._nest_first: - # only wait the delay time if an inner loop will not inherit it - self._wait(delay) - - try: - for f in callables: - # below is useful but too verbose even at debug - # log.debug('Going through callables at this sweep step.' - # ' Calling {}'.format(f)) - f(first_delay=delay, - loop_indices=new_indices, - current_values=new_values) - - # after the first action, no delay is inherited - delay = 0 - except _QcodesBreak: - break - - # after the first setpoint, delay reverts to the loop delay - delay = self.delay - - # now check for a background task and execute it if it's - # been long enough since the last time - # don't let exceptions in the background task interrupt - # the loop - # if the background task fails twice consecutively, stop - # executing it - if self.bg_task is not None: - t = time.time() - if t - last_task >= self.bg_min_delay: - try: - self.bg_task() - except Exception: - if self.last_task_failed: - self.bg_task = None - self.last_task_failed = True - log.exception("Failed to execute bg task") - - last_task = t - - # run the background task one last time to catch the last setpoint(s) - if self.bg_task is not None: - log.debug('Running the background task one last time.') - self.bg_task() - - # the loop is finished - run the .then actions - #log.debug('Finishing loop, running the .then actions...') - for f in self._compile_actions(self.then_actions, ()): - #log.debug('...running .then action {}'.format(f)) - f() - - # run the bg_final_task from the bg_task: - if self.bg_final_task is not None: - log.debug('Running the bg_final_task') - self.bg_final_task() - - def _wait(self, delay): - if delay: - finish_clock = time.perf_counter() + delay - t = wait_secs(finish_clock) - time.sleep(t) +from qcodes.utils import full_class, issue_deprecation_warning + +try: + from qcodes_loop.actions import ( + BreakIf, + Task, + Wait, + _actions_snapshot, + _Measure, + _Nest, + _QcodesBreak, + ) + from qcodes_loop.data.data_array import DataArray + from qcodes_loop.data.data_set import new_data + from qcodes_loop.loops import ( + ActiveLoop, + Loop, + active_data_set, + active_loop, + tprint, + wait_secs, + ) +except ImportError as e: + raise ImportError( + "qcodes.loops is deprecated and has moved to " + "the package `qcodes_loop`. Please install qcodes_loop directly or " + "with `pip install qcodes[loop]" + ) from e +issue_deprecation_warning("qcodes.loops module", alternative="qcodes_loop.loops") diff --git a/qcodes/measure.py b/qcodes/measure.py index d451f8ee5a3..2f1ca8af3fa 100644 --- a/qcodes/measure.py +++ b/qcodes/measure.py @@ -1,154 +1,18 @@ from datetime import datetime from typing import Optional, Sequence -from qcodes.actions import _actions_snapshot -from qcodes.loops import Loop from qcodes.metadatable import Metadatable from qcodes.parameters import Parameter -from qcodes.utils import full_class - - -class Measure(Metadatable): - """ - Create a DataSet from a single (non-looped) set of actions. - - Args: - *actions (Any): sequence of actions to perform. Any action that is - valid in a ``Loop`` can be used here. If an action is a gettable - ``Parameter``, its output will be included in the DataSet. - Scalars returned by an action will be saved as length-1 arrays, - with a dummy setpoint for consistency with other DataSets. - """ - dummy_parameter = Parameter(name='single', - label='Single Measurement', - set_cmd=None, get_cmd=None) - - def __init__(self, *actions): - super().__init__() - self._dummyLoop = Loop(self.dummy_parameter[0]).each(*actions) - - def run_temp(self, **kwargs): - """ - Wrapper to run this measurement as a temporary data set - """ - return self.run(quiet=True, location=False, **kwargs) - - def get_data_set(self, *args, **kwargs): - return self._dummyLoop.get_data_set(*args, **kwargs) - - def run(self, use_threads=False, quiet=False, station=None, **kwargs): - """ - Run the actions in this measurement and return their data as a DataSet - - Args: - quiet (Optional[bool]): Set True to not print anything except - errors. Default False. - - station (Optional[Station]): the ``Station`` this measurement - pertains to. Defaults to ``Station.default`` if one is defined. - Only used to supply metadata. - - use_threads (Optional[bool]): whether to parallelize ``get`` - operations using threads. Default False. - - location (Optional[Union[str, bool]]): the location of the - DataSet, a string whose meaning depends on formatter and io, - or False to only keep in memory. May be a callable to provide - automatic locations. If omitted, will use the default - DataSet.location_provider - - name (Optional[str]): if location is default or another provider - function, name is a string to add to location to make it more - readable/meaningful to users - - formatter (Optional[Formatter]): knows how to read and write the - file format. Default can be set in DataSet.default_formatter - - io (Optional[io_manager]): knows how to connect to the storage - (disk vs cloud etc) - - location, name formatter and io are passed to ``data_set.new_data`` - along with any other optional keyword arguments. - - returns: - a DataSet object containing the results of the measurement - """ - - data_set = self._dummyLoop.get_data_set(**kwargs) - - # set the DataSet to local for now so we don't save it, since - # we're going to massage it afterward - original_location = data_set.location - data_set.location = False - - # run the measurement as if it were a Loop - self._dummyLoop.run(use_threads=use_threads, - station=station, quiet=True) - - # look for arrays that are unnecessarily nested, and un-nest them - all_unnested = True - for array in data_set.arrays.values(): - if array.ndim == 1: - if array.is_setpoint: - dummy_setpoint = array - else: - # we've found a scalar - so keep the dummy setpoint - all_unnested = False - else: - # The original return was an array, so take off the extra dim. - # (This ensures the outer dim length was 1, otherwise this - # will raise a ValueError.) - array.ndarray.shape = array.ndarray.shape[1:] - - # TODO: DataArray.shape masks ndarray.shape, and a user *could* - # change it, thinking they were reshaping the underlying array, - # but this would a) not actually reach the ndarray right now, - # and b) if it *did* and the array was reshaped, this array - # would be out of sync with its setpoint arrays, so bad things - # would happen. So we probably want some safeguards in place - # against this - array.shape = array.ndarray.shape - - array.set_arrays = array.set_arrays[1:] - - array.init_data() - - # Do we still need the dummy setpoint array at all? - if all_unnested: - del data_set.arrays[dummy_setpoint.array_id] - if hasattr(data_set, 'action_id_map'): - del data_set.action_id_map[dummy_setpoint.action_indices] - - # now put back in the DataSet location and save it - data_set.location = original_location - data_set.write() - - # metadata: ActiveLoop already provides station snapshot, but also - # puts in a 'loop' section that we need to replace with 'measurement' - # but we use the info from 'loop' to ensure consistency and avoid - # duplication. - LOOP_SNAPSHOT_KEYS = ['ts_start', 'ts_end', 'use_threads'] - data_set.add_metadata({'measurement': { - k: data_set.metadata['loop'][k] for k in LOOP_SNAPSHOT_KEYS - }}) - del data_set.metadata['loop'] - - # actions are included in self.snapshot() rather than in - # LOOP_SNAPSHOT_KEYS because they are useful if someone just - # wants a local snapshot of the Measure object - data_set.add_metadata({'measurement': self.snapshot()}) - - data_set.save_metadata() - - if not quiet: - print(repr(data_set)) - print(datetime.now().strftime('acquired at %Y-%m-%d %H:%M:%S')) - - return data_set - - def snapshot_base(self, update: Optional[bool] = False, - params_to_skip_update: Optional[Sequence[str]] = None): - return { - '__class__': full_class(self), - 'actions': _actions_snapshot(self._dummyLoop.actions, update) - } +from qcodes.utils import full_class, issue_deprecation_warning + +try: + from qcodes_loop.actions import _actions_snapshot + from qcodes_loop.loops import Loop + from qcodes_loop.measure import Measure +except ImportError as e: + raise ImportError( + "qcodes.measure is deprecated and has moved to " + "the package `qcodes_loop`. Please install qcodes_loop directly or " + "with `pip install qcodes[loop]" + ) from e +issue_deprecation_warning("qcodes.measure module", alternative="qcodes_loop.measure") diff --git a/qcodes/parameters/array_parameter.py b/qcodes/parameters/array_parameter.py index 5c3f9632300..1ba2aae7622 100644 --- a/qcodes/parameters/array_parameter.py +++ b/qcodes/parameters/array_parameter.py @@ -7,7 +7,12 @@ import numpy as np -from qcodes.data.data_array import DataArray +try: + from qcodes_loop.data.data_array import DataArray + + has_loop = True +except ImportError as e: + has_loop = False from .parameter_base import ParameterBase from .sequence_helpers import is_sequence_of @@ -149,13 +154,21 @@ def __init__( # require one setpoint per dimension of shape sp_shape = (len(shape),) - sp_types = ( - nt, - DataArray, - collections.abc.Sequence, - collections.abc.Iterator, - np.ndarray, - ) + if has_loop: + sp_types: tuple[type, ...] = ( + nt, + DataArray, + collections.abc.Sequence, + collections.abc.Iterator, + np.ndarray, + ) + else: + sp_types = ( + nt, + collections.abc.Sequence, + collections.abc.Iterator, + np.ndarray, + ) if setpoints is not None and not is_sequence_of( setpoints, sp_types, shape=sp_shape ): diff --git a/qcodes/parameters/multi_parameter.py b/qcodes/parameters/multi_parameter.py index 52aba567550..552814d12ef 100644 --- a/qcodes/parameters/multi_parameter.py +++ b/qcodes/parameters/multi_parameter.py @@ -6,7 +6,13 @@ import numpy as np -from qcodes.data.data_array import DataArray +try: + from qcodes_loop.data.data_array import DataArray + + has_loop = True +except ImportError as e: + has_loop = False + from .parameter_base import ParameterBase from .sequence_helpers import is_sequence_of @@ -174,13 +180,21 @@ def __init__( ) self.shapes = shapes - sp_types = ( - nt, - DataArray, - Sequence, - Iterator, - np.ndarray, - ) + if has_loop: + sp_types: tuple[type, ...] = ( + nt, + DataArray, + Sequence, + Iterator, + np.ndarray, + ) + else: + sp_types = ( + nt, + Sequence, + Iterator, + np.ndarray, + ) if not _is_nested_sequence_or_none(setpoints, sp_types, shapes): raise ValueError("setpoints must be a tuple of tuples of arrays") diff --git a/qcodes/plots/__init__.py b/qcodes/plots/__init__.py index e69de29bb2d..c24ab92278f 100644 --- a/qcodes/plots/__init__.py +++ b/qcodes/plots/__init__.py @@ -0,0 +1,3 @@ +from qcodes.utils import issue_deprecation_warning + +issue_deprecation_warning("qcodes.plots", alternative="qcodes_loop.plots") diff --git a/qcodes/plots/base.py b/qcodes/plots/base.py index 838379880b4..fee31ba36f2 100644 --- a/qcodes/plots/base.py +++ b/qcodes/plots/base.py @@ -1,308 +1,14 @@ """ -Live plotting in Jupyter notebooks +Deprecated """ - - -class BasePlot: - latest_plot = None - """ - Auto-updating plot connected to a Jupyter notebook - - Args: - interval (int): period in seconds between update checks - default 1 - - data_keys (str): sequence of keys in trace config can contain data - that we should look for updates in. - default 'xyz' (treated as a sequence) but add more if - for example marker size or color can contain data - """ - - def __init__(self, interval=1, data_keys='xyz'): - BasePlot.latest_plot = self - self.data_keys = data_keys - self.traces = [] - self.data_updaters = set() - self.interval = interval - self.standardunits = ['V', 's', 'J', 'W', 'm', 'eV', 'A', 'K', 'g', - 'Hz', 'rad', 'T', 'H', 'F', 'Pa', 'C', 'Ω', 'Ohm', - 'S'] - - def clear(self): - """ - Clears the plot window and removes all subplots and traces - so that the window can be reused. - """ - # any derived class should implement this - raise NotImplementedError - # typically traces and subplots should be cleared as well as the - # figure window for the particular backend - # TODO(giulioungaretti) the following unreachable lines should really - # be documentation. - self.traces = [] - self.subplots = [] - - def replace(self, *args, updater=None, **kwargs): - """ - Clear all content and add new trace. - - Args: - args: optional way to provide x/y/z data without keywords - If the last one is 1D, may be `y` or `x`, `y` - If the last one is 2D, may be `z` or `x`, `y`, `z` - - updater: a callable (with no args) that updates the data in this trace - if omitted, we will look for DataSets referenced in this data, and - call their sync methods. - - **kwargs: passed on to self.add() - """ - self.clear() - self.add(*args, updater=updater, **kwargs) - - def add(self, *args, updater=None, **kwargs): - """ - Add one trace to this plot. - - Args: - args: optional way to provide x/y/z data without keywords - If the last one is 1D, may be `y` or `x`, `y` - If the last one is 2D, may be `z` or `x`, `y`, `z` - - updater: a callable (with no args) that updates the data in this trace - if omitted, we will look for DataSets referenced in this data, and - call their sync methods. - - kwargs: after inserting info found in args and possibly in set_arrays - into `x`, `y`, and optionally `z`, these are passed along to - self.add_to_plot. - - Returns: - Plot handle for trace - - Examples: - To use custom labels and units pass for example: - - >>> plot.add(x=set, y=amplitude, - >>> xlabel="set", - >>> xunit="V", - >>> ylabel= "Amplitude", - >>> yunit ="V") - - Array shapes for 2D plots: - x:(1D-length m), y:(1D-length n), z: (2D- n*m array) - """ - # TODO(giulioungaretti): replace with an explicit version, see expand trace - self.expand_trace(args, kwargs) - plot_object = self.add_to_plot(**kwargs) - self.add_updater(updater, kwargs) - - return plot_object - - def add_to_plot(self, **kwargs): - """ - Add a trace the plot itself (typically called by self.add, - which incorporates args into kwargs, so the subclass doesn't - need to worry about this). Data will be in `x`, `y`, and optionally - `z`. - - Should be implemented by a subclass, and each call should append - a dictionary to self.traces, containing at least {'config': kwargs} - """ - raise NotImplementedError - - def add_updater(self, updater, plot_config): - """ - Add an updater to the plot. - - Args: - updater (Callable): callable (with no args) that updates the data in this trace - if omitted, we will look for DataSets referenced in this data, and - call their sync methods. - plot_config (dict): this is a dictionary that gets populated inside - add() via expand_trace(). - The reason this is here is to fetch from the data_set the sync method - to use it as an updater. - """ - if updater is not None: - self.data_updaters.add(updater) - else: - for key in self.data_keys: - data_array = plot_config.get(key, '') - if hasattr(data_array, 'data_set'): - if data_array.data_set is not None: - self.data_updaters.add(data_array.data_set.sync) - - # If previous data on this plot became static, perhaps because - # its measurement loop finished, the updater may have been halted. - # If we have new update functions, re-activate the updater - # by reinstating its update interval - if self.data_updaters: - if hasattr(self, 'update_widget'): - self.update_widget.interval = self.interval - - def get_default_title(self): - """ - Get the default title, which for a plot is just a list of DataSet locations. - A custom title can be set when adding any trace (via either __init__ or add. - these kwargs all eventually end up in self.traces[i]['config']) and it looks - like we will take the first title we find from any trace... otherwise, if no - trace specifies a title, then we combine whatever dataset locations we find. - - Note: (alexj): yeah, that's awkward, isn't it, and it looks like a weird - implementation, feel free to change it 👼 - - Returns: - str: the title of the figure - """ - title_parts = [] - for trace in self.traces: - config = trace['config'] - if 'title' in config: # can be passed using **kw - return config['title'] - for part in self.data_keys: - data_array = config.get(part, '') - if hasattr(data_array, 'data_set'): - if data_array.data_set is not None: - location = data_array.data_set.location - if location and location not in title_parts: - title_parts.append(location) - return ', '.join(title_parts) - - @staticmethod - def get_label(data_array): - """ - Look for a label in data_array falling back on name. - - Args: - data_array (DataArray): data array to get label from - - Returns: - str: label or name of the data_array - - """ - # TODO this should really be a static method - name = (getattr(data_array, 'label', '') or - getattr(data_array, 'name', '')) - unit = getattr(data_array, 'unit', '') - return name, unit - - @staticmethod - def expand_trace(args, kwargs): - """ - Complete the x, y (and possibly z) data definition for a trace. - - Also modifies kwargs in place so that all the data needed to fully - specify the trace is present (ie either x and y or x and y and z) - - Both ``__init__`` (for the first trace) and the ``add`` method support - multiple ways to specify the data in the trace: - - As ``*args``: - - ``add(y)`` or ``add(z)`` specify just the main 1D or 2D data, with - the setpoint axis or axes implied. - - ``add(x, y)`` or ``add(x, y, z)`` specify all axes of the data. - And as ``**kwargs``: - - ``add(x=x, y=y, z=z)`` you specify exactly the data you want on - each axis. Any but the last (y or z) can be omitted, which allows - for all of the same forms as with ``*args``, plus x and z or y and - z, with just one axis implied from the setpoints of the z data. - - This method takes any of those forms and converts them into a complete - set of kwargs, containing all of the explicit or implied data to be used - in plotting this trace. - - Args: - args (Tuple[DataArray]): positional args, as passed to either - ``__init__`` or ``add`` - kwargs (Dict(DataArray]): keyword args, as passed to either - ``__init__`` or ``add``. kwargs may contain non-data items in - keys other than x, y, and z. - - Raises: - ValueError: if the shape of the data does not match that of args - ValueError: if the data is provided twice - """ - # TODO(giulioungaretti): replace with an explicit version: - # return the new kwargs instead of modifying in place - # TODO this should really be a static method - if args: - if hasattr(args[-1][0], '__len__'): - # 2D (or higher... but ignore this for now) - # this test works for both numpy arrays and bare sequences - axletters = 'xyz' - ndim = 2 - else: - axletters = 'xy' - ndim = 1 - - if len(args) not in (1, len(axletters)): - raise ValueError('{}D data needs 1 or {} unnamed args'.format( - ndim, len(axletters))) - - arg_axletters = axletters[-len(args):] - - for arg, arg_axletters in zip(args, arg_axletters): - if arg_axletters in kwargs: - raise ValueError(arg_axletters + ' data provided twice') - kwargs[arg_axletters] = arg - - # reset axletters, we may or may not have found them above - axletters = 'xyz' if 'z' in kwargs else 'xy' - main_data = kwargs[axletters[-1]] - if hasattr(main_data, 'set_arrays'): - num_axes = len(axletters) - 1 - # things will probably fail if we try to plot arrays of the - # wrong dimension... but we'll give it a shot anyway. - set_arrays = main_data.set_arrays[-num_axes:] - # for 2D: y is outer loop, which is earlier in set_arrays, - # and x is the inner loop... is this the right convention? - set_axletters = reversed(axletters[:-1]) - for axletter, set_array in zip(set_axletters, set_arrays): - if axletter not in kwargs: - kwargs[axletter] = set_array - - def update(self): - """ - Update the data in this plot, using the updaters given with - MatPlot.add() or in the included DataSets, then include this in - the plot. - - This is a wrapper routine that the update widget calls, - inside this we call self.update() which should be subclassed - """ - any_updates = False - for updater in self.data_updaters: - updates = updater() - if updates is not False: - any_updates = True - - self.update_plot() - - # once all updaters report they're finished (by returning exactly - # False) we stop updating the plot. - if any_updates is False: - self.halt() - - def update_plot(self): - """ - Update the plot itself (typically called by self.update). - Should be implemented by a subclass - """ - raise NotImplementedError - - def halt(self): - """ - Stop automatic updates to this plot, by canceling its update widget - """ - if hasattr(self, 'update_widget'): - self.update_widget.halt() - - def save(self, filename=None): - """ - Save current plot to filename - - Args: - filename (Optional[str]): Location of the file - """ - raise NotImplementedError +from qcodes.utils import issue_deprecation_warning + +try: + from qcodes_loop.plots.base import BasePlot +except ImportError as e: + raise ImportError( + "qcodes.plots.base is deprecated and has moved to " + "the package `qcodes_loop`. Please install qcodes_loop directly or " + "with `pip install qcodes[loop]" + ) from e +issue_deprecation_warning("qcodes.plots.base", alternative="qcodes_loop.plots.base") diff --git a/qcodes/plots/colors.py b/qcodes/plots/colors.py index 519820e1db5..fba324c5960 100644 --- a/qcodes/plots/colors.py +++ b/qcodes/plots/colors.py @@ -1,135 +1,17 @@ -# default colors and colorscales, taken from plotly -color_cycle = [ - '#1f77b4', # muted blue - '#ff7f0e', # safety orange - '#2ca02c', # cooked asparagus green - '#d62728', # brick red - '#9467bd', # muted purple - '#8c564b', # chestnut brown - '#e377c2', # raspberry yogurt pink - '#7f7f7f', # middle gray - '#bcbd22', # curry yellow-green - '#17becf' # blue-teal -] - - -colorscales_raw = { - 'Greys': [[0, 'rgb(0,0,0)'], [1, 'rgb(255,255,255)']], - - 'YlGnBu': [ - [0, 'rgb(8, 29, 88)'], [0.125, 'rgb(37, 52, 148)'], - [0.25, 'rgb(34, 94, 168)'], [0.375, 'rgb(29, 145, 192)'], - [0.5, 'rgb(65, 182, 196)'], [0.625, 'rgb(127, 205, 187)'], - [0.75, 'rgb(199, 233, 180)'], [0.875, 'rgb(237, 248, 217)'], - [1, 'rgb(255, 255, 217)']], - - 'Greens': [ - [0, 'rgb(0, 68, 27)'], [0.125, 'rgb(0, 109, 44)'], - [0.25, 'rgb(35, 139, 69)'], [0.375, 'rgb(65, 171, 93)'], - [0.5, 'rgb(116, 196, 118)'], [0.625, 'rgb(161, 217, 155)'], - [0.75, 'rgb(199, 233, 192)'], [0.875, 'rgb(229, 245, 224)'], - [1, 'rgb(247, 252, 245)']], - - 'YlOrRd': [ - [0, 'rgb(128, 0, 38)'], [0.125, 'rgb(189, 0, 38)'], - [0.25, 'rgb(227, 26, 28)'], [0.375, 'rgb(252, 78, 42)'], - [0.5, 'rgb(253, 141, 60)'], [0.625, 'rgb(254, 178, 76)'], - [0.75, 'rgb(254, 217, 118)'], [0.875, 'rgb(255, 237, 160)'], - [1, 'rgb(255, 255, 204)']], - - 'bluered': [[0, 'rgb(0,0,255)'], [1, 'rgb(255,0,0)']], - - # modified RdBu based on - # www.sandia.gov/~kmorel/documents/ColorMaps/ColorMapsExpanded.pdf - 'RdBu': [ - [0, 'rgb(5, 10, 172)'], [0.35, 'rgb(106, 137, 247)'], - [0.5, 'rgb(190,190,190)'], [0.6, 'rgb(220, 170, 132)'], - [0.7, 'rgb(230, 145, 90)'], [1, 'rgb(178, 10, 28)']], - # Scale for non-negative numeric values - 'Reds': [ - [0, 'rgb(220, 220, 220)'], [0.2, 'rgb(245, 195, 157)'], - [0.4, 'rgb(245, 160, 105)'], [1, 'rgb(178, 10, 28)']], - # Scale for non-positive numeric values - 'Blues': [ - [0, 'rgb(5, 10, 172)'], [0.35, 'rgb(40, 60, 190)'], - [0.5, 'rgb(70, 100, 245)'], [0.6, 'rgb(90, 120, 245)'], - [0.7, 'rgb(106, 137, 247)'], [1, 'rgb(220, 220, 220)']], - - 'picnic': [ - [0, 'rgb(0,0,255)'], [0.1, 'rgb(51,153,255)'], - [0.2, 'rgb(102,204,255)'], [0.3, 'rgb(153,204,255)'], - [0.4, 'rgb(204,204,255)'], [0.5, 'rgb(255,255,255)'], - [0.6, 'rgb(255,204,255)'], [0.7, 'rgb(255,153,255)'], - [0.8, 'rgb(255,102,204)'], [0.9, 'rgb(255,102,102)'], - [1, 'rgb(255,0,0)']], - - 'rainbow': [ - [0, 'rgb(150,0,90)'], [0.125, 'rgb(0, 0, 200)'], - [0.25, 'rgb(0, 25, 255)'], [0.375, 'rgb(0, 152, 255)'], - [0.5, 'rgb(44, 255, 150)'], [0.625, 'rgb(151, 255, 0)'], - [0.75, 'rgb(255, 234, 0)'], [0.875, 'rgb(255, 111, 0)'], - [1, 'rgb(255, 0, 0)']], - - 'portland': [ - [0, 'rgb(12,51,131)'], [0.25, 'rgb(10,136,186)'], - [0.5, 'rgb(242,211,56)'], [0.75, 'rgb(242,143,56)'], - [1, 'rgb(217,30,30)']], - - 'jet': [ - [0, 'rgb(0,0,131)'], [0.125, 'rgb(0,60,170)'], - [0.375, 'rgb(5,255,255)'], [0.625, 'rgb(255,255,0)'], - [0.875, 'rgb(250,0,0)'], [1, 'rgb(128,0,0)']], - - 'hot': [ - [0, 'rgb(0,0,0)'], [0.3, 'rgb(230,0,0)'], - [0.6, 'rgb(255,210,0)'], [1, 'rgb(255,255,255)']], - - 'blackbody': [ - [0, 'rgb(0,0,0)'], [0.2, 'rgb(230,0,0)'], - [0.4, 'rgb(230,210,0)'], [0.7, 'rgb(255,255,255)'], - [1, 'rgb(160,200,255)']], - - 'earth': [ - [0, 'rgb(0,0,130)'], [0.1, 'rgb(0,180,180)'], - [0.2, 'rgb(40,210,40)'], [0.4, 'rgb(230,230,50)'], - [0.6, 'rgb(120,70,20)'], [1, 'rgb(255,255,255)']], - - 'electric': [ - [0, 'rgb(0,0,0)'], [0.15, 'rgb(30,0,100)'], - [0.4, 'rgb(120,0,100)'], [0.6, 'rgb(160,90,0)'], - [0.8, 'rgb(230,200,0)'], [1, 'rgb(255,250,220)']], - - 'viridis': [ - [0, '#440154'], [0.06274509803921569, '#48186a'], - [0.12549019607843137, '#472d7b'], [0.18823529411764706, '#424086'], - [0.25098039215686274, '#3b528b'], [0.3137254901960784, '#33638d'], - [0.3764705882352941, '#2c728e'], [0.4392156862745098, '#26828e'], - [0.5019607843137255, '#21918c'], [0.5647058823529412, '#1fa088'], - [0.6274509803921569, '#28ae80'], [0.6901960784313725, '#3fbc73'], - [0.7529411764705882, '#5ec962'], [0.8156862745098039, '#84d44b'], - [0.8784313725490196, '#addc30'], [0.9411764705882353, '#d8e219'], - [1, '#fde725']] -} - - -def make_rgba(colorscale): - return [(v, one_rgba(c)) for v, c in colorscale] - - -def one_rgba(c): - ''' - convert a single color value to (r, g, b, a) - input can be an rgb string 'rgb(r,g,b)', '#rrggbb' - if we decide we want more we can make more, but for now this is just - to convert plotly colorscales to pyqtgraph tuples - ''' - if c[0] == '#' and len(c) == 7: - return (int(c[1:3], 16), int(c[3:5], 16), int(c[5:7], 16), 255) - if c[:4] == 'rgb(': - return tuple(map(int, c[4:-1].split(','))) + (255,) - raise ValueError('one_rgba only supports rgb(r,g,b) and #rrggbb colors') - - -colorscales = {} -for scale_name, scale in colorscales_raw.items(): - colorscales[scale_name] = make_rgba(scale) +from qcodes.utils import issue_deprecation_warning + +try: + from qcodes_loop.plots.colors import ( + color_cycle, + colorscales, + colorscales_raw, + make_rgba, + one_rgba, + ) +except ImportError as e: + raise ImportError( + "qcodes.plots.colors is deprecated and has moved to " + "the package `qcodes_loop`. Please install qcodes_loop directly or " + "with `pip install qcodes[loop]" + ) from e +issue_deprecation_warning("qcodes.plots.colors", alternative="qcodes_loop.plots.colors") diff --git a/qcodes/plots/pyqtgraph.py b/qcodes/plots/pyqtgraph.py index f4e8abda31d..ef04739cbde 100644 --- a/qcodes/plots/pyqtgraph.py +++ b/qcodes/plots/pyqtgraph.py @@ -15,626 +15,18 @@ import qcodes import qcodes.utils.qt_helpers - -from .base import BasePlot -from .colors import color_cycle, colorscales - -TransformState = namedtuple('TransformState', 'translate scale revisit') - -log = logging.getLogger(__name__) - - -class QtPlot(BasePlot): - """ - Plot x/y lines or x/y/z heatmap data. The first trace may be included - in the constructor, other traces can be added with QtPlot.add(). - - For information on how ``x/y/z *args`` are handled see ``add()`` in the - base plotting class. - - Args: - *args: shortcut to provide the x/y/z data. See BasePlot.add - - figsize: (width, height) tuple in pixels to pass to GraphicsWindow - default (1000, 600) - interval: period in seconds between update checks - default 0.25 - theme: tuple of (foreground_color, background_color), where each is - a valid Qt color. default (dark gray, white), opposite the - pyqtgraph default of (white, black) - fig_x_pos: fraction of screen width to place the figure at - 0 is all the way to the left and - 1 is all the way to the right. - default None let qt decide. - fig_y_pos: fraction of screen width to place the figure at - 0 is all the way to the top and - 1 is all the way to the bottom. - default None let qt decide. - **kwargs: passed along to QtPlot.add() to add the first data trace - """ - proc = None - rpg = None - # we store references to plots to keep the garbage collections from - # destroying the windows. To keep memory consumption within bounds we - # limit this to an arbitrary number of plots here using a deque - # The issue is that even when closing a window it's difficult to - # remove it from the list. This could potentially be done with a - # close event on win but this is difficult with remote proxy process - # as the list of plots lives in the main process and the plot locally - # in a remote process - max_len = qcodes.config['gui']['pyqtmaxplots'] - max_len = cast(int, max_len) - plots: Deque['QtPlot'] = deque(maxlen=max_len) - - def __init__( - self, - *args, - figsize: Tuple[int, int] = (1000, 600), - interval=0.25, - window_title="", - theme=((60, 60, 60), "w"), - show_window=True, - remote=True, - fig_x_position=None, - fig_y_position=None, - **kwargs, - ): - super().__init__(interval) - - if 'windowTitle' in kwargs.keys(): - warnings.warn("windowTitle argument has been changed to " - "window_title. Please update your call to QtPlot") - temp_wt = kwargs.pop('windowTitle') - if not window_title: - window_title = temp_wt - self.theme = theme - - if remote: - if not self.__class__.proc: - self._init_qt() - else: - # overrule the remote pyqtgraph class - self.rpg = pg - self.qc_helpers = qcodes.utils.qt_helpers - try: - # _init_qt will set self.rpg so it cannot be None here - assert self.rpg is not None - self.win = self.rpg.GraphicsLayoutWidget(title=window_title) - self.win.show() - except (ClosedError, ConnectionResetError) as err: - # the remote process may have crashed. In that case try restarting - # it - if remote: - log.warning("Remote plot responded with {} \n" - "Restarting remote plot".format(err)) - self._init_qt() - # _init_qt will set self.rpg so it cannot be None here - assert self.rpg is not None - self.win = self.rpg.GraphicsLayoutWidget(title=window_title) - self.win.show() - else: - raise err - self.win.setBackground(theme[1]) - self.win.resize(*figsize) - self._orig_fig_size = figsize - - self.set_relative_window_position(fig_x_position, fig_y_position) - self.subplots: List[Union[PlotItem, ObjectProxy]] = [self.add_subplot()] - - if args or kwargs: - self.add(*args, **kwargs) - - if not show_window: - self.win.hide() - - self.plots.append(self) - - def set_relative_window_position(self, fig_x_position, fig_y_position): - if fig_x_position is not None or fig_y_position is not None: - _, _, width, height = QtGui.QDesktopWidget().screenGeometry().getCoords() - if fig_y_position is not None: - y_pos = height * fig_y_position - else: - y_pos = self.win.y() - if fig_x_position is not None: - x_pos = width * fig_x_position - else: - x_pos = self.win.x() - self.win.move(x_pos, y_pos) - - @classmethod - def _init_qt(cls): - # starting the process for the pyqtgraph plotting - # You do not want a new process to be created every time you start a - # run, so this only starts once and stores the process in the class - pg.mkQApp() - cls.proc = pgmp.QtProcess() # pyqtgraph multiprocessing - cls.rpg = cls.proc._import("pyqtgraph") - cls.qc_helpers = cls.proc._import("qcodes.utils.qt_helpers") - - def clear(self) -> None: - """ - Clears the plot window and removes all subplots and traces - so that the window can be reused. - """ - self.win.clear() - self.traces = [] - self.subplots = [] - - def add_subplot(self): - subplot_object = self.win.addPlot() - - for side in ('left', 'bottom'): - ax = subplot_object.getAxis(side) - ax.setPen(self.theme[0]) - ax._qcodes_label = "" - - return subplot_object - - def add_to_plot(self, subplot=1, **kwargs): - if subplot > len(self.subplots): - for i in range(subplot - len(self.subplots)): - self.subplots.append(self.add_subplot()) - subplot_object = self.subplots[subplot - 1] - - if 'name' in kwargs: - if subplot_object.legend is None: - subplot_object.addLegend(offset=(-30,30)) - - if 'z' in kwargs: - plot_object = self._draw_image(subplot_object, **kwargs) - else: - plot_object = self._draw_plot(subplot_object, **kwargs) - - self._update_labels(subplot_object, kwargs) - prev_default_title = self.get_default_title() - - self.traces.append({ - 'config': kwargs, - 'plot_object': plot_object - }) - - if prev_default_title == self.win.windowTitle(): - self.win.setWindowTitle(self.get_default_title()) - self.fixUnitScaling() - - return plot_object - - def _draw_plot(self, subplot_object, y, x=None, color=None, width=None, - antialias=None, **kwargs): - if 'pen' not in kwargs: - if color is None: - cycle = color_cycle - color = cycle[len(self.traces) % len(cycle)] - if width is None: - # there are currently very significant performance issues - # with a penwidth larger than one - width = 1 - kwargs['pen'] = self.rpg.mkPen(color, width=width) - - if antialias is None: - # looks a lot better antialiased, but slows down with many points - # TODO: dynamically update this based on total # of points - antialias = (len(y) < 1000) - - # If a marker symbol is desired use the same color as the line - if any([('symbol' in key) for key in kwargs]): - if 'symbolPen' not in kwargs: - symbol_pen_width = 0.5 if antialias else 1.0 - kwargs["symbolPen"] = self.rpg.mkPen("#444", width=symbol_pen_width) - if "symbolBrush" not in kwargs: - kwargs["symbolBrush"] = color - - # suppress warnings when there are only NaN to plot - with warnings.catch_warnings(): - warnings.filterwarnings('ignore', 'All-NaN axis encountered') - warnings.filterwarnings('ignore', 'All-NaN slice encountered') - pl = subplot_object.plot(*self._line_data(x, y), - antialias=antialias, **kwargs) - return pl - - def _line_data(self, x, y): - return [self._clean_array(arg) for arg in [x, y] if arg is not None] - - def _draw_image(self, subplot_object, z, x=None, y=None, cmap=None, - zlabel=None, - zunit=None, - **kwargs): - if cmap is None: - cmap = qcodes.config['gui']['defaultcolormap'] - img = self.rpg.ImageItem() - subplot_object.addItem(img) - - hist = self.rpg.HistogramLUTItem() - hist.setImageItem(img) - hist.axis.setPen(self.theme[0]) - - if zunit is None: - _, zunit = self.get_label(z) - if zlabel is None: - zlabel, _ = self.get_label(z) - - hist.axis.setLabel(zlabel, zunit) - - # TODO - ensure this goes next to the correct subplot? - self.win.addItem(hist) - - plot_object = { - 'image': img, - 'hist': hist, - 'histlevels': hist.getLevels(), - 'cmap': cmap, - 'scales': { - 'x': TransformState(0, 1, True), - 'y': TransformState(0, 1, True) - } - } - - self._update_image(plot_object, {'x': x, 'y': y, 'z': z}) - self._update_cmap(plot_object) - - return plot_object - - def _update_image(self, plot_object, config): - z = config['z'] - img = plot_object['image'] - hist = plot_object['hist'] - scales = plot_object['scales'] - - # make sure z is a *new* numpy float array (pyqtgraph barfs on ints), - # and replace nan with minimum val bcs I can't figure out how to make - # pyqtgraph handle nans - though the source does hint at a way: - # http://www.pyqtgraph.org/documentation/_modules/pyqtgraph/widgets/ColorMapWidget.html - # see class RangeColorMapItem - z = np.asfarray(z).T - with warnings.catch_warnings(): - warnings.simplefilter('error') - try: - z_range = (np.nanmin(z), np.nanmax(z)) - except: - # we get a warning here when z is entirely NaN - # nothing to plot, so give up. - return - z[np.where(np.isnan(z))] = z_range[0] - - hist_range = hist.getLevels() - if hist_range == plot_object['histlevels']: - plot_object['histlevels'] = z_range - hist.setLevels(*z_range) - hist_range = z_range - - img.setImage(self._clean_array(z), levels=hist_range) - - scales_changed = False - for axletter, axscale in scales.items(): - if axscale.revisit: - axdata = config.get(axletter, None) - newscale = self._get_transform(axdata) - if (newscale.translate != axscale.translate or - newscale.scale != axscale.scale): - scales_changed = True - scales[axletter] = newscale - - if scales_changed: - img.resetTransform() - tr = QtGui.QTransform.fromTranslate( - scales["x"].translate, scales["y"].translate - ) - tr_scale = QtGui.QTransform.fromScale(scales["x"].scale, scales["y"].scale) - img.setTransform(tr_scale * tr) - - def _update_cmap(self, plot_object): - gradient = plot_object['hist'].gradient - gradient.setColorMap(self._cmap(plot_object['cmap'])) - - def set_cmap(self, cmap, traces=None): - if isinstance(traces, int): - traces = (traces,) - elif traces is None: - traces = range(len(self.traces)) - - for i in traces: - plot_object = self.traces[i]['plot_object'] - if not isinstance(plot_object, dict) or 'hist' not in plot_object: - continue - - plot_object['cmap'] = cmap - self._update_cmap(plot_object) - - def _get_transform(self, array): - """ - pyqtgraph seems to only support uniform pixels in image plots. - - for a given setpoint array, extract the linear transform it implies - if the setpoint data is *not* linear (or close to it), or if it's not - uniform in any nested dimensions, issue a warning and return the - default transform of 0, 1 - - returns namedtuple TransformState(translate, scale, revisit) - - in pyqtgraph: - translate means how many pixels to shift the image, away - from the bottom or left edge being at zero on the axis - scale means the data delta - - revisit is True if we just don't have enough info to scale yet, - but we might later. - """ - - if array is None: - return TransformState(0, 1, True) - - # do we have enough confidence in the setpoint data we've seen - # so far that we don't have to repeat this as more data comes in? - revisit = False - - # somewhat arbitrary - if the first 20% of the data or at least - # 10 rows is uniform, assume it's uniform thereafter - MINROWS = 10 - MINFRAC = 0.2 - - # maximum setpoint deviation from linear to accept is 10% of a pixel - MAXPX = 0.1 - - if hasattr(array[0], '__len__'): - # 2D array: check that all (non-empty) elements are congruent - inner_len = max(len(row) for row in array) - collapsed = np.array([np.nan] * inner_len) - rows_before_trusted = max(MINROWS, len(array) * MINFRAC) - for i, row in enumerate(array): - for j, val in enumerate(row): - if np.isnan(val): - if i < rows_before_trusted: - revisit = True - continue - if np.isnan(collapsed[j]): - collapsed[j] = val - elif val != collapsed[j]: - warnings.warn( - 'nonuniform nested setpoint array passed to ' - 'pyqtgraph. ignoring, using default scaling.') - return TransformState(0, 1, False) - else: - collapsed = array - - if np.isnan(collapsed).any(): - revisit = True - - indices_setpoints = list(zip(*((i, s) for i, s in enumerate(collapsed) - if not np.isnan(s)))) - if not indices_setpoints: - return TransformState(0, 1, revisit) - - indices, setpoints = indices_setpoints - npts = len(indices) - if npts == 1: - indices = indices + (indices[0] + 1,) - setpoints = setpoints + (setpoints[0] + 1,) - - i0 = indices[0] - s0 = setpoints[0] - total_di = indices[-1] - i0 - total_ds = setpoints[-1] - s0 - - if total_ds == 0: - warnings.warn('zero setpoint range passed to pyqtgraph. ' - 'ignoring, using default scaling.') - return TransformState(0, 1, False) - - for i, s in zip(indices[1:-1], setpoints[1:-1]): - icalc = i0 + (s - s0) * total_di / total_ds - if np.abs(i - icalc) > MAXPX: - warnings.warn('nonlinear setpoint array passed to pyqtgraph. ' - 'ignoring, using default scaling.') - return TransformState(0, 1, False) - - scale = total_ds / total_di - # extra 0.5 translation to get the first setpoint at the center of - # the first pixel - translate = s0 - (i0 + 0.5) * scale - - return TransformState(translate, scale, revisit) - - def _update_labels(self, subplot_object, config): - """ - Updates x and y labels, by default tries to extract label from - the DataArray objects located in the trace config. Custom labels - can be specified the **kwargs "xlabel" and "ylabel". Custom units - can be specified using the kwargs xunit, ylabel - """ - for axletter, side in (('x', 'bottom'), ('y', 'left')): - ax = subplot_object.getAxis(side) - # danger: 🍝 - # find if any kwarg from plot.add in the base class - # matches xlabel or ylabel, signaling a custom label - if axletter + "label" in config and not getattr(ax, "_qcodes_label", None): - label = config[axletter + "label"] - else: - label = None - - # find if any kwarg from plot.add in the base class - # matches xunit or yunit, signaling a custom unit - if axletter + "unit" in config and not getattr(ax, "_qcodes_label", None): - unit = config[axletter + "unit"] - else: - unit = None - - # find ( more hope to) unit and label from - # the data array inside the config - if axletter in config and not getattr(ax, "_qcodes_label", None): - # now if we did not have any kwarg for label or unit - # fallback to the data_array - if unit is None: - _, unit = self.get_label(config[axletter]) - if label is None: - label, _ = self.get_label(config[axletter]) - - # pyqtgraph doesn't seem able to get labels, only set - # so we'll store it in the axis object and hope the user - # doesn't set it separately before adding all traces - ax._qcodes_label = label - ax._qcodes_unit = unit - ax.setLabel(label, unit) - - def update_plot(self): - for trace in self.traces: - config = trace['config'] - plot_object = trace['plot_object'] - if 'z' in config: - self._update_image(plot_object, config) - else: - plot_object.setData(*self._line_data(config['x'], config['y'])) - - def _clean_array(self, array): - """ - we can't send a DataArray to remote pyqtgraph for some reason, - so send the plain numpy array - """ - if hasattr(array, 'ndarray') and isinstance(array.ndarray, np.ndarray): - return array.ndarray - return array - - def _cmap(self, scale): - if isinstance(scale, str): - if scale in colorscales: - values, colors = zip(*colorscales[scale]) - else: - raise ValueError(scale + ' not found in colorscales') - elif len(scale) == 2: - values, colors = scale - - return self.rpg.ColorMap(values, colors) - - def _repr_png_(self): - """ - Create a png representation of the current window. - """ - image = self.win.grab() - byte_array = self.rpg.QtCore.QByteArray() - buffer = self.rpg.QtCore.QBuffer(byte_array) - buffer.open(self.rpg.QtCore.QIODevice.ReadWrite) - image.save(buffer, 'PNG') - buffer.close() - - if hasattr(byte_array, '_getValue'): - return bytes(byte_array._getValue()) - else: - return bytes(byte_array) - - def save(self, filename=None): - """ - Save current plot to filename, by default - to the location corresponding to the default - title. - - Args: - filename (Optional[str]): Location of the file - """ - default = f"{self.get_default_title()}.png" - filename = filename or default - image = self.win.grab() - image.save(filename, "PNG", 0) - - def setGeometry(self, x, y, w, h): - """ Set geometry of the plotting window """ - self.win.setGeometry(x, y, w, h) - - def autorange(self, reset_colorbar: bool=False) -> None: - """ - Auto range all limits in case they were changed during interactive - plot. Reset colormap if changed and resize window to original size. - - Args: - reset_colorbar: Should the limits and colorscale of the colorbar - be reset. Off by default - """ - # seem to be a bug in mypy but the type of self.subplots cannot be - # deducted even when typed above so ignore it and cast for now - subplots = self.subplots - for subplot in subplots: - vBox = subplot.getViewBox() - vBox.enableAutoRange(vBox.XYAxes) - cmap = None - # resize histogram - for trace in self.traces: - if 'plot_object' in trace.keys(): - if (isinstance(trace['plot_object'], dict) and - 'hist' in trace['plot_object'].keys() and - reset_colorbar): - cmap = trace['plot_object']['cmap'] - maxval = trace['config']['z'].max() - minval = trace['config']['z'].min() - trace['plot_object']['hist'].setLevels(minval, maxval) - trace['plot_object']['hist'].vb.autoRange() - if cmap: - self.set_cmap(cmap) - # set window back to original size - self.win.resize(*self._orig_fig_size) - - def fixUnitScaling(self, startranges: Optional[Dict[str, Dict[str, Union[float,int]]]]=None): - """ - Disable SI rescaling if units are not standard units and limit - ranges to data if known. - - Args: - - startranges: The plot can automatically infer the full ranges - array parameters. However it has no knowledge of the - ranges or regular parameters. You can explicitly pass - in the values here as a dict of the form - {'paramtername': {max: value, min:value}} - """ - axismapping = {'x': 'bottom', - 'y': 'left'} - standardunits = self.standardunits - # seem to be a bug in mypy but the type of self.subplots cannot be - # deducted even when typed above so ignore it and cast for now - subplots = self.subplots - for i, plot in enumerate(subplots): - # make a dict mapping axis labels to axis positions - for axis in ('x', 'y', 'z'): - if self.traces[i]['config'].get(axis) is not None: - unit = getattr(self.traces[i]['config'][axis], 'unit', None) - if unit is not None and unit not in standardunits: - if axis in ('x', 'y'): - ax = plot.getAxis(axismapping[axis]) - else: - # 2D measurement - # Then we should fetch the colorbar - ax = self.traces[i]['plot_object']['hist'].axis - ax.enableAutoSIPrefix(False) - # because updateAutoSIPrefix called from - # enableAutoSIPrefix doesnt actually take the - # value of the argument into account we have - # to manually replicate the update here - ax.autoSIPrefixScale = 1.0 - ax.setLabel(unitPrefix='') - ax.picture = None - ax.update() - - # set limits either from dataset or - setarr = getattr(self.traces[i]['config'][axis], 'ndarray', None) - arrmin = None - arrmax = None - if setarr is not None and not np.all(np.isnan(setarr)): - arrmax = np.nanmax(setarr) - arrmin = np.nanmin(setarr) - elif startranges is not None: - try: - paramname = self.traces[i]['config'][axis].full_name - arrmax = startranges[paramname]['max'] - arrmin = startranges[paramname]['min'] - except (IndexError, KeyError, AttributeError): - continue - - if axis == 'x': - rangesetter = getattr(plot.getViewBox(), 'setXRange') - elif axis == 'y': - rangesetter = getattr(plot.getViewBox(), 'setYRange') - else: - rangesetter = None - - if (rangesetter is not None - and arrmin is not None - and arrmax is not None): - rangesetter(arrmin, arrmax) +from qcodes.utils import issue_deprecation_warning + +try: + from qcodes_loop.plots.base import BasePlot + from qcodes_loop.plots.colors import color_cycle, colorscales + from qcodes_loop.plots.pyqtgraph import QtPlot, TransformState +except ImportError as e: + raise ImportError( + "qcodes.plots.pyqtgraph is deprecated and has moved to " + "the package `qcodes_loop`. Please install qcodes_loop directly or " + "with `pip install qcodes[loop]" + ) from e +issue_deprecation_warning( + "qcodes.plots.pyqtgraph", alternative="qcodes_loop.plots.pyqtgraph" +) diff --git a/qcodes/plots/qcmatplotlib.py b/qcodes/plots/qcmatplotlib.py index 7994fe2e81c..7b2120bc193 100644 --- a/qcodes/plots/qcmatplotlib.py +++ b/qcodes/plots/qcmatplotlib.py @@ -10,499 +10,18 @@ from numpy.ma import getmask, masked_invalid import qcodes -from qcodes.data.data_array import DataArray - -from .base import BasePlot - - -class MatPlot(BasePlot): - """ - Plot x/y lines or x/y/z heatmap data. The first trace may be included - in the constructor, other traces can be added with MatPlot.add() - - Args: - *args: Sequence of data to plot. Each element will have its own subplot. - An element can be a single array, or a sequence of arrays. In the - latter case, all arrays will be plotted in the same subplot. - - figsize (Tuple[float, float]): (width, height) tuple in inches to pass - to plt.figure. If not provided, figsize is determined from - subplots shape - - interval: period in seconds between update checks - - subplots: either a sequence (args) or mapping (kwargs) to pass to - plt.subplots. default is a single simple subplot (1, 1) - you can use this to pass kwargs to the plt.figure constructor - - num: integer or None - specifies the index of the matplotlib figure window to use. If None - then open a new window - - **kwargs: passed along to MatPlot.add() to add the first data trace - """ - - # Maximum default number of subplot columns. Used to determine shape of - # subplots when not explicitly provided - max_subplot_columns = 3 - - def __init__(self, *args, figsize=None, interval=1, subplots=None, num=None, - **kwargs): - super().__init__(interval) - - if subplots is None: - # Subplots is equal to number of args, or 1 if no args provided - subplots = max(len(args), 1) - - self._init_plot(subplots, figsize, num=num) - - # Add data to plot if passed in args, kwargs are passed to all subplots - for k, arg in enumerate(args): - if isinstance(arg, Sequence): - # Arg consists of multiple elements, add all to same subplot - for subarg in arg: - self[k].add(subarg, **kwargs) - else: - # Arg is single element, add to subplot - self[k].add(arg, **kwargs) - - self.tight_layout() - - def __getitem__(self, key): - """ - Subplots can be accessed via indices. - Args: - key: subplot idx - - Returns: - Subplot with idx key - """ - return self.subplots[key] - - def _init_plot(self, subplots=None, figsize=None, num=None): - import matplotlib.pyplot as plt - - if isinstance(subplots, Mapping): - if figsize is None: - figsize = (6, 4) - self.fig, self.subplots = plt.subplots(figsize=figsize, num=num, squeeze=False, **subplots) - else: - # Format subplots as tuple (nrows, ncols) - if isinstance(subplots, int): - # self.max_subplot_columns defines the limit on how many - # subplots can be in one row. Adjust subplot rows and columns - # accordingly - nrows = int(np.ceil(subplots / self.max_subplot_columns)) - ncols = min(subplots, self.max_subplot_columns) - subplots = (nrows, ncols) - if subplots is None: - subplots = (1,1) - if figsize is None: - # Adjust figsize depending on rows and columns in subplots - figsize = self.default_figsize(subplots) - - self.fig, self.subplots = plt.subplots(*subplots, num=num, - figsize=figsize, - squeeze=False) - - # squeeze=False ensures that subplots is always a 2D array independent - # of the number of subplots. - # However the qcodes api assumes that subplots is always a 1D array - # so flatten here - self.subplots = self.subplots.flatten() - - for k, subplot in enumerate(self.subplots): - # Include `add` method to subplots, making it easier to add data to - # subplots. Note that subplot kwarg is 1-based, to adhere to - # Matplotlib standards - subplot.add = partial(self.add, subplot=k+1) - - self.title = self.fig.suptitle('') - - def clear(self, subplots=None, figsize=None): - """ - Clears the plot window and removes all subplots and traces - so that the window can be reused. - """ - self.traces = [] - self.fig.clf() - self._init_plot(subplots, figsize, num=self.fig.number) - - def add_to_plot(self, use_offset: bool=False, **kwargs): - """ - adds one trace to this MatPlot. - - Args: - use_offset: Whether or not ticks can have an offset - **kwargs: with the exceptions given in the notes below - (mostly the data!), these are passed directly to - the matplotlib plotting routine. - - Returns: - Plot handle for trace - - Notes: - The following special cases apply for kwargs that are - not passed directly to the plotting routine. - - * `subplot`: the 1-based axes number to append to (default 1) - * if kwargs include `z`, we will draw a heatmap (ax.pcolormesh) - `x`, `y`, and `z` are passed as positional args to pcolormesh - * without `z` we draw a scatter/lines plot (ax.plot) - `x`, `y`, and `fmt` (if present) are passed as positional - args - """ - # TODO some way to specify overlaid axes? - # Note that there is a conversion from subplot kwarg, which is - # 1-based, to subplot idx, which is 0-based. - ax = self[kwargs.get('subplot', 1) - 1] - if 'z' in kwargs: - plot_object = self._draw_pcolormesh(ax, **kwargs) - else: - plot_object = self._draw_plot(ax, **kwargs) - - # Specify if axes ticks can have offset or not - ax.ticklabel_format(useOffset=use_offset) - - self._update_labels(ax, kwargs) - prev_default_title = self.get_default_title() - - self.traces.append({ - 'config': kwargs, - 'plot_object': plot_object - }) - - if prev_default_title == self.title.get_text(): - # in case the user has updated title, don't change it anymore - self.title.set_text(self.get_default_title()) - - return plot_object - - def _update_labels(self, ax, config): - for axletter in ("x", "y"): - if axletter+'label' in config: - label = config[axletter+'label'] - else: - label = None - - # find if any kwarg from plot.add in the base class - # matches xunit or yunit, signaling a custom unit - if axletter+'unit' in config: - unit = config[axletter+'unit'] - else: - unit = None - - # find ( more hope to) unit and label from - # the data array inside the config - getter = getattr(ax, f"get_{axletter}label") - if axletter in config and not getter(): - # now if we did not have any kwarg for label or unit - # fallback to the data_array - if unit is None: - _, unit = self.get_label(config[axletter]) - if label is None: - label, _ = self.get_label(config[axletter]) - elif getter(): - # The axis already has label. Assume that is correct - # We should probably check consistent units and error or warn - # if not consistent. It's also not at all clear how to handle - # labels/names as these will in general not be consistent on - # at least one axis - return - axsetter = getattr(ax, f"set_{axletter}label") - axsetter(f"{label} ({unit})") - - @staticmethod - def default_figsize(subplots): - """ - Provides default figsize for given subplots. - Args: - subplots (Tuple[Int, Int]): shape (nrows, ncols) of subplots - - Returns: - Tuple[float, float]: (width, height) of default figsize - for given subplot shape - """ - if not isinstance(subplots, tuple): - raise TypeError(f'Subplots {subplots} must be a tuple') - return (3 + 3 * subplots[1], 1 + 3 * subplots[0]) - - def update_plot(self): - """ - update the plot. The DataSets themselves have already been updated - in update, here we just push the changes to the plot. - """ - # matplotlib doesn't know how to autoscale to a pcolormesh after the - # first draw (relim ignores it...) so we have to do this ourselves - - from matplotlib.transforms import Bbox - - bboxes = dict(zip(self.subplots, [[] for p in self.subplots])) - - for trace in self.traces: - config = trace['config'] - plot_object = trace['plot_object'] - if 'z' in config: - # pcolormesh doesn't seem to allow editing x and y data, only z - # so instead, we'll remove and re-add the data. - if plot_object: - plot_object.remove() - - ax = self[config.get('subplot', 1) - 1] - kwargs = deepcopy(config) - # figsize may be passed in as part of config. - # pcolormesh will raise an error if this is passed to it - # so strip it here. - if 'figsize' in kwargs: - kwargs.pop('figsize') - plot_object = self._draw_pcolormesh(ax, **kwargs) - trace['plot_object'] = plot_object - - if plot_object: - bboxes[plot_object.axes].append( - plot_object.get_datalim(plot_object.axes.transData)) - else: - for axletter in 'xy': - setter = 'set_' + axletter + 'data' - if axletter in config: - getattr(plot_object, setter)(config[axletter]) - - for ax in self.subplots: - if ax.get_autoscale_on(): - ax.relim() - if bboxes[ax]: - bbox = Bbox.union(bboxes[ax]) - if np.all(np.isfinite(ax.dataLim)): - # should take care of the case of lines + heatmaps - # where there's already a finite dataLim from relim - ax.dataLim.set(Bbox.union(ax.dataLim, bbox)) - else: - # when there's only a heatmap, relim gives inf bounds - # so just completely overwrite it - ax.dataLim = bbox - ax.autoscale() - - self.fig.canvas.draw() - - def _draw_plot(self, ax, y, x=None, fmt=None, subplot=1, - xlabel=None, - ylabel=None, - zlabel=None, - xunit=None, - yunit=None, - zunit=None, - **kwargs): - # NOTE(alexj)stripping out subplot because which subplot we're in is - # already described by ax, and it's not a kwarg to matplotlib's ax.plot. - # But I didn't want to strip it out of kwargs earlier because it should - # stay part of trace['config']. - args = [arg for arg in [x, y, fmt] if arg is not None] - - line, = ax.plot(*args, **kwargs) - return line - - @staticmethod - def _make_args_for_pcolormesh(args_masked, x, y): - """ - Make args for pcolormesh. - pcolormesh accepts as args either - C - a (potentially) masked array - or - x, y, C where x and y are the colour box edge arrays and - are NOT allowed to be masked - """ - - if x is not None and y is not None: - # If x and y are provided, modify the arrays such that they - # correspond to grid corners instead of grid centers. - # This is to ensure that pcolormesh centers correctly and - # does not ignore edge points. - args = [] - for k, arr in enumerate(args_masked[:-1]): - # If a two-dimensional array is provided, only consider the - # first row/column, depending on the axis - if arr.ndim > 1: - arr = arr[0] if k == 0 else arr[:, 0] - # first extrapolate to fill any empty values Matplotlib 2.2 no - # longer support nans in x and y for pcolormesh. - if np.ma.is_masked(arr[1]): - step_size = 1. - # Only the first element is not nan. We have to guess the - # step size - else: - # the average stepsize is our best guess - step_size = np.ma.average(np.ma.diff(arr)) - - last_good_value = arr[np.logical_not(arr.mask)][-1] - extrapolation_start = last_good_value+step_size - n_invalid = np.sum(arr.mask) - extrapolation_stop = extrapolation_start+step_size*(n_invalid-1) - # numpy (1.14) has a deprecation warning related to shared - # masks. Let's silence this by making sure that this is - # not shared before modifying the mask - arr.unshare_mask() - arr[arr.mask] = np.linspace(extrapolation_start, - extrapolation_stop, - num=n_invalid) - # Now shift array to get edge coordinates rather than - # centre coordinates - arr_shift = np.insert(arr, 0, arr[0]) - arr_shift[0] -= step_size/2 - arr_shift[1:] += step_size/2 - args.append(arr_shift) - args.append(args_masked[-1]) - else: - # Only the masked value of z is used as a mask - args = args_masked[-1:] - - return args - - def _draw_pcolormesh(self, ax, z, x=None, y=None, subplot=1, - xlabel=None, - ylabel=None, - zlabel=None, - xunit=None, - yunit=None, - zunit=None, - nticks=None, - **kwargs): - # NOTE(alexj)stripping out subplot because which subplot we're in is already - # described by ax, and it's not a kwarg to matplotlib's ax.plot. But I - # didn't want to strip it out of kwargs earlier because it should stay - # part of trace['config']. - - args_masked = [masked_invalid(arg) for arg in [x, y, z] - if arg is not None] - - if np.any([np.all(getmask(arg)) for arg in args_masked]): - # if the z array is masked, don't draw at all - # there's nothing to draw, and anyway it throws a warning - # pcolormesh does not accept masked x and y axes, so we do not need - # to check for them. - return False - if 'cmap' not in kwargs: - kwargs['cmap'] = qcodes.config['gui']['defaultcolormap'] - - args = self._make_args_for_pcolormesh(args_masked, x, y) - - pc = ax.pcolormesh(*args, **kwargs) - - # Set x and y limits if arrays are provided - if x is not None and y is not None: - ax.set_xlim(np.nanmin(args[0]), np.nanmax(args[0])) - ax.set_ylim(np.nanmin(args[1]), np.nanmax(args[1])) - - # Specify preferred number of ticks with labels - if nticks and ax.get_xscale() != 'log' and ax.get_yscale != 'log': - ax.locator_params(nbins=nticks) - - if getattr(ax, 'qcodes_colorbar', None): - # update_normal doesn't seem to work... - ax.qcodes_colorbar.update_bruteforce(pc) - else: - # TODO: what if there are several colormeshes on this subplot, - # do they get the same colorscale? - # We should make sure they do, and have it include - # the full range of both. - ax.qcodes_colorbar = self.fig.colorbar(pc, ax=ax) - - # ideally this should have been in _update_labels, but - # the colorbar doesn't necessarily exist there. - # I guess we could create the colorbar no matter what, - # and just give it a dummy mappable to start, so we could - # put this where it belongs. - if zunit is None: - _, zunit = self.get_label(z) - if zlabel is None: - zlabel, _ = self.get_label(z) - - label = f"{zlabel} ({zunit})" - ax.qcodes_colorbar.set_label(label) - - # Scale colors if z has elements - cmin = np.nanmin(args_masked[-1]) - cmax = np.nanmax(args_masked[-1]) - ax.qcodes_colorbar.mappable.set_clim(cmin, cmax) - - return pc - - def save(self, filename=None): - """ - Save current plot to filename, by default - to the location corresponding to the default - title. - - Args: - filename (Optional[str]): Location of the file - """ - default = f"{self.get_default_title()}.png" - filename = filename or default - self.fig.savefig(filename) - - def tight_layout(self): - """ - Perform a tight layout on the figure. A bit of additional spacing at - the top is also added for the title. - """ - self.fig.tight_layout(rect=[0, 0, 1, 0.95]) - - def rescale_axis(self): - """ - Rescale axis and units for axis that are in standard units - i.e. V, s J ... to m μ, m - This scales units defined in BasePlot.standardunits only - to avoid prefixes on combined or non standard units - """ - - from matplotlib import ticker - - def scale_formatter(i, pos, scale): - return f"{i * scale:g}" - - for i, subplot in enumerate(self.subplots): - traces = [trace for trace in self.traces if trace['config'].get('subplot', None) == i+1] - if not traces: - continue - else: - # TODO: include all traces when calculating maxval etc. - trace = traces[0] - for axis in 'x', 'y', 'z': - if axis in trace['config'] and isinstance(trace['config'][axis], DataArray): - unit = trace['config'][axis].unit - label = trace['config'][axis].label - maxval = np.nanmax(abs(trace['config'][axis].ndarray)) - units_to_scale = self.standardunits - - # allow values up to a <1000. i.e. nV is used up to 1000 nV - prefixes = ['n', 'μ', 'm', '', 'k', 'M', 'G'] - thresholds = [10**(-6 + 3*n) for n in range(len(prefixes))] - scales = [10**(9 - 3*n) for n in range(len(prefixes))] - - if unit in units_to_scale: - scale = 1 - new_unit = unit - for prefix, threshold, trialscale in zip(prefixes, - thresholds, - scales): - if maxval < threshold: - scale = trialscale - new_unit = prefix + unit - break - # special case the largest - if maxval > thresholds[-1]: - scale = scales[-1] - new_unit = prefixes[-1] + unit - - tx = ticker.FuncFormatter( - partial(scale_formatter, scale=scale)) - new_label = f"{label} ({new_unit})" - if axis in ('x', 'y'): - getattr(subplot, - f"{axis}axis").set_major_formatter( - tx) - getattr(subplot, f"set_{axis}label")( - new_label) - else: - subplot.qcodes_colorbar.formatter = tx - subplot.qcodes_colorbar.set_label(new_label) - subplot.qcodes_colorbar.update_ticks() +from qcodes.utils import issue_deprecation_warning + +try: + from qcodes_loop.data.data_array import DataArray + from qcodes_loop.plots.base import BasePlot + from qcodes_loop.plots.qcmatplotlib import MatPlot +except ImportError as e: + raise ImportError( + "qcodes.plots.qcmatplotlib is deprecated and has moved to " + "the package `qcodes_loop`. Please install qcodes_loop directly or " + "with `pip install qcodes[loop]" + ) from e +issue_deprecation_warning( + "qcodes.plots.qcmatplotlib module", alternative="qcodes_loop.plots.qcmatplotlib" +) diff --git a/qcodes/tests/legacy/__init__.py b/qcodes/tests/legacy/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/qcodes/tests/legacy/data_mocks.py b/qcodes/tests/legacy/data_mocks.py deleted file mode 100644 index 0c2ad436217..00000000000 --- a/qcodes/tests/legacy/data_mocks.py +++ /dev/null @@ -1,150 +0,0 @@ -import numpy - -from qcodes.data.data_array import DataArray -from qcodes.data.data_set import new_data -from qcodes.data.io import DiskIO - - -class MockFormatter: - def read(self, data_set): - data_set.has_read_data = True - - def write(self, data_set, io_manager, location, write_metadata=False): - data_set.has_written_data = True - - def read_metadata(self, data_set): - data_set.has_read_metadata = True - - def write_metadata(self, data_set, io_manager, location, read_first=True): - data_set.has_written_metadata = True - - -class RecordingMockFormatter: - def __init__(self): - self.write_calls = [] - self.modified_ranges = [] - self.last_saved_indices = [] - self.write_metadata_calls = [] - - def write(self, data_set, io_manager, location, force_write=False): - self.write_calls.append((io_manager.base_location, location)) - - self.modified_ranges.append({ - array_id: array.modified_range - for array_id, array in data_set.arrays.items() - }) - - self.last_saved_indices.append({ - array_id: array.last_saved_index - for array_id, array in data_set.arrays.items() - }) - - def write_metadata(self, data_set, io_manager, location, read_first=True): - self.write_metadata_calls.append((io_manager.base_location, - location, read_first)) - - -class MatchIO: - def __init__(self, existing_matches, fmt=None): - self.existing_matches = existing_matches - self.fmt = fmt or '{}{}.something' - - def list(self, location, **kwargs): - return [self.fmt.format(location, i) for i in self.existing_matches] - - def join(self, *args): - return DiskIO('.').join(*args) - - -class MockLive: - arrays = 'whole lotta data' - - -class MockArray: - array_id = 'noise' - - def init_data(self): - self.ready = True - - -def DataSet1D(location=None, name=None): - # DataSet with one 1D array with 5 points - - # TODO: since y lists x as a set_array, it should automatically - # set is_setpoint=True for x, shouldn't it? Any reason we woundn't - # want that? - x = DataArray(name='x', label='X', preset_data=(1., 2., 3., 4., 5.), - is_setpoint=True) - y = DataArray(name='y', label='Y', preset_data=(3., 4., 5., 6., 7.), - set_arrays=(x,)) - return new_data(arrays=(x, y), location=location, name=name) - - -def DataSet2D(location=None, name=None): - # DataSet with one 2D array with 4 x 6 points - yy, xx = numpy.meshgrid(range(4), range(6)) - zz = xx**2+yy**2 - # outer setpoint should be 1D - xx = xx[:, 0] - x = DataArray(name='x', label='X', preset_data=xx, is_setpoint=True) - y = DataArray(name='y', label='Y', preset_data=yy, set_arrays=(x,), - is_setpoint=True) - z = DataArray(name='z', label='Z', preset_data=zz, set_arrays=(x, y)) - return new_data(arrays=(x, y, z), location=location, name=name) - - -def file_1d(): - return '\n'.join([ - '# x_set\ty', - '# "X"\t"Y"', - '# 5', - '1\t3', - '2\t4', - '3\t5', - '4\t6', - '5\t7', '']) - - -def DataSetCombined(location=None, name=None): - # Complex DataSet with two 1D and two 2D arrays - x = DataArray(name='x', label='X!', preset_data=(16., 17.), - is_setpoint=True) - y1 = DataArray(name='y1', label='Y1 value', preset_data=(18., 19.), - set_arrays=(x,)) - y2 = DataArray(name='y2', label='Y2 value', preset_data=(20., 21.), - set_arrays=(x,)) - - yset = DataArray(name='y', label='Y', preset_data=(22., 23., 24.), - is_setpoint=True) - yset.nest(2, 0, x) - z1 = DataArray(name='z1', label='Z1', - preset_data=((25., 26., 27.), (28., 29., 30.)), - set_arrays=(x, yset)) - z2 = DataArray(name='z2', label='Z2', - preset_data=((31., 32., 33.), (34., 35., 36.)), - set_arrays=(x, yset)) - return new_data(arrays=(x, y1, y2, yset, z1, z2), location=location, - name=name) - - -def files_combined(): - return [ - '\n'.join([ - '# x_set\ty1\ty2', - '# "X!"\t"Y1 value"\t"Y2 value"', - '# 2', - '16\t18\t20', - '17\t19\t21', '']), - - '\n'.join([ - '# x_set\ty_set\tz1\tz2', - '# "X!"\t"Y"\t"Z1"\t"Z2"', - '# 2\t3', - '16\t22\t25\t31', - '16\t23\t26\t32', - '16\t24\t27\t33', - '', - '17\t22\t28\t34', - '17\t23\t29\t35', - '17\t24\t30\t36', '']) - ] diff --git a/qcodes/tests/legacy/test_combined_loop.py b/qcodes/tests/legacy/test_combined_loop.py deleted file mode 100644 index bbf5d6eb167..00000000000 --- a/qcodes/tests/legacy/test_combined_loop.py +++ /dev/null @@ -1,311 +0,0 @@ -from unittest import TestCase - -import hypothesis.strategies as hst -import numpy as np -from hypothesis import given, settings - -from qcodes.actions import Task -from qcodes.data.location import FormatLocation -from qcodes.loops import Loop -from qcodes.parameters import Parameter, combine - -from ..instrument_mocks import DummyInstrument - - -class TestLoopCombined(TestCase): - @classmethod - def setUpClass(cls): - cls.dmm = DummyInstrument(name="dmm", gates=['voltage', - 'somethingelse']) - cls.dmm.somethingelse.get = lambda: 1 - - @classmethod - def tearDownClass(cls): - cls.dmm.close() - del cls.dmm - - @given( - npoints=hst.integers(2, 100), - x_start_stop=hst.lists( - hst.integers(min_value=-800, max_value=400), - min_size=2, - max_size=2, - unique=True, - ).map( - sorted # type: ignore[arg-type] - ), - y_start_stop=hst.lists( - hst.integers(min_value=-800, max_value=400), - min_size=2, - max_size=2, - unique=True, - ).map( - sorted # type: ignore[arg-type] - ), - z_start_stop=hst.lists( - hst.integers(min_value=-800, max_value=400), - min_size=2, - max_size=2, - unique=True, - ).map( - sorted # type: ignore[arg-type] - ), - ) - @settings(max_examples=10, deadline=300) - def testLoopCombinedParameterPrintTask(self, npoints, x_start_stop, - y_start_stop, z_start_stop): - - x_set = np.linspace(x_start_stop[0], x_start_stop[1], npoints) - y_set = np.linspace(y_start_stop[0], y_start_stop[1], npoints) - z_set = np.linspace(z_start_stop[0], z_start_stop[1], npoints) - setpoints = np.hstack((x_set.reshape(npoints, 1), - y_set.reshape(npoints, 1), - z_set.reshape(npoints, 1))) - - parameters = [Parameter(name, get_cmd=None, set_cmd=None) - for name in ["X", "Y", "Z"]] - - sweep_values = combine(*parameters, - name="combined").sweep(setpoints) - - def ataskfunc(): - a = 1+1 - - def btaskfunc(): - b = 1+2 - - atask = Task(ataskfunc) - btask = Task(btaskfunc) - - loc_fmt = 'data/{date}/#{counter}_{name}_{date}_{time}' - rcd = {'name': 'printTask'} - loc_provider = FormatLocation(fmt=loc_fmt, record=rcd) - loop = Loop(sweep_values).each(atask, btask) - data = loop.run(location=loc_provider, quiet=True) - np.testing.assert_array_equal(data.arrays['X'].ndarray, x_set) - np.testing.assert_array_equal(data.arrays['Y'].ndarray, y_set) - np.testing.assert_array_equal(data.arrays['Z'].ndarray, z_set) - - @given( - npoints=hst.integers(2, 100), - x_start_stop=hst.lists( - hst.integers(min_value=-800, max_value=400), - min_size=2, - max_size=2, - unique=True, - ).map( - sorted # type: ignore[arg-type] - ), - y_start_stop=hst.lists( - hst.integers(min_value=-800, max_value=400), - min_size=2, - max_size=2, - unique=True, - ).map( - sorted # type: ignore[arg-type] - ), - z_start_stop=hst.lists( - hst.integers(min_value=-800, max_value=400), - min_size=2, - max_size=2, - unique=True, - ).map( - sorted # type: ignore[arg-type] - ), - ) - @settings(max_examples=10, deadline=None) - def testLoopCombinedParameterTwice(self, npoints, x_start_stop, - y_start_stop, z_start_stop): - x_set = np.linspace(x_start_stop[0], x_start_stop[1], npoints) - y_set = np.linspace(y_start_stop[0], y_start_stop[1], npoints) - z_set = np.linspace(z_start_stop[0], z_start_stop[1], npoints) - setpoints = np.hstack((x_set.reshape(npoints, 1), - y_set.reshape(npoints, 1), - z_set.reshape(npoints, 1))) - parameters = [Parameter(name, get_cmd=None, set_cmd=None) - for name in ["X", "Y", "Z"]] - sweep_values = combine(*parameters, - name="combined").sweep(setpoints) - - def wrapper(): - counter = 0 - - def inner(): - nonlocal counter - counter += 1 - return counter - - return inner - - self.dmm.voltage.get = wrapper() - loc_fmt = 'data/{date}/#{counter}_{name}_{date}_{time}' - rcd = {'name': 'parameterTwice'} - loc_provider = FormatLocation(fmt=loc_fmt, record=rcd) - loop = Loop(sweep_values).each(self.dmm.voltage, self.dmm.voltage) - data = loop.run(location=loc_provider, quiet=True) - np.testing.assert_array_equal(data.arrays['X'].ndarray, x_set) - np.testing.assert_array_equal(data.arrays['Y'].ndarray, y_set) - np.testing.assert_array_equal(data.arrays['Z'].ndarray, z_set) - np.testing.assert_array_equal(data.arrays['dmm_voltage_0'].ndarray, - np.arange(1, npoints*2, 2)) - np.testing.assert_array_equal(data.arrays['dmm_voltage_1'].ndarray, - np.arange(2, npoints*2+1, 2)) - - @given( - npoints=hst.integers(2, 100), - x_start_stop=hst.lists( - hst.integers(min_value=-800, max_value=400), - min_size=2, - max_size=2, - unique=True, - ).map( - sorted # type: ignore[arg-type] - ), - y_start_stop=hst.lists( - hst.integers(min_value=-800, max_value=400), - min_size=2, - max_size=2, - unique=True, - ).map( - sorted # type: ignore[arg-type] - ), - z_start_stop=hst.lists( - hst.integers(min_value=-800, max_value=400), - min_size=2, - max_size=2, - unique=True, - ).map( - sorted # type: ignore[arg-type] - ), - ) - @settings(max_examples=10, deadline=600) - def testLoopCombinedParameterAndMore(self, npoints, x_start_stop, - y_start_stop, z_start_stop): - x_set = np.linspace(x_start_stop[0], x_start_stop[1], npoints) - y_set = np.linspace(y_start_stop[0], y_start_stop[1], npoints) - z_set = np.linspace(z_start_stop[0], z_start_stop[1], npoints) - setpoints = np.hstack((x_set.reshape(npoints, 1), - y_set.reshape(npoints, 1), - z_set.reshape(npoints, 1))) - parameters = [Parameter(name, get_cmd=None, set_cmd=None) - for name in ["X", "Y", "Z"]] - sweep_values = combine(*parameters, - name="combined").sweep(setpoints) - - def wrapper(): - counter = 0 - - def inner(): - nonlocal counter - counter += 1 - return counter - - return inner - - self.dmm.voltage.get = wrapper() - loc_fmt = 'data/{date}/#{counter}_{name}_{date}_{time}' - rcd = {'name': 'parameterAndMore'} - loc_provider = FormatLocation(fmt=loc_fmt, record=rcd) - loop = Loop(sweep_values).each(self.dmm.voltage, - self.dmm.somethingelse, self.dmm.voltage) - data = loop.run(location=loc_provider, quiet=True) - np.testing.assert_array_equal(data.arrays['X'].ndarray, x_set) - np.testing.assert_array_equal(data.arrays['Y'].ndarray, y_set) - np.testing.assert_array_equal(data.arrays['Z'].ndarray, z_set) - np.testing.assert_array_equal(data.arrays['dmm_voltage_0'].ndarray, - np.arange(1, npoints * 2, 2)) - np.testing.assert_array_equal(data.arrays['dmm_somethingelse'].ndarray, - np.ones(npoints)) - np.testing.assert_array_equal(data.arrays['dmm_voltage_2'].ndarray, - np.arange(2, npoints * 2 + 1, 2)) - - @given( - npoints=hst.integers(2, 50), - npoints_outer=hst.integers(2, 25), - x_start_stop=hst.lists( - hst.integers(min_value=-800, max_value=400), - min_size=2, - max_size=2, - unique=True, - ).map( - sorted # type: ignore[arg-type] - ), - y_start_stop=hst.lists( - hst.integers(min_value=-800, max_value=400), - min_size=2, - max_size=2, - unique=True, - ).map( - sorted # type: ignore[arg-type] - ), - z_start_stop=hst.lists( - hst.integers(min_value=-800, max_value=400), - min_size=2, - max_size=2, - unique=True, - ).map( - sorted # type: ignore[arg-type] - ), - ) - @settings(max_examples=10, deadline=None) - def testLoopCombinedParameterInside(self, npoints, npoints_outer, - x_start_stop, y_start_stop, - z_start_stop): - x_set = np.linspace(x_start_stop[0], x_start_stop[1], npoints_outer) - y_set = np.linspace(y_start_stop[0], y_start_stop[1], npoints) - z_set = np.linspace(z_start_stop[0], z_start_stop[1], npoints) - - setpoints = np.hstack((y_set.reshape(npoints, 1), - z_set.reshape(npoints, 1))) - - parameters = [Parameter(name, get_cmd=None, set_cmd=None) - for name in ["X", "Y", "Z"]] - sweep_values = combine(parameters[1], parameters[2], - name="combined").sweep(setpoints) - - def ataskfunc(): - a = 1+1 - - def btaskfunc(): - b = 1+2 - - atask = Task(ataskfunc) - btask = Task(btaskfunc) - - def wrapper(): - counter = 0 - - def inner(): - nonlocal counter - counter += 1 - return counter - - return inner - - self.dmm.voltage.get = wrapper() - loc_fmt = 'data/{date}/#{counter}_{name}_{date}_{time}' - rcd = {'name': 'parameterInside'} - loc_provider = FormatLocation(fmt=loc_fmt, record=rcd) - loop = Loop(parameters[0].sweep(x_start_stop[0], x_start_stop[1], - num=npoints_outer)).loop(sweep_values)\ - .each(self.dmm.voltage, atask, self.dmm.somethingelse, - self.dmm.voltage, btask) - data = loop.run(location=loc_provider, quiet=True) - np.testing.assert_array_equal(data.arrays['X_set'].ndarray, x_set) - np.testing.assert_array_equal(data.arrays['Y'].ndarray, - np.repeat(y_set.reshape(1, npoints), - npoints_outer, axis=0)) - np.testing.assert_array_equal(data.arrays['Z'].ndarray, - np.repeat(z_set.reshape(1, npoints), - npoints_outer, axis=0)) - - np.testing.assert_array_equal(data.arrays['dmm_voltage_0'].ndarray, - np.arange(1, npoints * npoints_outer * 2, - 2).reshape(npoints_outer, - npoints)) - np.testing.assert_array_equal(data.arrays['dmm_voltage_3'].ndarray, - np.arange(2, npoints * npoints_outer * 2 - + 1, 2).reshape(npoints_outer, - npoints)) - np.testing.assert_array_equal(data.arrays['dmm_somethingelse'].ndarray, - np.ones((npoints_outer, npoints))) diff --git a/qcodes/tests/legacy/test_data.py b/qcodes/tests/legacy/test_data.py deleted file mode 100644 index 3216a4ca103..00000000000 --- a/qcodes/tests/legacy/test_data.py +++ /dev/null @@ -1,657 +0,0 @@ -import logging -import os -import pickle -from unittest import TestCase - -import numpy as np -import pandas as pd -import xarray as xr - -from qcodes.data.data_array import DataArray, data_array_to_xarray_dictionary -from qcodes.data.data_set import ( - DataSet, - load_data, - new_data, - qcodes_dataset_to_xarray_dataset, - xarray_dataset_to_qcodes_dataset, -) -from qcodes.data.io import DiskIO -from qcodes.data.location import FormatLocation -from qcodes.logger.logger import LogCapture - -from ..common import strip_qc -from .data_mocks import ( - DataSet1D, - DataSet2D, - DataSetCombined, - MatchIO, - MockFormatter, - RecordingMockFormatter, -) - - -class TestDataArray(TestCase): - - def test_attributes(self): - pname = 'Betty Sue' - plabel = 'The best apple pie this side of Wenatchee' - pfullname = 'bert' - - class MockParam: - name = pname - label = plabel - - def __init__(self, full_name=None): - self.full_name = full_name - - name = 'Oscar' - label = 'The grouch. GRR!' - fullname = 'ernie' - array_id = 24601 - set_arrays = ('awesomeness', 'chocolate content') - shape = 'Ginornous' - action_indices = (1, 2, 3, 4, 5) - - p_data = DataArray(parameter=MockParam(pfullname), name=name, - label=label, full_name=fullname) - p_data2 = DataArray(parameter=MockParam(pfullname)) - - # explicitly given name and label override parameter vals - self.assertEqual(p_data.name, name) - self.assertEqual(p_data.label, label) - self.assertEqual(p_data.full_name, fullname) - self.assertEqual(p_data2.name, pname) - self.assertEqual(p_data2.label, plabel) - self.assertEqual(p_data2.full_name, pfullname) - # test default values - self.assertIsNone(p_data.array_id) - self.assertEqual(p_data.shape, ()) - self.assertEqual(p_data.action_indices, ()) - self.assertEqual(p_data.set_arrays, ()) - self.assertIsNone(p_data.ndarray) - - np_data = DataArray(name=name, label=label, array_id=array_id, - set_arrays=set_arrays, shape=shape, - action_indices=action_indices) - self.assertEqual(np_data.name, name) - self.assertEqual(np_data.label, label) - # no full name or parameter - use name - self.assertEqual(np_data.full_name, name) - # test simple assignments - self.assertEqual(np_data.array_id, array_id) - self.assertEqual(np_data.set_arrays, set_arrays) - self.assertEqual(np_data.shape, shape) - self.assertEqual(np_data.action_indices, action_indices) - - name_data = DataArray(name=name) - self.assertEqual(name_data.label, name) - - blank_data = DataArray() - self.assertIsNone(blank_data.name) - - def test_preset_data(self): - onetwothree = [ - # lists and tuples work - [1.0, 2.0, 3.0], - (1.0, 2.0, 3.0), - - # iterators get automatically cast to floats - (i + 1 for i in range(3)), - map(float, range(1, 4)), - - # and of course numpy arrays themselves work - np.array([1.0, 2.0, 3.0]), - ] - - expected123 = [1.0, 2.0, 3.0] - - for item in onetwothree: - data = DataArray(preset_data=item) - self.assertEqual(data.ndarray.tolist(), expected123) - self.assertEqual(data.shape, (3, )) - - # you can re-initialize a DataArray with the same shape data, - # but not with a different shape - list456 = [4, 5, 6] - data.init_data(data=list456) - self.assertEqual(data.ndarray.tolist(), list456) - with self.assertRaises(ValueError): - data.init_data([1, 2]) - self.assertEqual(data.ndarray.tolist(), list456) - self.assertEqual(data.shape, (3, )) - - # you can call init_data again with no data, and nothing changes - data.init_data() - self.assertEqual(data.ndarray.tolist(), list456) - self.assertEqual(data.shape, (3, )) - - # multidimensional works too - list2d = [[1, 2], [3, 4]] - data2 = DataArray(preset_data=list2d) - self.assertEqual(data2.ndarray.tolist(), list2d) - self.assertEqual(data2.shape, (2, 2)) - - def test_init_data_error(self): - data = DataArray(preset_data=[1, 2]) - data.shape = (3, ) - - # not sure when this would happen... but if you call init_data - # and it notices an inconsistency between shape and the actual - # data that's already there, it raises an error - with self.assertRaises(ValueError): - data.init_data() - - def test_clear(self): - nan = float('nan') - data = DataArray(preset_data=[1, 2]) - data.clear() - # sometimes it's annoying that nan != nan - self.assertEqual(repr(data.ndarray.tolist()), repr([nan, nan])) - - def test_edit_and_mark(self): - data = DataArray(preset_data=[[1, 2], [3, 4]]) - self.assertEqual(data[0].tolist(), [1, 2]) - self.assertEqual(data[0, 1], 2) - - data.modified_range = None - self.assertIsNone(data.last_saved_index) - - self.assertEqual(len(data), 2) - data[0] = np.array([5, 6]) - data[1, 0] = 7 - self.assertEqual(data.ndarray.tolist(), [[5, 6], [7, 4]]) - - self.assertEqual(data.modified_range, (0, 2)) - - # as if we saved the first two points... the third should still - # show as modified - data.mark_saved(1) - self.assertEqual(data.last_saved_index, 1) - self.assertEqual(data.modified_range, (2, 2)) - - # now we save the third point... no modifications left. - data.mark_saved(2) - self.assertEqual(data.last_saved_index, 2) - self.assertEqual(data.modified_range, None) - - data.clear_save() - self.assertEqual(data.last_saved_index, None) - self.assertEqual(data.modified_range, (0, 2)) - - def test_edit_and_mark_slice(self): - data = DataArray(preset_data=[[1] * 5] * 6) - - self.assertEqual(data.shape, (6, 5)) - data.modified_range = None - - data[:4:2, 2:] = 2 - self.assertEqual(data.tolist(), [ - [1, 1, 2, 2, 2], - [1, 1, 1, 1, 1], - [1, 1, 2, 2, 2], - [1, 1, 1, 1, 1], - [1, 1, 1, 1, 1], - [1, 1, 1, 1, 1] - ]) - self.assertEqual(data.modified_range, (2, 14)) - - def test_repr(self): - array2d = [[1, 2], [3, 4]] - arrayrepr = repr(np.array(array2d)) - array_id = (3, 4) - data = DataArray(preset_data=array2d) - - self.assertEqual(repr(data), 'DataArray[2,2]:\n' + arrayrepr) - - data.array_id = array_id - self.assertEqual(repr(data), 'DataArray[2,2]: ' + str(array_id) + - '\n' + arrayrepr) - - def test_nest_empty(self): - data = DataArray() - - self.assertEqual(data.shape, ()) - - mock_set_array = 'not really an array but we don\'t check' - mock_set_array2 = 'another one' - - data.nest(2, action_index=44, set_array=mock_set_array) - data.nest(3, action_index=66, set_array=mock_set_array2) - - # the array doesn't exist until you initialize it - self.assertIsNone(data.ndarray) - - # but other attributes are set - self.assertEqual(data.shape, (3, 2)) - self.assertEqual(data.action_indices, (66, 44)) - self.assertEqual(data.set_arrays, (mock_set_array2, mock_set_array)) - - data.init_data() - self.assertEqual(data.ndarray.shape, (3, 2)) - - # after initializing data, you can't nest anymore because this isn't - # a preset array - with self.assertRaises(RuntimeError): - data.nest(4) - - def test_nest_preset(self): - data = DataArray(preset_data=[1, 2]) - data.nest(3) - self.assertEqual(data.shape, (3, 2)) - self.assertEqual(data.ndarray.tolist(), [[1, 2]] * 3) - self.assertEqual(data.action_indices, ()) - self.assertEqual(data.set_arrays, (data,)) - - # test that the modified range gets correctly set to - # (0, 2*3-1 = 5) - self.assertEqual(data.modified_range, (0, 5)) - - # you need a set array for all but the inner nesting - with self.assertRaises(TypeError): - data.nest(4) - - def test_data_set_property(self): - data = DataArray(preset_data=[1, 2]) - self.assertIsNone(data.data_set) - - mock_data_set = 'pretend this is a DataSet, we don\'t check type' - mock_data_set2 = 'you can only assign to another after first clearing' - data.data_set = mock_data_set - self.assertEqual(data.data_set, mock_data_set) - - with self.assertRaises(RuntimeError): - data.data_set = mock_data_set2 - - data.data_set = None - self.assertIsNone(data.data_set) - data.data_set = mock_data_set2 - self.assertEqual(data.data_set, mock_data_set2) - - def test_fraction_complete(self): - data = DataArray(shape=(5, 10)) - self.assertIsNone(data.ndarray) - self.assertEqual(data.fraction_complete(), 0.0) - - data.init_data() - self.assertEqual(data.fraction_complete(), 0.0) - - # index = 1 * 10 + 7 - add 1 (for index 0) and you get 18 - # each index is 2% of the total, so this is 36% - data[1, 7] = 1 - self.assertEqual(data.fraction_complete(), 18 / 50) - - # add a last_saved_index but modified_range is still bigger - data.mark_saved(13) - self.assertEqual(data.fraction_complete(), 18 / 50) - - # now last_saved_index wins - data.mark_saved(19) - self.assertEqual(data.fraction_complete(), 20 / 50) - - # now pretend we get more info from syncing - data.synced_index = 22 - self.assertEqual(data.fraction_complete(), 23 / 50) - - def test_to_xarray(self): - data = DataArray(preset_data=[1, 2]) - array_dict = data_array_to_xarray_dictionary(data) - xarray_dataarray = data.to_xarray() - - def test_xarray_conversions(self): - da = DataSet1D(name="TestDataArray_test_xarray_conversions").x_set - - xarray_dictionary = data_array_to_xarray_dictionary(da) - - xarray_dataarray = da.to_xarray() - da_transformed = DataArray.from_xarray(xarray_dataarray) - - for key in ["array_id", "unit", "label"]: - self.assertEqual(getattr(da, key), getattr(da_transformed, key)) - - -class TestLoadData(TestCase): - - def test_no_saved_data(self): - with self.assertRaises(IOError): - load_data('_no/such/file_') - - def test_load_false(self): - with self.assertRaises(ValueError): - load_data(False) - - def test_get_read(self): - data = load_data(formatter=MockFormatter(), location='here!') - self.assertEqual(data.has_read_data, True) - self.assertEqual(data.has_read_metadata, True) - - -class TestDataSetMetaData(TestCase): - - def test_snapshot(self): - data = new_data(location=False) - expected_snap = { - '__class__': 'qcodes.data.data_set.DataSet', - 'location': False, - 'arrays': {}, - 'formatter': 'qcodes.data.gnuplot_format.GNUPlotFormat', - } - snap = strip_qc(data.snapshot()) - - # handle io separately so we don't need to figure out our path - self.assertIn('DiskIO', snap['io']) - del snap['io'] - self.assertEqual(snap, expected_snap) - - # even though we removed io from the snapshot, it's still in .metadata - self.assertIn('io', data.metadata) - - # then do the same transformations to metadata to check it too - del data.metadata['io'] - strip_qc(data.metadata) - self.assertEqual(data.metadata, expected_snap) - - # location is False so read_metadata should be a noop - data.metadata = {'food': 'Fried chicken'} - data.read_metadata() - self.assertEqual(data.metadata, {'food': 'Fried chicken'}) - - # snapshot should never delete things from metadata, only add or update - data.metadata['location'] = 'Idaho' - snap = strip_qc(data.snapshot()) - expected_snap['food'] = 'Fried chicken' - del snap['io'] - self.assertEqual(snap, expected_snap) - - -class TestNewData(TestCase): - - @classmethod - def setUpClass(cls): - cls.original_lp = DataSet.location_provider - - @classmethod - def tearDownClass(cls): - DataSet.location_provider = cls.original_lp - - def test_overwrite(self): - io = MatchIO([1]) - - with self.assertRaises(FileExistsError): - new_data(location='somewhere', io=io) - - data = new_data(location='somewhere', io=io, overwrite=True,) - self.assertEqual(data.location, 'somewhere') - - def test_location_functions(self): - def my_location(io, record): - return 'data/{}'.format((record or {}).get('name') or 'LOOP!') - - def my_location2(io, record): - name = (record or {}).get('name') or 'loop?' - return f'data/{name}/folder' - - DataSet.location_provider = my_location - - self.assertEqual(new_data().location, 'data/LOOP!') - self.assertEqual(new_data(name='cheese').location, 'data/cheese') - - data = new_data(location=my_location2) - self.assertEqual(data.location, 'data/loop?/folder') - data = new_data(location=my_location2, name='iceCream') - self.assertEqual(data.location, 'data/iceCream/folder') - - -class TestDataSet(TestCase): - - def test_constructor_errors(self): - # no location - only allowed with load_data - with self.assertRaises(ValueError): - DataSet() - # wrong type - with self.assertRaises(ValueError): - DataSet(location=42) - - def test_write_copy(self): - data = DataSet1D(location=False) - mockbase = os.path.abspath('some_folder') - data.io = DiskIO(mockbase) - - mr = (2, 3) - mr_full = (0, 4) - lsi = 1 - data.x_set.modified_range = mr - data.y.modified_range = mr - data.x_set.last_saved_index = lsi - data.y.last_saved_index = lsi - - with self.assertRaises(TypeError): - data.write_copy() - - with self.assertRaises(TypeError): - data.write_copy(path='some/path', io_manager=DiskIO('.')) - - with self.assertRaises(TypeError): - data.write_copy(path='some/path', location='something/else') - - data.formatter = RecordingMockFormatter() - data.write_copy(path='/some/abs/path') - self.assertEqual(data.formatter.write_calls, - [(None, '/some/abs/path')]) - self.assertEqual(data.formatter.write_metadata_calls, - [(None, '/some/abs/path', False)]) - # check that the formatter gets called as if nothing has been saved - self.assertEqual(data.formatter.modified_ranges, - [{'x_set': mr_full, 'y': mr_full}]) - self.assertEqual(data.formatter.last_saved_indices, - [{'x_set': None, 'y': None}]) - # but the dataset afterward has its original mods back - self.assertEqual(data.x_set.modified_range, mr) - self.assertEqual(data.y.modified_range, mr) - self.assertEqual(data.x_set.last_saved_index, lsi) - self.assertEqual(data.y.last_saved_index, lsi) - - # recreate the formatter to clear the calls attributes - data.formatter = RecordingMockFormatter() - data.write_copy(location='some/rel/path') - self.assertEqual(data.formatter.write_calls, - [(mockbase, 'some/rel/path')]) - self.assertEqual(data.formatter.write_metadata_calls, - [(mockbase, 'some/rel/path', False)]) - - mockbase2 = os.path.abspath('some/other/folder') - io2 = DiskIO(mockbase2) - - with self.assertRaises(ValueError): - # if location=False we need to specify it in write_copy - data.write_copy(io_manager=io2) - - data.location = 'yet/another/path' - data.formatter = RecordingMockFormatter() - data.write_copy(io_manager=io2) - self.assertEqual(data.formatter.write_calls, - [(mockbase2, 'yet/another/path')]) - self.assertEqual(data.formatter.write_metadata_calls, - [(mockbase2, 'yet/another/path', False)]) - - def test_pickle_dataset(self): - # Test pickling of DataSet object - # If the data_manager is set to None, then the object should pickle. - loc_fmt = 'data/{date}/#{counter}_{name}_{date}_{time}' - rcd = {'name': 'test_pickle_dataset'} - loc_provider = FormatLocation(fmt=loc_fmt, record=rcd) - m = DataSet2D(location=loc_provider, - name="test_pickle_dataset") - pickle.dumps(m) - - def test_default_parameter(self): - loc_fmt = 'data/{date}/#{counter}_{name}_{date}_{time}' - rcd = {'name': 'test_default_parameter'} - loc_provider = FormatLocation(fmt=loc_fmt, record=rcd) - # Test whether the default_array function works - m = DataSet2D(name="test_default_parameter", - location=loc_provider) - - # test we can run with default arguments - name = m.default_parameter_name() - - # test with paramname - name = m.default_parameter_name(paramname='z') - self.assertEqual(name, 'z') - # test we can get the array instead of the name - array = m.default_parameter_array(paramname='z') - self.assertEqual(array, m.z) - - # first non-setpoint array - array = m.default_parameter_array() - self.assertEqual(array, m.z) - - # test with metadata - m.metadata = dict({'default_parameter_name': 'x_set'}) - name = m.default_parameter_name() - self.assertEqual(name, 'x_set') - - # test the fallback: no name matches, no non-setpoint array - x = DataArray(name='x', label='X', preset_data=( - 1., 2., 3., 4., 5.), is_setpoint=True) - m = new_data(arrays=(x,), name='onlysetpoint') - name = m.default_parameter_name(paramname='dummy') - self.assertEqual(name, 'x_set') - - def test_fraction_complete(self): - empty_data = new_data(arrays=(), location=False) - self.assertEqual(empty_data.fraction_complete(), 0.0) - - data = DataSetCombined(location=False, - name="test_fraction_complete") - self.assertEqual(data.fraction_complete(), 1.0) - - # alter only the measured arrays, check that only these are used - # to calculate fraction_complete - data.y1.modified_range = (0, 0) # 1 of 2 - data.y2.modified_range = (0, 0) # 1 of 2 - data.z1.modified_range = (0, 2) # 3 of 6 - data.z2.modified_range = (0, 2) # 3 of 6 - self.assertEqual(data.fraction_complete(), 0.5) - - # mark more things complete using last_saved_index and synced_index - data.y1.last_saved_index = 1 # 2 of 2 - data.z1.synced_index = 5 # 6 of 6 - self.assertEqual(data.fraction_complete(), 0.75) - - def mock_sync(self): - i = self.sync_index - self.syncing_array[i] = i - self.sync_index = i + 1 - return self.sync_index < self.syncing_array.size - - def failing_func(self): - raise RuntimeError('it is called failing_func for a reason!') - - def logging_func(self): - logging.info(f'background at index {self.sync_index}') - - def test_complete(self): - array = DataArray(name='y', shape=(5,)) - array.init_data() - data = new_data(arrays=(array,), location=False) - self.syncing_array = array - self.sync_index = 0 - data.sync = self.mock_sync - bf = DataSet.background_functions - bf['fail'] = self.failing_func - bf['log'] = self.logging_func - - with LogCapture() as logs: - # grab info and warnings but not debug messages - logging.getLogger().setLevel(logging.INFO) - data.complete(delay=0.001) - - logs = logs.value - - expected_logs = [ - 'waiting for DataSet to complete', - 'DataSet: 0% complete', - 'RuntimeError: it is called failing_func for a reason!', - 'background at index 1', - 'DataSet: 20% complete', - 'RuntimeError: it is called failing_func for a reason!', - 'background function fail failed twice in a row, removing it', - 'background at index 2', - 'DataSet: 40% complete', - 'background at index 3', - 'DataSet: 60% complete', - 'background at index 4', - 'DataSet: 80% complete', - 'background at index 5', - 'DataSet is complete' - ] - - log_index = 0 - for line in expected_logs: - self.assertIn(line, logs, logs) - try: - log_index_new = logs.index(line, log_index) - except ValueError: - raise ValueError('line {} not found after {} in: \n {}'.format( - line, log_index, logs)) - self.assertTrue(log_index_new >= log_index, logs) - log_index = log_index_new + len(line) + 1 # +1 for \n - self.assertEqual(log_index, len(logs), logs) - - def test_remove_array(self): - loc_fmt = 'data/{date}/#{counter}_{name}_{date}_{time}' - rcd = {'name': 'test_remove_array'} - loc_provider = FormatLocation(fmt=loc_fmt, record=rcd) - m = DataSet2D(name="test_remove_array", - location=loc_provider) - m.remove_array('z') - _ = m.__repr__() - self.assertFalse('z' in m.arrays) - - def test_xarray_conversions(self): - qd = DataSet1D(name="TestNewData_test_xarray_conversions") - xarray_data_set = qcodes_dataset_to_xarray_dataset(qd) - qd_transformed = xarray_dataset_to_qcodes_dataset(xarray_data_set) - m = qd.default_parameter_array() - mt = qd_transformed.default_parameter_array() - - for key in ["name", "unit"]: - self.assertEqual(getattr(m, key), getattr(mt, key)) - qd2 = DataSet2D(name="TestNewData_test_xarray_conversions") - xarray_data_set = qcodes_dataset_to_xarray_dataset(qd2) - qd2_transformed = xarray_dataset_to_qcodes_dataset(xarray_data_set) - - m = qd2.default_parameter_array() - mt = qd2_transformed.default_parameter_array() - for key in ["name", "unit"]: - self.assertEqual(getattr(m, key), getattr(mt, key)) - - xds = qd.to_xarray() - qds = DataSet.from_xarray(xds) - - def test_xarray_example_conversion(self): - times = pd.date_range("2000-01-01", "2000-1-31", name="time") - shape = (31, 3) - xarray_dataset = xr.Dataset( - {"tmin": (("time", "location"), np.random.rand(*shape)), - "tmax": (("time", "location"), np.random.rand(*shape)), - }, {"time": times, "location": ["IA", "IN", "IL"]},) - - qd = DataSet.from_xarray(xarray_dataset) - xarray_dataset2 = qd.to_xarray() - - self.assertEqual(qd.default_parameter_array().shape, xarray_dataset.tmin.shape) - self.assertEqual(list(xarray_dataset.coords.keys()), list(xarray_dataset2.coords.keys())) - self.assertEqual(list(xarray_dataset.data_vars.keys()), list(xarray_dataset2.data_vars.keys())) - self.assertEqual(xarray_dataset.tmin.shape, xarray_dataset2.tmin.shape) - - def test_dataset_conversion_transpose_regression(self): - qd = DataSet2D(name="test") - qd.x_set.label = "X label" - qd.x_set.unit = "seconds" - ds = qd.to_xarray().transpose() - qdt = DataSet.from_xarray(ds) - - self.assertEqual(qd.x_set.label, qdt.x_set.label) - self.assertEqual(qd.x_set.unit, qdt.x_set.unit) - self.assertEqual([a.name for a in qdt.z.set_arrays], ["y_set", "x_set"]) diff --git a/qcodes/tests/legacy/test_format.py b/qcodes/tests/legacy/test_format.py deleted file mode 100644 index b43014ce9ce..00000000000 --- a/qcodes/tests/legacy/test_format.py +++ /dev/null @@ -1,436 +0,0 @@ -import os -from unittest import TestCase - -from qcodes.data.data_array import DataArray -from qcodes.data.data_set import DataSet, load_data, new_data -from qcodes.data.format import Formatter -from qcodes.data.gnuplot_format import GNUPlotFormat -from qcodes.data.location import FormatLocation -from qcodes.logger.logger import LogCapture - -from .data_mocks import DataSet1D, DataSetCombined, file_1d, files_combined - - -class TestBaseFormatter(TestCase): - def setUp(self): - self.io = DataSet.default_io - self.locations = ('_simple1d_', '_combined_') - - for location in self.locations: - self.assertFalse(self.io.list(location)) - - def tearDown(self): - for location in self.locations: - self.io.remove_all(location) - - def test_overridable_methods(self): - formatter = Formatter() - loc_fmt = 'data/{date}/#{counter}_{name}_{date}_{time}' - rcd = {'name': 'test_overridable_methods'} - loc_provider = FormatLocation(fmt=loc_fmt, record=rcd) - data = DataSet1D(name="test_overridable", - location=loc_provider) - - with self.assertRaises(NotImplementedError): - formatter.write(data, data.io, data.location) - with self.assertRaises(NotImplementedError): - formatter.read_one_file(data, 'a file!', set()) - - with self.assertRaises(NotImplementedError): - formatter.write_metadata(data, data.io, data.location) - with self.assertRaises(NotImplementedError): - formatter.read_metadata(data) - - def test_no_files(self): - formatter = Formatter() - data = DataSet1D( - name="test_no_file", - location=self.locations[0]) - with self.assertRaises(IOError): - formatter.read(data) - - def test_init_and_bad_read(self): - location = self.locations[0] - path = f'./{location}/bad.dat' - - class MyFormatter(Formatter): - def read_one_file(self, data_set, f, ids_read): - s = f.read() - if 'garbage' not in s: - raise Exception('reading the wrong file?') - - # mark this file as read, before generating an error - if not hasattr(data_set, 'files_read'): - data_set.files_read = [] - data_set.files_read.append(f.name) - raise ValueError('garbage in, garbage out') - - def read_metadata(self, data_set): - pass - - formatter = MyFormatter() - data = DataSet1D(location=location, - name="test_init_and_bad_read") - data.x_set.ndarray = None - data.y.ndarray = None - - os.makedirs(os.path.dirname(path), exist_ok=True) - with open(path, 'w') as f: - f.write('garbage') - - with LogCapture() as logs: - formatter.read(data) - - # we tried to read this file but it generated an error - self.assertEqual(logs.value.count('error reading file'), 1, logs.value) - self.assertEqual(data.files_read, [os.path.abspath(path)]) - - expected_array_repr = repr([float('nan')] * 5) - self.assertEqual(repr(data.x_set.tolist()), expected_array_repr) - self.assertEqual(repr(data.y.tolist()), expected_array_repr) - - def test_group_arrays(self): - formatter = Formatter() - data = DataSetCombined(name="test_group_arrays") - - groups = formatter.group_arrays(data.arrays) - - self.assertEqual(len(groups), 2, groups) - groups.sort(key=lambda grp: len(grp.set_arrays)) - - g1d, g2d = groups - - self.assertEqual(g1d.shape, (2,)) - self.assertEqual(g1d.set_arrays, (data.x_set,)) - self.assertEqual(g1d.data, (data.y1, data.y2)) - self.assertEqual(g1d.name, 'x_set') - - self.assertEqual(g2d.shape, (2, 3)) - self.assertEqual(g2d.set_arrays, (data.x_set, data.y_set)) - self.assertEqual(g2d.data, (data.z1, data.z2)) - self.assertEqual(g2d.name, 'x_set_y_set') - - def test_match_save_range(self): - formatter = Formatter() - data = DataSet1D(name="test_match_save_range") - - group = formatter.group_arrays(data.arrays)[0] - - # no matter what else, if nothing is listed as modified - # then save_range is None - data.x_set.modified_range = data.y.modified_range = None - for lsi_x in [None, 0, 3]: - data.x_set.last_saved_index = lsi_x - for lsi_y in [None, 1, 4]: - data.y.last_saved_index = lsi_y - for fe in [True, False]: - save_range = formatter.match_save_range( - group, file_exists=fe) - self.assertEqual(save_range, None) - - # consistent last_saved_index: if it's None or within the - # modified range, or if file does not exist, we need to overwrite - # otherwise start just after last_saved_index - for lsi, start in [(None, 0), (0, 1), (1, 2), (2, 3), (3, 0), (4, 0)]: - data.x_set.last_saved_index = data.y.last_saved_index = lsi - - # inconsistent modified_range: if only_complete is False, expands - # to greatest extent so these situations are identical - # if only_complete is True, only gets to the last common point - for xmr, ymr, last_common in ( - [(4, 4), (3, 3), 3], - [(3, 4), None, None], - [None, (3, 4), None]): - data.x_set.modified_range = xmr - data.y.modified_range = ymr - - save_range = formatter.match_save_range( - group, file_exists=False, only_complete=False) - self.assertEqual(save_range, (0, 4)) - - save_range = formatter.match_save_range( - group, file_exists=True, only_complete=False) - self.assertEqual(save_range, (start, 4)) - - save_all = formatter.match_save_range(group, file_exists=False) - save_inc = formatter.match_save_range(group, file_exists=True) - if last_common: - # if last_saved_index is greater than we would otherwise - # save, we still go up to last_saved_index (wouldn't want - # this write to delete data!) - last_save = max(last_common, lsi) if lsi else last_common - self.assertEqual(save_all, (0, last_save), - (lsi, xmr, ymr)) - self.assertEqual(save_inc, (start, last_save), - (lsi, xmr, ymr)) - else: - if lsi is None: - self.assertIsNone(save_all) - else: - self.assertEqual(save_all, (0, lsi)) - self.assertIsNone(save_inc) - - # inconsistent last_saved_index: need to overwrite if there are any - # modifications - data.x_set.last_saved_index = 1 - data.y.last_saved_index = 2 - data.x_set.modified_range = data.y.modified_range = (3, 4) - save_range = formatter.match_save_range(group, file_exists=True) - self.assertEqual(save_range, (0, 4)) - - -class TestGNUPlotFormat(TestCase): - def setUp(self): - self.io = DataSet.default_io - self.locations = ('_simple1d_', '_combined_') - - for location in self.locations: - self.assertFalse(self.io.list(location)) - - def tearDown(self): - for location in self.locations: - self.io.remove_all(location) - - def checkArraysEqual(self, a, b): - self.checkArrayAttrs(a, b) - - self.assertEqual(len(a.set_arrays), len(b.set_arrays)) - for sa, sb in zip(a.set_arrays, b.set_arrays): - self.checkArrayAttrs(sa, sb) - - def checkArrayAttrs(self, a, b): - self.assertEqual(a.tolist(), b.tolist()) - self.assertEqual(a.label, b.label) - self.assertEqual(a.array_id, b.array_id) - - def test_full_write(self): - formatter = GNUPlotFormat() - location = self.locations[0] - data = DataSet1D( - name="test_full_write", - location=location) - - formatter.write(data, data.io, data.location) - - with open(location + '/x_set.dat') as f: - self.assertEqual(f.read(), file_1d()) - - # check that we can add comment lines randomly into the file - # as long as it's after the first three lines, which are comments - # with well-defined meaning, - # and that we can un-quote the labels - lines = file_1d().split('\n') - lines[1] = lines[1].replace('"', '') - lines[3:3] = ['# this data is awesome!'] - lines[6:6] = ['# the next point is my favorite.'] - with open(location + '/x_set.dat', 'w') as f: - f.write('\n'.join(lines)) - - # normally this would be just done by data2 = load_data(location) - # but we want to work directly with the Formatter interface here - data2 = DataSet(location=location) - formatter.read(data2) - - self.checkArraysEqual(data2.x_set, data.x_set) - self.checkArraysEqual(data2.y, data.y) - - # data has been saved - self.assertEqual(data.y.last_saved_index, 4) - # data2 has been read back in, should show the same - # last_saved_index - self.assertEqual(data2.y.last_saved_index, 4) - - # while we're here, check some errors on bad reads - - # first: trying to read into a dataset that already has the - # wrong size - x = DataArray(name='x_set', label='X', preset_data=(1., 2.)) - y = DataArray(name='y', label='Y', preset_data=(3., 4.), - set_arrays=(x,)) - data3 = new_data(arrays=(x, y), location=location + 'XX') - # initially give it a different location so we can make it without - # error, then change back to the location we want. - data3.location = location - with LogCapture() as logs: - formatter.read(data3) - - self.assertTrue('ValueError' in logs.value, logs.value) - - # no problem reading again if only data has changed, it gets - # overwritten with the disk copy - data2.x_set[2] = 42 - data2.y[2] = 99 - formatter.read(data2) - self.assertEqual(data2.x_set[2], 3) - self.assertEqual(data2.y[2], 5) - - def test_format_options(self): - formatter = GNUPlotFormat(extension='.splat', terminator='\r', - separator=' ', comment='?:', - number_format='5.2f') - location = self.locations[0] - data = DataSet1D( - name="test_format_option", - location=location) - - formatter.write(data, data.io, data.location) - - # TODO - Python3 uses universal newlines for read and write... - # which means '\n' gets converted on write to the OS standard - # (os.linesep) and all of the options we support get converted - # back to '\n' on read. So I'm tempted to just take out terminator - # as an option rather than turn this feature off. - odd_format = '\n'.join([ - '?:x_set y', - '?:"X" "Y"', - '?:5', - ' 1.00 3.00', - ' 2.00 4.00', - ' 3.00 5.00', - ' 4.00 6.00', - ' 5.00 7.00', '']) - - with open(location + '/x_set.splat') as f: - self.assertEqual(f.read(), odd_format) - - def add_star(self, path): - """ - Args: - path(str): path to gnu plot data file - - Write a start to file at path if exists. Else record that the file - does not exist, in the obscure counter self.stars_before_write, starts - are written only if a file exists, i.e. "after_write" - """ - if os.path.isfile(path): - with open(path, 'a') as f: - f.write('*') - else: - self.stars_before_write += 1 - - def test_incremental_write(self): - formatter = GNUPlotFormat() - location = self.locations[0] - location2 = self.locations[1] # use 2nd location for reading back in - data = DataSet1D( - name="test_incremental_write", - location=location) - path = location + '/x_set.dat' - - data_copy = DataSet1D(False) - - # empty the data and mark it as unmodified - data.x_set[:] = float('nan') - data.y[:] = float('nan') - data.x_set.modified_range = None - data.y.modified_range = None - - # simulate writing after every value comes in, even within - # one row (x comes first, it's the setpoint) - # we'll add a '*' after each write and check that they're - # in the right places afterward, ie we don't write any given - # row until it's done and we never totally rewrite the file - self.stars_before_write = 0 - for i, (x, y) in enumerate(zip(data_copy.x_set, data_copy.y)): - data.x_set[i] = x - formatter.write(data, data.io, data.location) - formatter.write(data, data.io, location2) - self.add_star(path) - - data.y[i] = y - formatter.write(data, data.io, data.location) - data.x_set.clear_save() - data.y.clear_save() - formatter.write(data, data.io, location2) - self.add_star(path) - - # we wrote to a second location without the stars, so we can read - # back in and make sure that we get the right last_saved_index - # for the amount of data we've read. - reread_data = load_data(location=location2, formatter=formatter, - io=data.io) - self.assertEqual(repr(reread_data.x_set.tolist()), - repr(data.x_set.tolist())) - self.assertEqual(repr(reread_data.y.tolist()), - repr(data.y.tolist())) - self.assertEqual(reread_data.x_set.last_saved_index, i) - self.assertEqual(reread_data.y.last_saved_index, i) - - starred_file = '\n'.join([ - '# x_set\ty', - '# "X"\t"Y"', - '# 5', - '1\t3', - '**2\t4', - '**3\t5', - '**4\t6', - '**5\t7', '*']) - - with open(path) as f: - self.assertEqual(f.read(), starred_file) - self.assertEqual(self.stars_before_write, 1) - - def test_constructor_errors(self): - with self.assertRaises(AttributeError): - # extension must be a string - GNUPlotFormat(extension=5) - - with self.assertRaises(ValueError): - # terminator must be \r, \n, or \r\n - GNUPlotFormat(terminator='\n\r') - - with self.assertRaises(ValueError): - # this is not CSV - separator must be whitespace - GNUPlotFormat(separator=',') - - with self.assertRaises(ValueError): - GNUPlotFormat(comment=' \r\n\t ') - - def test_read_errors(self): - formatter = GNUPlotFormat() - - # non-comment line at the beginning - location = self.locations[0] - data = DataSet(location=location) - os.makedirs(location, exist_ok=True) - with open(location + '/x_set.dat', 'w') as f: - f.write('1\t2\n' + file_1d()) - with LogCapture() as logs: - formatter.read(data) - - self.assertTrue('ValueError' in logs.value, logs.value) - - # same data array in 2 files - location = self.locations[1] - data = DataSet(location=location) - os.makedirs(location, exist_ok=True) - with open(location + '/x_set.dat', 'w') as f: - f.write('\n'.join(['# x_set\ty', - '# "X"\t"Y"', '# 2', '1\t2', '3\t4'])) - with open(location + '/q.dat', 'w') as f: - f.write('\n'.join(['# q\ty', '# "Q"\t"Y"', '# 2', '1\t2', '3\t4'])) - with LogCapture() as logs: - formatter.read(data) - - self.assertTrue('ValueError' in logs.value, logs.value) - - def test_multifile(self): - formatter = GNUPlotFormat() - location = self.locations[1] - data = DataSetCombined(location) - - formatter.write(data, data.io, data.location) - - filex, filexy = files_combined() - - with open(location + '/x_set.dat') as f: - self.assertEqual(f.read(), filex) - with open(location + '/x_set_y_set.dat') as f: - self.assertEqual(f.read(), filexy) - - data2 = DataSet(location=location) - formatter.read(data2) - - for array_id in ('x_set', 'y1', 'y2', 'y_set', 'z1', 'z2'): - self.checkArraysEqual(data2.arrays[array_id], - data.arrays[array_id]) diff --git a/qcodes/tests/legacy/test_generic_formatter.py b/qcodes/tests/legacy/test_generic_formatter.py deleted file mode 100644 index 54cbd43dfd3..00000000000 --- a/qcodes/tests/legacy/test_generic_formatter.py +++ /dev/null @@ -1,53 +0,0 @@ -from unittest import TestCase - -import numpy as np - -import qcodes -import qcodes.measure -from qcodes.data.data_set import load_data -from qcodes.data.gnuplot_format import GNUPlotFormat -from qcodes.data.hdf5_format import HDF5Format, HDF5FormatMetadata -from qcodes.parameters import Parameter -from qcodes.tests.legacy.data_mocks import DataSet2D - - -#%% -class TestFormatters(TestCase): - - def setUp(self): - self.formatters = [GNUPlotFormat, HDF5Format, HDF5FormatMetadata] - self.metadata = {'subdict': {'stringlist': ['P1']}, 'string': 'P1', - 'int': 1, 'list': [1, 2], 'numpyarray': np.array([1])} - - def test_read_write(self): - for f in self.formatters: - print('test formatter %s' % f) - dataset = DataSet2D(name="test_read_write") - dataset.formatter = f() - - dataset.add_metadata(self.metadata) - dataset.write(write_metadata=True) - - dataset2 = load_data(dataset.location, formatter=f()) - self.assertEqual(list(dataset.arrays.keys()), - list(dataset2.arrays.keys())) - # strings should be read and written identically - self.assertEqual(dataset.metadata['string'], - dataset2.metadata['string']) - - -class TestNoSorting(TestCase): - """ - (WilliamHPNielsen): I'm not too sure where this test belongs... It tests - that parameters with non-sortable keys can be saved using the gnuplot - formatter, so I guess it goes here. - """ - - param = Parameter( - name="mixed_val_mapping_param", - get_cmd=lambda: np.random.randint(1, 3), - val_mapping={1: 1, "2": 2}, - ) - - def test_can_measure(self): - qcodes.measure.Measure(self.param).run(name="test_no_sorting") diff --git a/qcodes/tests/legacy/test_hdf5formatter.py b/qcodes/tests/legacy/test_hdf5formatter.py deleted file mode 100644 index aa2895a409e..00000000000 --- a/qcodes/tests/legacy/test_hdf5formatter.py +++ /dev/null @@ -1,341 +0,0 @@ -import os -from shutil import copy -from unittest import TestCase - -import h5py -import numpy as np - -import qcodes.data -from qcodes.data.data_array import DataArray -from qcodes.data.data_set import DataSet, load_data, new_data -from qcodes.data.hdf5_format import HDF5Format, str_to_bool -from qcodes.data.location import FormatLocation -from qcodes.loops import Loop -from qcodes.station import Station -from qcodes.tests.common import compare_dictionaries -from qcodes.tests.instrument_mocks import MockParabola - -from .data_mocks import DataSet1D, DataSet2D - - -class TestHDF5_Format(TestCase): - def setUp(self): - self.io = DataSet.default_io - self.formatter = HDF5Format() - # Set up the location provider to always store test data in - # "qc.tests.unittest_data - cur_fp = os.path.dirname(__file__) - base_fp = os.path.abspath(os.path.join(cur_fp, '../unittest_data')) - self.loc_provider = FormatLocation( - fmt=base_fp+'/{date}/#{counter}_{name}_{time}') - DataSet.location_provider = self.loc_provider - - def checkArraysEqual(self, a, b): - """ - Checks if arrays are equal - """ - # Modified from GNUplot would be better to have this in some module - self.checkArrayAttrs(a, b) - np.testing.assert_array_equal(a, b) - if len(a.set_arrays) > 1: - for i, set_arr in enumerate(a.set_arrays): - np.testing.assert_array_equal(set_arr, b.set_arrays[i]) - else: - np.testing.assert_array_equal(a.set_arrays, b.set_arrays) - - for sa, sb in zip(a.set_arrays, b.set_arrays): - self.checkArrayAttrs(sa, sb) - - def checkArrayAttrs(self, a, b): - self.assertEqual(a.tolist(), b.tolist()) - self.assertEqual(a.label, b.label) - self.assertEqual(a.array_id, b.array_id) - - def test_full_write_read_1D(self): - """ - Test writing and reading a file back in - """ - # location = self.locations[0] - data = DataSet1D(name='test1D_full_write', - location=self.loc_provider) - # print('Data location:', os.path.abspath(data.location)) - self.formatter.write(data) - # Used because the formatter has no nice find file method - - # Test reading the same file through the DataSet.read - data2 = DataSet(location=data.location, formatter=self.formatter) - data2.read() - self.checkArraysEqual(data2.x_set, data.x_set) - self.checkArraysEqual(data2.y, data.y) - self.formatter.close_file(data) - self.formatter.close_file(data2) - - def test_full_write_read_2D(self): - """ - Test writing and reading a file back in - """ - data = DataSet2D(location=self.loc_provider, name='test2D') - self.formatter.write(data) - # Test reading the same file through the DataSet.read - data2 = DataSet(location=data.location, formatter=self.formatter) - data2.read() - self.checkArraysEqual(data2.x_set, data.x_set) - self.checkArraysEqual(data2.y_set, data.y_set) - self.checkArraysEqual(data2.z, data.z) - - self.formatter.close_file(data) - self.formatter.close_file(data2) - - def test_incremental_write(self): - data = DataSet1D(location=self.loc_provider, name='test_incremental') - location = data.location - data_copy = DataSet1D(False) - - # # empty the data and mark it as unmodified - data.x_set[:] = float('nan') - data.y[:] = float('nan') - data.x_set.modified_range = None - data.y.modified_range = None - - # simulate writing after every value comes in, even within - # one row (x comes first, it's the setpoint) - for i, (x, y) in enumerate(zip(data_copy.x_set, data_copy.y)): - data.x_set[i] = x - self.formatter.write(data) - data.y[i] = y - self.formatter.write(data) - data2 = DataSet(location=location, formatter=self.formatter) - data2.read() - self.checkArraysEqual(data2.arrays['x_set'], data_copy.arrays['x_set']) - self.checkArraysEqual(data2.arrays['y'], data_copy.arrays['y']) - - self.formatter.close_file(data) - self.formatter.close_file(data2) - - def test_metadata_write_read(self): - """ - Test is based on the snapshot of the 1D dataset. - Having a more complex snapshot in the metadata would be a better test. - """ - data = DataSet1D(location=self.loc_provider, name='test_metadata') - data.snapshot() # gets the snapshot, not added upon init - self.formatter.write(data) # write_metadata is included in write - data2 = DataSet(location=data.location, formatter=self.formatter) - data2.read() - self.formatter.close_file(data) - self.formatter.close_file(data2) - metadata_equal, err_msg = compare_dictionaries( - data.metadata, data2.metadata, - 'original_metadata', 'loaded_metadata') - self.assertTrue(metadata_equal, msg='\n'+err_msg) - - def test_loop_writing(self): - # pass - station = Station() - MockPar = MockParabola(name='Loop_writing_test') - station.add_component(MockPar) - # # added to station to test snapshot at a later stage - loop = Loop(MockPar.x[-100:100:20]).each(MockPar.skewed_parabola) - data1 = loop.run(name='MockLoop_hdf5_test', - formatter=self.formatter) - data2 = DataSet(location=data1.location, formatter=self.formatter) - data2.read() - for key in data2.arrays.keys(): - self.checkArraysEqual(data2.arrays[key], data1.arrays[key]) - - metadata_equal, err_msg = compare_dictionaries( - data1.metadata, data2.metadata, - 'original_metadata', 'loaded_metadata') - self.assertTrue(metadata_equal, msg='\n'+err_msg) - self.formatter.close_file(data1) - self.formatter.close_file(data2) - MockPar.close() - - def test_partial_dataset(self): - data = qcodes.data.data_set.new_data(formatter=self.formatter) - data_array = qcodes.data.data_array.DataArray(array_id='test_partial_dataset', shape=(10,)) - data_array.init_data() - data_array.ndarray[0] = 1 - data.add_array(data_array) - data.write() - data.read() - - def test_loop_writing_2D(self): - # pass - station = Station() - MockPar = MockParabola(name='Loop_writing_test_2D') - station.add_component(MockPar) - loop = Loop(MockPar.x[-100:100:20]).loop( - MockPar.y[-50:50:10]).each(MockPar.skewed_parabola) - data1 = loop.run(name='MockLoop_hdf5_test', - formatter=self.formatter) - data2 = DataSet(location=data1.location, formatter=self.formatter) - data2.read() - for key in data2.arrays.keys(): - self.checkArraysEqual(data2.arrays[key], data1.arrays[key]) - - metadata_equal, err_msg = compare_dictionaries( - data1.metadata, data2.metadata, - 'original_metadata', 'loaded_metadata') - self.assertTrue(metadata_equal, msg='\n'+err_msg) - self.formatter.close_file(data1) - self.formatter.close_file(data2) - MockPar.close() - - def test_closed_file(self): - data = DataSet1D(location=self.loc_provider, name='test_closed') - # closing before file is written should not raise error - self.formatter.close_file(data) - self.formatter.write(data) - # Used because the formatter has no nice find file method - self.formatter.close_file(data) - # Closing file twice should not raise an error - self.formatter.close_file(data) - - def test_reading_into_existing_data_array(self): - data = DataSet1D(location=self.loc_provider, - name='test_read_existing') - # closing before file is written should not raise error - self.formatter.write(data) - - data2 = DataSet(location=data.location, - formatter=self.formatter) - d_array = DataArray(name='dummy', - array_id='x_set', # existing array id in data - label='bla', unit='a.u.', is_setpoint=False, - set_arrays=(), preset_data=np.zeros(5)) - data2.add_array(d_array) - # test if d_array refers to same as array x_set in dataset - self.assertTrue(d_array is data2.arrays['x_set']) - data2.read() - # test if reading did not overwrite dataarray - self.assertTrue(d_array is data2.arrays['x_set']) - # Testing if data was correctly updated into dataset - self.checkArraysEqual(data2.arrays['x_set'], data.arrays['x_set']) - self.checkArraysEqual(data2.arrays['y'], data.arrays['y']) - self.formatter.close_file(data) - self.formatter.close_file(data2) - - def test_dataset_closing(self): - data = DataSet1D(location=self.loc_provider, name='test_closing') - self.formatter.write(data, flush=False) - fp = data._h5_base_group.filename - self.formatter.close_file(data) - fp3 = fp[:-5]+'_3.hdf5' - copy(fp, fp3) - # Should not raise an error because the file was properly closed - F3 = h5py.File(fp3, mode='a') - - def test_dataset_flush_after_write(self): - data = DataSet1D(name='test_flush', location=self.loc_provider) - self.formatter.write(data, flush=True) - fp = data._h5_base_group.filename - fp2 = fp[:-5]+'_2.hdf5' - # the file cannot be copied unless the ref is deleted first - del data._h5_base_group - copy(fp, fp2) - # Opening this copy should not raise an error - F2 = h5py.File(fp2, mode='a') - - def test_dataset_finalize_closes_file(self): - data = DataSet1D(name='test_finalize', location=self.loc_provider) - # closing before file is written should not raise error - self.formatter.write(data, flush=False) - fp = data._h5_base_group.filename - - # Attaching the formatter like this should not be neccesary - data.formatter = self.formatter - data.finalize() - # the file cannot be copied unless the ref is deleted first - del data._h5_base_group - fp3 = fp[:-5]+'_4.hdf5' - copy(fp, fp3) - # Should now not raise an error because the file was properly closed - F3 = h5py.File(fp3, mode='a') - - def test_double_closing_gives_warning(self): - data = DataSet1D(name='test_double_close', - location=self.loc_provider) - # closing before file is written should not raise error - self.formatter.write(data, flush=False) - self.formatter.close_file(data) - with self.assertLogs(): - # Test that this raises a logging message - self.formatter.close_file(data) - - def test_dataset_with_missing_attrs(self): - data1 = new_data(formatter=self.formatter, location=self.loc_provider, - name='test_missing_attr') - arr = DataArray(array_id='arr', preset_data=np.linspace(0, 10, 21)) - data1.add_array(arr) - data1.write() - # data2 = DataSet(location=data1.location, formatter=self.formatter) - # data2.read() - data2 = load_data(location=data1.location, - formatter=self.formatter) - # cannot use the check arrays equal as I expect the attributes - # to not be equal - np.testing.assert_array_equal(data2.arrays['arr'], data1.arrays['arr']) - - def test_read_writing_dicts_withlists_to_hdf5(self): - some_dict = {} - some_dict['list_of_ints'] = list(np.arange(5)) - some_dict['list_of_floats'] = list(np.arange(5.1)) - some_dict['list_of_mixed_type'] = list([1, '1']) - fp = self.loc_provider( - io=DataSet.default_io, - record={'name': 'test_dict_writing'})+'.hdf5' - F = h5py.File(fp, mode='a') - - self.formatter.write_dict_to_hdf5(some_dict, F) - new_dict = {} - self.formatter.read_dict_from_hdf5(new_dict, F) - dicts_equal, err_msg = compare_dictionaries( - some_dict, new_dict, - 'written_dict', 'loaded_dict') - self.assertTrue(dicts_equal, msg='\n'+err_msg) - - def test_str_to_bool(self): - self.assertEqual(str_to_bool('True'), True) - self.assertEqual(str_to_bool('False'), False) - with self.assertRaises(ValueError): - str_to_bool('flse') - - def test_writing_unsupported_types_to_hdf5(self): - """ - Tests writing of - - unsuported list type attr - - nested dataset - """ - some_dict = {} - some_dict['list_of_ints'] = list(np.arange(5)) - some_dict['list_of_floats'] = list(np.arange(5.1)) - some_dict['weird_dict'] = {'a': 5} - data1 = new_data(formatter=self.formatter, location=self.loc_provider, - name='test_missing_attr') - some_dict['nested_dataset'] = data1 - - some_dict['list_of_dataset'] = [data1, data1] - - fp = self.loc_provider( - io=DataSet.default_io, - record={'name': 'test_dict_writing'})+'.hdf5' - F = h5py.File(fp, mode='a') - self.formatter.write_dict_to_hdf5(some_dict, F) - new_dict = {} - self.formatter.read_dict_from_hdf5(new_dict, F) - # objects are not identical but the string representation should be - self.assertEqual(str(some_dict['nested_dataset']), - new_dict['nested_dataset']) - self.assertEqual(str(some_dict['list_of_dataset']), - new_dict['list_of_dataset']) - - F['weird_dict'].attrs['list_type'] = 'unsuported_list_type' - with self.assertRaises(NotImplementedError): - self.formatter.read_dict_from_hdf5(new_dict, F) - - def test_writing_metadata(self): - # test for issue reported in 442 - data = DataSet2D(location=self.loc_provider, name='MetaDataTest') - data.metadata = {'a': ['hi', 'there']} - self.formatter.write(data, write_metadata=True) diff --git a/qcodes/tests/legacy/test_location_provider.py b/qcodes/tests/legacy/test_location_provider.py deleted file mode 100644 index 0fce07e206b..00000000000 --- a/qcodes/tests/legacy/test_location_provider.py +++ /dev/null @@ -1,118 +0,0 @@ -from datetime import datetime -from unittest import TestCase - -from qcodes.data.location import FormatLocation, SafeFormatter - -from .data_mocks import MatchIO - - -class TestSafeFormatter(TestCase): - def test_normal_formatting(self): - formatter = SafeFormatter() - self.assertEqual(formatter.format('{}{{{}}}', 1, 2), '1{2}') - self.assertEqual(formatter.format('{apples}{{{oranges}}}', - apples='red', oranges='unicorn'), - 'red{unicorn}') - - def test_missing(self): - formatter = SafeFormatter() - self.assertEqual(formatter.format('{}'), '{0}') - self.assertEqual(formatter.format('{cheese}', fruit='pears'), - '{cheese}') - - -def _default(time: datetime, formatter: FormatLocation, counter:str, name: str): - date = time.strftime(formatter.fmt_date) - mytime = time.strftime(formatter.fmt_time) - fmted = formatter.formatter.format(formatter.default_fmt, - date=date, - counter=counter, - time=mytime, - name=name) - return fmted - - -class TestFormatLocation(TestCase): - def test_default(self): - lp = FormatLocation() - fmt = '%Y-%m-%d/%H-%M-%S' - - name = "name" - self.assertEqual(lp(MatchIO([]), {'name': name}), - _default(datetime.now(), lp, "001", name)) - - # counter starts at +1 using MatchIo undocumented magic argument - start_magic_value = 5 - self.assertEqual( - lp(MatchIO(["", f"{start_magic_value:03d}"]), {"name": name}), - _default(datetime.now(), lp, f"{start_magic_value+1:03d}", name), - ) - - def test_fmt_subparts(self): - lp = FormatLocation(fmt='{date}/{time}', fmt_date='%d-%b-%Y', fmt_time='%I-%M%p', - fmt_counter='##{:.1f}~') - fmt = '%d-%b-%Y/%I-%M%p' - - self.assertEqual(lp(MatchIO([])), - datetime.now().strftime(fmt)) - self.assertEqual(lp(MatchIO([]), {'name': 'who?'}), - datetime.now().strftime(fmt) + '_who?') - - self.assertEqual(lp(MatchIO([''])), - datetime.now().strftime(fmt) + '_##2.0~') - self.assertEqual(lp(MatchIO(['', '9'])), - datetime.now().strftime(fmt) + '_##10.0~') - self.assertEqual(lp(MatchIO(['', '12345']), {'name': 'you!'}), - datetime.now().strftime(fmt) + '_you!_##12346.0~') - - def test_record_call(self): - lp = FormatLocation(fmt='{date}/#{counter}_{time}_{name}_{label}') - - # counter starts at 1 if it's a regular part of the format string - expected = datetime.now().strftime('%Y-%m-%d/#001_%H-%M-%S_Joe_fruit') - self.assertEqual(lp(MatchIO([]), {'name': 'Joe', 'label': 'fruit'}), - expected) - - expected = datetime.now().strftime('%Y-%m-%d/#1000_%H-%M-%S_Ga_As') - self.assertEqual(lp(MatchIO(['999']), {'name': 'Ga', 'label': 'As'}), - expected) - - # missing label - expected = datetime.now().strftime( - '%Y-%m-%d/#001_%H-%M-%S_Fred_{label}') - self.assertEqual(lp(MatchIO([]), {'name': 'Fred'}), expected) - - def test_record_override(self): - # this one needs 'c' filled in at call time - lp = FormatLocation(fmt='{a}_{b}_{c}', record={'a': 'A', 'b': 'B'}) - io = MatchIO([]) - - self.assertEqual(lp(io, {'c': 'C'}), 'A_B_C') - self.assertEqual(lp(io, {'a': 'aa', 'c': 'cc'}), 'aa_B_cc') - # extra keys just get discarded - self.assertEqual(lp(io, {'c': 'K', 'd': 'D'}), 'A_B_K') - - self.assertEqual(lp(MatchIO([])), 'A_B_{c}') - self.assertEqual(lp(MatchIO([]), {'a': 'AA'}), 'AA_B_{c}') - - # this one has defaults for everything, so nothing is needed at - # call time, but things can still be overridden - lp = FormatLocation(fmt='{d}_{e}', record={'d': 'D', 'e': 'E'}) - - self.assertEqual(lp(MatchIO([])), 'D_E') - self.assertEqual(lp(MatchIO([]), {'d': 'T'}), 'T_E') - - def test_errors(self): - io = MatchIO([]) - - # gives the wrong integer (extra 0 at the end!) - with self.assertRaises(ValueError): - FormatLocation(fmt_counter='{:03}0') - - # you're not allowed to give a counter. Whatcha trying to do anyway? - # I'm tempted to say the same about time and date, but will leave them - # overridable for now. - with self.assertRaises(KeyError): - FormatLocation()(io, {'counter': 100}) - with self.assertRaises(KeyError): - FormatLocation(record={'counter': 100})(io) diff --git a/qcodes/tests/legacy/test_loop.py b/qcodes/tests/legacy/test_loop.py deleted file mode 100644 index 2f876910a5f..00000000000 --- a/qcodes/tests/legacy/test_loop.py +++ /dev/null @@ -1,512 +0,0 @@ -import os -from datetime import datetime -from unittest import TestCase -from unittest.mock import patch - -import numpy as np - -from qcodes.actions import BreakIf, Task, Wait, _QcodesBreak -from qcodes.data.data_array import DataArray -from qcodes.logger.logger import LogCapture -from qcodes.loops import Loop -from qcodes.parameters import MultiParameter, Parameter -from qcodes.station import Station -from qcodes.validators import Numbers - -from ..instrument_mocks import DummyInstrument, MultiGetter - - -class NanReturningParameter(MultiParameter): - - def __init__(self, name, instrument, names=('first', 'second'), - shapes=((), ())): - - super().__init__(name=name, names=names, shapes=shapes, - instrument=instrument) - - def get_raw(self): # this results in a nan-filled DataArray - return (13,) - - -class TestLoop(TestCase): - @classmethod - def setUpClass(cls): - cls.p1 = Parameter('p1', get_cmd=None, set_cmd=None, vals=Numbers(-10, 10)) - cls.p2 = Parameter('p2', get_cmd=None, set_cmd=None, vals=Numbers(-10, 10)) - cls.p3 = Parameter('p3', get_cmd=None, set_cmd=None, vals=Numbers(-10, 10)) - cls.instr = DummyInstrument('dummy_bunny') - cls.p4_crazy = NanReturningParameter('p4_crazy', instrument=cls.instr) - Station() - - @classmethod - def tearDownClass(cls): - cls.instr.close() - - def test_nesting(self): - loop = Loop(self.p1[1:3:1], 0.001).loop( - self.p2[3:5:1], 0.001).loop( - self.p3[5:7:1], 0.001) - active_loop = loop.each(self.p1, self.p2, self.p3) - data = active_loop.run_temp() - - self.assertEqual(data.p1_set.tolist(), [1, 2]) - self.assertEqual(data.p2_set.tolist(), [[3, 4]] * 2) - self.assertEqual(data.p3_set.tolist(), [[[5, 6]] * 2] * 2) - - self.assertEqual(data.p1.tolist(), [[[1, 1]] * 2, [[2, 2]] * 2]) - self.assertEqual(data.p2.tolist(), [[[3, 3], [4, 4]]] * 2) - self.assertEqual(data.p3.tolist(), [[[5, 6]] * 2] * 2) - - def test_nesting_2(self): - loop = Loop(self.p1[1:3:1]).each( - self.p1, - Loop(self.p2[3:5:1]).each( - self.p1, - self.p2, - Loop(self.p3[5:7:1]).each( - self.p1, - self.p2, - self.p3))) - - data = loop.run_temp() - keys = set(data.arrays.keys()) - - self.assertEqual(data.p1_set.tolist(), [1, 2]) - self.assertEqual(data.p2_set.tolist(), [[3, 4]] * 2) - self.assertEqual(data.p3_set.tolist(), [[[5, 6]] * 2] * 2) - - self.assertEqual(data.p1_0.tolist(), [1, 2]) - - # TODO(alexcjohnson): these names are extra confusing... - # perhaps we should say something like always include *all* indices - # unless you can get rid of them all (ie that param only shows up - # once, but separately for set and measured) - self.assertEqual(data.p1_1_0.tolist(), [[1, 1], [2, 2]]) - self.assertEqual(data.p2_1.tolist(), [[3, 4]] * 2) - - self.assertEqual(data.p1_1_2_0.tolist(), [[[1, 1]] * 2, [[2, 2]] * 2]) - self.assertEqual(data.p2_2_1.tolist(), [[[3, 3], [4, 4]]] * 2) - self.assertEqual(data.p3.tolist(), [[[5, 6]] * 2] * 2) - - # make sure rerunning this doesn't cause any problems - data2 = loop.run_temp() - keys2 = set(data.arrays.keys()) - self.assertEqual(keys, keys2) - - def test_repr(self): - loop2 = Loop(self.p2[3:5:1], 0.001).each(self.p2) - loop = Loop(self.p1[1:3:1], 0.001).each(self.p3, - self.p2, - loop2, - self.p1) - active_loop = loop - data = active_loop.run_temp() - expected = ('DataSet:\n' - ' location = False\n' - ' | | | \n' - ' Setpoint | p1_set | p1 | (2,)\n' - ' Measured | p3 | p3 | (2,)\n' - ' Measured | p2_1 | p2 | (2,)\n' - ' Setpoint | p2_set | p2 | (2, 2)\n' - ' Measured | p2_2_0 | p2 | (2, 2)\n' - ' Measured | p1 | p1 | (2,)') - self.assertEqual(data.__repr__(), expected) - - def test_measurement_with_many_nans(self): - loop = Loop(self.p1.sweep(0, 1, num=10), - delay=0.05).each(self.p4_crazy) - ds = loop.get_data_set(name="test_measurement_with_many_nans") - loop.run() - - # assert that both the snapshot and the datafile are there - self.assertEqual(len(os.listdir(ds.location)), 2) - - def test_tasks_callable_arguments(self): - data = Loop(self.p1[1:3:1], 0.01).each( - Task(self.p2.set, self.p1), - Task(self.p3.set, self.p1.get), - self.p2, self.p3).run_temp() - - self.assertEqual(data.p2.tolist(), [1, 2]) - self.assertEqual(data.p3.tolist(), [1, 2]) - - def test_func(*args, **kwargs): - self.assertEqual(args, (1, 2)) - self.assertEqual(kwargs, {'a_kwarg': 4}) - - data = Loop(self.p1[1:2:1], 0.01).each( - Task(self.p2.set, lambda: self.p1.get() * 2), - Task(test_func, self.p1, lambda: self.p1.get() * 2, - a_kwarg=lambda: self.p1.get() * 4), - self.p2, self.p3).run_temp() - - self.assertEqual(data.p2.tolist(), [2]) - - @patch('time.sleep') - def test_delay0(self, sleep_mock): - self.p2.set(3) - - loop = Loop(self.p1[1:3:1]).each(self.p2) - - self.assertEqual(loop.delay, 0) - - data = loop.run_temp() - self.assertEqual(data.p1_set.tolist(), [1, 2]) - self.assertEqual(data.p2.tolist(), [3, 3]) - - self.assertEqual(sleep_mock.call_count, 0) - - def test_bad_delay(self): - for val, err in [(-1, ValueError), (-0.1, ValueError), - (None, TypeError), ('forever', TypeError)]: - with self.assertRaises(err): - Loop(self.p1[1:3:1], val) - - with self.assertRaises(err): - Wait(val) - - def test_composite_params(self): - # this one has names and shapes - mg = MultiGetter(one=1, onetwo=(1, 2)) - self.assertTrue(hasattr(mg, 'names')) - self.assertTrue(hasattr(mg, 'shapes')) - self.assertEqual(mg.name, 'multigetter') - self.assertFalse(hasattr(mg, 'shape')) - loop = Loop(self.p1[1:3:1], 0.001).each(mg) - data = loop.run_temp() - - self.assertEqual(data.p1_set.tolist(), [1, 2]) - self.assertEqual(data.one.tolist(), [1, 1]) - self.assertEqual(data.onetwo.tolist(), [[1, 2]] * 2) - self.assertEqual(data.index0_set.tolist(), [[0, 1]] * 2) - - # give it setpoints, names, and labels - mg.setpoints = (None, ((10, 11),)) - sp_name = 'highest' - mg.setpoint_names = (None, (sp_name,)) - sp_label = 'does it go to 11?' - mg.setpoint_labels = (None, (sp_label,)) - - data = loop.run_temp() - - self.assertEqual(data.highest_set.tolist(), [[10, 11]] * 2) - self.assertEqual(data.highest_set.label, sp_label) - - # setpoints as DataArray - name and label here override - # setpoint_names and setpoint_labels attributes - new_sp_name = 'bgn' - new_sp_label = 'boogie nights!' - sp_dataarray = DataArray(preset_data=[6, 7], name=new_sp_name, - label=new_sp_label) - mg.setpoints = (None, (sp_dataarray,)) - - data = loop.run_temp() - self.assertEqual(data.bgn_set.tolist(), [[6, 7]] * 2) - self.assertEqual(data.bgn_set.label, new_sp_label) - - # muck things up and test for errors - mg.setpoints = (None, ((1, 2, 3),)) - with self.assertRaises(ValueError): - loop.run_temp() - - mg.setpoints = (None, ((1, 2), (3, 4))) - with self.assertRaises(ValueError): - loop.run_temp() - - del mg.setpoints, mg.setpoint_names, mg.setpoint_labels - mg.names = mg.names + ('extra',) - with self.assertRaises(ValueError): - loop.run_temp() - - del mg.names - with self.assertRaises(ValueError): - loop.run_temp() - - # this one still has names and shapes - mg = MultiGetter(arr=(4, 5, 6)) - self.assertTrue(hasattr(mg, 'name')) - self.assertFalse(hasattr(mg, 'shape')) - self.assertTrue(hasattr(mg, 'names')) - self.assertTrue(hasattr(mg, 'shapes')) - loop = Loop(self.p1[1:3:1], 0.001).each(mg) - data = loop.run_temp() - - self.assertEqual(data.p1_set.tolist(), [1, 2]) - self.assertEqual(data.arr.tolist(), [[4, 5, 6]] * 2) - self.assertEqual(data.index0_set.tolist(), [[0, 1, 2]] * 2) - - mg = MultiGetter(arr2d=((21, 22), (23, 24))) - loop = Loop(self.p1[1:3:1], 0.001).each(mg) - data = loop.run_temp() - - self.assertEqual(data.p1_set.tolist(), [1, 2]) - self.assertEqual(data.arr2d.tolist(), [[[21, 22], [23, 24]]] * 2) - self.assertEqual(data.index0_set.tolist(), [[0, 1]] * 2) - self.assertEqual(data.index1_set.tolist(), [[[0, 1]] * 2] * 2) - - def test_bad_actors(self): - def f(): - return 42 - - class NoName: - def get(self): - return 42 - - class HasName: - def get(self): - return 42 - - name = 'IHazName!' - - class HasNames: - def get(self): - return 42 - - names = 'Namezz' - - # first two minimal working gettables - Loop(self.p1[1:3:1]).each(HasName()) - Loop(self.p1[1:3:1]).each(HasNames()) - - for bad_action in (f, 42, NoName()): - with self.assertRaises(TypeError): - # include a good action too, just to make sure we look - # at the whole list - Loop(self.p1[1:3:1]).each(self.p1, bad_action) - - with self.assertRaises(ValueError): - # invalid sweep values - Loop(self.p1[-20:20:1]).each(self.p1) - - def test_very_short_delay(self): - with LogCapture() as logs: - Loop(self.p1[1:3:1], 1e-9).each(self.p1).run_temp() - - self.assertEqual(logs.value.count('negative delay'), 2, logs.value) - - def test_zero_delay(self): - with LogCapture() as logs: - Loop(self.p1[1:3:1]).each(self.p1).run_temp() - - self.assertEqual(logs.value.count('negative delay'), 0, logs.value) - - def test_breakif(self): - nan = float('nan') - loop = Loop(self.p1[1:6:1]) - data = loop.each(self.p1, BreakIf(lambda: self.p1.get() >= 3)).run_temp() - self.assertEqual(repr(data.p1.tolist()), - repr([1., 2., 3., nan, nan])) - - data = loop.each(BreakIf(lambda: self.p1.get_latest.get() >= 3), self.p1).run_temp() - self.assertEqual(repr(data.p1.tolist()), - repr([1., 2., nan, nan, nan])) - - with self.assertRaises(TypeError): - BreakIf(True) - with self.assertRaises(TypeError): - BreakIf(self.p1.set) - - def test_then_construction(self): - loop = Loop(self.p1[1:6:1]) - task1 = Task(self.p1.set, 2) - task2 = Wait(0.02) - loop2 = loop.then(task1) - loop3 = loop2.then(task2, task1) - loop4 = loop3.then(task2, overwrite=True) - loop5 = loop4.each(self.p1, BreakIf(lambda: self.p1.get() >= 3)) - loop6 = loop5.then(task1) - loop7 = loop6.then(task1, overwrite=True) - - # original loop is untouched, same as .each and .loop - self.assertEqual(loop.then_actions, ()) - - # but loop2 has the task we asked for - self.assertEqual(loop2.then_actions, (task1,)) - - # loop3 gets the other tasks appended - self.assertEqual(loop3.then_actions, (task1, task2, task1)) - - # loop4 gets only the new one - self.assertEqual(loop4.then_actions, (task2,)) - - # tasks survive .each - self.assertEqual(loop5.then_actions, (task2,)) - - # and ActiveLoop.then works the same way as Loop.then - self.assertEqual(loop6.then_actions, (task2, task1)) - self.assertEqual(loop7.then_actions, (task1,)) - - # .then rejects Loops and others that are valid loop actions - for action in (loop2, loop7, BreakIf(lambda: self.p1() >= 3), self.p1, - True, 42): - with self.assertRaises(TypeError): - loop.then(action) - - def check_snap_ts(self, container, key, ts_set): - self.assertIn(container[key], ts_set) - del container[key] - - def test_then_action(self): - self.maxDiff = None - nan = float('nan') - self.p1.set(5) - f_calls, g_calls = [], [] - - def f(): - f_calls.append(1) - - def g(): - g_calls.append(1) - - breaker = BreakIf(lambda: self.p1() >= 3) - ts1 = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - # evaluate param snapshots now since later value will change - p1snap = self.p1.snapshot() - self.p2.set(2) - p2snap = self.p2.snapshot() - self.p3.set(3) - p3snap = self.p3.snapshot() - data = Loop(self.p1[1:6:1]).each( - self.p1, breaker - ).then( - Task(self.p1.set, 2), Wait(0.01), Task(f) - ).run_temp() - ts2 = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - - self.assertEqual(repr(data.p1.tolist()), - repr([1., 2., 3., nan, nan])) - self.assertEqual(self.p1.get(), 2) - self.assertEqual(len(f_calls), 1) - - # this loop makes use of all the features, so use it to test - # DataSet metadata - loopmeta = data.metadata['loop'] - # assuming the whole loop takes < 1 sec, all timestamps - # should each be the same as one of the bounding times - self.check_snap_ts(loopmeta, 'ts_start', (ts1, ts2)) - self.check_snap_ts(loopmeta, 'ts_end', (ts1, ts2)) - self.check_snap_ts(loopmeta['sweep_values']['parameter'], - 'ts', (ts1, ts2)) - self.check_snap_ts(loopmeta['actions'][0], 'ts', (ts1, ts2)) - del p1snap['ts'], p2snap['ts'], p3snap['ts'] - - self.assertEqual(data.metadata, { - 'station': { - 'instruments': {}, - 'parameters': {}, - 'components': {}, - 'config': None, - }, - 'loop': { - 'use_threads': False, - '__class__': 'qcodes.loops.ActiveLoop', - 'sweep_values': { - 'parameter': p1snap, - 'values': [{'first': 1, 'last': 5, 'num': 5, - 'type': 'linear'}] - }, - 'delay': 0, - 'actions': [p1snap, breaker.snapshot()], - 'then_actions': [ - {'type': 'Task', 'func': repr(self.p1.set)}, - {'type': 'Wait', 'delay': 0.01}, - {'type': 'Task', 'func': repr(f)} - ] - } - }) - - # now test a nested loop with .then inside and outside - f_calls[:] = [] - - Loop(self.p1[1:3:1]).each( - Loop(self.p2[1:3:1]).each(self.p2).then(Task(g)) - ).then(Task(f)).run_temp() - - self.assertEqual(len(f_calls), 1) - self.assertEqual(len(g_calls), 2) - - # Loop.loop nesting always just makes the .then actions run after - # the outer loop - f_calls[:] = [] - Loop(self.p1[1:3:1]).then(Task(f)).loop(self.p2[1:3:1]).each( - self.p1 - ).run_temp() - self.assertEqual(len(f_calls), 1) - - f_calls[:] = [] - Loop(self.p1[1:3:1]).loop(self.p2[1:3:1]).then(Task(f)).each( - self.p1 - ).run_temp() - self.assertEqual(len(f_calls), 1) - - f_calls[:] = [] - Loop(self.p1[1:3:1]).loop(self.p2[1:3:1]).each( - self.p1 - ).then(Task(f)).run_temp() - self.assertEqual(len(f_calls), 1) - - -class AbortingGetter(Parameter): - """ - A manual parameter that can only be measured n times - before it aborts the loop that's measuring it. - """ - def __init__(self, *args, count=1, msg=None, **kwargs): - self._count = self._initial_count = count - # also need a _signal_queue, but that has to be added later - super().__init__(*args, **kwargs) - - def get_raw(self): - self._count -= 1 - if self._count <= 0: - raise _QcodesBreak - return self.cache.raw_value - - def reset(self): - self._count = self._initial_count - - -class Test_halt(TestCase): - def test_halt(self): - abort_after = 3 - self.res = list(np.arange(0, abort_after-1, 1.)) - [self.res.append(float('nan')) for i in range(0, abort_after-1)] - - p1 = AbortingGetter('p1', count=abort_after, vals=Numbers(-10, 10), set_cmd=None) - loop = Loop(p1.sweep(0, abort_after, 1), 0.005).each(p1) - # we want to test what's in data, so get it ahead of time - # because loop.run will not return. - data = loop.get_data_set(location=False) - - loop.run(quiet=True) - self.assertEqual(repr(data.p1.tolist()), repr(self.res)) - - -class TestMetaData(TestCase): - def test_basic(self): - p1 = AbortingGetter('p1', count=2, vals=Numbers(-10, 10), set_cmd=None) - sv = p1[1:3:1] - loop = Loop(sv) - - # not sure why you'd do it, but you *can* snapshot a Loop - expected = { - '__class__': 'qcodes.loops.Loop', - 'sweep_values': sv.snapshot(), - 'delay': 0, - 'then_actions': [] - } - self.assertEqual(loop.snapshot(), expected) - loop = loop.then(Task(p1.set, 0), Wait(0.123)) - expected['then_actions'] = [ - {'type': 'Task', 'func': repr(p1.set)}, - {'type': 'Wait', 'delay': 0.123} - ] - - # then test snapshot on an ActiveLoop - breaker = BreakIf(lambda: p1.get_latest() > 3) - self.assertEqual(breaker.snapshot()['type'], 'BreakIf') - loop = loop.each(p1, breaker) - expected['__class__'] = 'qcodes.loops.ActiveLoop' - expected['actions'] = [p1.snapshot(), breaker.snapshot()] - - self.assertEqual(loop.snapshot(), expected) diff --git a/qcodes/tests/legacy/test_measure.py b/qcodes/tests/legacy/test_measure.py deleted file mode 100644 index de4868a5422..00000000000 --- a/qcodes/tests/legacy/test_measure.py +++ /dev/null @@ -1,90 +0,0 @@ -from datetime import datetime -from unittest import TestCase - -import numpy as np -from numpy.testing import assert_array_equal - -from qcodes.data.location import FormatLocation -from qcodes.measure import Measure -from qcodes.parameters import Parameter - -from ..instrument_mocks import MultiGetter, MultiSetPointParam - - -class TestMeasure(TestCase): - def setUp(self): - self.p1 = Parameter('P1', initial_value=1, get_cmd=None, set_cmd=None) - - def test_simple_scalar(self): - data = Measure(self.p1).run_temp() - - self.assertEqual(data.single_set.tolist(), [0]) - self.assertEqual(data.P1.tolist(), [1]) - self.assertEqual(len(data.arrays), 2, data.arrays) - - self.assertNotIn('loop', data.metadata) - - meta = data.metadata['measurement'] - self.assertEqual(meta['__class__'], 'qcodes.measure.Measure') - self.assertEqual(len(meta['actions']), 1) - self.assertFalse(meta['use_threads']) - - ts_start = datetime.strptime(meta['ts_start'], '%Y-%m-%d %H:%M:%S') - ts_end = datetime.strptime(meta['ts_end'], '%Y-%m-%d %H:%M:%S') - self.assertGreaterEqual(ts_end, ts_start) - - def test_simple_array(self): - data = Measure(MultiGetter(arr=(1.2, 3.4))).run_temp() - - self.assertEqual(data.index0_set.tolist(), [0, 1]) - self.assertEqual(data.arr.tolist(), [1.2, 3.4]) - self.assertEqual(len(data.arrays), 2, data.arrays) - - def test_array_and_scalar(self): - self.p1.set(42) - data = Measure(MultiGetter(arr=(5, 6)), self.p1).run_temp() - - self.assertEqual(data.single_set.tolist(), [0]) - self.assertEqual(data.P1.tolist(), [42]) - self.assertEqual(data.index0_set.tolist(), [0, 1]) - self.assertEqual(data.arr.tolist(), [5, 6]) - self.assertEqual(len(data.arrays), 4, data.arrays) - - -class TestMeasureMulitParameter(TestCase): - def setUp(self): - self.p1 = MultiSetPointParam() - - def test_metadata(self): - loc_fmt = 'data/{date}/#{counter}_{name}_{date}_{time}' - rcd = {'name': 'test_metadata'} - param_name_1 = "multi_setpoint_param_this" - param_name_2 = "multi_setpoint_param_that" - setpoint_name = "multi_setpoint_param_this_setpoint_set" - loc_provider = FormatLocation(fmt=loc_fmt, record=rcd) - c = Measure(self.p1).run(location=loc_provider) - self.assertEqual(c.metadata['arrays'][param_name_1]['unit'], 'this unit') - self.assertEqual(c.metadata['arrays'][param_name_1]['name'], param_name_1) - self.assertEqual(c.metadata['arrays'][param_name_1]['label'], 'this label') - self.assertEqual(c.metadata['arrays'][param_name_1]['is_setpoint'], False) - self.assertEqual(c.metadata['arrays'][param_name_1]['shape'], (5,)) - assert_array_equal(getattr(c, param_name_1).ndarray, np.zeros(5)) - - self.assertEqual(c.metadata['arrays'][param_name_2]['unit'], 'that unit') - self.assertEqual(c.metadata['arrays'][param_name_2]['name'], param_name_2) - self.assertEqual(c.metadata['arrays'][param_name_2]['label'], 'that label') - self.assertEqual(c.metadata['arrays'][param_name_2]['is_setpoint'], False) - self.assertEqual(c.metadata['arrays'][param_name_2]['shape'], (5,)) - assert_array_equal(getattr(c, param_name_2).ndarray, np.ones(5)) - - self.assertEqual(c.metadata['arrays'][setpoint_name]['unit'], - 'this setpointunit') - self.assertEqual(c.metadata['arrays'][setpoint_name]['name'], - "multi_setpoint_param_this_setpoint") - self.assertEqual(c.metadata['arrays'][setpoint_name]['label'], - 'this setpoint') - self.assertEqual(c.metadata['arrays'][setpoint_name] - ['is_setpoint'], True) - self.assertEqual(c.metadata['arrays'][setpoint_name]['shape'], - (5,)) - assert_array_equal(getattr(c, setpoint_name).ndarray, np.linspace(5, 9, 5)) diff --git a/qcodes/tests/legacy/test_plots.py b/qcodes/tests/legacy/test_plots.py deleted file mode 100644 index 9c8a9d1723d..00000000000 --- a/qcodes/tests/legacy/test_plots.py +++ /dev/null @@ -1,90 +0,0 @@ -""" -Tests for plotting system. -Legacy in many ways: - - - assume X server running - - just test "window creation" -""" -import os -from unittest import TestCase, skipIf - -import numpy as np - -try: - noQtPlot = False - from qcodes.plots.pyqtgraph import QtPlot -except Exception: - noQtPlot = True - -try: - noMatPlot = False - import matplotlib - - from qcodes.plots.qcmatplotlib import MatPlot - matplotlib.use('Agg') - import matplotlib.pyplot as plt -except Exception: - noMatPlot = True - - -@skipIf(noQtPlot, '***pyqtgraph plotting cannot be tested***') -class TestQtPlot(TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def test_creation(self): - """ - Simple test function which created a QtPlot window - """ - plotQ = QtPlot(remote=False, show_window=False, interval=0) - plotQ.add_subplot() - - def test_simple_plot(self): - main_QtPlot = QtPlot( - window_title='Main plotmon of TestQtPlot', - figsize=(600, 400)) - - x = np.arange(0, 10e-6, 1e-9) - f = 2e6 - y = np.cos(2*np.pi*f*x) - - for j in range(4): - main_QtPlot.add(x=x, y=y, - xlabel='Time', xunit='s', - ylabel='Amplitude', yunit='V', - subplot=j+1, - symbol='o', symbolSize=5) - - def test_return_handle(self): - plotQ = QtPlot(remote=False) - return_handle = plotQ.add([1, 2, 3]) - self.assertIs(return_handle, plotQ.subplots[0].items[0]) - - -@skipIf(noMatPlot, '***matplotlib plotting cannot be tested***') -class TestMatPlot(TestCase): - - def setUp(self): - pass - - def tearDown(self): - pass - - def test_creation(self): - """ - Simple test function which created a MatPlot window - """ - plotM = MatPlot(interval=0) - plt.close(plotM.fig) - - def test_return_handle(self): - plotM = MatPlot(interval=0) - returned_handle = plotM.add([1, 2, 3]) - line_handle = plotM[0].get_lines()[0] - self.assertIs(returned_handle, line_handle) - plotM.clear() - plt.close(plotM.fig) diff --git a/qcodes/tests/legacy/test_qcmatplotlib_functions.py b/qcodes/tests/legacy/test_qcmatplotlib_functions.py deleted file mode 100644 index 85330e8a326..00000000000 --- a/qcodes/tests/legacy/test_qcmatplotlib_functions.py +++ /dev/null @@ -1,88 +0,0 @@ -from itertools import product -from typing import Tuple - -import numpy as np - -from qcodes.plots.qcmatplotlib import MatPlot - - -def make_simulated_xyz(xrange: np.ndarray, - yrange: np.ndarray, - step: int, - interrupt_at: int=0) -> Tuple[np.ndarray, - np.ndarray, - np.ndarray]: - """ - Make x, y, and z np.arrays like a Loop measurement would. In particular - get the positions of nans right. - - Args: - xrange: a 1D array with inner loop set values - yrange: a 1D array with outer loop set values - step: is the outer loop (y) step, describes how far we are in the - measurement (0: all NaNs, len(y): all data) - interrupt_at: inner loop step to interrupt at - - Returns: - (x, y, z) where z is random noise - """ - y = yrange.copy() - y[step:] = np.nan - - x = np.empty((len(yrange), len(xrange))) - x.fill(np.nan) - z = x.copy() - for stepind in range(step): - xrow = xrange.copy() - if stepind == step - 1: - xrow[interrupt_at:] = np.nan - zrow = np.random.randn(len(xrange)) - if stepind == step - 1: - zrow[interrupt_at:] = np.nan - x[stepind, :] = xrow - z[stepind, :] = zrow - - return x, y, z - - -def test_make_args_for_pcolormesh(): - # We test some common situations - # - # y in the outer loop setpoints, i.e. of shape (N,) - # x is the inner loop setpoints, i.e. of shape (N, M) - # z is the data, i.e. of shape (N, M) - - N = 10 # y - M = 25 # x - - xrange = np.linspace(-1, 1, M) - yrange = np.linspace(-10, 0.5, N) - - # up scans, down scans - - for xsign, ysign in product([-1, 1], repeat=2): - - x, y, z = make_simulated_xyz(xsign*xrange, - ysign*yrange, - step=N//2+1) - - args_masked = [np.ma.masked_invalid(arg) for arg in [x, y, z]] - - args = MatPlot._make_args_for_pcolormesh(args_masked, x, y) - - assert len(args[0]) == M + 1 - assert len(args[1]) == N + 1 - - # an interrupted scan - - x, y, z = make_simulated_xyz(xsign*xrange, - ysign*yrange, - step=N//2+1, - interrupt_at=M//2+1) - - args_masked = [np.ma.masked_invalid(arg) for arg in [x, y, z]] - - args = MatPlot._make_args_for_pcolormesh(args_masked, x, y) - - assert len(args[0]) == M + 1 - assert len(args[1]) == N + 1 diff --git a/qcodes/tests/legacy/test_threading.py b/qcodes/tests/legacy/test_threading.py deleted file mode 100644 index 7956690412d..00000000000 --- a/qcodes/tests/legacy/test_threading.py +++ /dev/null @@ -1,31 +0,0 @@ -import gc -from unittest import TestCase - -from qcodes.actions import UnsafeThreadingException -from qcodes.loops import Loop -from qcodes.tests.instrument_mocks import DummyInstrument - - -class TestUnsafeThreading(TestCase): - - def setUp(self): - self.inst1 = DummyInstrument(name='inst1', - gates=['v1', 'v2']) - self.inst2 = DummyInstrument(name='inst2', - gates=['v1', 'v2']) - - def tearDown(self): - self.inst1.close() - self.inst2.close() - - del self.inst1 - del self.inst2 - - gc.collect() - - def test_unsafe_exception(self): - to_meas = (self.inst1.v1, self.inst1.v2) - loop = Loop(self.inst2.v1.sweep(0, 1, num=10)).each(*to_meas) - - with self.assertRaises(UnsafeThreadingException): - loop.run(use_threads=True) diff --git a/qcodes/tests/legacy/test_waitsecs.py b/qcodes/tests/legacy/test_waitsecs.py deleted file mode 100644 index 446f5e124d7..00000000000 --- a/qcodes/tests/legacy/test_waitsecs.py +++ /dev/null @@ -1,22 +0,0 @@ -import time -from datetime import datetime - -import pytest - -from qcodes.logger.logger import LogCapture -from qcodes.loops import wait_secs - - -def test_bad_calls(): - bad_args = [None, datetime.now()] - for arg in bad_args: - with pytest.raises(TypeError): - wait_secs(arg) - - -def test_warning(): - with LogCapture() as logs: - secs_out = wait_secs(time.perf_counter() - 1) - assert secs_out == 0 - - assert logs.value.count("negative delay") == 1, logs.value diff --git a/qcodes/tests/test_channels.py b/qcodes/tests/test_channels.py index 18714b2abb1..bf40d1ec7f9 100644 --- a/qcodes/tests/test_channels.py +++ b/qcodes/tests/test_channels.py @@ -7,9 +7,7 @@ from hypothesis import HealthCheck, given, settings from numpy.testing import assert_allclose, assert_array_equal -from qcodes.data.location import FormatLocation from qcodes.instrument import ChannelList, ChannelTuple, Instrument, InstrumentChannel -from qcodes.loops import Loop from qcodes.parameters import Parameter from qcodes.tests.instrument_mocks import DummyChannel, DummyChannelInstrument from qcodes.validators import Numbers @@ -625,190 +623,6 @@ def test_names(dci): [ex_inst_name, ex_chan_name, ex_subchan_name, ex_param_name] -def test_loop_simple(dci): - loc_fmt = 'data/{date}/#{counter}_{name}_{date}_{time}' - rcd = {'name': 'loopSimple'} - loc_provider = FormatLocation(fmt=loc_fmt, record=rcd) - loop = Loop(dci.channels[0].temperature.sweep(0, 300, 10), - 0.001).each(dci.A.temperature) - data = loop.run(location=loc_provider) - assert_array_equal(data.dci_ChanA_temperature_set.ndarray, - data.dci_ChanA_temperature.ndarray) - - -def test_loop_measure_all_channels(dci): - p1 = Parameter(name='p1', vals=Numbers(-10, 10), get_cmd=None, - set_cmd=None) - loc_fmt = 'data/{date}/#{counter}_{name}_{date}_{time}' - rcd = {'name': 'allChannels'} - loc_provider = FormatLocation(fmt=loc_fmt, record=rcd) - loop = Loop(p1.sweep(-10, 10, 1), 1e-6).\ - each(dci.channels.temperature) - data = loop.run(location=loc_provider) - assert data.p1_set.ndarray.shape == (21, ) - assert len(data.arrays) == 7 - for chan in ['A', 'B', 'C', 'D', 'E', 'F']: - assert getattr( - data, - f'dci_Chan{chan}_temperature' - ).ndarray.shape == (21,) - - -def test_loop_measure_channels_individually(dci): - p1 = Parameter(name='p1', vals=Numbers(-10, 10), get_cmd=None, - set_cmd=None) - loc_fmt = 'data/{date}/#{counter}_{name}_{date}_{time}' - rcd = {'name': 'channelsIndividually'} - loc_provider = FormatLocation(fmt=loc_fmt, record=rcd) - loop = Loop(p1.sweep(-10, 10, 1), 1e-6).each(dci. - channels[0].temperature, - dci. - channels[1].temperature, - dci. - channels[2].temperature, - dci. - channels[3].temperature) - data = loop.run(location=loc_provider) - assert data.p1_set.ndarray.shape == (21, ) - for chan in ['A', 'B', 'C', 'D']: - assert getattr( - data, f'dci_Chan{chan}_temperature' - ).ndarray.shape == (21,) - - -@given(values=hst.lists(hst.floats(0, 300), min_size=4, max_size=4)) -@settings(max_examples=10, deadline=None, suppress_health_check=(HealthCheck.function_scoped_fixture,)) -def test_loop_measure_channels_by_name(dci, values): - p1 = Parameter(name='p1', vals=Numbers(-10, 10), get_cmd=None, - set_cmd=None) - for i in range(4): - dci.channels[i].temperature(values[i]) - loc_fmt = 'data/{date}/#{counter}_{name}_{date}_{time}' - rcd = {'name': 'channelsByName'} - loc_provider = FormatLocation(fmt=loc_fmt, record=rcd) - loop = Loop(p1.sweep(-10, 10, 1), 1e-6).each( - dci.A.temperature, - dci.B.temperature, - dci.C.temperature, - dci.D.temperature - ) - data = loop.run(location=loc_provider) - assert data.p1_set.ndarray.shape == (21, ) - for i, chan in enumerate(['A', 'B', 'C', 'D']): - assert getattr( - data, f'dci_Chan{chan}_temperature' - ).ndarray.shape == (21,) - assert getattr( - data, f'dci_Chan{chan}_temperature' - ).ndarray.max() == values[i] - assert getattr( - data, f'dci_Chan{chan}_temperature' - ).ndarray.min() == values[i] - - -@given(loop_channels=hst.lists(hst.integers(0, 3), min_size=2, max_size=2, - unique=True), - measure_channel=hst.integers(0, 3)) -@settings(max_examples=10, deadline=800, - suppress_health_check=(HealthCheck.function_scoped_fixture,)) -def test_nested_loop_over_channels(dci, loop_channels, measure_channel): - channel_to_label = {0: 'A', 1: 'B', 2: 'C', 3: "D"} - loc_fmt = 'data/{date}/#{counter}_{name}_{date}_{time}' - rcd = {'name': 'nestedLoopOverChannels'} - loc_provider = FormatLocation(fmt=loc_fmt, record=rcd) - loop = Loop(dci.channels[loop_channels[0]].temperature. - sweep(0, 10, 0.5)) - loop = loop.loop(dci.channels[loop_channels[1]].temperature. - sweep(50, 51, 0.1)) - loop = loop.each(dci.channels[measure_channel].temperature) - data = loop.run(location=loc_provider) - - assert getattr( - data, - f'dci_Chan{channel_to_label[loop_channels[0]]}_temperature_set' - ).ndarray.shape == (21,) - assert getattr( - data, - f'dci_Chan{channel_to_label[loop_channels[1]]}_temperature_set' - ).ndarray.shape == (21, 11,) - assert getattr( - data, - f'dci_Chan{channel_to_label[measure_channel]}_temperature' - ).ndarray.shape == (21, 11) - - assert_array_equal(getattr( - data, - f'dci_Chan{channel_to_label[loop_channels[0]]}_temperature_set' - ).ndarray, np.arange(0, 10.1, 0.5)) - - expected_array = np.repeat(np.arange(50, 51.01, 0.1).reshape(1, 11), - 21, axis=0) - array = getattr( - data, - f'dci_Chan{channel_to_label[loop_channels[1]]}_temperature_set' - ).ndarray - assert_allclose(array, expected_array) - - -def test_loop_slicing_multiparameter_raises(dci): - with pytest.raises(NotImplementedError): - loop = Loop(dci.A.temperature.sweep(0, 10, 1), 0.1) - loop.each(dci.channels[0:2].dummy_multi_parameter).run() - - -def test_loop_multiparameter_by_name(dci): - loc_fmt = 'data/{date}/#{counter}_{name}_{date}_{time}' - rcd = {'name': 'multiParamByName'} - loc_provider = FormatLocation(fmt=loc_fmt, record=rcd) - loop = Loop(dci.A.temperature.sweep(0, 10, 1), 0.1) - data = loop.each(dci.A.dummy_multi_parameter)\ - .run(location=loc_provider) - _verify_multiparam_data(data) - assert 'multi_setpoint_param_this_setpoint_set' in data.arrays.keys() - - -def test_loop_multiparameter_by_index(dci): - loc_fmt = 'data/{date}/#{counter}_{name}_{date}_{time}' - rcd = {'name': 'loopByIndex'} - loc_provider = FormatLocation(fmt=loc_fmt, record=rcd) - loop = Loop(dci.channels[0].temperature.sweep(0, 10, 1), - 0.1) - data = loop.each(dci.A.dummy_multi_parameter)\ - .run(location=loc_provider) - _verify_multiparam_data(data) - - -def test_loop_slicing_arrayparameter(dci): - loc_fmt = 'data/{date}/#{counter}_{name}_{date}_{time}' - rcd = {'name': 'loopSlicing'} - loc_provider = FormatLocation(fmt=loc_fmt, record=rcd) - loop = Loop(dci.A.temperature.sweep(0, 10, 1), 0.1) - data = loop.each(dci.channels[0:2].dummy_array_parameter)\ - .run(location=loc_provider) - _verify_array_data(data, channels=('A', 'B')) - - -def test_loop_arrayparameter_by_name(dci): - loc_fmt = 'data/{date}/#{counter}_{name}_{date}_{time}' - rcd = {'name': 'arrayParamByName'} - loc_provider = FormatLocation(fmt=loc_fmt, record=rcd) - loop = Loop(dci.A.temperature.sweep(0, 10, 1), 0.1) - data = loop.each(dci.A.dummy_array_parameter)\ - .run(location=loc_provider) - _verify_array_data(data) - - -def test_loop_arrayparameter_by_index(dci): - loc_fmt = 'data/{date}/#{counter}_{name}_{date}_{time}' - rcd = {'name': 'arrayParamByIndex'} - loc_provider = FormatLocation(fmt=loc_fmt, record=rcd) - loop = Loop(dci.channels[0].temperature.sweep(0, 10, 1), - 0.1) - data = loop.each(dci.A.dummy_array_parameter)\ - .run(location=loc_provider) - _verify_array_data(data) - - def test_root_instrument(dci): assert dci.root_instrument is dci for channel in dci.channels: diff --git a/qcodes/tests/test_slack.py b/qcodes/tests/test_slack.py deleted file mode 100644 index 83c71d34606..00000000000 --- a/qcodes/tests/test_slack.py +++ /dev/null @@ -1,367 +0,0 @@ -from unittest.mock import call - -import pytest -from requests.exceptions import ConnectTimeout, HTTPError, ReadTimeout -from urllib3.exceptions import ReadTimeoutError - -from qcodes.parameters import Parameter - - -class AnyStringWith(str): - def __eq__(self, other): - return self in other - - -@pytest.fixture(name='mock_webclient', autouse=True) -def setup_webclient(mocker): - mock_slack_sdk_module = mocker.MagicMock(name='slack_sdk_module') - mock_webclient = mocker.MagicMock(name='WebclientMock') - mock_slack_sdk_module.WebClient = mocker.MagicMock() - mock_slack_sdk_module.WebClient.return_value = mock_webclient - mocker.patch.dict('sys.modules', slack_sdk=mock_slack_sdk_module) - - response = {'members': [{'name': 'dummyuser', 'id': 'DU123'}]} - mock_webclient.users_list.return_value = response - - def mock_conversations_list(types): - if 'im' in types.split(','): - return {'channels': [{'user': 'DU123', 'id': 'CH234'}]} - else: - return None - - mock_webclient.conversations_list.side_effect = mock_conversations_list - - return mock_webclient - - -@pytest.fixture(name='slack') -def slack_fixture(): - return setup_slack() - - -def setup_slack(): - slack_config = { - 'bot_name': 'bot', - 'token': '123', - 'names': ['dummyuser'] - } - import qcodes.extensions.slack # pylint: disable=import-outside-toplevel - - slack = qcodes.extensions.slack.Slack(config=slack_config, auto_start=False) - - return slack - - -def test_convert_command_should_convert_floats(): - import qcodes.extensions.slack # pylint: disable=import-outside-toplevel - - cmd, arg, kwarg = qcodes.extensions.slack.convert_command("comm 0.234 key=0.1") - assert cmd == "comm" - assert arg == [pytest.approx(0.234)] - assert kwarg == {'key': pytest.approx(0.1)} - - -def test_slack_instance_should_contain_supplied_usernames(slack): - assert 'dummyuser' in slack.users.keys() - - -def test_slack_instance_should_get_config_from_qc_config(): - from qcodes import config as cf # pylint: disable=import-outside-toplevel - slack_config = { - 'bot_name': 'bot', - 'token': '123', - 'names': ['dummyuser'] - } - cf.add(key='slack', value=slack_config) - import qcodes.extensions.slack # pylint: disable=import-outside-toplevel - - slack = qcodes.extensions.slack.Slack(config=None, auto_start=False) - assert 'dummyuser' in slack.users.keys() - - -def test_slack_instance_should_start(mocker): - slack_config = { - 'bot_name': 'bot', - 'token': '123', - 'names': ['dummyuser'] - } - mock_thread_start = mocker.patch('threading.Thread.start') - import qcodes.extensions.slack # pylint: disable=import-outside-toplevel - - _ = qcodes.extensions.slack.Slack(config=slack_config) - - mock_thread_start.assert_called() - - -def test_slack_instance_should_not_start_when_already_started(mocker): - slack_config = { - 'bot_name': 'bot', - 'token': '123', - 'names': ['dummyuser'] - } - mock_thread_start = mocker.patch('threading.Thread.start') - mock_thread_start.side_effect = RuntimeError - - import qcodes.extensions.slack # pylint: disable=import-outside-toplevel - - _ = qcodes.extensions.slack.Slack(config=slack_config) - - mock_thread_start.assert_called() - - -def test_slack_instance_should_start_and_stop(mocker): - slack_config = { - 'bot_name': 'bot', - 'token': '123', - 'names': ['dummyuser'] - } - mocker.patch('threading.Thread.start') - - import qcodes.extensions.slack # pylint: disable=import-outside-toplevel - - slack = qcodes.extensions.slack.Slack(config=slack_config, interval=0) - slack.stop() - - assert not slack._is_active - - -def test_slack_instance_should_return_username_from_id(mock_webclient, slack): - def mock_users_info(user): - if user == 'DU123': - return {'user': {'name': 'dummyuser', 'id': 'DU123'}} - else: - return None - - mock_webclient.users_info.side_effect = mock_users_info - - assert {'name': 'dummyuser', 'id': 'DU123'} == slack.user_from_id('DU123') - - -def test_slack_instance_should_get_im_ids(mock_webclient): - def conversations_history(channel, limit=None): - if channel == 'CH234': - messages = [{'user': 'DU123', 'text': f'm{i}', - 'ts': f'{45.5 + i}'} for i in range(limit)] - response = {'messages': messages} - return response - else: - return None - - mock_webclient.conversations_history.side_effect = conversations_history - - slack = setup_slack() - - assert slack.users['dummyuser']['im_id'] == 'CH234' - assert slack.users['dummyuser']['last_ts'] == pytest.approx(45.5) - - -def test_slack_instance_should_get_im_ids_with_zero_messages(mock_webclient): - def conversations_history(channel, limit=None): - if channel == 'CH234': - response = {'messages': []} - return response - else: - return None - - mock_webclient.conversations_history.side_effect = conversations_history - slack = setup_slack() - - assert slack.users['dummyuser']['last_ts'] is None - - -def test_slack_instance_should_get_im_messages_w_count(slack, mock_webclient): - def conversations_history(channel, limit=None): - if channel == 'CH234': - messages = [{'user': 'DU123', 'text': f'message{i}'} - for i in range(limit)] - response = {'messages': messages} - return response - else: - return None - - mock_webclient.conversations_history.side_effect = conversations_history - - messages = slack.get_im_messages('dummyuser', count=3) - assert len(messages) == 3 - - -def test_slack_instance_should_get_im_messages_without_channel(mock_webclient): - def conversations_history(channel, limit=None): - if channel == 'CH234': - messages = [{'user': 'DU123', 'text': f'm{i}', - 'ts': f'{45.5 + i}'} for i in range(limit)] - response = {'messages': messages} - return response - else: - return None - - mock_webclient.conversations_history.side_effect = conversations_history - - def mock_conversations_list(types): - if 'im' in types.split(','): - return {'channels': []} - else: - return None - - mock_webclient.conversations_list.side_effect = mock_conversations_list - - slack = setup_slack() - - messages = slack.get_im_messages('dummyuser') - assert len(messages) == 0 - - -def test_slack_instance_should_get_new_im_messages(mock_webclient): - def generator_function(): - total = 8 - new = 3 - while True: - new_messages = [{'user': 'DU123', 'text': f'm{i}', - 'ts': f'{45.5 + i}'} for i in range(total)][-new:] - response = {'messages': new_messages} - yield response - total += new - - generator = generator_function() - - def conversations_history(channel, limit=None, oldest=None): - if channel == 'CH234': - return next(generator) - else: - return None - - mock_webclient.conversations_history.side_effect = conversations_history - - slack = setup_slack() - - new_messages = slack.get_new_im_messages() - assert len(new_messages['dummyuser']) == 3 - - -def test_slack_instance_should_update(slack): - slack.update() - assert slack.tasks == [] - - -def test_slack_instance_should_update_with_task_returning_false(slack): - slack.add_task('finished', channel='CH234') - slack.update() - assert slack.tasks == [] - - -def test_slack_instance_should_update_with_task_returning_true(slack, mocker): - mocker.patch("qcodes.extensions.slack.active_loop", return_value=not None) - - slack.add_task('finished', channel='CH234') - slack.update() - task_added = slack.tasks[-1] - - assert 'Slack.check_msmt_finished' in str(task_added.func) - - -def test_slack_instance_should_update_with_exception(slack, mocker): - method_name = "qcodes.extensions.slack.Slack.get_new_im_messages" - mock_get_new_im_messages = mocker.patch(method_name) - mocker.patch('warnings.warn') - mocker.patch('logging.info') - - for exception in [ReadTimeout, HTTPError, ConnectTimeout, - ReadTimeoutError('pool', 'url', 'message')]: - mock_get_new_im_messages.side_effect = exception - slack.update() - assert slack.tasks == [] - - -def test_slack_instance_should_give_help_message(slack): - message = slack.help_message() - expected_message = '\nAvailable commands: `plot`, `msmt`, ' \ - '`measurement`, `notify`, `help`, `task`' - assert message == expected_message - - -def test_slack_instance_should_handle_messages(mock_webclient, slack): - messages = {'dummyuser': [{'user': 'DU123', 'text': 'help'}]} - slack.handle_messages(messages) - expected_text = 'Results: \nAvailable commands: `plot`, ' \ - '`msmt`, `measurement`, `notify`, `help`, `task`' - expected_output = {'channel': 'CH234', - 'text': expected_text} - - mock_webclient.chat_postMessage.assert_called_with(**expected_output) - - -def test_slack_inst_should_handle_messages_w_args_kw(mock_webclient, slack): - text = 'task finished key=1' - messages = {'dummyuser': [{'user': 'DU123', 'text': text}]} - slack.handle_messages(messages) - expected_output = {'channel': 'CH234', 'text': 'Added task "finished"'} - mock_webclient.chat_postMessage.assert_called_with(**expected_output) - - -def test_slack_inst_should_handle_messages_w_parameter(mock_webclient, slack): - slack.commands.update({'comm': Parameter(name='param')}) - messages = {'dummyuser': [{'user': 'DU123', 'text': 'comm'}]} - slack.handle_messages(messages) - expected_output = {'channel': 'CH234', 'text': 'Executing comm'} - mock_webclient.chat_postMessage.assert_called_with(**expected_output) - - -def test_slack_inst_should_handle_messages_w_exception(mock_webclient, slack): - messages = {'dummyuser': [{'user': 'DU123', 'text': 'help toomany'}]} - slack.handle_messages(messages) - text = "help_message() takes 1 positional argument but 2 were given\n" - expected_output = {"channel": "CH234", "text": AnyStringWith(text)} - mock_webclient.chat_postMessage.assert_called_with(**expected_output) - - -def test_slack_inst_should_handle_messages_w_unkn_cmd(mock_webclient, slack): - messages = {'dummyuser': [{'user': 'DU123', 'text': 'comm'}]} - slack.handle_messages(messages) - text = 'Command comm not understood. Try `help`' - expected_output = {'channel': 'CH234', 'text': text} - mock_webclient.chat_postMessage.assert_called_with(**expected_output) - - -def test_slack_inst_should_add_unknown_task_command(mock_webclient, slack): - slack.add_task('tcomm', channel='CH234') - text = 'Task command tcomm not understood' - expected_output = {'channel': 'CH234', 'text': text} - mock_webclient.chat_postMessage.assert_called_with(**expected_output) - - -def test_slack_inst_should_upload_latest_plot(mock_webclient, slack, mocker): - method_name = "qcodes.extensions.slack.BasePlot.latest_plot" - mocker.patch(method_name, return_value=not None) - mocker.patch('os.remove') - slack.upload_latest_plot(channel='CH234') - expected_output = {'channels': 'CH234', 'file': AnyStringWith('.jpg')} - mock_webclient.files_upload.assert_called_with(**expected_output) - - -def test_slack_inst_should_not_fail_upl_latest_wo_plot(mock_webclient, slack): - slack.upload_latest_plot(channel='CH234') - expected_output = {'channel': 'CH234', 'text': 'No latest plot'} - mock_webclient.chat_postMessage.assert_called_with(**expected_output) - - -def test_slack_inst_should_print_measurement(mock_webclient, slack, mocker): - dataset = mocker.MagicMock() - dataset.fraction_complete.return_value = 0.123 - mocker.patch("qcodes.extensions.slack.active_data_set", return_value=dataset) - - slack.print_measurement_information(channel='CH234') - - print(mock_webclient.chat_postMessage.calls) - - text1 = f"Measurement is {0.123 * 100:.0f}% complete" - expected_out1 = {"channel": "CH234", "text": text1} - expected_out2 = {"channel": "CH234", "text": AnyStringWith("MagicMock")} - actual = mock_webclient.chat_postMessage.call_args_list - expected = [call(**expected_out1), call(**expected_out2)] - assert actual == expected - - -def test_slack_inst_should_print_measurement_wo_latest(mock_webclient, slack): - slack.print_measurement_information(channel='CH234') - expected_output = {'channel': 'CH234', 'text': 'No latest dataset found'} - mock_webclient.chat_postMessage.assert_called_with(**expected_output) diff --git a/qcodes/utils/deprecate.py b/qcodes/utils/deprecate.py index 0d0b1db0e62..b87571506b7 100644 --- a/qcodes/utils/deprecate.py +++ b/qcodes/utils/deprecate.py @@ -31,7 +31,7 @@ def issue_deprecation_warning( what: str, reason: Optional[str] = None, alternative: Optional[str] = None, - stacklevel: int = 2, + stacklevel: int = 3, ) -> None: """ Issue a `QCoDeSDeprecationWarning` with a consistently formatted message diff --git a/qcodes/utils/helpers.py b/qcodes/utils/helpers.py index f67a9d0d816..3114483904d 100644 --- a/qcodes/utils/helpers.py +++ b/qcodes/utils/helpers.py @@ -10,7 +10,6 @@ # libraries. from ruamel.yaml import YAML -from qcodes.loops import tprint, wait_secs from qcodes.parameters.named_repr import named_repr from qcodes.parameters.permissive_range import permissive_range from qcodes.parameters.sequence_helpers import is_sequence, is_sequence_of @@ -31,7 +30,6 @@ from .json_utils import NumpyJSONEncoder from .partial_utils import partial_with_docstring from .path_helpers import QCODES_USER_PATH_ENV, get_qcodes_path, get_qcodes_user_path -from .qt_helpers import foreground_qt_window from .spyder_utils import add_to_spyder_UMR_excludelist diff --git a/qcodes/utils/magic.py b/qcodes/utils/magic.py index cc05c31f2bb..dce6f743913 100644 --- a/qcodes/utils/magic.py +++ b/qcodes/utils/magic.py @@ -3,168 +3,16 @@ from IPython import get_ipython # type: ignore[attr-defined] from IPython.core.magic import Magics, line_cell_magic, magics_class - -@magics_class -class QCoDeSMagic(Magics): - """Magics related to code management (loading, saving, editing, ...).""" - - def __init__(self, *args, **kwargs): - """ - Setup Magic. All args and kwargs are passed to super class. - """ - self._knowntemps = set() - super().__init__(*args, **kwargs) - - @line_cell_magic - def measurement(self, line, cell=None): - """ - Create ``qcodes.Loop`` measurement mimicking Python ``for`` syntax via - iPython magic. - - Upon execution of a notebook cell, the code is transformed from the - for loop structure to a QCoDeS Loop before being executed. - Can be run by having ``%%measurement`` in the first line of a cell, - followed by the measurement name (see below for an example). - - The for loop syntax differs slightly from a Python ``for`` loop, - as it uses ``for {iterable}`` instead of ``for {element} in {iterable}``. - The reason is that ``{element}`` cannot be accessed (yet) in QCoDeS loops. - - Comments (#) are ignored in the loop. - Any code after the loop will also be run, if separated by a blank - line from the loop. - - The Loop object is by default stored in a variable named ``loop``, - and the dataset in ``data``, and these can be overridden using options. - Must be run in a Jupyter Notebook. - Delays can be provided in a loop by adding ``-d {delay}`` after ``for``. - - The following options can be passed along with the measurement name - (e.g. ``%%measurement -px -d data_name {measurement_name})``:: - - -p : print transformed code - -x : Do not execute code - -d : Use custom name for dataset - -l : Use custom name for Loop - - An example for a loop cell is as follows:: - - %%measurement {-options} {measurement_name} - for {sweep_vals}: - {measure_parameter1} - {measure_parameter2} - for -d 1 {sweep_vals2}: - {measure_parameter3} - - ... - - which will be internally transformed to:: - - import qcodes - loop = qcodes.Loop({sweep_vals}).each( - {measure_parameter1}, - {measure_parameter2}, - qcodes.Loop({sweep_vals2}, delay=1).each( - {measure_parameter3})) - data = loop.get_data_set(name={measurement_name}) - - ... - - An explicit example of the line ``for {sweep_vals}:`` could be - ``for sweep_parameter.sweep(0, 42, step=1):`` - - """ - - if cell is None: - # No loop provided, print documentation - print(self.measurement.__doc__) - return - - # Parse line, get measurement name and any possible options - options, msmt_name = self.parse_options(line, 'pd:l:x') - data_name = options.get('d', 'data') - loop_name = options.get('l', 'loop') - - lines = cell.splitlines() - assert lines[0][:3] == 'for', "Measurement must start with for loop" - - contents = f'import qcodes\n{loop_name} = ' - previous_level = 0 - k = None - for k, line in enumerate(lines): - line, level = line.lstrip(), int((len(line)-len(line.lstrip())) / 4) - - if not line: - # Empty line, end of loop - break - elif line[0] == '#': - # Ignore comment - continue - else: - line_representation = ' ' * level * 4 - if level < previous_level: - # Exiting inner loop, close bracket - line_representation += '),' * (previous_level - level) - line_representation += '\n' + ' ' * level * 4 - - if line[:3] == 'for': - # New loop - for_opts, for_code = self.parse_options(line[4:-1], 'd:') - if 'd' in for_opts: - # Delay option provided - line_representation += ('qcodes.Loop({}, ' - 'delay={}).each(\n' - ''.format(for_code, - for_opts["d"])) - else: - line_representation += ('qcodes.Loop({}).each(\n' - ''.format(for_code)) - else: - # Action in current loop - line_representation += f'{line},\n' - contents += line_representation - - # Remember level for next iteration (might exit inner loop) - previous_level = level - - # Add closing brackets for any remaining loops - contents += ')' * previous_level + '\n' - # Add dataset - contents += "{} = {}.get_data_set(name='{}')".format(data_name, - loop_name, - msmt_name) - if k is not None: - for line in lines[k + 1 :]: - contents += "\n" + line - - if 'p' in options: - print(contents) - - if 'x' not in options: - # Execute contents - assert self.shell is not None - self.shell.run_cell(contents, store_history=True, silent=True) - - -def register_magic_class(cls=QCoDeSMagic, magic_commands=True): - """ - Registers a iPython magic class. - - Args: - cls: Magic class to register. - magic_commands (List): List of magic commands within the class to - register. If not specified, all magic commands are registered. - - """ - - ip = get_ipython() - if ip is None: - raise RuntimeError("No IPython shell found") - else: - if magic_commands is not True: - assert cls.magics is not None - # filter out any magic commands that are not in magic_commands - cls.magics = {line_cell: {key: val for key, val in magics.items() - if key in magic_commands} - for line_cell, magics in cls.magics.items()} - ip.magics_manager.register(cls) +from qcodes.utils import issue_deprecation_warning + +try: + from qcodes_loop.utils.magic import QCoDeSMagic, register_magic_class +except ImportError as e: + raise ImportError( + "qcodes.utils.magic is deprecated and has moved to " + "the package `qcodes_loop`. Please install qcodes_loop directly or " + "with `pip install qcodes[loop]" + ) from e +issue_deprecation_warning( + "qcodes.utils.magic module", alternative="qcodes_loop.utils.magic" +) diff --git a/qcodes/utils/qt_helpers.py b/qcodes/utils/qt_helpers.py index 16fcb74e366..ce692514e43 100644 --- a/qcodes/utils/qt_helpers.py +++ b/qcodes/utils/qt_helpers.py @@ -1,51 +1,14 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from PyQt5.QtWidgets import QMainWindow - - -def foreground_qt_window(window: "QMainWindow") -> None: - """ - Try as hard as possible to bring a qt window to the front. This - will use pywin32 if installed and running on windows as this - seems to be the only reliable way to foreground a window. The - build-in qt functions often doesn't work. Note that to use this - with pyqtgraphs remote process you should use the ref in that module - as in the example below. - - Args: - window: Handle to qt window to foreground. - Examples: - >>> Qtplot.qt_helpers.foreground_qt_window(plot.win) - """ - try: - import win32con # pyright: ignore[reportMissingModuleSource] - from win32gui import SetWindowPos # pyright: ignore[reportMissingModuleSource] - - # use the idea from - # https://stackoverflow.com/questions/12118939/how-to-make-a-pyqt4-window-jump-to-the-front - SetWindowPos( - int(window.winId()), - win32con.HWND_TOPMOST, - # = always on top. only reliable way to bring it to the front on windows - 0, - 0, - 0, - 0, - win32con.SWP_NOMOVE | win32con.SWP_NOSIZE | win32con.SWP_SHOWWINDOW, - ) - SetWindowPos( - int(window.winId()), - win32con.HWND_NOTOPMOST, - # disable the always on top, but leave window at its top position - 0, - 0, - 0, - 0, - win32con.SWP_NOMOVE | win32con.SWP_NOSIZE | win32con.SWP_SHOWWINDOW, - ) - except ImportError: - pass - window.show() - window.raise_() - window.activateWindow() +from qcodes.utils import issue_deprecation_warning + +try: + from qcodes_loop.utils.qt_helpers import foreground_qt_window +except ImportError as e: + raise ImportError( + "qcodes.utils.qt_helpers is deprecated and has moved to " + "the package `qcodes_loop`. Please install qcodes_loop directly or " + "with `pip install qcodes[loop]" + ) from e + +issue_deprecation_warning( + "qcodes.utils.qt_helpers module", alternative="qcodes_loop.utils.qt_helpers" +) diff --git a/qcodes/utils/slack.py b/qcodes/utils/slack.py index 1c3b371cbeb..1d8d09381d1 100644 --- a/qcodes/utils/slack.py +++ b/qcodes/utils/slack.py @@ -1,11 +1,17 @@ -import warnings +from qcodes.utils import issue_deprecation_warning + +try: + from qcodes_loop.extensions.slack import Slack, SlackTimeoutWarning, convert_command +except ImportError as e: + raise ImportError( + "qcodes.utils.slack is deprecated and has moved to " + "the package `qcodes_loop`. Please install qcodes_loop directly or " + "with `pip install qcodes[loop]" + ) from e -from qcodes.extensions.slack import Slack, SlackTimeoutWarning, convert_command -# todo enable warning once new api is in release -# warnings.warn( -# "qcodes.utils.slack module is deprecated. " -# "Please update to import from qcodes.extensions" -# ) __all__ = ["Slack", "SlackTimeoutWarning", "convert_command"] +issue_deprecation_warning( + "qcodes.utils.slack module", alternative="qcodes_loop.extensions.slack" +)