diff --git a/docs/community/objects.rst b/docs/community/objects.rst index 0e0cb451cd3..50b21ff180d 100644 --- a/docs/community/objects.rst +++ b/docs/community/objects.rst @@ -36,9 +36,13 @@ to use. Normal text shows the container includes and uses of this object. Station ------- -Read more about :ref:`station_api`. +A convenient container for instruments, parameters, and more. + +More information: + +- `Station example notebook <../examples/Station.ipynb>`_ +- :ref:`station_api` API reference -.. todo:: is this how we want it ? or like the one below ? .. _instrument : diff --git a/docs/examples/Configuring_QCoDeS.ipynb b/docs/examples/Configuring_QCoDeS.ipynb index 288bb24cf7b..466357a4fa7 100644 --- a/docs/examples/Configuring_QCoDeS.ipynb +++ b/docs/examples/Configuring_QCoDeS.ipynb @@ -49,7 +49,7 @@ " 'defaultcolormap': 'hot'},\n", " 'user': {'scriptfolder': 'c:\\\\Users\\\\jenielse\\\\myscripts\\\\',\n", " 'mainfolder': 'c:\\\\Users\\\\jenielse\\\\mymainfolder\\\\'},\n", - " 'station_configurator': {'enable_forced_reconnect': False,\n", + " 'station': {'enable_forced_reconnect': False,\n", " 'default_folder': 'c:\\\\Users\\\\jenielse\\\\mymainfolder\\\\',\n", " 'default_file': 'instrument_config.yaml'}}" ] @@ -82,7 +82,7 @@ " 'pyqtmaxplots': 100,\n", " 'defaultcolormap': 'hot'},\n", " 'user': {'scriptfolder': '.', 'mainfolder': '.'},\n", - " 'station_configurator': {'enable_forced_reconnect': False,\n", + " 'station': {'enable_forced_reconnect': False,\n", " 'default_folder': '.',\n", " 'default_file': 'instrument_config.yml'}}" ] @@ -235,7 +235,7 @@ " 'defaultcolormap': 'hot'},\n", " 'user': {'scriptfolder': 'c:\\\\Users\\\\jenielse\\\\myscripts\\\\',\n", " 'mainfolder': 'c:\\\\Users\\\\jenielse\\\\mymainfolder\\\\'},\n", - " 'station_configurator': {'enable_forced_reconnect': False,\n", + " 'station': {'enable_forced_reconnect': False,\n", " 'default_folder': 'c:\\\\Users\\\\jenielse\\\\mymainfolder\\\\',\n", " 'default_file': 'instrument_config.yaml'}}" ] diff --git a/docs/examples/DataSet/Dataset Context Manager.ipynb b/docs/examples/DataSet/Dataset Context Manager.ipynb index 28b262b819f..b1847d8b26b 100644 --- a/docs/examples/DataSet/Dataset Context Manager.ipynb +++ b/docs/examples/DataSet/Dataset Context Manager.ipynb @@ -6706,7 +6706,9 @@ } ], "source": [ - "datasaver.dataset.get_data_as_pandas_dataframe()['dmm_v1'][0:10].to_xarray()" + "#df_sliced = datasaver.dataset.get_data_as_pandas_dataframe()['dmm_v1'].sort_index()[0:10]\n", + "#df_sliced.index = df_sliced.index.remove_unused_levels()\n", + "#df_sliced.to_xarray()" ] }, { diff --git a/docs/examples/DataSet/Working with snapshots.ipynb b/docs/examples/DataSet/Working with snapshots.ipynb index b80de68a3b3..de090dec833 100644 --- a/docs/examples/DataSet/Working with snapshots.ipynb +++ b/docs/examples/DataSet/Working with snapshots.ipynb @@ -7,10 +7,11 @@ "# Working with snapshots\n", "\n", "Here, the following topics are going to be covered:\n", - "* What is a snapshot\n", - "* How to create it\n", - "* How it is saved next to the measurement data\n", - "* How to extract snapshot from the dataset" + "\n", + "- What is a snapshot\n", + "- How to create it\n", + "- How it is saved next to the measurement data\n", + "- How to extract snapshot from the dataset" ] }, { @@ -272,9 +273,9 @@ "\n", "Experimental setups are large, and instruments tend to be quite complex in that they comprise many parameters and other stateful parts. It would be very time-consuming for the user to manually go through every instrument and parameter, and collect the snapshot data.\n", "\n", - "Here is where the concept of station comes into play. Instruments, parameters, and other submodules can be added to a station. In turn, the station has its `snapshot` method that allows to create a collective, single snapshot of all the instruments, parameters, and submodules.\n", + "Here is where the concept of station comes into play. Instruments, parameters, and other submodules can be added to a [Station object](../Station.ipynb) ([nbviewer.jupyter.org link](https://nbviewer.jupyter.org/github/QCoDeS/Qcodes/tree/master/docs/examples/Station.ipynb)). In turn, the station has its `snapshot` method that allows to create a collective, single snapshot of all the instruments, parameters, and submodules.\n", "\n", - "Note that in this article the focus is on the snapshot feature of the QCoDeS `Station`, while it has some other (mostly legacy) features." + "Note that in this article the focus is on the snapshot feature of the QCoDeS `Station`, while it has some other features (also some legacy once)." ] }, { @@ -799,7 +800,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python [default]", "language": "python", "name": "python3" }, @@ -813,7 +814,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.6" + "version": "3.6.8" }, "toc": { "base_numbering": 1, diff --git a/docs/examples/Station.ipynb b/docs/examples/Station.ipynb new file mode 100644 index 00000000000..91bcf58574d --- /dev/null +++ b/docs/examples/Station.ipynb @@ -0,0 +1,606 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Station\n", + "\n", + "Here, the following topics are going to be covered:\n", + "\n", + "- What is a station\n", + "- How to create it, and work with it\n", + "- Snapshot of a station\n", + "- Configuring station using a YAML configuration file" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Useful imports" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from pprint import pprint # for pretty-printing python variables like 'dict'\n", + "\n", + "import qcodes\n", + "from qcodes import Parameter, Station\n", + "from qcodes.tests.instrument_mocks import DummyInstrument" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## What is a Station\n", + "\n", + "Experimental setups are large, comprising of many instruments; and instruments tend to be quite complex in that they comprise many parameters and other stateful parts. It deems useful to have a bucket where all of them can be conveniently stored, and accessed.\n", + "\n", + "Here is where the concept of station comes into play. Instruments, parameters, and other \"components\" can be added to a station. As a result, the user gets an station instance that can be referred to in order to access those \"components\".\n", + "\n", + "Moreover, stations are very helpful when capturing the state of the experimental setup, known as snapshot. Refer to the respective section about station snapshot below.\n", + "\n", + "Last but not least, station can be configured in a text file which simplifies initialization of the instruments. Read more of this below." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## How to create Station and work with it\n", + "\n", + "For further sections we will need a dummy parameter, and a dummy instrument." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "# A dummy self-standing parameter\n", + "p = Parameter('p', label='Parameter P', unit='kg', set_cmd=None, get_cmd=None)\n", + "p.set(123)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "# A dummy instrument with three parameters\n", + "instr = DummyInstrument('instr', gates=['input', 'output', 'gain'])\n", + "instr.gain(42)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Creating a Station" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'instr'" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "station = Station()\n", + "\n", + "station.add_component(p)\n", + "station.add_component(instr)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'p': ,\n", + " 'instr': }" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Now station contains both `p` and `instr`\n", + "station.components" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note that it is also possible to add components to a station via arguments of its constructor, like this:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "station = Station(p, instr)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Access Station components" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now that the components have beed added to the station, it is possible to access them as attributes of the station (using the \"dot\" notation). With this feature, users can use tab-completion to find the instrument in the station they'd like to access." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "# Let's confirm that station's `p` is \n", + "# actually the `p` parameter defined above\n", + "assert station.p is p" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Removing component from Station" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Removing components from the station should be done with `remove_component` method - just pass it a name of the component you'd like to remove:" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "station.remove_component('p')" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'instr': }" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Now station contains only `instr`\n", + "station.components" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Default Station\n", + "\n", + "The `Station` class is designed in such a way that it always contains a reference to a `default` station object (the `Station.default` attribute). The constructor of the station object has a `default` keyword argument that allows to specify whether the resulting instance shall be stored as a default station, or not.\n", + "\n", + "This feature is a convenience. Other objects which consume an instance of `Station` as an argument (for example, `Measurement`) can now implement a logic to resort to `Station.default` in case an `Station` instance was not explicitly given to them." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Snapshot of a Station\n", + "\n", + "The station has a `snapshot` method that allows to create a collective, single snapshot of all the instruments, parameters, and submodules that have been added to it. It would be very time-consuming for the user to manually go through every instrument and parameter, and collect the snapshot data.\n", + "\n", + "For example, the `Measurement` object accepts a station argument exactly for the purpose of storing a snapshot of the whole experimental setup next to the measured data.\n", + "\n", + "Read more about snapshots in general, how to work with them, station's snapshot in particular, and more -- in [\"Working with snapshots\" example notebook](DataSet/Working with snapshots.ipynb) ([nbviewer.jupyter.org link](https://nbviewer.jupyter.org/github/QCoDeS/Qcodes/tree/master/docs/examples/DataSet/Working with snapshots.ipynb))." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Configuring Station using YAML configuration file\n", + "\n", + "The initialization part of the code where one instantiates instruments, set's up proper initial values for parameters, etc. can be quite long and tedious to maintain. For example, when a certain instrument is no longer needed, usually users just comment out the lines of initialization script which are related to the said instrument, and re-run the initialization script. Sharing initialization scripts is also difficult because each user may have a different expecation on the format it.\n", + "\n", + "These (and more) concerns are to be solved by YAML configuration of the `Station` (formerly known to some users under the name `StationConfigurator`).\n", + "\n", + "The YAML configuration file allows to statically and uniformly specify settings of all the instruments (and their parameters) that the measurement setup (the \"physical\" station) consists of, and load them with those settings on demand. The `Station` object implements convenient methods for this.\n", + "\n", + "The YAML configuration, if used, is stored in the station as a component with name `config`, and is thus included in the snapshot of the whole station.\n", + "\n", + "Note that one is not obliged to use the YAML configuration for setting up one's `Station` (for example, as at the top of this notebook).\n", + "\n", + "Below the following is discussed:\n", + "\n", + "- The structure of the YAML configuration file\n", + "- `Station`s methods realted to working with the YAML configuration\n", + "- Entries in QCoDeS config that are related to `Station`" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Example of YAML Station configuration" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Below is an example YAML station configuration file. All the fields are explained in the inline comments. Read it through.\n", + "\n", + "When exploring the YAML file capabilities, please note the difference between `parameters` section and `add_parameters` section. In the example file below, for the `QDac` instrument, the `Bx` parameter is going to be a new, additional parameter. This new `Bx` parameter will have its `limits`, `scale`, etc. __different__ from its \"source\" parameter `ch02.v` that it controls. Specifically this means that when setting `Bx` to `2.0`:\n", + "\n", + "1. the value of `2.0` is being validated against the limits of `Bx` (`0.0, 3.0`),\n", + "2. then the raw (\"scaled\") value of `130.468` (`= 2.0 * 65.234`) is passed to the `ch02.v` parameter,\n", + "3. then that value of `130.468` is validated against the limits of `ch02.v` (`0.0, 1.5e+3`),\n", + "4. then the raw (\"scaled\") value of `1.30468` (`= 130.468 * 0.01`) is finally passed to the physical instrument.\n", + "\n", + "Please also note that when trying in numbers in exponential form, it is required to provide `+` and `-` signs after `e`, for example, `7.8334e+5` and `2.5e-23`. Refer to YAML file format specification for more information." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```yaml\n", + "# Example YAML Station configuration file\n", + "#\n", + "# This file gets snapshotted and can be read back from the JSON \n", + "# snapshot for every experiment run.\n", + "#\n", + "# All fields are optional unless explicitly mentioned otherwise.\n", + "#\n", + "# As in all YAML files a one-line notation can also be used\n", + "# instead of nesting notation.\n", + "#\n", + "# The file starts with a list of loadable instruments instances,\n", + "# i.e. there can be two entries for two instruments of the same\n", + "# type if you want to specify two different use cases \n", + "# e.g. \"dmm1-readout\" and \"dmm1-calibration\".\n", + "#\n", + "instruments:\n", + " # Each instrument is specified by its name.\n", + " # This name is what is looked up by the `load_instrument`\n", + " # method of `Station`.\n", + " # Simulated instruments can also be specified here, just put\n", + " # the path to the similation .yaml file as the value of the\n", + " # \"init\"->\"visalib\" field (see below for an example of the\n", + " # \"init\" section as well as an example of specifying \n", + " # a simulated instrument).\n", + " qdac:\n", + " # Full import path to the python module that contains\n", + " # the driver class of the instrument.\n", + " # Required field.\n", + " driver: qcodes.instrument_drivers.QDev.QDac_channels\n", + " # The name of the class of the instrument driver.\n", + " # Required field.\n", + " type: QDac\n", + " # Visa address of the instrument.\n", + " # Note that this field can also be specified in the\n", + " # \"init\" section (see below) but the address specified \n", + " # here will overrule the address from the \"init\" section.\n", + " # Essentially, specifying address here allows avoiding\n", + " # the \"init\" section completely when address is the only\n", + " # neccesary argument that the instrument driver needs.\n", + " # For obvious reasons, this field is required for VISA\n", + " # instruments.\n", + " address: ASRL4::INSTR\n", + " # If an instrument with this name is already instantiated,\n", + " # and this field is true, then the existing instrument \n", + " # instance will be closed before instantiating this new one.\n", + " # If this field is false, or left out, closing will not\n", + " # happen.\n", + " enable_forced_reconnect: true\n", + " #\n", + " # The \"init\" section specifies constant arguments that are\n", + " # to be passed to the __init__ function of the instrument.\n", + " # Note that it is the instrument's driver class that defines\n", + " # the allowed arguments, for example, here \"update_currents\"\n", + " # is an argument that is specific to \"QDac\" driver.\n", + " init:\n", + " terminator: \\n\n", + " update_currents: false\n", + " #\n", + " # Setting up properties of parameters that already exist on\n", + " # the instrument.\n", + " parameters:\n", + " # Each parameter is specified by its name from the\n", + " # instrument driver class.\n", + " # Note that \"dot: notation can be used to specify \n", + " # parameters in (sub)channels and submodules.\n", + " ch01.v:\n", + " # If an alias is specified, the paramater becomes \n", + " # accessible under another name, so that you can write\n", + " # `qdac.cutter_gate(0.2)` instead of `qdac.ch01.v(0.2)`.\n", + " # Note that the parameter instance does not get copied,\n", + " # so that `(qdac.ch01.v is qdac.cutter_gate) == True`.\n", + " alias: cutter_gate\n", + " # Set new label.\n", + " label: Cutter Gate Voltage\n", + " # Set new unit.\n", + " unit: mV\n", + " # Set new scale.\n", + " scale: 0.001\n", + " # Set new post_delay.\n", + " post_delay: 0\n", + " # Set new inter_delay.\n", + " inter_delay: 0.01\n", + " # Set new step.\n", + " step: 1e-4\n", + " # If this field is given, and contains two \n", + " # comma-separated numbers like here, then the parameter\n", + " # gets a new `Numbers` validator with these values as\n", + " # lower and upper limits, respectively (in this case, it\n", + " # is `Numbers(-0.1, 0.1)`).\n", + " limits: -0.1,0.1\n", + " # Set the parameter to this given initial value upon\n", + " # instrument initialization.\n", + " # Note that if the current value on the physical\n", + " # instrument is different, the parameter will be ramped\n", + " # with the delays and step specified in this file.\n", + " initial_value: 0.01\n", + " # In case this values equals to true, upon loading this\n", + " # instrument from this configuration this parameter will\n", + " # be appended to the list of parameters that are \n", + " # displayed in QCoDeS `Monitor`.\n", + " monitor: true\n", + " # As in all YAML files a one-line notation can also be \n", + " # used, here is an example.\n", + " ch02.v: {scale: 0.01, limits: '0.0,1.5e+3', label: my label}\n", + " ch04.v: {alias: Q1lplg1, monitor: true}\n", + " #\n", + " # This section allows to add new parameters to the\n", + " # instrument instance which are based on existing parameters\n", + " # of the instrument. This functionality is based on the use\n", + " # of the `DelegateParameter` class.\n", + " add_parameters:\n", + " # For example, here we define a parameter that represents\n", + " # magnetic field control. Setting and getting this \n", + " # parameter will actually set/get a specific DAC channel.\n", + " # So this new magnetic field parameter is playing a role\n", + " # of a convenient proxy - it is much more convenient to \n", + " # perform a measurement where \"Bx\" is changed in tesla as\n", + " # opposed to where some channel of some DAC is changed in\n", + " # volts and one has to clutter the measurement code with\n", + " # the mess of conversion factors and more.\n", + " # Every new parameter definition starts with a name of\n", + " # the new parameter.\n", + " Bx:\n", + " # This field specifies the parameter which \"getter\" and \n", + " # \"setter\" will be used when calling `get`/`set` on this\n", + " # new parameter.\n", + " # Required field.\n", + " source: ch02.v\n", + " # Set the label. Otherwise, the one of the source parameter\n", + " # will be used.\n", + " label: Magnetic Field X-Component\n", + " # Set the unit. Otherwise, the one of the source parameter\n", + " # will be used.\n", + " unit: T\n", + " # Other fields have the same purpose and behavior as for\n", + " # the entries in the `add_parameter` section.\n", + " scale: 65.243\n", + " inter_delay: 0.001\n", + " post_delay: 0.05\n", + " step: 0.001\n", + " limits: 0.0,3.0\n", + " initial_value: 0.0\n", + " # For the sake of example, we decided not to monitor this\n", + " # parameter in QCoDeS `Monitor`.\n", + " #monitor: true\n", + " #\n", + " # More example instruments, just for the sake of example.\n", + " # Note that configuring simulated instruments also works,\n", + " # see the use of 'visalib' argument field below\n", + " dmm1:\n", + " driver: qcodes.instrument_drivers.agilent.Agilent_34400A\n", + " type: Agilent_34400A\n", + " enable_forced_reconnect: true\n", + " address: GPIB::1::65535::INSTR\n", + " init:\n", + " visalib: 'Agilent_34400A.yaml@sim'\n", + " parameters:\n", + " volt: {monitor: true}\n", + " mock_dac:\n", + " driver: qcodes.tests.instrument_mocks\n", + " type: DummyInstrument\n", + " enable_forced_reconnect: true\n", + " init:\n", + " # To pass an list of items use {}.\n", + " gates: {\"ch1\", \"ch2\"}\n", + " add_parameters:\n", + " Bx: {source: ch1, label: Bx, unit: T,\n", + " scale: 28, limits: \"-1,1\", monitor: true}\n", + " mock_dac2:\n", + " driver: qcodes.tests.instrument_mocks\n", + " type: DummyInstrument\n", + " enable_forced_reconnect: true\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### QCoDeS config entries related to Station" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "QCoDeS config contains entries that are related to the `Station` and its YAML configuration. Refer to [the description of the 'station' section in QCoDeS config](http://qcodes.github.io/Qcodes/user/configuration.html?highlight=station#default-config) for more specific information." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Using Station with YAML configuration file" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This section is going to briefly describe the usage of `Station` with the YAML configuration file. For details, refer to the docstrings of the methods of the `Station` class.\n", + "\n", + "A `Station` with a YAML configuration file can be created by passing the file name (or file name with absolute path) to `Station`s constructor. File name and location resolution also takes into account related entries in the `'station'` section of the QCoDeS config, refer to their documentation for more information.\n", + "\n", + "```python\n", + "station = Station(config_file='qutech_station_25.yaml')\n", + "```\n", + "\n", + "Alternatively, `load_config_file` method can be called on an already instantiated station to load the config file.\n", + "\n", + "```python\n", + "station = Station()\n", + "stataion.load_config_file=r'Q:\\\\station_data\\\\qutech_station_25.yaml')\n", + "```\n", + "\n", + "In case the configuration is already available as a YAML string, then that configuration can be loaded using `Station`'s `load_config` method, refer to it's docstring and signature for more information.\n", + "\n", + "Once the YAML configuration is loaded, the `load_instrument` method of the `Station` can be used to instantiate a particular instrument that is described in the YAML configuration. Calling this method not only will return an instance of the linstantiated instrument, but will also add it to the station object.\n", + "\n", + "For example, to instantiate the `qdac` instrument from the YAML configuration example from the section above, just execute the following:\n", + "```python\n", + "loaded_qdac = station.load_instrument('qdac')\n", + "```\n", + "\n", + "Note the `load_instrument`'s `revive_instance` argument, as well as `enable_force_reconnect` setting from the YAML configuration - these define what to do in case an instrument with the given name has already been instantiated in this python session.\n", + "\n", + "There is a more convenient way to load the instruments. Upon load of the YAML configuration, convenient `load_` methods are being generated on the `Station` object. Users can make use of tab-completion in their development environments to list what instruments can be loads by the station object from the loaded YAML configuration. For example, loading the QDac above can also be done like this:\n", + "```python\n", + "conveniently_loaded_qdac = station.load_qdac()\n", + "```\n", + "\n", + "Note that instruments are instantiated only when `load_*` methods are called. This means that loading a YAML configuration does NOT automatically instantiate anything.\n", + "\n", + "For the instruments loaded with the `load_*` methods, it is recommended to use `Station`'s `close_and_remove_instrument` method for closing and removing those from the station." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python [default]", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.8" + }, + "toc": { + "base_numbering": 1, + "nav_menu": {}, + "number_sections": true, + "sideBar": true, + "skip_h1_title": false, + "title_cell": "Table of Contents", + "title_sidebar": "Contents", + "toc_cell": false, + "toc_position": {}, + "toc_section_display": true, + "toc_window_display": false + }, + "varInspector": { + "cols": { + "lenName": 16, + "lenType": 16, + "lenVar": 40 + }, + "kernels_config": { + "python": { + "delete_cmd_postfix": "", + "delete_cmd_prefix": "del ", + "library": "var_list.py", + "varRefreshCmd": "print(var_dic_list())" + }, + "r": { + "delete_cmd_postfix": ") ", + "delete_cmd_prefix": "rm(", + "library": "var_list.r", + "varRefreshCmd": "cat(var_dic_list()) " + } + }, + "types_to_exclude": [ + "module", + "function", + "builtin_function_or_method", + "instance", + "_Feature" + ], + "window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/environment.yml b/environment.yml index 645e6fd0f25..188605153a2 100644 --- a/environment.yml +++ b/environment.yml @@ -18,11 +18,11 @@ dependencies: - pytest-runner - spyder - pyzmq + - ruamel_yaml - wrapt - pyyaml - lxml - scipy - - ruamel_yaml - gitpython - pandas - testpath>=0.4.2 # 0.4.1 is bad due to https://github.com/conda-forge/testpath-feedstock/issues/7 @@ -30,3 +30,4 @@ dependencies: - pip: - websockets>=7.0 - broadbean>=0.9.1 + - ruamel.yaml diff --git a/qcodes/__init__.py b/qcodes/__init__.py index f09c3ff247e..d012a9c49ab 100644 --- a/qcodes/__init__.py +++ b/qcodes/__init__.py @@ -63,6 +63,7 @@ ArrayParameter, MultiParameter, ParameterWithSetpoints, + DelegateParameter, StandardParameter, ManualParameter, ScaledParameter, diff --git a/qcodes/config/qcodesrc.json b/qcodes/config/qcodesrc.json index 611a6d3f7a7..550798cdb2c 100644 --- a/qcodes/config/qcodesrc.json +++ b/qcodes/config/qcodesrc.json @@ -62,10 +62,11 @@ "scriptfolder": ".", "mainfolder": "." }, - "station_configurator": { + "station": { "enable_forced_reconnect": false, "default_folder": ".", - "default_file": "instrument_config.yml" + "default_file": null, + "use_monitor": false }, "GUID_components": { "location": 0, diff --git a/qcodes/config/qcodesrc_schema.json b/qcodes/config/qcodesrc_schema.json index a64b5f480b6..e2527efcfb3 100644 --- a/qcodes/config/qcodesrc_schema.json +++ b/qcodes/config/qcodesrc_schema.json @@ -235,26 +235,31 @@ }, "description": "Optional feature for qdev-wrappers package: controls user settings of qcodes" }, - "station_configurator": { + "station": { "type": "object", "properties": { "enable_forced_reconnect": { "type": "boolean", "default": false, - "description": "if set to true, on instantiation of an existing instrument, the existing will be disconnected." + "description": "If set to true, on instantiation of an existing instrument from station configuration, the existing instrument instance will be closed." }, "default_folder": { "type": ["string", "null"], "default": null, - "description": "default folder where to look for a station configurator config file" + "description": "Default folder where to look for a YAML station configuration file" }, "default_file": { "type": ["string", "null"], "default": null, - "description": "default file name, specifying the file to load, when none is specified. Can be a relative or absolute path" + "description": "Default file name, specifying a YAML station configuration file to load, when none is specified. The path can be absolute, relative to the current folder or relative to the default folder as specified in the qcodes config." + }, + "use_monitor": { + "type": "boolean", + "default": false, + "description": "Update the monitor based on the monitor attribute specified in the instruments section of the station config yaml file." } }, - "description": "Optional feature for qdev-wrappers package: Setting for the StationConfigurator." + "description": "Settings for QCoDeS Station." }, "GUID_components":{ "type": "object", diff --git a/qcodes/dataset/descriptions.py b/qcodes/dataset/descriptions.py index a7a268a7282..4959d56757e 100644 --- a/qcodes/dataset/descriptions.py +++ b/qcodes/dataset/descriptions.py @@ -2,9 +2,11 @@ from typing import Dict, Any, Union, cast import json +from qcodes.utils.helpers import YAML from qcodes.dataset.dependencies import (InterDependencies, InterDependencies_, new_to_old) + SomeDeps = Union[InterDependencies, InterDependencies_] @@ -81,26 +83,10 @@ def _is_description_old_style(serialized_object: Dict[str, Any]) -> bool: else: return False - @staticmethod - def _ruamel_importer(): - try: - from ruamel_yaml import YAML - except ImportError: - try: - from ruamel.yaml import YAML - except ImportError: - raise ImportError('No ruamel module found. Please install ' - 'either ruamel.yaml or ruamel_yaml to ' - 'use the methods to_yaml and from_yaml') - return YAML - def to_yaml(self) -> str: """ Output the run description as a yaml string """ - - YAML = self._ruamel_importer() - yaml = YAML() with io.StringIO() as stream: yaml.dump(self.serialize(), stream=stream) @@ -120,9 +106,6 @@ def from_yaml(cls, yaml_str: str) -> 'RunDescriber': Parse a yaml string (the return of `to_yaml`) into a RunDescriber object """ - - YAML = cls._ruamel_importer() - yaml = YAML() # yaml.load returns an OrderedDict, but we need a dict ser = dict(yaml.load(yaml_str)) diff --git a/qcodes/instrument/parameter.py b/qcodes/instrument/parameter.py index 3d2b594f50e..cc77bc28ec9 100644 --- a/qcodes/instrument/parameter.py +++ b/qcodes/instrument/parameter.py @@ -12,8 +12,8 @@ to all parameter types, such as ramping and scaling of values, adding delays (see documentation for details). -This module defines four classes of parameters as well as some more specialized -ones: +This module defines the following basic classes of parameters as well as some +more specialized ones: - :class:`.Parameter` is the base class for scalar-valued parameters. Two primary ways in which it can be used: @@ -34,6 +34,11 @@ legacy :class:`qcodes.loops.Loop` and :class:`qcodes.measure.Measure` measurement types. +- :class:`.DelegateParameter` is intended proxy-ing other parameters. + It forwards its ``get`` and ``set`` to the underlying source parameter, + while allowing to specify label/unit/etc that is different from the + source parameter. + - :class:`.ArrayParameter` is an older base class for array-valued parameters. For any new driver we strongly recommend using :class:`.ParameterWithSetpoints` which is both more flexible and @@ -930,8 +935,11 @@ def __init__(self, name: str, **kwargs) -> None: super().__init__(name=name, instrument=instrument, vals=vals, **kwargs) - # Enable set/get methods if get_cmd/set_cmd is given - # Called first so super().__init__ can wrap get/set methods + # Enable set/get methods from get_cmd/set_cmd if given and + # no `get`/`set` or `get_raw`/`set_raw` methods have been defined + # in the scope of this class. + # (previous call to `super().__init__` wraps existing get_raw/set_raw to + # get/set methods) if not hasattr(self, 'get') and get_cmd is not False: if get_cmd is None: if max_val_age is not None: @@ -1130,6 +1138,58 @@ def validate(self, value: ParamDataType) -> None: super().validate(value) +class DelegateParameter(Parameter): + """ + The `DelegateParameter` wraps a given `source`-parameter. Setting/getting + it results in a set/get of the source parameter with the provided + arguments. + + The reason for using a `DelegateParameter` instead of the source parameter + is to provide all the functionality of the Parameter base class without + overwriting properties of the source: for example to set a different + Scaling factor and unit on the `DelegateParameter` without changing those + in the source parameter + """ + + def __init__(self, name: str, source: Parameter, *args, **kwargs): + self.source = source + + for ka, param in zip(('unit', 'label', 'snapshot_value'), + ('unit', 'label', '_snapshot_value')): + kwargs[ka] = kwargs.get(ka, getattr(self.source, param)) + + for cmd in ('set_cmd', 'get_cmd'): + if cmd in kwargs: + raise KeyError(f'It is not allowed to set "{cmd}" of a ' + f'DelegateParameter because the one of the ' + f'source parameter is supposed to be used.') + + super().__init__(name, *args, **kwargs) + + # Disable the warnings until MultiParameter has been + # replaced and name/label/unit can live in _BaseParameter + # pylint: disable=method-hidden + def get_raw(self): + return self.source.get() + + # same as for `get_raw` + # pylint: disable=method-hidden + def set_raw(self, value): + self.source(value) + + def snapshot_base(self, + update: bool = False, + params_to_skip_update: Sequence[str] = None): + snapshot = super().snapshot_base( + update=update, + params_to_skip_update=params_to_skip_update + ) + snapshot.update( + {'source_parameter': self.source.snapshot(update=update)} + ) + return snapshot + + class ArrayParameter(_BaseParameter): """ A gettable parameter that returns an array of values. diff --git a/qcodes/station.py b/qcodes/station.py index d5c26e66883..a4518756b08 100644 --- a/qcodes/station.py +++ b/qcodes/station.py @@ -1,17 +1,52 @@ """Station objects - collect all the equipment you use to do an experiment.""" -from typing import Dict, List, Optional, Sequence, Any - +from contextlib import suppress +from typing import ( + Dict, List, Optional, Sequence, Any, cast, AnyStr, IO) +from functools import partial +import importlib +import logging +import os +from copy import deepcopy, copy +from collections import UserDict +from typing import Union + +import qcodes from qcodes.utils.metadata import Metadatable -from qcodes.utils.helpers import make_unique, DelegateAttributes +from qcodes.utils.helpers import ( + make_unique, DelegateAttributes, YAML, checked_getattr) -from qcodes.instrument.base import Instrument -from qcodes.instrument.parameter import Parameter -from qcodes.instrument.parameter import ManualParameter -from qcodes.instrument.parameter import StandardParameter +from qcodes.instrument.base import Instrument, InstrumentBase +from qcodes.instrument.parameter import ( + Parameter, ManualParameter, StandardParameter, + DelegateParameter) +import qcodes.utils.validators as validators +from qcodes.monitor.monitor import Monitor from qcodes.actions import _actions_snapshot +log = logging.getLogger(__name__) + +PARAMETER_ATTRIBUTES = ['label', 'unit', 'scale', 'inter_delay', 'post_delay', + 'step', 'offset'] + + +def get_config_enable_forced_reconnect() -> bool: + return qcodes.config["station"]["enable_forced_reconnect"] + + +def get_config_default_folder() -> Optional[str]: + return qcodes.config["station"]["default_folder"] + + +def get_config_default_file() -> Optional[str]: + return qcodes.config["station"]["default_file"] + + +def get_config_use_monitor() -> Optional[str]: + return qcodes.config["station"]["use_monitor"] + + class Station(Metadatable, DelegateAttributes): """ @@ -43,11 +78,12 @@ class Station(Metadatable, DelegateAttributes): treated as attributes of self """ - default = None # type: 'Station' + default: Optional['Station'] = None def __init__(self, *components: Metadatable, - monitor: Any=None, default: bool=True, - update_snapshot: bool=True, **kwargs) -> None: + config_file: Optional[str] = None, + use_monitor: Optional[bool] = None, default: bool = True, + update_snapshot: bool = True, **kwargs) -> None: super().__init__(**kwargs) # when a new station is defined, store it in a class variable @@ -59,16 +95,21 @@ def __init__(self, *components: Metadatable, if default: Station.default = self - self.components = {} # type: Dict[str, Metadatable] + self.components: Dict[str, Metadatable] = {} for item in components: self.add_component(item, update_snapshot=update_snapshot) - self.monitor = monitor + self.use_monitor = use_monitor + self.config_file = config_file - self.default_measurement = [] # type: List + self.default_measurement: List[Any] = [] + self._added_methods: List[str] = [] + self._monitor_parameters: List[Parameter] = [] - def snapshot_base(self, update: bool=False, - params_to_skip_update: Sequence[str]=None) -> Dict: + self.load_config_file(self.config_file) + + def snapshot_base(self, update: bool = False, + params_to_skip_update: Sequence[str] = None) -> Dict: """ State of the station as a JSON-compatible dict. @@ -116,8 +157,8 @@ def snapshot_base(self, update: bool=False, return snap - def add_component(self, component: Metadatable, name: str=None, - update_snapshot: bool=True) -> str: + def add_component(self, component: Metadatable, name: str = None, + update_snapshot: bool = True) -> str: """ Record one component as part of this Station. @@ -128,8 +169,8 @@ def add_component(self, component: Metadatable, name: str=None, of each component as it is added to the Station, default true Returns: - str: The name assigned this component, which may have been changed to - make it unique among previously added components. + str: The name assigned this component, which may have been changed + to make it unique among previously added components. """ try: @@ -217,3 +258,245 @@ def __getitem__(self, key): return self.components[key] delegate_attr_dicts = ['components'] + + def load_config_file(self, filename: Optional[str] = None): + """ + Loads a configuration from a YAML file. If `filename` is not specified + the default file name from the qcodes config will be used. + + Loading of a configuration will update the snapshot of the station and + make the instruments described in the config file available for + instantiation with the :meth:`load_instrument` method. + + Additionally the shortcut methods `load_` will be + updated. + """ + def get_config_file_path( + filename: Optional[str] = None) -> Optional[str]: + filename = filename or get_config_default_file() + if filename is None: + return None + search_list = [filename] + if (not os.path.isabs(filename) and + get_config_default_folder() is not None): + config_folder = cast(str, get_config_default_folder()) + search_list += [os.path.join(config_folder, filename)] + for p in search_list: + if os.path.isfile(p): + return p + return None + + path = get_config_file_path(filename) + if path is None: + if filename is not None: + raise FileNotFoundError(path) + else: + if get_config_default_file() is not None: + log.warning( + 'Could not load default config for Station: \n' + f'File {get_config_default_file()} not found. \n' + 'You can change the default config file in ' + '`qcodesrc.json`.') + return + + with open(path, 'r') as f: + self.load_config(f) + + def load_config(self, config: Union[str, IO[AnyStr]]) -> None: + """ + Loads a config from a supplied string or file/stream handle. + + Loading of a configuration will update the snapshot of the station and + make the instruments described in the config file available for + instantiation with the :meth:`load_instrument` method. + + Additionally the shortcut methods `load_` will be + updated. + """ + def update_station_configuration_snapshot(): + class StationConfig(UserDict): + def snapshot(self, update=True): + return self + + self.components['config'] = StationConfig(self._config) + + def update_load_instrument_methods(): + # create shortcut methods to instantiate instruments via + # `load_()` so that autocompletion can be used + # first remove methods that have been added by a previous + # :meth:`load_config_file` call + while len(self._added_methods): + delattr(self, self._added_methods.pop()) + + # add shortcut methods + for instrument_name in self._instrument_config.keys(): + method_name = f'load_{instrument_name}' + if method_name.isidentifier(): + setattr(self, method_name, partial( + self.load_instrument, identifier=instrument_name)) + self._added_methods.append(method_name) + else: + log.warning(f'Invalid identifier: ' + + f'for the instrument {instrument_name} no ' + + f'lazy loading method {method_name} could ' + + 'be created in the Station.') + self._config = YAML().load(config) + self._instrument_config = self._config['instruments'] + update_station_configuration_snapshot() + update_load_instrument_methods() + + def close_and_remove_instrument(self, + instrument: Union[Instrument, str] + ) -> None: + """ + Safely close instrument and remove from station and monitor list + """ + # remove parameters related to this instrument from the + # monitor list + if isinstance(instrument, str): + instrument = Instrument.find_instrument(instrument) + + self._monitor_parameters = [v for v in self._monitor_parameters + if v.root_instrument is not instrument] + # remove instrument from station snapshot + self.remove_component(instrument.name) + # del will remove weakref and close the instrument + instrument.close() + del instrument + + def load_instrument(self, identifier: str, + revive_instance: bool = False, + **kwargs) -> Instrument: + """ + Creates an :class:`~.Instrument` instance as described by the + loaded config file. + + Args: + identifier: the identfying string that is looked up in the yaml + configuration file, which identifies the instrument to be added + revive_instance: If true, try to return an instrument with the + specified name instead of closing it and creating a new one. + **kwargs: additional keyword arguments that get passed on to the + __init__-method of the instrument to be added. + """ + # try to revive the instrument + if revive_instance and Instrument.exist(identifier): + return Instrument.find_instrument(identifier) + + # load file + # try to reload file on every call. This makes script execution a + # little slower but makes the overall workflow more convenient. + self.load_config_file(self.config_file) + + # load from config + if identifier not in self._instrument_config.keys(): + raise RuntimeError(f'Instrument {identifier} not found in ' + 'instrument config file') + instr_cfg = self._instrument_config[identifier] + + # TODO: add validation of config for better verbose errors: + + # check if instrument is already defined and close connection + if instr_cfg.get('enable_forced_reconnect', + get_config_enable_forced_reconnect()): + with suppress(KeyError): + self.close_and_remove_instrument(identifier) + + # instantiate instrument + init_kwargs = instr_cfg.get('init', {}) + # somebody might have a empty init section in the config + init_kwargs = {} if init_kwargs is None else init_kwargs + if 'address' in instr_cfg: + init_kwargs['address'] = instr_cfg['address'] + if 'port' in instr_cfg: + init_kwargs['port'] = instr_cfg['port'] + # make explicitly passed arguments overide the ones from the config + # file. + # We are mutating the dict below + # so make a copy to ensure that any changes + # does not leek into the station config object + # specifically we may be passing non pickleable + # instrument instances via kwargs + instr_kwargs = deepcopy(init_kwargs) + instr_kwargs.update(kwargs) + name = instr_kwargs.pop('name', identifier) + + module = importlib.import_module(instr_cfg['driver']) + instr_class = getattr(module, instr_cfg['type']) + instr = instr_class(name, **instr_kwargs) + + # local function to refactor common code from defining new parameter + # and setting existing one + def resolve_parameter_identifier(instrument: InstrumentBase, + identifier: str) -> Parameter: + + parts = identifier.split('.') + try: + for level in parts[:-1]: + instrument = checked_getattr(instrument, level, + InstrumentBase) + except TypeError: + raise RuntimeError( + f'Cannot resolve `{level}` in {identifier} to an ' + f'instrument/channel for base instrument ' + f'{instrument!r}.') + try: + return checked_getattr(instrument, parts[-1], Parameter) + except TypeError: + raise RuntimeError( + f'Cannot resolve parameter identifier `{identifier}` to ' + f'a parameter on instrument {instrument!r}.') + + def setup_parameter_from_dict(instr: Instrument, name: str, + options: Dict[str, Any]): + parameter = resolve_parameter_identifier(instr, name) + for attr, val in options.items(): + if attr in PARAMETER_ATTRIBUTES: + # set the attributes of the parameter, that map 1 to 1 + setattr(parameter, attr, val) + # extra attributes that need parsing + elif attr == 'limits': + lower, upper = [float(x) for x in val.split(',')] + parameter.vals = validators.Numbers(lower, upper) + elif attr == 'monitor' and val is True: + self._monitor_parameters.append(parameter) + elif attr == 'alias': + setattr(instr, val, parameter) + elif attr == 'initial_value': + # skip value attribute so that it gets set last + # when everything else has been set up + pass + else: + log.warning(f'Attribute {attr} not recognized when ' + f'instatiating parameter \"{parameter.name}\"') + if 'initial_value' in options: + parameter.set(options['initial_value']) + + def add_parameter_from_dict(instr: Instrument, name: str, + options: Dict[str, Any]): + # keep the original dictionray intact for snapshot + options = copy(options) + if 'source' in options: + instr.add_parameter( + name, + DelegateParameter, + source=resolve_parameter_identifier(instr, + options['source'])) + options.pop('source') + else: + instr.add_parameter(name, Parameter) + setup_parameter_from_dict(instr, name, options) + + def update_monitor(): + if ((self.use_monitor is None and get_config_use_monitor()) + or self.use_monitor): + # restart Monitor + Monitor(*self._monitor_parameters) + + for name, options in instr_cfg.get('parameters', {}).items(): + setup_parameter_from_dict(instr, name, options) + for name, options in instr_cfg.get('add_parameters', {}).items(): + add_parameter_from_dict(instr, name, options) + self.add_component(instr) + update_monitor() + return instr diff --git a/qcodes/tests/dataset/test_descriptions.py b/qcodes/tests/dataset/test_descriptions.py index 57f373c3fcb..22fba3c3123 100644 --- a/qcodes/tests/dataset/test_descriptions.py +++ b/qcodes/tests/dataset/test_descriptions.py @@ -2,8 +2,10 @@ from qcodes.dataset.param_spec import ParamSpec from qcodes.dataset.descriptions import RunDescriber +from qcodes.utils.helpers import YAML from qcodes.dataset.dependencies import InterDependencies, old_to_new + @pytest.fixture def some_paramspecs(): """ @@ -85,14 +87,7 @@ def test_serialization_and_back(some_paramspecs): def test_yaml_creation_and_loading(some_paramspecs): - - try: - YAML = RunDescriber._ruamel_importer() - except ImportError: - pytest.skip('No ruamel module installed, skipping test') - yaml = YAML() - for group in some_paramspecs.values(): paramspecs = group.values() idp = InterDependencies(*paramspecs) diff --git a/qcodes/tests/test_config.py b/qcodes/tests/test_config.py index e9c7b620cc1..5de5ff643f5 100644 --- a/qcodes/tests/test_config.py +++ b/qcodes/tests/test_config.py @@ -331,7 +331,7 @@ def test_update_from_path(path_to_config_file_on_disk): # check that the settings NOT specified in our config file on path # are still saved as configurations assert cfg['gui']['notebook'] is True - assert cfg['station_configurator']['default_folder'] == '.' + assert cfg['station']['default_folder'] == '.' expected_path = os.path.join(path_to_config_file_on_disk, 'qcodesrc.json') diff --git a/qcodes/tests/test_parameter.py b/qcodes/tests/test_parameter.py index 1b9448c5b20..b3df2dff702 100644 --- a/qcodes/tests/test_parameter.py +++ b/qcodes/tests/test_parameter.py @@ -14,7 +14,7 @@ from qcodes import Function from qcodes.instrument.parameter import ( Parameter, ArrayParameter, MultiParameter, ManualParameter, - InstrumentRefParameter, ScaledParameter) + InstrumentRefParameter, ScaledParameter, DelegateParameter) import qcodes.utils.validators as vals from qcodes.tests.instrument_mocks import DummyInstrument from qcodes.utils.helpers import create_on_off_val_mapping @@ -1241,9 +1241,64 @@ def test_deprecated_param_warns(): assert a.get() == 11 assert a.get_count == 2 assert a.set_count == 1 - # check that wrapper functionality works e.g stepping is performed correctly + # check that wrapper functionality works e.g stepping is performed + # correctly a.step = 1 a.set(20) assert a.set_count == 1+9 assert a.get() == 20 assert a.get_count == 3 + + +def test_delegate_parameter_init(): + """ + Test that the lable and unit get used from source parameter if not + specified otherwise. + """ + p = Parameter('testparam', set_cmd=None, get_cmd=None, + label='Test Parameter', unit='V') + d = DelegateParameter('test_delegate_parameter', p) + assert d.label == p.label + assert d.unit == p.unit + + d = DelegateParameter('test_delegate_parameter', p, unit='Ohm') + assert d.label == p.label + assert not d.unit == p.unit + assert d.unit == 'Ohm' + + +def test_delegate_parameter_get_set_raises(): + """ + Test that providing a get/set_cmd kwarg raises an error. + """ + p = Parameter('testparam', set_cmd=None, get_cmd=None) + for kwargs in ({'set_cmd': None}, {'get_cmd': None}): + with pytest.raises(KeyError) as e: + DelegateParameter('test_delegate_parameter', p, **kwargs) + assert str(e.value).startswith('\'It is not allowed to set') + + +def test_delegate_parameter_scaling(): + p = Parameter('testparam', set_cmd=None, get_cmd=None, offset=1, scale=2) + d = DelegateParameter('test_delegate_parameter', p, offset=3, scale=5) + + p(1) + assert p.raw_value == 3 + assert d() == (1-3)/5 + + d(2) + assert d.raw_value == 2*5+3 + assert d.raw_value == p() + + +def test_delegate_parameter_snapshot(): + p = Parameter('testparam', set_cmd=None, get_cmd=None, + offset=1, scale=2, initial_value=1) + d = DelegateParameter('test_delegate_parameter', p, offset=3, scale=5, + initial_value=2) + + snapshot = d.snapshot() + source_snapshot = snapshot.pop('source_parameter') + assert source_snapshot == p.snapshot() + assert snapshot['value'] == 2 + assert source_snapshot['value'] == 13 diff --git a/qcodes/tests/test_station.py b/qcodes/tests/test_station.py index 3ffcefb886e..14cdf281dc6 100644 --- a/qcodes/tests/test_station.py +++ b/qcodes/tests/test_station.py @@ -1,11 +1,26 @@ import pytest - +import tempfile +import json +from pathlib import Path +from typing import Optional + +import qcodes +import qcodes.utils.validators as validators +from qcodes.utils.helpers import get_qcodes_path +from qcodes.instrument.parameter import DelegateParameter from qcodes import Instrument from qcodes.station import Station -from qcodes.tests.instrument_mocks import DummyInstrument -from qcodes.tests.test_combined_par import DumyPar from qcodes.instrument.parameter import Parameter +from qcodes.monitor.monitor import Monitor +from qcodes.tests.instrument_mocks import ( + DummyInstrument) +from qcodes.tests.test_combined_par import DumyPar +from qcodes.tests.test_config import default_config +@pytest.fixture(autouse=True) +def use_default_config(): + with default_config(): + yield @pytest.fixture(autouse=True) def set_default_station_to_none(): @@ -201,3 +216,420 @@ def test_station_after_instrument_is_closed(): with pytest.raises(KeyError, match='Component bob is not part of the ' 'station'): station.remove_component('bob') + +@pytest.fixture +def example_station_config(): + """ + Returns path to temp yaml file with station config. + """ + sims_path = get_qcodes_path('instrument', 'sims') + test_config = f""" +instruments: + lakeshore: + driver: qcodes.instrument_drivers.Lakeshore.Model_336 + type: Model_336 + enable_forced_reconnect: true + address: GPIB::2::65535::INSTR + init: + visalib: '{sims_path}lakeshore_model336.yaml@sim' + mock_dac: + driver: qcodes.tests.instrument_mocks + type: DummyInstrument + enable_forced_reconnect: true + init: + gates: {{"ch1", "ch2"}} + parameters: + ch1: + monitor: true + mock_dac2: + driver: qcodes.tests.instrument_mocks + type: DummyInstrument + """ + with tempfile.TemporaryDirectory() as tmpdirname: + filename = Path(tmpdirname, 'station_config.yaml') + with filename.open('w') as f: + f.write(test_config) + yield str(filename) + + +def test_dynamic_reload_of_file(example_station_config): + st = Station(config_file=example_station_config) + mock_dac = st.load_instrument('mock_dac') + assert 'ch1' in mock_dac.parameters + with open(example_station_config, 'r') as f: + filedata = f.read().replace('ch1', 'gate1') + with open(example_station_config, 'w') as f: + f.write(filedata) + mock_dac = st.load_instrument('mock_dac') + assert 'ch1' not in mock_dac.parameters + assert 'gate1' in mock_dac.parameters + + +def station_from_config_str(config: str) -> Station: + st = Station(config_file=None) + st.load_config(config) + return st + + +def station_config_has_been_loaded(st: Station) -> bool: + return "config" in st.components.keys() + + +@pytest.fixture +def example_station(example_station_config): + return Station(config_file=example_station_config) + + +# instrument loading related tests +def test_station_config_path_resolution(example_station_config): + config = qcodes.config["station"] + + # There is no default yaml file present that defines a station + # so we expect the station config not to be loaded. + assert not station_config_has_been_loaded(Station()) + + path = Path(example_station_config) + config["default_file"] = str(path) + # Now the default file with the station configuration is specified, and + # this file exists, hence we expect the Station to have the station + # configuration loaded upon initialization. + assert station_config_has_been_loaded(Station()) + + config["default_file"] = path.name + config["default_folder"] = str(path.parent) + # Here the default_file setting contains only the file name, and the + # default_folder contains the path to the folder where this file is + # located, hence we again expect that the station configuration is loaded + # upon station initialization. + assert station_config_has_been_loaded(Station()) + + config["default_file"] = 'random.yml' + config["default_folder"] = str(path.parent) + # In this case, the station configuration file specified in the qcodes + # config does not exist, hence the initialized station is not expected to + # have station configuration loaded. + assert not station_config_has_been_loaded(Station()) + + config["default_file"] = str(path) + config["default_folder"] = r'C:\SomeOtherFolder' + # In this case, the default_file setting of the qcodes config contains + # absolute path to the station configuration file, while the default_folder + # setting is set to some non-existent folder. + # In this situation, the value of the default_folder will be ignored, + # but because the file specified in default_file setting exists, + # the station will be initialized with the loaded configuration. + assert station_config_has_been_loaded(Station()) + + config["default_file"] = None + config["default_folder"] = str(path.parent) + # When qcodes config has only the default_folder setting specified to an + # existing folder, and default_file setting is not specified, then + # passing the name of a station configuration file, that exists in that + # default_folder, as an argument to the Station is expected to result + # in a station with loaded configuration. + assert station_config_has_been_loaded(Station(config_file=path.name)) + + config["default_file"] = None + config["default_folder"] = None + # In case qcodes config does not have default_file and default_folder + # settings specified, passing an absolute file path as an argument to the + # station is expected to result in a station with loaded configuration. + assert station_config_has_been_loaded(Station(config_file=str(path))) + + +def test_station_configuration_is_a_component_of_station(example_station): + assert station_config_has_been_loaded(example_station) + +@pytest.fixture +def simple_mock_station(): + yield station_from_config_str( + """ +instruments: + mock: + driver: qcodes.tests.instrument_mocks + type: DummyInstrument + """) + +def test_simple_mock_config(simple_mock_station): + st = simple_mock_station + assert station_config_has_been_loaded(st) + assert hasattr(st, 'load_mock') + mock_snapshot = st.snapshot()['components']['config']\ + ['instruments']['mock'] + assert mock_snapshot['driver'] == "qcodes.tests.instrument_mocks" + assert mock_snapshot['type'] == "DummyInstrument" + assert 'mock' in st.config['instruments'] + + +def test_simple_mock_load_mock(simple_mock_station): + st = simple_mock_station + mock = st.load_mock() + assert isinstance(mock, DummyInstrument) + assert mock.name == 'mock' + assert st.components['mock'] is mock + + +def test_simple_mock_load_instrument(simple_mock_station): + st = simple_mock_station + mock = st.load_instrument('mock') + assert isinstance(mock, DummyInstrument) + assert mock.name == 'mock' + assert st.components['mock'] is mock + + +def test_enable_force_reconnect() -> None: + def get_instrument_config(enable_forced_reconnect: Optional[bool]) -> str: + return f""" +instruments: + mock: + driver: qcodes.tests.instrument_mocks + type: DummyInstrument + {f'enable_forced_reconnect: {enable_forced_reconnect}' + if enable_forced_reconnect is not None else ''} + init: + gates: {{"ch1", "ch2"}} + """ + + def assert_on_reconnect(*, use_user_cfg: Optional[bool], + use_instr_cfg: Optional[bool], + expect_failure: bool) -> None: + qcodes.config["station"]\ + ['enable_forced_reconnect'] = use_user_cfg + st = station_from_config_str( + get_instrument_config(use_instr_cfg)) + st.load_instrument('mock') + if expect_failure: + with pytest.raises(KeyError) as excinfo: + st.load_instrument('mock') + assert ("Another instrument has the name: mock" + in str(excinfo.value)) + else: + st.load_instrument('mock') + Instrument.close_all() + + for use_user_cfg in [None, True, False]: + assert_on_reconnect(use_user_cfg=use_user_cfg, + use_instr_cfg=False, + expect_failure=True) + assert_on_reconnect(use_user_cfg=use_user_cfg, + use_instr_cfg=True, + expect_failure=False) + + assert_on_reconnect(use_user_cfg=True, + use_instr_cfg=None, + expect_failure=False) + assert_on_reconnect(use_user_cfg=False, + use_instr_cfg=None, + expect_failure=True) + assert_on_reconnect(use_user_cfg=None, + use_instr_cfg=None, + expect_failure=True) + + +def test_revive_instance(): + st = station_from_config_str(""" +instruments: + mock: + driver: qcodes.tests.instrument_mocks + type: DummyInstrument + enable_forced_reconnect: true + init: + gates: {"ch1"} + """) + mock = st.load_instrument('mock') + mock2 = st.load_instrument('mock') + assert mock is not mock2 + assert mock is not st.mock + assert mock2 is st.mock + + mock3 = st.load_instrument('mock', revive_instance=True) + assert mock3 == mock2 + assert mock3 == st.mock + + +def test_init_parameters(): + st = station_from_config_str(""" +instruments: + mock: + driver: qcodes.tests.instrument_mocks + type: DummyInstrument + enable_forced_reconnect: true + init: + gates: {"ch1", "ch2"} + """) + mock = st.load_instrument('mock') + for ch in ["ch1", "ch2"]: + assert ch in mock.parameters.keys() + assert len(mock.parameters) == 3 # there is also IDN + + # Overwrite parameter + mock = st.load_instrument('mock', gates=["TestGate"]) + assert "TestGate" in mock.parameters.keys() + assert len(mock.parameters) == 2 # there is also IDN + + + # test address + sims_path = get_qcodes_path('instrument', 'sims') + st = station_from_config_str(f""" +instruments: + lakeshore: + driver: qcodes.instrument_drivers.Lakeshore.Model_336 + type: Model_336 + enable_forced_reconnect: true + address: GPIB::2::INSTR + init: + visalib: '{sims_path}lakeshore_model336.yaml@sim' + """) + st.load_instrument('lakeshore') + + +def test_name_init_kwarg(simple_mock_station): + # special case of `name` as kwarg + st = simple_mock_station + mock = st.load_instrument('mock', name='test') + assert mock.name == 'test' + assert st.components['test'] is mock + + +def test_setup_alias_parameters(): + st = station_from_config_str(""" +instruments: + mock: + driver: qcodes.tests.instrument_mocks + type: DummyInstrument + enable_forced_reconnect: true + init: + gates: {"ch1"} + parameters: + ch1: + unit: mV + label: main gate + scale: 2 + offset: 1 + limits: -10, 10 + alias: gate_a + initial_value: 9 + + """) + mock = st.load_instrument('mock') + p = getattr(mock, 'gate_a') + assert isinstance(p, qcodes.Parameter) + assert p.unit == 'mV' + assert p.label == 'main gate' + assert p.scale == 2 + assert p.offset == 1 + assert isinstance(p.vals, validators.Numbers) + assert str(p.vals) == '' + assert p() == 9 + mock.ch1(1) + assert p() == 1 + p(3) + assert mock.ch1() == 3 + assert p.raw_value == 7 + assert mock.ch1.raw_value == 7 + +def test_setup_delegate_parameters(): + st = station_from_config_str(""" +instruments: + mock: + driver: qcodes.tests.instrument_mocks + type: DummyInstrument + enable_forced_reconnect: true + init: + gates: {"ch1"} + parameters: + ch1: + unit: V + label: ch1 + scale: 1 + offset: 0 + limits: -10, 10 + add_parameters: + gate_a: + source: ch1 + unit: mV + label: main gate + scale: 2 + offset: 1 + limits: -6, 6 + initial_value: 2 + + """) + mock = st.load_instrument('mock') + p = getattr(mock, 'gate_a') + assert isinstance(p, DelegateParameter) + assert p.unit == 'mV' + assert p.label == 'main gate' + assert p.scale == 2 + assert p.offset == 1 + assert isinstance(p.vals, validators.Numbers) + assert str(p.vals) == '' + assert p() == 2 + assert mock.ch1.unit == 'V' + assert mock.ch1.label == 'ch1' + assert mock.ch1.scale == 1 + assert mock.ch1.offset == 0 + assert isinstance(p.vals, validators.Numbers) + assert str(mock.ch1.vals) == '' + assert mock.ch1() == 5 + mock.ch1(7) + assert p() == 3 + assert p.raw_value == 7 + assert mock.ch1.raw_value == 7 + assert (json.dumps(mock.ch1.snapshot()) == + json.dumps(p.snapshot()['source_parameter'])) + + +def test_channel_instrument(): + """Test that parameters from instrument's submodule also get configured correctly""" + st = station_from_config_str(""" +instruments: + mock: + driver: qcodes.tests.instrument_mocks + type: DummyChannelInstrument + enable_forced_reconnect: true + parameters: + A.temperature: + unit: mK + add_parameters: + T: + source: A.temperature + """) + mock = st.load_instrument('mock') + assert mock.A.temperature.unit == 'mK' + assert mock.T.unit == 'mK' + + +def test_monitor_not_loaded_by_default(example_station_config): + st = Station(config_file=example_station_config) + st.load_instrument('mock_dac') + assert Monitor.running is None + + +def test_monitor_loaded_if_specified(example_station_config): + st = Station(config_file=example_station_config, use_monitor=True) + st.load_instrument('mock_dac') + assert Monitor.running is not None + assert len(Monitor.running._parameters) == 1 + assert Monitor.running._parameters[0].name == 'ch1' + Monitor.running.stop() + + +def test_monitor_loaded_by_default_if_in_config(example_station_config): + qcodes.config["station"]['use_monitor'] = True + st = Station(config_file=example_station_config) + st.load_instrument('mock_dac') + assert Monitor.running is not None + assert len(Monitor.running._parameters) == 1 + assert Monitor.running._parameters[0].name == 'ch1' + Monitor.running.stop() + + +def test_monitor_not_loaded_if_specified(example_station_config): + st = Station(config_file=example_station_config, use_monitor=False) + st.load_instrument('mock_dac') + assert Monitor.running is None + + + + diff --git a/qcodes/utils/helpers.py b/qcodes/utils/helpers.py index a4703c1248d..3bbcacd545a 100644 --- a/qcodes/utils/helpers.py +++ b/qcodes/utils/helpers.py @@ -7,7 +7,7 @@ import os from collections.abc import Iterator, Sequence, Mapping from copy import deepcopy -from typing import Dict, List, Any +from typing import Dict, Any, TypeVar, Type, List from contextlib import contextmanager from asyncio import iscoroutinefunction from inspect import signature @@ -16,6 +16,7 @@ import numpy as np +import qcodes from qcodes.utils.deprecate import deprecate @@ -392,9 +393,9 @@ class DelegateAttributes: 2. keys of each dict in delegate_attr_dicts (in order) 3. attributes of each object in delegate_attr_objects (in order) """ - delegate_attr_dicts = [] # type: List[str] - delegate_attr_objects = [] # type: List[str] - omit_delegate_attrs = [] # type: List[str] + delegate_attr_dicts: List[str] = [] + delegate_attr_objects: List[str] = [] + omit_delegate_attrs: List[str] = [] def __getattr__(self, key): if key in self.omit_delegate_attrs: @@ -703,3 +704,43 @@ def abstractmethod(funcobj): funcobj.__qcodes_is_abstract_method__ = True return funcobj + +def _ruamel_importer(): + try: + from ruamel_yaml import YAML + except ImportError: + try: + from ruamel.yaml import YAML + except ImportError: + raise ImportError('No ruamel module found. Please install ' + 'either ruamel.yaml or ruamel_yaml.') + return YAML + +# YAML module to be imported. Resovles naming issues of YAML from pypi and +# anaconda +YAML = _ruamel_importer() + + +def get_qcodes_path(*subfolder) -> str: + """ + Return full file path of the QCoDeS module. Additional arguments will be + appended as subfolder. + + """ + path = os.sep.join(qcodes.__file__.split(os.sep)[:-1]) + return os.path.join(path, *subfolder) + os.sep + + +X = TypeVar('X') + + +def checked_getattr(instance: Any, + attribute: str, + expected_type: Type[X]) -> X: + """ + Like `getattr` but raises type error if not of expected type. + """ + attr: Any = getattr(instance, attribute) + if not isinstance(attr, expected_type): + raise TypeError() + return attr diff --git a/requirements.txt b/requirements.txt index 1ca4a326446..0e6543a309b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,6 +6,7 @@ h5py==2.9.0 PyQt5==5.9.* QtPy jsonschema +ruamel.yaml pyzmq>=16.0.2 broadbean>=0.9.1 wrapt diff --git a/setup.py b/setup.py index 7cfd93514e0..6d63f16cf8f 100644 --- a/setup.py +++ b/setup.py @@ -67,6 +67,7 @@ def readme(): 'h5py>=2.6', 'websockets>=7.0', 'jsonschema', + 'ruamel.yaml', 'pyzmq', 'wrapt', 'pandas', @@ -125,4 +126,4 @@ def readme(): print(valueerror_template.format( module_name, module.__version__, min_version, extra)) except: - print(othererror_template.format(module_name)) \ No newline at end of file + print(othererror_template.format(module_name))