diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 00000000000..94a25f7f4cb
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/examples/driver_examples/Qcodes example with AMI430.ipynb b/docs/examples/driver_examples/Qcodes example with AMI430.ipynb
new file mode 100644
index 00000000000..a78b304679b
--- /dev/null
+++ b/docs/examples/driver_examples/Qcodes example with AMI430.ipynb
@@ -0,0 +1,454 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "*Testing on mock hardware*\n",
+ "\n",
+ "We have performed a lot of stand alone tests (tests with mocked hardware) in qcodes/tests/test_ami430.py. In particular, we have tested: \n",
+ "- If the driver remembers the internal setpoints if these are given in cartesian, spherical and cylindrical coordinates\n",
+ "- Check that we send to correct setpoint instructions to the individual instruments if inputs are cartesian, spherical or cylindrical coordinates\n",
+ "- Test that we can call the measured parameters (e.g. cartesian_measured) without exceptions occuring. \n",
+ "- Check that instruments which need to ramp down are given adjustment instructions first\n",
+ "- Check that field limit exceptions are raised properly\n",
+ "- Check that the driver remembers theta and phi coordinates even if the vector norm is zero. \n",
+ "- Test that a warning is issued when the maximum ramp rate is increased \n",
+ "- Test that an exception is raised when we try to set a ramp rate which is higher then the maximum allowed value.\n",
+ "\n",
+ "Furthermore, in qcodes/tests/test_field_vector.py we have tested if the cartesian to spherical/cylindircal coordinate transformations and vis-versa has been correctly implemented by asserting symmetry rules. \n",
+ "\n",
+ "*Testing on actual hardware*\n",
+ "\n",
+ "However, we also need to test with actual hardware, which is documented in this notebook. On individual instruments we need to run the following tests: \n",
+ "- Test if communcation is established with the individual instruments\n",
+ "- Test that we can correctly set current values of individual sources\n",
+ "- Test that we can correctly measure current values of individual sources \n",
+ "- Test that we can put the sources in paused mode \n",
+ "- Test that the ramp rates are properly set \n",
+ "\n",
+ "With the 3D driver, we will test the following: \n",
+ "- Test that the correct set points are reached if we give inputs in cartesian, spherical or cylindrical coordinates \n",
+ "- Test that we can set theta and phi to non-zero values which are remembered if r is ramped from zero to r > 0. \n",
+ "- Test that instruments are ramped up and down in the correct order, with ramp downs occuring before ramp ups. \n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import time\n",
+ "import numpy as np\n",
+ "import matplotlib.pyplot as plt \n",
+ "\n",
+ "from qcodes.instrument_drivers.american_magnetics.AMI430 import AMI430, AMI430_3D\n",
+ "from qcodes.math.field_vector import FieldVector"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Connected to: AMERICAN MAGNETICS INC. 430 (serial:2.01, firmware:None) in 0.60s\n",
+ "Connected to: AMERICAN MAGNETICS INC. 430 (serial:2.01, firmware:None) in 0.62s\n",
+ "Connected to: AMERICAN MAGNETICS INC. 430 (serial:2.01, firmware:None) in 0.57s\n"
+ ]
+ }
+ ],
+ "source": [
+ "# Check if we can establish communication with the power sources\n",
+ "ix = AMI430(\"x\", address=\"169.254.146.116\", port=7180)\n",
+ "iy = AMI430(\"y\", address=\"169.254.136.91\", port=7180)\n",
+ "iz = AMI430(\"z\", address=\"169.254.21.127\", port=7180)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "coil constant = 0.076 T/A\n",
+ "current rating = 79.1 A\n",
+ "current ramp rate limit = 0.06 A/s\n"
+ ]
+ }
+ ],
+ "source": [
+ "# lets test an individual instrument first. We select the z axis. \n",
+ "instrument = iz\n",
+ "\n",
+ "# Since the set method of the driver only excepts fields in Tesla and we want to check if the correct \n",
+ "# currents are applied, we need to convert target currents to target fields. For this reason we need \n",
+ "# the coil constant. \n",
+ "coil_const = instrument._coil_constant\n",
+ "current_rating = instrument._current_rating\n",
+ "current_ramp_limit = instrument._current_ramp_limit\n",
+ "print(\"coil constant = {} T/A\".format(coil_const))\n",
+ "print(\"current rating = {} A\".format(current_rating))\n",
+ "print(\"current ramp rate limit = {} A/s\".format(current_ramp_limit))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Target field is 0.076 T\n",
+ "Measured field is 0.07604 T\n",
+ "Measured current is = 1.0005263157894737 A\n"
+ ]
+ }
+ ],
+ "source": [
+ "# Let see if we can set and get the field in Tesla \n",
+ "target_current = 1.0 # [A] The current we want to set \n",
+ "target_field = coil_const * target_current # [T]\n",
+ "print(\"Target field is {} T\".format(target_field))\n",
+ "instrument.field(target_field)\n",
+ "\n",
+ "field = instrument.field() # This gives us the measured field\n",
+ "print(\"Measured field is {} T\".format(field))\n",
+ "# The current should be \n",
+ "current = field / coil_const\n",
+ "print(\"Measured current is = {} A\".format(current))\n",
+ "# We have verified with manual inspection that the current has indeed ben reached "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 24,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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\nEoSIiET0/1ZMKCB1nHB9AAAAAElFTkSuQmCC\n",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "slope = 0.9993355432517128. A value close to one means the correct ramp times are used\n",
+ "offset = 7.364001916301564. An offset indicates that there is a fixed delay is added to a ramp request\n"
+ ]
+ }
+ ],
+ "source": [
+ "# Verify that the ramp rate is indeed how it is specified \n",
+ "ramp_rate = instrument.ramp_rate() # get the ramp rate\n",
+ "instrument.field(0) # make sure we are back at zero amps\n",
+ "\n",
+ "target_fields = [0.1, 0.3, 0.7, 1.5] # [T]\n",
+ "t_setting = []\n",
+ "t_actual = []\n",
+ "\n",
+ "for target_field in target_fields:\n",
+ "\n",
+ " current_field = instrument.field()\n",
+ " ts = abs(target_field - current_field) / ramp_rate\n",
+ " t_setting.append(ts)\n",
+ " \n",
+ " tb = time.time()\n",
+ " instrument.field(target_field)\n",
+ " te = time.time()\n",
+ " ta = te - tb\n",
+ " t_actual.append(ta)\n",
+ "\n",
+ "fig, ax = plt.subplots()\n",
+ "ax.plot(t_setting, t_actual, \".-\")\n",
+ "plt.xlabel(\"ramp time calculated from settings [s]\")\n",
+ "plt.ylabel(\"measured ramp time [s]\")\n",
+ "plt.show()\n",
+ "slope, offset = np.polyfit(t_setting, t_actual, 1)\n",
+ "print(\"slope = {}. A value close to one means the correct ramp times are used\".format(slope))\n",
+ "print(\"offset = {}. An offset indicates that there is a fixed delay is added to a ramp request\".format(offset))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "metadata": {
+ "collapsed": true
+ },
+ "outputs": [],
+ "source": [
+ "# Lets move back to zero Amp\n",
+ "instrument.field(0)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Lets test the 3D driver now. \n",
+ "field_limit = [ # If any of the field limit functions are satisfied we are in the safe zone.\n",
+ " lambda x, y, z: x == 0 and y == 0 and z < 3, # We can have higher field along the z-axis if x and y are zero.\n",
+ " lambda x, y, z: np.linalg.norm([x, y, z]) < 2\n",
+ "]\n",
+ "\n",
+ "i3d = AMI430_3D(\n",
+ " \"AMI430-3D\", \n",
+ " ix,\n",
+ " iy,\n",
+ " iz,\n",
+ " field_limit=field_limit\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {
+ "collapsed": true
+ },
+ "outputs": [],
+ "source": [
+ "def current_to_field(name, current):\n",
+ " \"\"\"We cannot set currents directly, so we need to calculate what fields need to be generated for \n",
+ " the desired currents\"\"\"\n",
+ " instrument = {\"x\": ix, \"y\": iy, \"z\": iz}[name]\n",
+ " coil_constant = instrument._coil_constant # [T/A]\n",
+ " field = current * coil_constant\n",
+ " return field"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 12,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "field target = [0.0386, 0.0121, 0.11399999999999999]\n",
+ "field measured = [0.03816, 0.01211, 0.11384]\n"
+ ]
+ }
+ ],
+ "source": [
+ "# Lets see if we can set a certain current using cartesian coordinates\n",
+ "current_target = [1.0, 0.5, 1.5]\n",
+ "# calculate the fields needed\n",
+ "field_target = [current_to_field(n, v) for n, v in zip([\"x\", \"y\", \"z\"], current_target)]\n",
+ "print(\"field target = {}\".format(field_target))\n",
+ "i3d.cartesian(field_target)\n",
+ "field_measured = i3d.cartesian_measured()\n",
+ "print(\"field measured = {}\".format(field_measured))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 13,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "field target = [0.1209643335863923, 19.536859161547078, 17.404721375291402]\n",
+ "field measured = [0.12077462481829535, 19.424782613847988, 17.532727661391853]\n"
+ ]
+ }
+ ],
+ "source": [
+ "# Lets move back to 0,0,0\n",
+ "i3d.cartesian([0, 0, 0])\n",
+ "\n",
+ "# Lets see if we can set a certain current using spherical coordinates\n",
+ "current_target = [1.0, 0.5, 1.5]\n",
+ "# calculate the fields needed\n",
+ "field_target_cartesian = [current_to_field(n, v) for n, v in zip([\"x\", \"y\", \"z\"], current_target)]\n",
+ "# calculate the field target in spherical coordinates \n",
+ "field_target_spherical = FieldVector(*field_target_cartesian).get_components(\"r\", \"theta\", \"phi\")\n",
+ "\n",
+ "print(\"field target = {}\".format(field_target_spherical))\n",
+ "i3d.spherical(field_target_spherical)\n",
+ "field_measured = i3d.spherical_measured()\n",
+ "print(\"field measured = {}\".format(field_measured))\n",
+ "# Like before, we see that the current target of 1.0, 0.5, 1.5 A for x, y and z have indeed been reached"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 15,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "field target = [0.040452070404368677, 17.404721375291402, 0.11399999999999999]\n",
+ "field measured = [0.040235668255914431, 17.51627743673798, 0.11365]\n"
+ ]
+ }
+ ],
+ "source": [
+ "# Lets move back to 0,0,0\n",
+ "i3d.cartesian([0, 0, 0])\n",
+ "\n",
+ "# Lets see if we can set a certain current using spherical coordinates\n",
+ "current_target = [1.0, 0.5, 1.5]\n",
+ "# calculate the fields needed\n",
+ "field_target_cartesian = [current_to_field(n, v) for n, v in zip([\"x\", \"y\", \"z\"], current_target)]\n",
+ "# calculate the field target in spherical coordinates \n",
+ "field_target_cylindrical = FieldVector(*field_target_cartesian).get_components(\"rho\", \"phi\", \"z\")\n",
+ "\n",
+ "print(\"field target = {}\".format(field_target_cylindrical))\n",
+ "i3d.cylindrical(field_target_cylindrical)\n",
+ "field_measured = i3d.cylindrical_measured()\n",
+ "print(\"field measured = {}\".format(field_measured))\n",
+ "# Like before, we see that the current target of 1.0, 0.5, 1.5 A for x, y and z have indeed been reached"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 16,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "field target = [0.0386, 0.01815, 0.076]\n"
+ ]
+ }
+ ],
+ "source": [
+ "# Test that ramping up and down occurs in the right order. \n",
+ "current_target = [1.0, 0.75, 1.0] # We ramp down the z, ramp up y and keep x the same \n",
+ "# We should see that z is adjusted first, then y. \n",
+ "# calculate the fields needed\n",
+ "field_target = [current_to_field(n, v) for n, v in zip([\"x\", \"y\", \"z\"], current_target)]\n",
+ "print(\"field target = {}\".format(field_target))\n",
+ "i3d.cartesian(field_target)\n",
+ "# Manual inspection has shown that z is indeed ramped down before y is ramped up "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 17,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "error successfully raised. The driver does not let you do stupid stuff\n"
+ ]
+ }
+ ],
+ "source": [
+ "# check that an exception is raised when we try to set a field which is out side of the safety limits \n",
+ "try:\n",
+ " i3d.cartesian([2.1, 0, 0])\n",
+ " print(\"something went wrong... we should not be able to do this :-(\")\n",
+ "except:\n",
+ " print(\"error successfully raised. The driver does not let you do stupid stuff\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 25,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "1: Measured field = [-0.00013, -1e-05, 3e-05] T\n",
+ "1: Theta measured = 98.72079692816342\n",
+ "1: Phi measured = 180.0\n",
+ "2: Measured field = [0.10484, -0.08797, 0.37558] T\n",
+ "2: Theta measured = 20.032264760097508. We see that the input theta of 20.0 has been remembered\n",
+ "2: Phi measured = -40.007172479040996. We see that the input phi of -40.0 has been remembered\n"
+ ]
+ }
+ ],
+ "source": [
+ "# Check that the driver remember theta/phi values if the set point norm is zero\n",
+ "\n",
+ "# lets go back to zero field\n",
+ "i3d.cartesian([0, 0, 0])\n",
+ "# Lets set theta and phi to a non zero value but keep the field magnitude at zero\n",
+ "field_target_spherical = [0, 20.0, -40.0]\n",
+ "i3d.spherical(field_target_spherical)\n",
+ "field_measured = i3d.cartesian_measured()\n",
+ "print(\"1: Measured field = {} T\".format(field_measured))\n",
+ "\n",
+ "# Note that theta_measured and phi_measured will give random values based on noisy current reading \n",
+ "# close to zero (this cannot be avoided)\n",
+ "theta_measured = i3d.theta_measured()\n",
+ "print(\"1: Theta measured = {}\".format(theta_measured))\n",
+ "phi_measured = i3d.phi_measured()\n",
+ "print(\"1: Phi measured = {}\".format(phi_measured))\n",
+ "\n",
+ "# now lets set the r value\n",
+ "i3d.field(0.4)\n",
+ "\n",
+ "field_measured = i3d.cartesian_measured()\n",
+ "print(\"2: Measured field = {} T\".format(field_measured))\n",
+ "# Now the measured angles should be as we intended\n",
+ "theta_measured = i3d.theta_measured()\n",
+ "print(\"2: Theta measured = {}. We see that the input theta of {} has been \"\n",
+ " \"remembered\".format(theta_measured, field_target_spherical[1]))\n",
+ "\n",
+ "phi_measured = i3d.phi_measured()\n",
+ "print(\"2: Phi measured = {}. We see that the input phi of {} has been \"\n",
+ " \"remembered\".format(phi_measured, field_target_spherical[2]))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "collapsed": true
+ },
+ "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.4.5"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/environment.yml b/environment.yml
new file mode 100644
index 00000000000..21e52feaf86
--- /dev/null
+++ b/environment.yml
@@ -0,0 +1,20 @@
+name: qcodes
+channels:
+ - defaults
+ - conda-forge
+dependencies:
+ - h5py=2.6.0
+ - matplotlib=2.0.2
+ - pyqtgraph
+ - python=3.6
+ - jsonschema
+ - pyvisa
+ - QtPy=1.2.0
+ - sip
+ - pyqt=5.6.0
+ - numpy
+ - jupyter=1.0.0
+ - typing
+ - hypothesis
+ - pytest
+ - pytest-runner
\ No newline at end of file
diff --git a/qcodes/instrument/base.py b/qcodes/instrument/base.py
index 77434bd4b96..12a72d2b395 100644
--- a/qcodes/instrument/base.py
+++ b/qcodes/instrument/base.py
@@ -1,16 +1,17 @@
"""Instrument base class."""
import logging
-import numpy as np
import time
import warnings
import weakref
from typing import Sequence
-from qcodes.utils.metadata import Metadatable
+import numpy as np
+
from qcodes.utils.helpers import DelegateAttributes, strip_attrs, full_class
+from qcodes.utils.metadata import Metadatable
from qcodes.utils.validators import Anything
-from .parameter import StandardParameter
from .function import Function
+from .parameter import StandardParameter
class InstrumentBase(Metadatable, DelegateAttributes):
@@ -38,13 +39,38 @@ class InstrumentBase(Metadatable, DelegateAttributes):
such as channel lists or logical groupings of parameters.
Usually populated via ``add_submodule``
"""
- def __init__(self, name, **kwargs):
+
+ def __init__(self, name, testing=False, **kwargs):
self.name = str(name)
+ self._testing = testing
+
+ if testing:
+ if hasattr(type(self), "mocker_class"):
+ mocker_class = type(self).mocker_class
+ self.mocker = mocker_class(name)
+ else:
+ raise ValueError("Testing turned on but no mocker class defined")
+
self.parameters = {}
self.functions = {}
self.submodules = {}
super().__init__(**kwargs)
+ def is_testing(self):
+ """Return True if we are testing"""
+ return self._testing
+
+ def get_mock_messages(self):
+ """
+ For testing purposes we might want to get log messages from the mocker.
+
+ Returns:
+ mocker_messages: list, str
+ """
+ if not self._testing:
+ raise ValueError("Cannot get mock messages if not in testing mode")
+ return self.mocker.get_log_messages()
+
def add_parameter(self, name, parameter_class=StandardParameter,
**kwargs):
"""
@@ -150,12 +176,13 @@ def snapshot_base(self, update: bool=False,
Returns:
dict: base snapshot
"""
- snap = {'functions': dict((name, func.snapshot(update=update))
- for name, func in self.functions.items()),
- 'submodules': dict((name, subm.snapshot(update=update))
- for name, subm in self.submodules.items()),
- '__class__': full_class(self),
- }
+
+ snap = {
+ "functions": {name: func.snapshot(update=update) for name, func in self.functions.items()},
+ "submodules": {name: subm.snapshot(update=update) for name, subm in self.submodules.items()},
+ "__class__": full_class(self)
+ }
+
snap['parameters'] = {}
for name, param in self.parameters.items():
update = update
@@ -335,12 +362,12 @@ class Instrument(InstrumentBase):
_all_instruments = {}
- def __init__(self, name, **kwargs):
+ def __init__(self, name, testing=False, **kwargs):
self._t0 = time.time()
if kwargs.pop('server_name', False):
warnings.warn("server_name argument not supported any more",
stacklevel=0)
- super().__init__(name, **kwargs)
+ super().__init__(name, testing=testing, **kwargs)
self.add_parameter('IDN', get_cmd=self.get_idn,
vals=Anything())
@@ -576,7 +603,10 @@ def write(self, cmd):
including the command and the instrument.
"""
try:
- self.write_raw(cmd)
+ if self._testing:
+ self.mocker.write(cmd)
+ else:
+ self.write_raw(cmd)
except Exception as e:
e.args = e.args + ('writing ' + repr(cmd) + ' to ' + repr(self),)
raise e
@@ -615,7 +645,13 @@ def ask(self, cmd):
including the command and the instrument.
"""
try:
- return self.ask_raw(cmd)
+ if self._testing:
+ answer = self.mocker.ask(cmd)
+ else:
+ answer = self.ask_raw(cmd)
+
+ return answer
+
except Exception as e:
e.args = e.args + ('asking ' + repr(cmd) + ' to ' + repr(self),)
raise e
diff --git a/qcodes/instrument/ip.py b/qcodes/instrument/ip.py
index 8f5808c7a05..0b69643648d 100644
--- a/qcodes/instrument/ip.py
+++ b/qcodes/instrument/ip.py
@@ -36,9 +36,9 @@ class IPInstrument(Instrument):
"""
def __init__(self, name, address=None, port=None, timeout=5,
- terminator='\n', persistent=True, write_confirmation=True,
+ terminator='\n', persistent=True, write_confirmation=True, testing=False,
**kwargs):
- super().__init__(name, **kwargs)
+ super().__init__(name, testing=testing, **kwargs)
self._address = address
self._port = port
@@ -88,7 +88,14 @@ def set_persistent(self, persistent):
else:
self._disconnect()
+ def flush_connection(self):
+ if not self._testing:
+ self._recv()
+
def _connect(self):
+ if self._testing:
+ return
+
if self._socket is not None:
self._disconnect()
diff --git a/qcodes/instrument/mockers/__init__.py b/qcodes/instrument/mockers/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/qcodes/instrument/mockers/ami430.py b/qcodes/instrument/mockers/ami430.py
new file mode 100644
index 00000000000..ab4d61cd88f
--- /dev/null
+++ b/qcodes/instrument/mockers/ami430.py
@@ -0,0 +1,217 @@
+import re
+import time
+from datetime import datetime
+
+
+class MockAMI430:
+ states = {
+ "RAMPING to target field/current": "1",
+ "HOLDING at the target field/current": "2",
+ "PAUSED": "3",
+ "Ramping in MANUAL UP mode": "4",
+ "Ramping in MANUAL DOWN mode": "5",
+ "ZEROING CURRENT (in progress)": "6",
+ "Quench detected": "7",
+ "At ZERO current": "8",
+ "Heating persistent switch": "9",
+ "Cooling persistent switch": "10"
+ }
+
+ field_units = {
+ "tesla": "1",
+ "kilogauss": "0"
+ }
+
+ ramp_rate_units = {
+ "A/s": "0",
+ "A/min": "1"
+ }
+
+ quench_state = {False: "0", True: "1"}
+
+ def __init__(self, name):
+
+ self.name = name
+ self.log_messages = []
+
+ self._field_mag = 0
+ self._field_target = 0
+ self._state = MockAMI430.states["HOLDING at the target field/current"]
+
+ self.handlers = {
+ "RAMP:RATE:UNITS": {
+ "get": MockAMI430.ramp_rate_units["A/s"],
+ "set": None
+ },
+ "FIELD:UNITS": {
+ "get": MockAMI430.field_units["tesla"],
+ "set": None
+ },
+ "*IDN": {
+ "get": "v0.1 Mock",
+ "set": None
+ },
+ "STATE": {
+ "get": self._getter("_state"),
+ "set": self._setter("_state")
+ },
+ "FIELD:MAG": {
+ "get": self._getter("_field_mag"),
+ "set": None
+ },
+ "QU": {
+ "get": MockAMI430.quench_state[False], # We are never in a quenching state so always return the
+ # same value
+ "set": None
+ },
+ "PERS": {
+ "get": "0",
+ "set": None
+ },
+ "PAUSE": {
+ "get": self._is_paused,
+ "set": self._do_pause
+ },
+ "CONF:FIELD:TARG": {
+ "get": None, # To get the field target, send a message "FIELD:TARG?"
+ "set": self._setter("_field_target")
+ },
+ "FIELD:TARG": {
+ "get": self._getter("_field_target"),
+ "set": None
+ },
+ "PS": {
+ "get": "0", # The heater is off
+ "set": None
+ },
+ "RAMP": {
+ "set": self._do_ramp,
+ "get": None
+ },
+ "RAMP:RATE:CURRENT": {
+ "get": "0.1000,50.0000",
+ "set": None
+ },
+ "COIL": {
+ "get": "1",
+ "set": None
+ }
+ }
+
+ @staticmethod
+ def message_parser(gs, msg_str, key):
+ """
+ * If gs = "get":
+ Let suppose key = "RAMP:RATE:UNITS", then if we get msg_str = "RAMP:RATE:UNITS?" then match will be True and
+ args = None. If msg_str = "RAMP:RATE:UNITS:10?" then match = True and args = "10". On the other hand if
+ key = "RAMP" then both "RAMP:RATE:UNITS?" and "RAMP:RATE:UNITS:10?" will cause match to be False
+
+ * If gs = "set"
+ If key = "STATE" and msg_str = "STATE 2,1" then match = True and args = "2,1". If key="STATE" and
+ msg_str = STATE:ELSE 2,1 then match is False.
+
+ Consult [1] for a complete description of the AMI430 protocol.
+
+ [1] http://www.americanmagnetics.com/support/manuals/mn-4Q06125PS-430.pdf
+
+ Parameters:
+ gs (string): "get", or "set"
+ msg_str (string): the message string the mock instrument gets.
+ key (string): one of the keys in self.handlers
+
+ Returns:
+ match (bool): if the key and the msg_str match, then match = True
+ args (string): if any arguments are present in the message string these will be passed along. This is
+ always None when match = False
+
+ """
+ if msg_str == key: # If the message string matches a key exactly we have a match with no arguments
+ return True, None
+
+ # We use regular expressions to find out if the message string and the key match. We need to replace reserved
+ # regular expression characters in the key. For instance replace "*IDN" with "\*IDN".
+ reserved_re_characters = "\^${}[]().*+?|<>-&"
+ for c in reserved_re_characters:
+ key = key.replace(c, "\{}".format(c))
+
+ s = {"get": "(:[^:]*)?\?$", "set": "([^:]+)"}[gs] # Get and set messages use different regular expression
+ # patterns to determine a match
+ search_string = "^" + key + s
+ r = re.search(search_string, msg_str)
+ match = r is not None
+
+ args = None
+ if match:
+ args = r.groups()[0]
+ if args is not None:
+ args = args.strip(":")
+
+ return match, args
+
+ def _getter(self, attribute):
+ return lambda _: getattr(self, attribute)
+
+ def _setter(self, attribute):
+ return lambda value: setattr(self, attribute, value)
+
+ def _log(self, msg):
+
+ now = datetime.now()
+ log_line = "[{}] {}: {}".format(now.strftime("%d:%m:%Y-%H:%M:%S.%f"), self.name, msg)
+ self.log_messages.append(log_line)
+
+ def _handle_messages(self, msg):
+ """
+ Parameters:
+ msg (string): a message received through the socket communication layer
+
+ Returns:
+ rval (string or None): If the type of message requests a value (a get message) then this value is returned
+ by this function. A set message will return a None value.
+ """
+
+ gs = {True: "get", False: "set"}[msg.endswith("?")] # A "get" message ends with a "?" and will invoke the get
+ # part of the handler defined in self.handlers.
+
+ rval = None
+ handler = None
+
+ for key in self.handlers: # Find which handler is suitable to handle the message
+ match, args = MockAMI430.message_parser(gs, msg, key)
+ if not match:
+ continue
+
+ handler = self.handlers[key][gs]
+ if callable(handler):
+ rval = handler(args)
+ else:
+ rval = handler
+
+ break
+
+ if handler is None:
+ self._log("Command {} unknown".format(msg))
+
+ return rval
+
+ def _do_pause(self, _):
+ self._state = MockAMI430.states["PAUSED"]
+
+ def _is_paused(self):
+ return self._state == MockAMI430.states["PAUSED"]
+
+ def _do_ramp(self, _):
+ self._log("Ramping to {}".format(self._field_target))
+ self._state = MockAMI430.states["RAMPING to target field/current"]
+ time.sleep(0.1) # Lets pretend to be ramping for a bit
+ self._field_mag = self._field_target
+ self._state = MockAMI430.states["HOLDING at the target field/current"]
+
+ def get_log_messages(self):
+ return self.log_messages
+
+ def ask(self, msg):
+ return self._handle_messages(msg)
+
+ def write(self, msg):
+ self._handle_messages(msg)
diff --git a/qcodes/instrument_drivers/american_magnetics/AMI430.py b/qcodes/instrument_drivers/american_magnetics/AMI430.py
index 69f4d9b9f60..e56c8b299f7 100644
--- a/qcodes/instrument_drivers/american_magnetics/AMI430.py
+++ b/qcodes/instrument_drivers/american_magnetics/AMI430.py
@@ -1,25 +1,14 @@
+import collections
import logging
-import numpy as np
import time
-
-from qcodes import Instrument, VisaInstrument, IPInstrument
-from qcodes.utils.validators import Numbers, Anything
-
from functools import partial
+import numpy as np
-def R_y(theta):
- """ Construct rotation matrix around y-axis. """
- return np.array([[np.cos(theta), 0, np.sin(theta)],
- [0, 1, 0],
- [-np.sin(theta), 0, np.cos(theta)]])
-
-
-def R_z(theta):
- """ Construct rotation matrix around z-axis. """
- return np.array([[np.cos(theta), -np.sin(theta), 0],
- [np.sin(theta), np.cos(theta), 0],
- [0, 0, 1]])
+from qcodes import Instrument, IPInstrument
+from qcodes.instrument.mockers.ami430 import MockAMI430
+from qcodes.math.field_vector import FieldVector
+from qcodes.utils.validators import Numbers, Anything
class AMI430(IPInstrument):
@@ -39,21 +28,32 @@ class AMI430(IPInstrument):
current_ramp_limit (float): current ramp limit in ampere per second
persistent_switch (bool): whether this magnet has a persistent switch
"""
- def __init__(self, name, address, port, coil_constant, current_rating,
- current_ramp_limit, persistent_switch=True,
- reset=False, terminator='\r\n', **kwargs):
- super().__init__(name, address, port, terminator=terminator,
- write_confirmation=False, **kwargs)
- self._parent_instrument = None
+ mocker_class = MockAMI430
+ default_current_ramp_limit = 0.06 # [A/s]
- self._coil_constant = coil_constant
- self._current_rating = current_rating
- self._current_ramp_limit = current_ramp_limit
- self._persistent_switch = persistent_switch
+ def __init__(self, name, address=None, port=None, persistent_switch=True,
+ reset=False, current_ramp_limit=None, terminator='\r\n', testing=False, **kwargs):
- self._field_rating = coil_constant * current_rating
- self._field_ramp_limit = coil_constant * current_ramp_limit
+ if None in [address, port] and not testing:
+ raise ValueError("The port and address values need to be given if not in testing mode")
+
+ if current_ramp_limit is None:
+ current_ramp_limit = AMI430.default_current_ramp_limit
+
+ elif current_ramp_limit > AMI430.default_current_ramp_limit:
+ warning_message = "Increasing maximum ramp rate: we have a default current ramp rate limit of {dcrl} " \
+ "A/s. We do not want to ramp faster then a set maximum so as to avoid quenching " \
+ "the magnet. A value of {dcrl} A/s seems like a safe, conservative value for any " \
+ "magnet. Change this value at your own responsibility after consulting the specs of " \
+ "your particular magnet".format(dcrl=AMI430.default_current_ramp_limit)
+ raise Warning(warning_message)
+
+ super().__init__(name, address, port, terminator=terminator, testing=testing,
+ write_confirmation=False, **kwargs)
+
+ self._parent_instrument = None
+ # If we are in testing mode there is no need to have pauses built-in when setting field values.
# Make sure the ramp rate time unit is seconds
if int(self.ask('RAMP:RATE:UNITS?')) == 1:
@@ -63,6 +63,19 @@ def __init__(self, name, address, port, coil_constant, current_rating,
if int(self.ask('FIELD:UNITS?')) == 0:
self.write('CONF:FIELD:UNITS 1')
+ ramp_rate_reply = self.ask("RAMP:RATE:CURRENT:1?")
+ current_rating = float(ramp_rate_reply.split(",")[1])
+
+ coil_constant = float(self.ask("COIL?"))
+ self._coil_constant = coil_constant
+ self._current_rating = current_rating
+
+ self._current_ramp_limit = current_ramp_limit
+ self._persistent_switch = persistent_switch
+
+ self._field_rating = coil_constant * current_rating
+ self._field_ramp_limit = coil_constant * current_ramp_limit
+
self.add_parameter('field',
get_cmd='FIELD:MAG?',
get_parser=float,
@@ -80,7 +93,7 @@ def __init__(self, name, address, port, coil_constant, current_rating,
get_cmd=self._get_ramp_rate,
set_cmd=self._set_ramp_rate,
unit='T/s',
- vals=Numbers(0, self._field_ramp_limit))
+ vals=Numbers(0, self._current_ramp_limit))
self.add_parameter('setpoint',
get_cmd='FIELD:TARG?',
@@ -120,11 +133,9 @@ def __init__(self, name, address, port, coil_constant, current_rating,
})
self.add_function('get_error', call_cmd='SYST:ERR?')
-
self.add_function('ramp', call_cmd='RAMP')
self.add_function('pause', call_cmd='PAUSE')
self.add_function('zero', call_cmd='ZERO')
-
self.add_function('reset', call_cmd='*RST')
if reset:
@@ -132,6 +143,13 @@ def __init__(self, name, address, port, coil_constant, current_rating,
self.connect_message()
+ def _sleep(self, t):
+ """Sleep for a number of seconds t. If we are in testing mode, commit this"""
+ if self._testing:
+ return
+ else:
+ time.sleep(t)
+
def _can_start_ramping(self):
"""
Check the current state of the magnet to see if we can start ramping
@@ -157,6 +175,9 @@ def _can_start_ramping(self):
return False
+ def set_field(self, value, *, perform_safety_check=True):
+ self._set_field(value, perform_safety_check=perform_safety_check)
+
def _set_field(self, value, *, perform_safety_check=True):
"""
Blocking method to ramp to a certain field
@@ -169,12 +190,10 @@ def _set_field(self, value, *, perform_safety_check=True):
# If part of a parent driver, set the value using that driver
if np.abs(value) > self._field_rating:
msg = 'Aborted _set_field; {} is higher than limit of {}'
-
raise ValueError(msg.format(value, self._field_rating))
if self._parent_instrument is not None and perform_safety_check:
self._parent_instrument._request_field_change(self, value)
-
return
if self._can_start_ramping():
@@ -189,23 +208,20 @@ def _set_field(self, value, *, perform_safety_check=True):
self.switch_heater_enabled(True)
self.ramp()
-
- time.sleep(0.5)
+ self._sleep(0.5)
# Wait until no longer ramping
while self.ramping_state() == 'ramping':
- time.sleep(0.3)
-
- time.sleep(2.0)
+ self._sleep(0.3)
+ self._sleep(2.0)
state = self.ramping_state()
- # If we are now holding, it was succesful
+ # If we are now holding, it was successful
if state == 'holding':
self.pause()
else:
msg = '_set_field({}) failed with state: {}'
-
raise Exception(msg.format(value, state))
def _ramp_to(self, value):
@@ -258,20 +274,18 @@ def _set_persistent_switch(self, on):
"""
if on:
self.write('PS 1')
-
- time.sleep(0.5)
+ self._sleep(0.5)
# Wait until heating is finished
while self.ramping_state() == 'heating switch':
- time.sleep(0.3)
+ self._sleep(0.3)
else:
self.write('PS 0')
-
- time.sleep(0.5)
+ self._sleep(0.5)
# Wait until cooling is finished
while self.ramping_state() == 'cooling switch':
- time.sleep(0.3)
+ self._sleep(0.3)
def _connect(self):
"""
@@ -280,456 +294,333 @@ def _connect(self):
:return: None
"""
super()._connect()
- print(self._recv())
+ self.flush_connection()
-class AMI430_2D(Instrument):
- """
- Virtual driver for a system of two AMI430 magnet power supplies.
- This driver provides methods that simplify setting fields as vectors.
- Args:
- name (string): a name for the instrument
- magnet_x (AMI430): magnet for the x component
- magnet_y (AMI430): magnet for the y component
- """
- def __init__(self, name, magnet_x, magnet_y, **kwargs):
+class AMI430_3D(Instrument):
+ def __init__(self, name, instrument_x, instrument_y, instrument_z, field_limit, **kwargs):
super().__init__(name, **kwargs)
- self.magnet_x, self.magnet_y = magnet_x, magnet_y
-
- self._angle_offset = 0.0
- self._angle = 0.0
- self._field = 0.0
-
- self.add_parameter('angle_offset',
- get_cmd=self._get_angle_offset,
- set_cmd=self._set_angle_offset,
- unit='deg',
- vals=Numbers())
-
- self.add_parameter('angle',
- get_cmd=self._get_angle,
- set_cmd=self._set_angle,
- unit='deg',
- vals=Numbers())
-
- self.add_parameter('field',
- get_cmd=self._get_field,
- set_cmd=self._set_field,
- unit='T',
- vals=Numbers())
-
- def _get_angle_offset(self):
- return np.degrees(self._angle_offset)
-
- def _set_angle_offset(self, angle):
- # Adjust the field if the offset angle is changed
- if self._angle_offset != np.radians(angle):
- self._angle_offset = np.radians(angle)
- self._set_field(self._field)
-
- def _get_angle(self):
- angle = np.arctan2(self.magnet_y.field(), self.magnet_x.field())
-
- return np.degrees(angle - self._angle_offset)
+ if not isinstance(name, str):
+ raise ValueError("Name should be a string")
- def _set_angle(self, angle):
- self._angle = np.radians(angle)
+ if not all([isinstance(instrument, Instrument) for instrument in [instrument_x, instrument_y, instrument_z]]):
+ raise ValueError("Instruments need to be instances of the class Instrument")
- self._set_field(self._field)
+ self._instrument_x = instrument_x
+ self._instrument_y = instrument_y
+ self._instrument_z = instrument_z
- def _get_field(self):
- x = self.magnet_x.field()
- y = self.magnet_y.field()
-
- return np.sqrt(x**2 + y**2)
-
- def _set_field(self, field):
- self._field = field
-
- B_x = field * np.cos(self._angle + self._angle_offset)
- B_y = field * np.sin(self._angle + self._angle_offset)
-
- # First ramp the magnet that is decreasing in field strength
- if np.abs(self.magnet_x.field()) < np.abs(B_x):
- self.magnet_x.field(B_x)
- self.magnet_y.field(B_y)
+ if repr(field_limit).isnumeric() or isinstance(field_limit, collections.Iterable):
+ self._field_limit = field_limit
else:
- self.magnet_y.field(B_y)
- self.magnet_x.field(B_x)
-
-
-class AMI430_3D(Instrument):
- """
- Virtual driver for a system of three AMI430 magnet power supplies.
- This driver provides methods that simplify setting fields in
- different coordinate systems.
-
- Cartesian, spherical and cylindrical coordinates are supported, with
- the following parameters:
-
- Carthesian: x, y, z
- Spherical: phi, theta, field
- Cylindrical: phi, rho, z
+ raise ValueError("field limit should either be a number or an iterable")
- For the spherical system theta is the polar angle from the positive
- z-axis to the negative z-axis (0 to pi). Phi is the azimuthal angle
- starting at the positive x-axis in the direction of the positive
- y-axis (0 to 2*pi).
-
- In the cylindrical system phi is identical to that in the spherical
- system, and z is identical to that in the cartesian system.
-
- All angles are set and returned in units of degrees and are automatically
- phase-wrapped.
-
- If you want to control the magnets in this virtual driver individually,
- one can set the _parent_instrument parameter of the magnet to None.
- This is done at your own risk, as it skips field strength checks and
- might result in a magnet quench.
-
- Example of instantiation:
-
- magnet = AMI430_3D('AMI430_3D',
- AMI430('AMI430_X', '192.168.2.3', 0.0146, 68.53, 0.2),
- AMI430('AMI430_Y', '192.168.2.2', 0.0426, 70.45, 0.05),
- AMI430('AMI430_Z', '192.168.2.1', 0.1107, 81.33, 0.08),
- field_limit=1.0)
-
- Args:
- name (string): a name for the instrument
- magnet_x (AMI430): magnet driver for the x component
- magnet_y (AMI430): magnet driver for the y component
- magnet_z (AMI430): magnet driver for the z component
- """
- def __init__(self, name, magnet_x, magnet_y, magnet_z, field_limit,
- **kwargs):
- super().__init__(name, **kwargs)
-
- # Register this instrument as the parent of the individual magnets
- for m in [magnet_x, magnet_y, magnet_z]:
- m._parent_instrument = self
-
- self._magnet_x = magnet_x
- self._magnet_y = magnet_y
- self._magnet_z = magnet_z
-
- # Make this into a parameter?
- self._field_limit = field_limit
-
- # Initialize the internal magnet field setpoints
- self.update_internal_setpoints()
+ self._set_point = FieldVector(
+ x=self._instrument_x.field(),
+ y=self._instrument_y.field(),
+ z=self._instrument_z.field()
+ )
# Get-only parameters that return a measured value
- self.add_parameter('cartesian_measured',
- get_cmd=partial(self._get_measured, 'x', 'y', 'z'),
- unit='T')
-
- self.add_parameter('x_measured',
- get_cmd=partial(self._get_measured, 'x'),
- unit='T')
-
- self.add_parameter('y_measured',
- get_cmd=partial(self._get_measured, 'y'),
- unit='T')
-
- self.add_parameter('z_measured',
- get_cmd=partial(self._get_measured, 'z'),
- unit='T')
-
- self.add_parameter('spherical_measured',
- get_cmd=partial(self._get_measured, 'field',
- 'theta',
- 'phi'),
- unit='T')
-
- self.add_parameter('phi_measured',
- get_cmd=partial(self._get_measured, 'phi'),
- unit='deg')
-
- self.add_parameter('theta_measured',
- get_cmd=partial(self._get_measured, 'theta'),
- unit='deg')
+ self.add_parameter(
+ 'cartesian_measured',
+ get_cmd=partial(self._get_measured, 'x', 'y', 'z'),
+ unit='T'
+ )
+
+ self.add_parameter(
+ 'x_measured',
+ get_cmd=partial(self._get_measured, 'x'),
+ unit='T'
+ )
+
+ self.add_parameter(
+ 'y_measured',
+ get_cmd=partial(self._get_measured, 'y'),
+ unit='T'
+ )
+
+ self.add_parameter(
+ 'z_measured',
+ get_cmd=partial(self._get_measured, 'z'),
+ unit='T'
+ )
+
+ self.add_parameter(
+ 'spherical_measured',
+ get_cmd=partial(
+ self._get_measured,
+ 'r',
+ 'theta',
+ 'phi'
+ ),
+ unit='T'
+ )
+
+ self.add_parameter(
+ 'phi_measured',
+ get_cmd=partial(self._get_measured, 'phi'),
+ unit='deg'
+ )
+
+ self.add_parameter(
+ 'theta_measured',
+ get_cmd=partial(self._get_measured, 'theta'),
+ unit='deg'
+ )
+
+ self.add_parameter(
+ 'field_measured',
+ get_cmd=partial(self._get_measured, 'r'),
+ unit='T')
+
+ self.add_parameter(
+ 'cylindrical_measured',
+ get_cmd=partial(self._get_measured,
+ 'rho',
+ 'phi',
+ 'z'),
+ unit='T')
+
+ self.add_parameter(
+ 'rho_measured',
+ get_cmd=partial(self._get_measured, 'rho'),
+ unit='T'
+ )
+
+ # Get and set parameters for the set points of the coordinates
+ self.add_parameter(
+ 'cartesian',
+ get_cmd=partial(self._get_setpoints, 'x', 'y', 'z'),
+ set_cmd=self._set_cartesian,
+ unit='T',
+ vals=Anything()
+ )
+
+ self.add_parameter(
+ 'x',
+ get_cmd=partial(self._get_setpoints, 'x'),
+ set_cmd=self._set_x,
+ unit='T',
+ vals=Numbers()
+ )
+
+ self.add_parameter(
+ 'y',
+ get_cmd=partial(self._get_setpoints, 'y'),
+ set_cmd=self._set_y,
+ unit='T',
+ vals=Numbers()
+ )
+
+ self.add_parameter(
+ 'z',
+ get_cmd=partial(self._get_setpoints, 'z'),
+ set_cmd=self._set_z,
+ unit='T',
+ vals=Numbers()
+ )
+
+ self.add_parameter(
+ 'spherical',
+ get_cmd=partial(
+ self._get_setpoints,
+ 'r',
+ 'theta',
+ 'phi'
+ ),
+ set_cmd=self._set_spherical,
+ unit='tuple?',
+ vals=Anything()
+ )
+
+ self.add_parameter(
+ 'phi',
+ get_cmd=partial(self._get_setpoints, 'phi'),
+ set_cmd=self._set_phi,
+ unit='deg',
+ vals=Numbers()
+ )
+
+ self.add_parameter(
+ 'theta',
+ get_cmd=partial(self._get_setpoints, 'theta'),
+ set_cmd=self._set_theta,
+ unit='deg',
+ vals=Numbers()
+ )
+
+ self.add_parameter(
+ 'field',
+ get_cmd=partial(self._get_setpoints, 'r'),
+ set_cmd=self._set_r,
+ unit='T',
+ vals=Numbers()
+ )
+
+ self.add_parameter(
+ 'cylindrical',
+ get_cmd=partial(
+ self._get_setpoints,
+ 'rho',
+ 'phi',
+ 'z'
+ ),
+ set_cmd=self._set_cylindrical,
+ unit='tuple?',
+ vals=Anything()
+ )
+
+ self.add_parameter(
+ 'rho',
+ get_cmd=partial(self._get_setpoints, 'rho'),
+ set_cmd=self._set_rho,
+ unit='T',
+ vals=Numbers()
+ )
+
+ def _verify_safe_setpoint(self, setpoint_values):
+
+ if repr(self._field_limit).isnumeric():
+ return np.linalg.norm(setpoint_values) < self._field_limit
+
+ return any([limit_function(*setpoint_values) for limit_function in self._field_limit])
- self.add_parameter('field_measured',
- get_cmd=partial(self._get_measured, 'field'),
- unit='T')
+ def _set_fields(self, values):
+ """
+ Set the fields of the x/y/z magnets. This function is called
+ whenever the field is changed and performs several safety checks
+ to make sure no limits are exceeded.
- self.add_parameter('cylindrical_measured',
- get_cmd=partial(self._get_measured, 'rho',
- 'phi',
- 'z'),
- unit='T')
+ Args:
+ values (tuple): a tuple of cartesian coordinates (x, y, z).
+ """
- self.add_parameter('rho_measured',
- get_cmd=partial(self._get_measured, 'rho'),
- unit='T')
+ # Check if exceeding the global field limit
+ if not self._verify_safe_setpoint(values):
+ raise ValueError("_set_fields aborted; field would exceed limit")
- # Get and set parameters for the setpoints of the coordinates
- self.add_parameter('cartesian',
- get_cmd=partial(self._get_setpoints, 'x', 'y', 'z'),
- set_cmd=self._set_fields,
- unit='T',
- vals=Anything())
+ # Check if the individual instruments are ready
+ for name, value in zip(["x", "y", "z"], values):
- self.add_parameter('x',
- get_cmd=partial(self._get_setpoints, 'x'),
- set_cmd=self._set_x,
- unit='T',
- vals=Numbers())
+ instrument = getattr(self, "_instrument_{}".format(name))
+ instrument.field.validate(value)
+ if instrument.ramping_state() == "ramping":
+ msg = '_set_fields aborted; magnet {} is already ramping'
+ raise ValueError(msg.format(instrument))
- self.add_parameter('y',
- get_cmd=partial(self._get_setpoints, 'y'),
- set_cmd=self._set_y,
- unit='T',
- vals=Numbers())
+ # Now that we know we can proceed, call the individual instruments
- self.add_parameter('z',
- get_cmd=partial(self._get_setpoints, 'z'),
- set_cmd=self._set_z,
- unit='T',
- vals=Numbers())
-
- self.add_parameter('spherical',
- get_cmd=partial(self._get_setpoints, 'field',
- 'theta',
- 'phi'),
- set_cmd=self._set_spherical,
- unit='tuple?',
- vals=Anything())
-
- self.add_parameter('phi',
- get_cmd=partial(self._get_setpoints, 'phi'),
- set_cmd=self._set_phi,
- unit='deg',
- vals=Numbers())
-
- self.add_parameter('theta',
- get_cmd=partial(self._get_setpoints, 'theta'),
- set_cmd=self._set_theta,
- unit='deg',
- vals=Numbers())
+ for operator in [np.less, np.greater]:
+ # First ramp the coils that are decreasing in field strength.
+ # This will ensure that we are always in a save region as far as the quenching of the magnets is concerned
+ for name, value in zip(["x", "y", "z"], values):
- self.add_parameter('field',
- get_cmd=partial(self._get_setpoints, 'field'),
- set_cmd=self._set_field,
- unit='T',
- vals=Numbers())
-
- self.add_parameter('cylindrical',
- get_cmd=partial(self._get_setpoints, 'rho',
- 'phi',
- 'z'),
- set_cmd=self._set_cylindrical,
- unit='tuple?',
- vals=Anything())
-
- self.add_parameter('rho',
- get_cmd=partial(self._get_setpoints, 'rho'),
- set_cmd=self._set_rho,
- unit='T',
- vals=Numbers())
+ instrument = getattr(self, "_instrument_{}".format(name))
+ current_actual = instrument.field()
+ # If the new set point is practically equal to the current one then do nothing
+ if np.isclose(value, current_actual, rtol=0, atol=1e-8):
+ continue
+ # evaluate if the new set point is lesser or greater then the current value
+ if not operator(abs(value), abs(current_actual)):
+ continue
- def update_internal_setpoints(self):
- """
- Set the internal setpoints to the measured field values.
- This can be done in case the magnets have been adjusted manually.
- """
- self.__x = self._magnet_x.field()
- self.__y = self._magnet_y.field()
- self.__z = self._magnet_z.field()
+ instrument.set_field(value, perform_safety_check=False)
- def _request_field_change(self, magnet, value):
+ def _request_field_change(self, instrument, value):
"""
This method is called by the child x/y/z magnets if they are set
individually. It results in additional safety checks being
performed by this 3D driver.
"""
- if magnet is self._magnet_x:
- self.x(value)
- elif magnet is self._magnet_y:
- self.y(value)
- elif magnet is self._magnet_z:
- self.z(value)
+ if instrument is self._instrument_x:
+ self._set_x(value)
+ elif instrument is self._instrument_y:
+ self._set_y(value)
+ elif instrument is self._instrument_z:
+ self._set_z(value)
else:
msg = 'This magnet doesnt belong to its specified parent {}'
-
raise NameError(msg.format(self))
- def _cartesian_to_other(self, x, y, z):
- """ Convert a cartesian set of coordinates to values of interest. """
- field = np.sqrt(x**2 + y**2 + z**2)
- phi = np.arctan2(y, x)
- rho = np.sqrt(x**2 + y**2)
-
- # Define theta to be 0 for zero field
- theta = 0.0
- if field > 0.0:
- theta = np.arccos(z / field)
-
- return phi, theta, field, rho
-
- def _from_xyz(self, x, y, z, *names):
- """
- Convert x/y/z values into the other coordinates and return a
- tuple of the requested values.
-
- Args:
- *names: a series of coordinate names as specified in the function.
- """
- phi, theta, field, rho = self._cartesian_to_other(x, y, z)
-
- coords = {
- 'x': x,
- 'y': y,
- 'z': z,
- 'field': field,
- 'phi': np.degrees(phi),
- 'theta': np.degrees(theta),
- 'rho': rho
- }
-
- returned = tuple(coords[name] for name in names)
-
- if len(returned) == 1:
- return returned[0]
- else:
- return returned
-
def _get_measured(self, *names):
- """ Return the measured coordinates specified in names. """
- x = self._magnet_x.field()
- y = self._magnet_y.field()
- z = self._magnet_z.field()
-
- return self._from_xyz(x, y, z, *names)
-
- def _get_setpoints(self, *names):
- """ Return the setpoints specified in names. """
- return self._from_xyz(self.__x, self.__y, self.__z, *names)
-
- def _set_x(self, value):
- self._set_fields((value, self.__y, self.__z))
- def _set_y(self, value):
- self._set_fields((self.__x, value, self.__z))
+ x = self._instrument_x.field()
+ y = self._instrument_y.field()
+ z = self._instrument_z.field()
+ measured_values = FieldVector(x=x, y=y, z=z).get_components(*names)
- def _set_z(self, value):
- self._set_fields((self.__x, self.__y, value))
+ # Convert angles from radians to degrees
+ d = dict(zip(names, measured_values))
+ return_value = [d[name] for name in names] # Do not do "return list(d.values())", because then there is no
+ # guaranty that the order in which the values are returned is the same as the original intention
+ if len(names) == 1:
+ return_value = return_value[0]
- def _set_spherical(self, values):
- field, theta, phi = values
+ return return_value
- phi, theta = np.radians(phi), np.radians(theta)
+ def _get_setpoints(self, *names):
- x = field * np.sin(theta) * np.cos(phi)
- y = field * np.sin(theta) * np.sin(phi)
- z = field * np.cos(theta)
+ measured_values = self._set_point.get_components(*names)
- self._set_fields((x, y, z))
+ # Convert angles from radians to degrees
+ d = dict(zip(names, measured_values))
+ return_value = [d[name] for name in names] # Do not do "return list(d.values())", because then there is no
+ # guaranty that the order in which the values are returned is the same as the original intention
+ if len(names) == 1:
+ return_value = return_value[0]
- def _set_phi(self, value):
- field, theta, phi = self._get_setpoints('field', 'theta', 'phi')
+ return return_value
- phi = np.radians(value)
+ def _set_cartesian(self, values):
+ x, y, z = values
+ self._set_point.set_vector(x=x, y=y, z=z)
+ self._set_fields(self._set_point.get_components("x", "y", "z"))
- self._set_spherical((field, theta, phi))
+ def _set_x(self, x):
+ self._set_point.set_component(x=x)
+ self._set_fields(self._set_point.get_components("x", "y", "z"))
- def _set_theta(self, value):
- field, theta, phi = self._get_setpoints('field', 'theta', 'phi')
+ def _set_y(self, y):
+ self._set_point.set_component(y=y)
+ self._set_fields(self._set_point.get_components("x", "y", "z"))
- theta = np.radians(value)
+ def _set_z(self, z):
+ self._set_point.set_component(z=z)
+ self._set_fields(self._set_point.get_components("x", "y", "z"))
- self._set_spherical((field, theta, phi))
+ def _set_spherical(self, values):
+ r, theta, phi = values
+ self._set_point.set_vector(r=r, theta=theta, phi=phi)
+ self._set_fields(self._set_point.get_components("x", "y", "z"))
- def _set_field(self, value):
- field, theta, phi = self._get_setpoints('field', 'theta', 'phi')
+ def _set_r(self, r):
+ self._set_point.set_component(r=r)
+ self._set_fields(self._set_point.get_components("x", "y", "z"))
- field = value
+ def _set_theta(self, theta):
+ self._set_point.set_component(theta=theta)
+ self._set_fields(self._set_point.get_components("x", "y", "z"))
- self._set_spherical((field, theta, phi))
+ def _set_phi(self, phi):
+ self._set_point.set_component(phi=phi)
+ self._set_fields(self._set_point.get_components("x", "y", "z"))
def _set_cylindrical(self, values):
- phi, rho, z = values
-
- phi = np.radians(phi)
-
- x = rho * np.cos(phi)
- y = rho * np.sin(phi)
-
- self._set_fields((x, y, z))
-
- def _set_rho(self, value):
- phi, rho = self._get_setpoints('phi', 'rho')
-
- rho = value
-
- self._set_cylindrical((phi, rho, self.__z))
-
- def _set_fields(self, values):
- """
- Set the fields of the x/y/z magnets. This function is called
- whenever the field is changed and performs several safety checks
- to make sure no limits are exceeded.
-
- Args:
- values (tuple): a tuple of cartesian coordinates (x, y, z).
- """
- x, y, z = values
-
- # Check if exceeding an individual magnet field limit
- # These will throw a ValueError on an invalid value
- self._magnet_x.field.validate(x)
- self._magnet_y.field.validate(y)
- self._magnet_z.field.validate(z)
-
- # Check if exceeding the global field limit
- if np.sqrt(x**2 + y**2 + z**2) > self._field_limit:
- msg = '_set_fields aborted; field would exceed limit of {} T'
-
- raise ValueError(msg.format(self._field_limit))
-
- # Check if the individual magnet are not already ramping
- for m in [self._magnet_x, self._magnet_y, self._magnet_z]:
- if m.ramping_state() == 'ramping':
- msg = '_set_fields aborted; magnet {} is already ramping'
-
- raise ValueError(msg.format(m))
-
- swept_x, swept_y, swept_z = False, False, False
-
- # First ramp the coils that are decreasing in field strength
- # If the new setpoint is practically equal to the current one
- # then leave it be
- if np.isclose(self.__x, x, rtol=0, atol=1e-8):
- swept_x = True
- elif np.abs(self._magnet_x.field()) > np.abs(x):
- self._magnet_x._set_field(x, perform_safety_check=False)
- swept_x = True
-
- if np.isclose(self.__y, y, rtol=0, atol=1e-8):
- swept_y = True
- elif np.abs(self._magnet_y.field()) > np.abs(y):
- self._magnet_y._set_field(y, perform_safety_check=False)
- swept_y = True
-
- if np.isclose(self.__z, z, rtol=0, atol=1e-8):
- swept_z = True
- elif np.abs(self._magnet_z.field()) > np.abs(z):
- self._magnet_z._set_field(z, perform_safety_check=False)
- swept_z = True
-
- # Finally, ramp up the coils that are increasing
- if not swept_x:
- self._magnet_x._set_field(x, perform_safety_check=False)
-
- if not swept_y:
- self._magnet_y._set_field(y, perform_safety_check=False)
-
- if not swept_z:
- self._magnet_z._set_field(z, perform_safety_check=False)
-
- # Set the new actual setpoints
- self.__x = x
- self.__y = y
- self.__z = z
+ rho, phi, z = values
+ self._set_point.set_vector(rho=rho, phi=phi, z=z)
+ self._set_fields(self._set_point.get_components("x", "y", "z"))
+
+ def _set_rho(self, rho):
+ self._set_point.set_component(rho=rho)
+ self._set_fields(self._set_point.get_components("x", "y", "z"))
+
+ def get_mocker_messages(self):
+ messages = []
+ for name in ["x", "y", "z"]:
+ instrument = getattr(self, "_instrument_{}".format(name))
+ if instrument.is_testing():
+ messages += instrument.get_mock_messages()
+
+ return messages
diff --git a/qcodes/math/__init__.py b/qcodes/math/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/qcodes/math/field_vector.py b/qcodes/math/field_vector.py
new file mode 100644
index 00000000000..b896aaa7fab
--- /dev/null
+++ b/qcodes/math/field_vector.py
@@ -0,0 +1,269 @@
+"""
+A convenient class to keep track of vectors representing physical fields. The idea is that a vector instance
+stores a representation in cartesian, spherical and cylindrical coordinates. Giving either (x, y, z) values or
+(rho, phi, z) values or (r, theta, phi) values at instantiation we will calculate the other representation immediately.
+"""
+
+import numpy as np
+
+
+class FieldVector(object):
+ attributes = ["x", "y", "z", "r", "theta", "phi", "rho"]
+
+ def __init__(self, x=None, y=None, z=None, r=None, theta=None, phi=None, rho=None):
+ """
+ Parameters:
+ x (float, optional): represents the norm of the projection of the vector along the x-axis
+ y (float, optional): represents the norm of the projection of the vector along the y-axis
+ z (float, optional): represents the norm of the projection of the vector along the z-axis
+ r (float, optional): represents the norm of the vector
+ theta (float, optional): represents the angle of the vector with respect to the positive z-axis
+ rho (float, optional): represents the norm of the projection of the vector on to the xy-plane
+ phi (float, optional): represents the angle of rho with respect to the positive x-axis
+
+ Note: All inputs are optional, however the user needs to either give (x, y, z) values,
+ (r, theta, phi) values or (phi, rho, z) values for meaningful computation
+ """
+ self._x = x
+ self._y = y
+ self._z = z
+
+ self._r = r
+ if theta is not None:
+ self._theta = np.radians(theta)
+ else:
+ self._theta = theta
+
+ if phi is not None:
+ self._phi = np.radians(phi)
+ else:
+ self._phi = phi
+
+ self._rho = rho
+
+ self._compute_unknowns()
+
+ def _set_attribute_value(self, attr_name, value):
+
+ if value is None:
+ return
+
+ attr_value = getattr(self, "_" + attr_name)
+
+ if attr_value is None:
+ setattr(self, "_" + attr_name, value)
+ else:
+ if not np.isclose(attr_value, value):
+ raise ValueError("Computed value of {} inconsistent with given value".format(attr_name))
+
+ def _set_attribute_values(self, attr_names, values):
+
+ for attr_name, value in zip(attr_names, values):
+ self._set_attribute_value(attr_name, value)
+
+ @staticmethod
+ def _cartesian_to_other(x, y, z):
+ """ Convert a cartesian set of coordinates to values of interest."""
+
+ if any([i is None for i in [x, y, z]]):
+ return None
+
+ phi = np.arctan2(y, x)
+ rho = np.sqrt(x ** 2 + y ** 2)
+ r = np.sqrt(x ** 2 + y ** 2 + z ** 2)
+ if r != 0:
+ theta = np.arccos(z / r)
+ else:
+ theta = 0
+
+ return x, y, z, r, theta, phi, rho
+
+ @staticmethod
+ def _spherical_to_other(r, theta, phi):
+ """Convert from spherical to other representations"""
+
+ if any([i is None for i in [r, theta, phi]]):
+ return None
+
+ z = r * np.cos(theta)
+ x = r * np.sin(theta) * np.cos(phi)
+ y = r * np.sin(theta) * np.sin(phi)
+ rho = np.sqrt(x ** 2 + y ** 2)
+
+ return x, y, z, r, theta, phi, rho
+
+ @staticmethod
+ def _cylindrical_to_other(phi, rho, z):
+ """Convert from cylindrical to other representations"""
+
+ if any([i is None for i in [phi, rho, z]]):
+ return None
+
+ x = rho * np.cos(phi)
+ y = rho * np.sin(phi)
+ r = np.sqrt(rho ** 2 + z ** 2)
+ if r != 0:
+ theta = np.arccos(z / r)
+ else:
+ theta = 0
+
+ return x, y, z, r, theta, phi, rho
+
+ def _compute_unknowns(self):
+ """
+ Compute all coordinates. To do this we need either the set (x, y, z) to contain no None values, or the set
+ (r, theta, phi), or the set (rho, phi, z). Given any of these sets, we can recompute the rest.
+
+ This function will raise an error if there are contradictory inputs (e.g. x=3, y=4, z=0 and rho=6).
+ """
+
+ for f in [
+ lambda: FieldVector._cartesian_to_other(self._x, self._y, self._z),
+ lambda: FieldVector._spherical_to_other(self._r, self._theta, self._phi),
+ lambda: FieldVector._cylindrical_to_other(self._phi, self._rho, self._z)
+ ]:
+ new_values = f()
+ if new_values is not None: # this will return None if any of the function arguments is None.
+ self._set_attribute_values(FieldVector.attributes, new_values)
+ break
+
+ def copy(self, other):
+ """Copy the properties of other vector to yourself"""
+ for att in FieldVector.attributes:
+ value = getattr(other, "_"+ att)
+ setattr(self, "_" + att, value)
+
+ def set_vector(self, **new_values):
+ """
+ Reset the the values of the vector
+
+ Examples:
+ >>> f = FieldVector(x=0, y=2, z=6)
+ >>> f.set_vector(x=9, y=3, z=1)
+ >>> f.set_vector(r=1, theta=30.0, phi=10.0)
+ >>> f.set_vector(x=9, y=0) # this should raise a value error: "Can only set vector with a complete value
+ # set"
+ >>> f.set_vector(x=9, y=0, r=3) # although mathematically it is possible to compute the complete vector
+ # from the values given, this is too hard to implement with generality (and not worth it) so this to will
+ # raise the above mentioned ValueError
+ """
+ names = sorted(list(new_values.keys()))
+ groups = [["x", "y", "z"], ["phi", "r", "theta"], ["phi", "rho", "z"]]
+ if names not in groups:
+ raise ValueError("Can only set vector with a complete value set")
+
+ new_vector = FieldVector(**new_values)
+ self.copy(new_vector)
+
+ def set_component(self, **new_values):
+ """
+ Set a single component of the vector to some new value. It is disallowed for the user to set vector components
+ manually as this can lead to inconsistencies (e.g. x and rho are not independent of each other, setting
+ one has to effect the other)
+
+ Examples:
+ >>> f = FieldVector(x=2, y=3, z=4)
+ >>> f.set_component(r=10) # Since r is part of the set (r, theta, phi) representing spherical coordinates,
+ # setting r means that theta and phi are kept constant and only r is changed. After changing r, (x, y, z)
+ # values are recomputed, as is the rho coordinate. Internally we arrange this by setting x, y, z and rho to
+ # None and calling self._compute_unknowns()
+
+ Parameters:
+ new_values (dict): keys representing parameter names and values the values to be set
+ """
+
+ if len(new_values) > 1:
+ raise NotImplementedError("Cannot set multiple components at once")
+ # It is not easy to properly implement a way of setting multiple components at the same time as we need to
+ # check for inconsistencies. E.g. how do we handle the following situation:
+ # >>> f = FieldVector(x=1, y=2, z=3)
+ # >>> f.set_component(y=5, r=10)
+ # After setting r=10, y will no longer be equal to 5.
+
+ items = list(new_values.items())
+ component_name = items[0][0]
+
+ if component_name in ["theta", "phi"]:
+ # convert angles to radians
+ value = np.radians(items[0][1])
+ else:
+ value = items[0][1]
+
+ setattr(self, "_" + component_name, value)
+ # Setting x (for example), we will keep y and z untouched, but set r, theta, phi and rho to None
+ # so these can be recomputed. This will keep things consistent.
+ groups = [["x", "y", "z"], ["r", "theta", "phi"], ["phi", "rho", "z"]]
+
+ for group in groups:
+ if component_name in group:
+
+ for att in FieldVector.attributes:
+ if att not in group:
+ setattr(self, "_" + att, None)
+
+ break
+
+ self._compute_unknowns()
+
+ def get_components(self, *names):
+ """Get field components by name"""
+
+ def convert_angle_to_degrees(name, value):
+ # Convert all angles to degrees
+ if name in ["theta", "phi"]:
+ return np.degrees(value)
+ else:
+ return value
+
+ components = [convert_angle_to_degrees(
+ name, getattr(self, "_" + name)
+ ) for name in names]
+
+ return components
+
+ def is_equal(self, other):
+ """
+ Returns True if other is equivalent to self, False otherwise
+ """
+ for name in ["x", "y", "z"]:
+ self_value = getattr(self, name)
+ other_value = getattr(other, name)
+ if not np.isclose(self_value, other_value):
+ return False
+
+ return True
+
+ # A method property allows us to confidently retrieve values but disallows modifying them
+ # We can do this:
+ # a = field_vector.x
+ # But this:
+ # field_vector.x = a
+ # Will raise an error.
+
+ @property
+ def x(self):
+ return self._x
+
+ @property
+ def y(self):
+ return self._y
+
+ @property
+ def z(self):
+ return self._z
+
+ @property
+ def rho(self):
+ return self._rho
+
+ @property
+ def theta(self):
+ return np.degrees(self._theta)
+
+ @property
+ def r(self):
+ return self._r
+
+ @property
+ def phi(self):
+ return np.degrees(self._phi)
diff --git a/qcodes/plots/qcmatplotlib.py b/qcodes/plots/qcmatplotlib.py
index 2dd93448b88..c5ce80b7c76 100644
--- a/qcodes/plots/qcmatplotlib.py
+++ b/qcodes/plots/qcmatplotlib.py
@@ -3,13 +3,13 @@
using the nbagg backend and matplotlib
"""
from collections import Mapping
+from collections import Sequence
from functools import partial
import matplotlib.pyplot as plt
-from matplotlib.transforms import Bbox
import numpy as np
+from matplotlib.transforms import Bbox
from numpy.ma import masked_invalid, getmask
-from collections import Sequence
from .base import BasePlot
@@ -82,8 +82,7 @@ def _init_plot(self, subplots=None, figsize=None, num=None):
if isinstance(subplots, Mapping):
if figsize is None:
figsize = (6, 4)
- self.fig, self.subplots = plt.subplots(figsize=figsize, num=num,
- **subplots, squeeze=False)
+ self.fig, self.subplots = plt.subplots(figsize=figsize, num=num, squeeze=False, **subplots)
else:
# Format subplots as tuple (nrows, ncols)
if isinstance(subplots, int):
diff --git a/qcodes/tests/test_ami430.py b/qcodes/tests/test_ami430.py
new file mode 100644
index 00000000000..62140ec59b8
--- /dev/null
+++ b/qcodes/tests/test_ami430.py
@@ -0,0 +1,329 @@
+"""
+A debug module for the AMI430 instrument driver. We cannot rely on the physical instrument to be present
+which is why we need to mock it.
+"""
+import re
+from datetime import datetime
+
+import numpy as np
+import pytest
+from hypothesis import given, settings
+from hypothesis.strategies import floats
+from hypothesis.strategies import tuples
+
+from qcodes.instrument_drivers.american_magnetics.AMI430 import AMI430, AMI430_3D
+from qcodes.math.field_vector import FieldVector
+
+field_limit = [ # If any of the field limit functions are satisfied we are in the safe zone.
+ lambda x, y, z: x == 0 and y == 0 and z < 3, # We can have higher field along the z-axis if x and y are zero.
+ lambda x, y, z: np.linalg.norm([x, y, z]) < 2
+]
+
+
+@pytest.fixture(scope='module')
+def current_driver(request):
+ """
+ Start three mock instruments representing current drivers for the x, y and z directions.
+ """
+
+ driver = AMI430_3D(
+ "AMI430-3D",
+ AMI430("x", testing=True),
+ AMI430("y", testing=True),
+ AMI430("z", testing=True),
+ field_limit
+ )
+
+ return driver
+
+
+def get_instruments_ramp_messages(current_driver):
+ """
+ Listen to the mock instruments and parse the messages to extract the ramping targets. This is useful in determining
+ if the set targets arrive at the individual instruments correctly. The time stamps are useful to test that the
+ messages arrive in the correct order.
+ """
+
+ search_string = "\[(.*)\] ([x, y, z]): Ramping to (.*)"
+ # We expect the log messages to be in the format "[