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": "iVBORw0KGgoAAAANSUhEUgAAAYgAAAEKCAYAAAAIO8L1AAAABHNCSVQICAgIfAhkiAAAAAlwSFlz\nAAALEgAACxIB0t1+/AAAIABJREFUeJzt3Xd8VGX2x/HPCb333kLvIBAUrKisoriIqKs/e1t1m25R\nAXsXddV117Wtirq6FoqKigUVdC2ooJCEDqH3DgZC2vn9cW/WGIcwhEwm5ft+vXhl7p1bzjwzzJnn\nlvOYuyMiIlJQQrwDEBGR0kkJQkREIlKCEBGRiJQgREQkIiUIERGJSAlCREQiUoIQEZGIlCBERCQi\nJQgREYmocrwDOBSNGzf2xMTEeIchIlKmzJ49e4u7NznQcmU6QSQmJjJr1qx4hyEiUqaY2cpoltMh\nJhERiUgJQkREIlKCEBGRiJQgREQkIiUIERGJSAlCREQiUoIQESljZq/czj+nL2X2yu0x3U+Zvg9C\nRKSi+XLpFi587hvcnaqVE3j5ikEMaNcgJvtSD0JEpIz4YukWfvPybHJynVyHrOxcZqZtjdn+1IMQ\nESnldu7J4p6p83l91hpa1KvO3sxccnJzqVI5gUEdGsVsv0oQIiKl2Pup67nlrXlsS8/k6uM68seh\nnZm3bhcz07YyqEOjmB1eAiUIEZFSadPuDG57ax7vpW6gR4u6jL9kIL1a1QNgQLsGMU0MeZQgRERK\nEXdnwuw13P3OfDKyc7lhWFd+fUwHqlQq+VPGShAiIqXEqq17uPGNFD5fuoXDExty35m96dikdtzi\nUYIQEYmznFxn/BfLeejDxVRKMO4e2YvzDm9LQoLFNS4lCBGROFq0YTejJyUzZ/UOTujWlLtH9qJl\n/RrxDgtQghARiYt92Tk8Pn0Zj89YSp3qVXj03MMY0bclZvHtNeSnBCEiUsK+W7Wd0ROTWbLpB0Ye\n1pJbf9mThrWqxjusn1GCEBEpIen7svnrh4t4/ssVtKhbnfGXDOT4bk3jHdZ+KUGIiJSA/y7ZzNjJ\nKazZvpeLBrfjhmHdqF2tdH8Fl+7oRETKuB17Mrn73QVMnL2GDk1qMeHqwQxMbBjvsKKiBCEiEgPu\nznupG7j1rXls35PJ747vyB9O6Ez1KpXiHVrUlCBERIrZxl0Z3PJmKh/O30ivVnV54bKB9GxZL95h\nHTQlCBGRYuLuvPbtau6ZuoDM7FzGntKNy49uT+U4lMkoDkoQIiLFYOXWdMZOTuHLZVs5on1Dxp3Z\nh/aNa8U7rEOiBCEicgiyc3IZ/8UKHpq2iCoJCdx7Rm/OHdgm7mUyioMShIhIES1Yv4vRk5JJXrOT\nod2bcffIXjSvVz3eYRUbJQgRkYO0LzuHxz5ZyhMzllGvRhUeO68fw3u3KFVlMoqDEoSIyEGYvXIb\noyelsHTTD4zq34pbhvegQSksk1EclCBERKKQvi+bBz9YxAtfraBlvRo8f+lAhnQtvWUyioMShIjI\nAcxYtImb3khl3c69XDw4ketO7lrqy2QUh5hdnGtmz5nZJjNLzTevoZlNM7Ml4d8G4Xwzs7+b2VIz\nSzaz/rGKS0QkWtvTM/nza3O4ZPy3VK+SwMSrB3P7iJ4VIjlADBME8DwwrMC8McDH7t4Z+DicBjgF\n6Bz+uxJ4IoZxiYgUyt15e+46hj78KVPmruMPJ3Ti3WuOYUC7slFDqbjELA26+2dmllhg9unAkPDx\nC8AMYHQ4/0V3d2CmmdU3sxbuvj5W8YmIRLJhZwY3v5nKRws20qd1PV664gi6t6gb77DioqT7Sc3y\nvvTdfb2Z5Z3haQWszrfcmnCeEoSIlIjcXOfVb1dz39QFZOXmctOp3bn0qMQyWyajOJSWA2mRLh72\niAuaXUlwGIq2bdvGMiYRqSBWbElnzORkZqZtY3CHRow7szftGpXtMhnFoaQTxMa8Q0dm1gLYFM5f\nA7TJt1xrYF2kDbj708DTAElJSRGTiIhINLJzcnn28+U8PG0xVSsnMG5Ub84Z2Kbc3fBWVCWdIKYA\nFwPjwr9v5Zv/ezN7FTgC2KnzDyISS/PXBWUyUtbu5KQezbhrZC+a1S0/ZTKKQ8wShJm9QnBCurGZ\nrQFuI0gMr5vZ5cAq4Oxw8anAqcBSYA9waaziEpGKLSMrh398soSnPk2jfs0qPH5+f07p1Vy9hghi\neRXT/+3nqRMjLOvA72IVi4gIwLcrtjF6UjJpm9M5a0Brbh7enfo1y2eZjOJQWk5Si4jEzA/7snng\n/YW8+NVKWjeowYuXHc6xXZrEO6xSTwlCRMq16Qs3cdMbKazflcGlRyVy3UldqVVB7oQ+VGolESmX\ntqVncufb83hzzjo6N63NxKuPZEC7BvEOq0xRghCRcsXdmTJ3HXe8PZ/dGVlce2Jnfnt8R6pVrhTv\n0MocJQgRKTfW79zLzW+k8vHCTfRtU58HzuxD1+Z14h1WmaUEISJlXm6u859vVjHuvYXk5Do3D+/O\npUe1p1I5GBc6npQgRKRMS9v8A2Mmp/DN8m0c1akR953Rh7aNasY7rHJBCUJEyqSsnFz+9d80/vbR\nEqpXTuCBs/pw9oDWuuGtGO03QZjZn6NYP93dnyrGeEREDih17U5GT0pm3rpdnNKrOXeM6ElTlcko\ndoX1IK4nGLinsHR8NaAEISIlIiMrh0c/XsLTn6XRsFZVnrygP8N6tYh3WOVWYQni3+5+Z2Erm5nq\n4YpIifg6bStjJqewfEs6v0pqzU2n9qBezSrxDqtc22+CcPcbDrRyNMuIiByK3RlZjHtvIS9/vYo2\nDWvw0uVHcHTnxvEOq0I44ElqM7sWGA/sBp4B+gFj3P3DGMcmIhXcxws2cvObqWzclcEVR7fnzyd1\noWZVXVtTUqJp6cvc/VEzOxloQlCKezygBCEiMbH1h33c8fZ8psxdR5dmtXn8/CPp11ZlMkpaNAki\n7yT1qcB4d59ruo5MRGLA3XlrzjrueHseP+zL5k9Du/CbIR2pWrnijgsdT9EkiNlm9iHQHhhrZnWA\n3NiGJSIVzdode7npjRRmLNpMv7b1uf/MPnRppjIZ8RRNgrgcOAxIc/c9ZtYIjfgmIsUkN9d56euV\n3P/eQnIdbj2tBxcfmagyGaVAYTfKNXf3De6eC3yXN9/dtwJb8y8T+zBFpDxauukHxk5O5tsV2zmm\nc2PuPaM3bRqqTEZpUVgPYirQ/wDrR7OMiMhPZOXk8vRnaTz60RJqVK3EX8/uy5n9W6lMRilTWILo\na2a7CnnegMKeFxH5mZQ1O7lhUjIL1u9ieO8W3DaiB03rqExGaVTYjXIaXUNEik1GVg6PfLSYZ/67\nnEa1qvLUhQM4uWfzeIclhdAdJyISc18t28rYycms2LqHcwe2Yeyp3alXQ2UySjslCBGJmV0ZWdw3\ndSGvfLOKtg1r8p8rjuDITiqTUVYoQYhITEybv5Gb30xh8+59XHlsB/40tAs1qurIdVkSVYIws6OB\nzu4+3syaALXdfXlsQxORsmjLD/u4fco83kleT7fmdXj6wiT6tqkf77CkCKIp1ncbkAR0JajBVAV4\nCTgqtqGJSFni7rzx/VrufGc+e/bl8JdfdOGq41QmoyyLpgdxBkEF1+8A3H1dWG5DRASANdv3cOMb\nqXy2eDMD2jXg/jN706mpvibKumgSRKa7u5k5aJAgEflRbq7z4lcreOCDRQDcMaInFw5qR4LKZJQL\n0SSI183sKaC+mf0auAz4V2zDEpHSbumm3YyelMLslds5rksT7jmjF60bqExGeXLABOHufzWzXxDc\nNd0VuNXdp8U8MhEplTKzc3nq02X845Ol1KxWiYd/1Zcz+qlMRnkU1VVM7j7NzL7OW97MGrr7tphG\nJiKlztzVOxg9KZmFG3ZzWp8W3D6iJ41rV4t3WBIj0VzFdBVwJ7CXYBwIAxzoENvQRKS02JuZw8PT\nFvHs58tpUqca/7ooiV/0aBbvsCTGoulBXAf0dPctsQ5GREqfL5duYczkFFZt28N5R7RlzCndqFtd\nZTIqgmgSxDJgT6wDEZHSZefeLO6buoBXv11NYqOavHrlIAZ1aBTvsKQERZMgxgJfhucg9uXNdPdr\nirpTM/sTcAXBoaoUghHqWgCvAg0J7rm40N0zi7oPESm6D+Zt4JY3U9manslVxwVlMqpXUZmMiiaa\nBPEU8AnBF/khj0VtZq2Aa4Ae7r7XzF4HzgVOBR5x91fN7EmCoU6fONT9iUj0Nu3O4PYp85iasoHu\nLery7MUD6d26XrzDkjiJJkFku/ufY7DfGmaWBdQE1gMnAOeFz78A3I4ShEiJcHcmzl7D3e8uYG9W\nDtef3JUrj+1AlUoqk1GRRZMgppvZlcDb/PQQU5Euc3X3tWb2V2AVwZVRHwKzgR3unh0utgZoVZTt\ni8jBWb1tDze+kcJ/l2whqV0Dxp3Zh05Na8c7LCkFokkQeb/qx+abV+TLXM2sAXA60B7YAUwATomw\nqO9n/SuBKwHatm1blBBEBMjJdV74cgUPfrCIBIO7Tu/J+UeoTIb8KJo7qdsX8z6HAsvdfTOAmU0G\njiQo5VE57EW0BtbtJ56ngacBkpKSIiYRESnc4o27uWFiMnNW72BI1ybcc0ZvWtWvEe+wpJTZb4Iw\nsxPc/RMzGxXpeXefXMR9rgIGmVlNgkNMJwKzgOnAWQRXMl0MvFXE7YvIfmRm5/L4jKX8c/pSaler\nzN/OOYzTD2upMhkSUWE9iOMIrl76ZYTnHChSgnD3r81sIsGlrNnA9wQ9gneBV83s7nDes0XZvohE\n9v2q7YyZlMKijbsZ0bclt/2yB41UJkMKYe6FH6Uxs/YFR4+LNC8ekpKSfNasWfEOQ6RU25OZzUMf\nLua5L5bTrE517jmjFyd2V5mMiszMZrt70oGWi+Yk9SSgf4F5E4EBRQlMRErO50u2MPaNZFZv28sF\ng9oyelg36qhMhkSpsHMQ3YCeQL0C5yHqAtVjHZiIFN3OPVnc/e58JsxeQ/vGtXjtykEcoTIZcpAK\n60F0BU4D6vPT8xC7gV/HMigRKbr3U9dzy1vz2JaeyW+GdOTaEzurTIYUyX4ThLu/BbxlZoPd/asS\njElEimDTrgxufWse78/bQM+WdRl/yUB6tVKZDCm6aO6DUHIQKcXcnQmz1nD3u/PJyM5l9LBuXHFM\ne5XJkEMW1YhyIlI6rdoalMn4fOkWDm/fkHGjetOhicpkSPFQghApg3JynfFfLOehDxdTKcG4e2Qv\nzju8rcpkSLGKZsjRRgSVVY8iuEHuc+BOd98a29BEJJJFG3Zzw6Rk5q7ewYndmnL3Gb1oUU9lMqT4\nRdODeBX4DDgznD4feI2gppKIlJB92Tn8c/oynpixlDrVq/D3/+vHL/u0UJkMiZloEkRDd78r3/Td\nZjYyVgGJyM99t2o7oycms2TTD5zRrxW3nNaDhrWqxjssKeeiHQ/iXOD1cPosgrpJIhJj6fuy+euH\ni3j+yxW0qFud8ZcO5PiuTeMdllQQ0SSIq4A/Ay+F0wlAupn9GXB3rxur4EQqss8Wb2bs5BTW7tjL\nRYPbccOwbtSuputKpOREcx9EnZIIREQCO/Zkctc7C5j03Ro6NKnFhKsHMzCxYbzDkgooqp8jZtYH\nSMy//CGMByEiEbg7U1M2cNuUVHbsyeL3x3fi9yd0UpkMiZtoLnN9DugDzANyw9lFHg9CRH5u464M\nbnkzlQ/nb6R3q3q8eNkR9Gipo7cSX9H0IAa5e4+YRyJSAbk7r327mnumLiAzO5exp3Tj8qPbU1ll\nMqQUiCZBfGVmPdx9fsyjEalAVmxJZ+zkFL5K28qgDg0ZN6oPiY1rxTsskf+JJkG8QJAkNgD7ACO4\neqlPTCMTKaeyc3J57ovlPDxtMVUSErhvVG/OSWqjMhlS6kSTIJ4DLgRS+PEchIgUwYL1uxg9KZnk\nNTsZ2r0Zd4/sRfN6Gn9LSqdoEsQqd58S80hEyrF92Tk89slSnpixjHo1qvDYef0Y3ltlMqR0iyZB\nLDSz/wBvExxiAnSZq0i0Zq3YxuhJySzbnM6o/q24ZXgPGqhMhpQB0SSIGgSJ4aR883SZq8gB/LAv\nmwffX8iLM1fSsl4Nnr90IENUJkPKkGjupL60JAIRKU9mLNrETW+ksm7nXi4enMh1J3dVmQwpc6K5\nUa46cDnQE/jf2TR3vyyGcYmUSdvTM7nrnflM/n4tHZvUYuLVgxnQTmUypGyK5ifNv4GFwMnAnQTj\nQSyIZVAiZY27807yem6fMo+de7O45oRO/O6ETlSrrDIZUnZFkyA6ufvZZna6u78QnrD+INaBiZQV\n63fu5ZY3U/lowSb6tK7HS1ccQfcWKpMhZV80CSIr/LvDzHoBGwgK94lUaLm5zivfrmLc1IVk5eZy\n06ndufSoRJXJkHIjmgTxtJk1AG4GpgC1gVtiGpVIKbd8SzpjJiXz9fJtDO7QiHFn9qZdI5XJkPKl\n0ARhZgnALnffTjAudYcSiUqklMrOyeWZz5fzyLTFVK2cwP1n9uZXSW10w5uUS4UmCHfPNbPf8+Nw\noyIV1rx1Oxk9KZnUtbs4qUcz7hrZi2Z1VSZDyq9oDjFNM7PrgNeA9LyZ7r4tZlGJlCIZWTn845Ml\nPPlpGg1qVuXx8/tzSq/m6jVIuRdNgsi73+F3+eY5OtwkFcC3YZmMtM3pnDWgNTcP7079miqTIRVD\nNHdSty+JQERKk90ZWTzw/iL+PXMlrRvU4MXLDufYLk3iHZZIidK9/yIFTF+4iZveSGH9rgwuO6o9\nfzmpC7VUJkMqoLh86s2sPvAM0IvgcNVlwCKC8xyJwArgV+HVUyIlYlt6Jne+PY8356yjc9PaTPrN\nkfRv2yDeYYnETbx+Fj0KvO/uZ5lZVaAmcCPwsbuPM7MxwBhgdJzikwrE3Zkydx13vD2f3RlZXHti\nZ357fEeVyZAKb78Jwsz6F7aiu39XlB2aWV3gWOCScDuZQKaZnQ4MCRd7AZiBEoTE2AfzNnD/+wtJ\n25xO3zb1eeDMPnRtXifeYYmUCoX1IB4K/1YHkoC5BONR9wG+Bo4u4j47AJuB8WbWF5gNXAs0c/f1\nAO6+3sxUOF9ixt25//2FPPlpGgCVE4ybh3dXchDJZ79FY9z9eHc/HlgJ9Hf3JHcfAPQDlh7CPisD\n/YEn3L0fwb0VY6Jd2cyuNLNZZjZr8+bNhxCGVFRrtu/houe++V9ygCBhfLNct/aI5BdNVbFu7p6S\nN+HuqcBhh7DPNcAad/86nJ5IkDA2mlkLgPDvpkgru/vTYbJKatJElx1K9Nydl79eycmPfMbsldv5\n9bEdqF4lgUoGVSonMKhDo3iHKFKqRHOSeoGZPQO8RHDF0QUcwngQ7r7BzFabWVd3XwScCMwP/10M\njAv/vlXUfYgUtHrbHsZMTuaLpVs5smMj7j+zD20a1mRYz+bMTNvKoA6NGNBOVyyJ5GfuXvgCwYhy\nvyE4sQxB0b4n3D2jyDs1O4zgMteqQBpwKUFv5nWgLbAKOPtA5TySkpJ81qxZRQ1DKoDcXOflb1Zx\n39QFGHDT8B783+EqricVm5nNdvekAy0XzZ3UGWb2JDA1/MV/yNx9DsGJ74JOLI7tiwCs2rqHGybN\nZWbaNo7p3Jj7RvWmdYOa8Q5LpMyIZkzqEcCDBL/224e//u909xGxDk6kKHJznX/PXMm49xZSKcEY\nN6o35wxUr0HkYEVzDuI24HCC+xJw9zlmlhi7kESKbuXWdK6fmMw3y7dxXJcm3DeqNy3r14h3WCJl\nUjQJItvdd+rXl5RmubnO81+u4IEPFlKlUgIPnNWHswe0Vq9B5BBEkyBSzew8oJKZdQauAb6MbVgi\n0Vu+JZ0bJs7l2xXbOb5rE+4d1ZsW9dRrEDlU0SSIPwA3AfuA/wAfAHfHMiiRaOTkOuO/WM6DHyyi\nWuUEHjq7L6P6t1KvQaSYHGhM6krAHe5+PUGSECkVlm3+gesnzOW7VTs4sVtT7h3VW8N/ihSzA41J\nnWNmA0oqGJEDycl1nv08jYc+XEz1KpV45Jy+jDxMvQaRWIjmENP3ZjYFmMBPx6SeHLOoRCJYumk3\n109M5vtVO/hFj2bcM7IXTdVrEImZaBJEQ2ArcEK+eQ4oQUiJyM7J5ZnPl/PwtMXUrFqJR889jBF9\nW6rXIBJj0dxJfWlJBCISyZKNu7luYjJzV+9gWM/m3DWyF03qVIt3WCIVQjR3Uo8n6DH8hLtfFpOI\nRAh6DU99lsajHy2hdvXKPHZeP4b3bqFeg0gJiuYQ0zv5HlcHzgDWxSYcEVi0YTfXT5xL8pqdnNq7\nOXee3ovGtdVrEClp0RximpR/2sxeAT6KWURSYWXl5PLUp8t49OMl1K1ehX+e15/hfVrEOyyRCiua\nHkRBnQlKcosUmwXrd3HdhLnMW7eL0/q04I4RPWmkXoNIXEVzDmI3Pz0HsQEYHbOIpELJysnl8enL\neGz6EurVqMIT5/fnlN7qNYiUBtEcYtIo7hIT89bt5PoJycxfv4vTD2vJbb/sScNaVeMdloiEoulB\nHAXMcfd0M7uAYPzoR919Zcyjk3IpMzuXx6Yv5fHpS6lfsypPXTiAk3s2j3dYIlJANOcgngD6mllf\n4AbgWeBF4LhYBiblU+ranVw3YS4LN+zmjH6tuO2XPahfU70GkdIo2vEg3MxOJ+g5PGtmF8c6MClf\n9mXn8NgnS3l8xjIa1arKMxclMbRHs3iHJSKFiCZB7DazscAFwLFhhdcqsQ1LypPkNTu4fkIyizbu\n5sz+rbn1tB7Uq6mPkEhpF02COAc4D7jc3TeYWVuCMapFCrUvO4dHP1rCU5+l0bh2VZ67JIkTuqnX\nIFJWRHMV0wbg4XzTqwjOQYjs15zVO7h+wlyWbPqBXyW15qbhPahXQ70GkbIkmquYBgH/ALoDVYFK\nwA/uXi/GsUkZlJGVw98+WsLTny2jWd3qPH/pQIZ0bRrvsESkCKI5xPQYcC7BeBBJwEUEd1OL/MR3\nq7Zz/YS5LNuczrkD23Dj8O7Ura5eg0hZFVWpDXdfamaV3D0HGG9mX8Y4LilDMrJyeHjaYp75bxrN\n61bnxcsO59guTeIdlogcomgSxB4zqwrMMbMHgPVArdiGJWXF7JXbuH5CMmlb0jnviLaMPaUbddRr\nECkXokkQFwIJwO+BPwFtgDNjGZSUfnszc3jow0U8+8VyWtarwUuXH8HRnRvHOywRKUbRXMW00sxq\nAC3c/Y4SiElKuW9XbOOGicks35LOBYPaMuaU7tSuVpTCwCJSmkVzFdMvgb8SXMHU3swOA+509xGx\nDk5Klz2Z2Tz4wSKe/3IFrerX4D9XHMGRndRrECmvovnZdztwODADwN3nmFlizCKSUunrtK3cMCmZ\nlVv3cNHgdowe1o1a6jWIlGvR1mLaqbGAK6b0fdk88P5CXvhqJW0b1uSVXw9icMdG8Q5LREpANAki\n1czOAyqZWWfgGkCXuVYAXy7bwuhJyazetpdLjkzkhmFdqVlVvQaRiiKa/+1/AG4C9gGvAB8Ad8Uy\nKImv9H3ZjHtvIf+euZLERjV5/arBHN6+YbzDEpESFs1VTHsIEsRNsQ9H4u2LpUGvYe2OvVx+dHuu\nO6krNapWindYIhIH0VzFlATcCCTmX97d+xzKjsOy4bOAte5+mpm1B14FGgLfARe6e+ah7EOitzsj\ni/veW8h/vl5F+8a1mHDVYJIS1WsQqciiOcT0MnA9kALkFuO+rwUWAHXD6fuBR9z9VTN7EricYDQ7\nibH/LtnMmEkprNu5l18f056/nNSV6lXUaxCp6KJJEJvdfUpx7tTMWgPDgXuAP1twidQJBONOALxA\ncHmtEkQM7c7I4t6pC3jlm9V0aFKLiVcfyYB2DeIdloiUEtEkiNvM7BngY4IT1QC4++RD2O/fCMa3\nrhNONwJ2uHt2OL0GaHUI25cD+HTxZsZOSmbDrgyuOq4DfxraRb0GEfmJaBLEpUA3gmFG8w4xOVCk\nBGFmpwGb3H22mQ3Jmx1hUd/P+lcCVwK0bdu2KCFUaLsysrjnnQW8Nms1nZrWZtJvjqRfW/UaROTn\nokkQfd29dzHu8yhghJmdClQnOAfxN6C+mVUOexGtgXWRVnb3p4GnAZKSkiImEYls+sJNjJ2cwqbd\nGfx2SEeuObGzeg0isl8JUSwz08x6FNcO3X2su7d290SCgYg+cffzgenAWeFiFwNvFdc+K7qde7K4\nbsJcLn3+W+rWqMwbvz2KG4Z1U3IQkUJF04M4GrjYzJYTnIMwwA/1MtcIRgOvmtndwPfAs8W8/Qrp\n4wUbufGNFLb8kMnvj+/EH07sRLXKSgwicmDRJIhhsdq5u8/gxyKAaQRFAaUY7NiTyZ1vz2fy92vp\n1rwOz1w0kN6tNYy4iEQvqvEgSiIQKT7T5ge9hu3pmVxzQid+f0JnqlaO5miiiMiPVHmtHNmenskd\nb8/jzTnr6N6iLuMvGUivVuo1iEjRKEGUE++nbuDmN1PZsSeTPw7tzG+HdFKvQUQOiRJEGbctPZPb\npszj7bnr6NGiLi9edjg9WtY98IoiIgegBFGGTU1Zzy1vprIrI4u//KILVw/pSJVK6jWISPFQgiiD\ntvywj9vemse7Kevp1aouL599BN2aq9cgIsVLCaKMeSd5Hbe+NY8fMrK5/uSuXHlsB/UaRCQmlCDK\ngNkrt/Pxgo18t2o7M9O20bd1PR48uy9dmtU58MoiIkWkBFHKzV6xjXP/NZOsnKDs1AVHtOP2ET2o\nrF6DiMSYvmVKsdXb9nDdxOT/JYcEgxb1qys5iEiJUA+iFMrOyWX8Fyt4eNpi3J3KCYa7U6VyAoM6\nNIp3eCJSQShBlDKpa3cyZnIyqWt3MbR7U+48vRfrd2YwM20rgzo00ohvIlJilCBKiT2Z2TwybTHP\nfr6cRrWr8fj5/TmlV3PMjJb1aygxiEiJU4IoBWYs2sRNb6SydsdezjuiLaOHdaNejSrxDktEKjgl\niDja8sM+7npnPm/NWUfHJrV4/arBHN6+YbzDEhEBlCDiwt2ZMHsN97y7gL2ZOfxxaGd+M6SjBvIR\nkVJFCaKELd+Szo2TU/gqbSsDExtw36jedGqqG95EpPRRgighmdm5PP3ZMv7+yVKqVU7g3jN6c+7A\nNiQkWLx1WYElAAANyElEQVRDExGJSAmiBMxeuZ0bJ6ewaONuhvduwW2/7EHTutXjHZaISKGUIGJo\nd0YWD36wiH/PXEnzutV55qIkhvZoFu+wRESiogQRIx/M28Btb81j4+4MLh6cyHUnd6V2NTW3iJQd\n+sYqZht2ZnDblFQ+mLeRbs3r8OSFAzisTf14hyUictCUIIpJbq7z8jereOC9hWTm5DJ6WDeuOKa9\nxmoQkTJLCaIYLN64m7GTU5i9cjtHdWrEPSN7k9i4VrzDEhE5JEoQhyAjK4d/Tl/Kk58uo3a1yjx0\ndl9G9W+FmS5dFZGyTwmiiGambeXGySmkbUlnVL9W3DS8O41qV4t3WCIixUYJ4iDt2JPJfVMX8tqs\n1bRpWIMXLzucY7s0iXdYIiLFTgkiSu7OO8nruePteWzfk8VVx3Xgjyd2oUZV1U8SkfJJCSIKa7bv\n4ZY3U5m+aDN9WtfjhcsOp2fLevEOS0QkppQgCpGdk8vzX67goQ8XYwa3nNaDS45MpJLqJ4lIBaAE\nsR+pa3cydnIKKWt3cnzXJtw1shetG9SMd1giIiVGCaKAPZnZ/O2jJTz7+XIa1KzKY+f1Y3jvFrp0\nVUQqHCWIfD5dvJmb30xh9ba9nDuwDWNP6U69mhr6U0QqJiUIgqE/735nPm/OWUeHxrV49cpBDOrQ\nKN5hiYjEVYVOEO7OxNlruGfqAtL3ZXPNCZ347fGdqF5Fl66KiJR4gjCzNsCLQHMgF3ja3R81s4bA\na0AisAL4lbtvj0UMs1du5/3U9cxM20rK2l0MaBcM/dmlmYb+FBHJE48eRDbwF3f/zszqALPNbBpw\nCfCxu48zszHAGGB0ce989srtnPv0V2TlOABXHtuBMcO6aehPEZECSrwWtbuvd/fvwse7gQVAK+B0\n4IVwsReAkbHY/8y0reTkBsmhkkG9GlWUHEREIojrYAVmlgj0A74Gmrn7egiSCNB0P+tcaWazzGzW\n5s2bD3qfgzo0omrlBCoZVKmcoJPRIiL7Ye4enx2b1QY+Be5x98lmtsPd6+d7fru7NyhsG0lJST5r\n1qyD3vfslduZmbaVQR0aMaBdobsQESl3zGy2uycdaLm4XMVkZlWAScDL7j45nL3RzFq4+3ozawFs\nitX+B7RroMQgInIAJX6IyYJbkp8FFrj7w/memgJcHD6+GHirpGMTEZEfxaMHcRRwIZBiZnPCeTcC\n44DXzexyYBVwdhxiExGRUIknCHf/HNjfZUMnlmQsIiKyf3G9iklEREovJQgREYlICUJERCKK230Q\nxcHMNgMr4x1HBI2BLfEOohRQOwTUDgG1Q6A0tEM7d29yoIXKdIIorcxsVjQ3oZR3aoeA2iGgdgiU\npXbQISYREYlICUJERCJSgoiNp+MdQCmhdgioHQJqh0CZaQedgxARkYjUgxARkYiUIA6BmbUxs+lm\ntsDM5pnZteH8hmY2zcyWhH8rROlYM6tkZt+b2TvhdHsz+zpsh9fMrGq8YywJZlbfzCaa2cLwszG4\nIn4mzOxP4f+LVDN7xcyqV4TPhJk9Z2abzCw137yI778F/m5mS80s2cz6xy/yn1OCODR5w6d2BwYB\nvzOzHgTDpX7s7p2Bj8PpiuBaghEC89wPPBK2w3bg8rhEVfIeBd53925AX4I2qVCfCTNrBVwDJLl7\nL6AScC4V4zPxPDCswLz9vf+nAJ3Df1cCT5RQjFFRgjgE8R4+tTQxs9bAcOCZcNqAE4CJ4SIVpR3q\nAscSlLTH3TPdfQcV8DNBUAy0hplVBmoC66kAnwl3/wzYVmD2/t7/04EXPTATqB+Oh1MqKEEUk6IM\nn1rO/A24AcgNpxsBO9w9O5xeQ5A8y7sOwGZgfHi47Rkzq0UF+0y4+1rgrwSl+9cDO4HZVMzPBOz/\n/W8FrM63XKlqEyWIYhAOnzoJ+KO774p3PCXNzE4DNrn77PyzIyxaES6Zqwz0B55w935AOuX8cFIk\n4TH204H2QEugFsHhlIIqwmeiMKX6/4kSxCEqbPjU8PmYDp9aShwFjDCzFcCrBIcR/kbQXc4bc6Q1\nsC4+4ZWoNcAad/86nJ5IkDAq2mdiKLDc3Te7exYwGTiSivmZgP2//2uANvmWK1VtogRxCDR8asDd\nx7p7a3dPJDgR+Ym7nw9MB84KFyv37QDg7huA1WbWNZx1IjCfCvaZIDi0NMjMaob/T/LaocJ9JkL7\ne/+nABeFVzMNAnbmHYoqDXSj3CEws6OB/wIp/Hjs/UaC8xCvA20Jh09194InrcolMxsCXOfup5lZ\nB4IeRUPge+ACd98Xz/hKgpkdRnCyviqQBlxK8GOsQn0mzOwO4ByCq/2+B64gOL5erj8TZvYKMISg\nautG4DbgTSK8/2HyfIzgqqc9wKXuPisecUeiBCEiIhHpEJOIiESkBCEiIhEpQYiISERKECIiEpES\nhIiIRKQEIcXKzP5oZjXzTU81s/pxjul2M7uuiOvOMLNCxw8u+Jqj3O6QvKq3EZ57Jazs+aeD2WYs\nhfEemW96ZFiYMm/6TjMbGsP9zzCzRWY2opBlzgmrokZsVzl4ShAVQHgTTkm9138kKMwGgLufGhar\nK89+8poPhZk1B4509z7u/kiB5yrvZ7WSMITgTug8I4H/JQh3v9XdP4pxDOe7+5T9PenurxHcayHF\nRAminDKzxHAsgseB74A2ZvaEmc0Ka/TfkW/ZFWZ2r5l9FT7f38w+MLNlZnZ1uMwQM/vMzN4ws/lm\n9mTBpGNm1xDU3ZluZtPzbbtxGM/CsHhdqpm9bGZDzeyLsEb+4eHytSyop/9tWOzu9P28vhvMLMXM\n5prZuHDer8P15prZpEi/6s2sk5l9FC7znZl1LPhr3sweM7NLIqz7s/bbz2s+KWzL78xsggW1ujCz\nYWEbfA6M2s9b9yHQ1MzmmNkx4S/ne83sU+BaM2tnZh+HPYyPzaxtuO3nw/imm1mamR0XtuMCM3t+\nP204Lnwvk83sr+G8JmHbfRv+O8qCQpRXA38K4zoOGAE8GE53DPd/Vr73/I7w9aeYWbd8254Wzn/K\nzFaGn41aZvZu+J6kmtk5+2mb/LFfky/2Vw+0vBSRu+tfOfwHJBLc3T0o37yG4d9KwAygTzi9AvhN\n+PgRIBmoAzQhKMIHwS/IDIJqpZWAacBZEfa7AmhccDqMJxvoTfDDZDbwHEGxstOBN8Pl7yW4uxag\nPrAYqFVgH6cAXwI1C7yuRvmWuRv4Q/j4doK7uyG4y/2M8HF1gl/+Q4B38q37GHBJ+HgGwZgGB2q/\nxuHjxsBneTEDo4Fbw32tJqj7bwR31b4Tof0SgdR80zOAx/NNvw1cHD6+LF+7PU9wh3Jee+4q0NaH\nFdhPQ2ARP94sWz/8+x/g6PBxW4IyMj9pw3z7OyvSdNgeeW3/W+CZfO06Nnw8jKAoXWPgTOBf+bZV\nL0K7/O99CKfXAdXyx57vc/qzdtW/ov1TD6J8W+lBjfk8vzKz7whKHPQk3yECgpowEJQN+drdd7v7\nZiDDfjyH8I27p7l7DvAKcPRBxrPc3VPcPReYRzCAiof7TAyXOQkYY2ZzCL4UqhN8UeU3FBjv7nsA\n/MeSFb3M7L9mlgKcH77G/zGzOkArd38jXC8jbxtRKqz98gwK538RvoaLgXZAt/D1Lwlf80sHsd/X\n8j0eTPAlDvBvfvoevJ2vPTcWaOvEAtvcRZDwnzGzUQRlHiBo28fC2KcAdcN2O1h5hStn59v30QRJ\nDHd/n2DAIMJ4h5rZ/WZ2jLvvjGL7ycDLZnYBwQ8PiYF4HtOU2EvPe2Bm7YHrgIHuvj087FA937J5\n9XBy8z3Om877nBSsy3KwdVoKbjf/PvP2YcCZ7r6okO3Yfvb9PDDS3eeGh4iGRFgvkmx+eri1esEF\nomi//PuY5u7/V2D9w/YTczTSC3ku/zajeQ+Dldyzw8N6JxIUWPw9QRXeBGCwu+/Nv7zZ/ppuv/L2\nn8NP39ufvwD3xWY2ADgVuM/MPnT3Ow+w/eEEAzONAG4xs57+4zgTUkzUg6g46hJ80ew0s2ZErs1/\nIIdbMKZwAkERts8jLLOb4PBUUX0A/MHCbyQz6xdhmQ+By/LOMZhZw3B+HWC9BSXYzy+4kgdjdawx\ns5HhetXCbawEeoTT9Qi+NAsqrP3yv+aZwFFm1incR00z6wIsBNqbWcdwuZ8kkIPwJcEXOuFrjPQe\nHFB4XqSeu08lOMl+WPjUhwTJIm+5vPkF39eivM+fA78Kt3sSkDcuc0tgj7u/RDDIUKHjMoefvzbu\nPp1gkKr6QO2DjEWioARRQbj7XIJDI/MIjv1/UYTNfAWMA1KB5cAbEZZ5Gngv74RtEdwFVAGSLRj0\n/a6CC4SHJ6YAs8JDIXmXsN5CcI5hGsEXciQXAteYWTLBl21zd19NcE4gGXiZoJ0K7rOw9vvfaw4P\ny10CvBLuYybQzd0zCMYcfjc8Sb0yuub4mWuAS8NtX0gwDnhR1AHeCbfzKZB3Se01QFJ48nc+wclp\nCM59nJF38pzgUNH1FlxI0LHgxvfjDuCk8DDdKQQjze0mOFfyTfhe3kRw/qgwlYCXwkOJ3xOMcV3e\nr5SLC1VzlahYvjLe8Y5FyiYzqwbkhIe3BhOMunfYgdYL151B8PkrtBS2PqfFSz0IESkpbYFvzWwu\n8Hfg1wex7jbgeTvAjXLA4/x48lsOkXoQIiISkXoQIiISkRKEiIhEpAQhIiIRKUGIiEhEShAiIhKR\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 "[