{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Lecture 6: Python for Instrument Control (1.5 Hours) #" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### ABSTRACT ###\n", "\n", "In this lecture we will see how we can use Python to make communicating with scientific instruments easier. We will:\n", "\n", "- learn about **unit support** for calculations in Python via two packages (Quantities and Pint),\n", "- go deeper into **classes** in Python and how they can be useful for designing interfaces to communicate to instruments,\n", "- **design a driver** for a demo instrument provided in the [git repo](https://github.com/QuinnPhys/PythonWorkshop-science) for this workshop.\n", "---" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Unitful Computing with python-quantities and Pint (15 Minutes)##" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Before getting to actually controlling instruments, we'll briefly mention how to use Python to perform numerical computations in a *unitful* way. We'll demonstrate using python-quantities, which provides a *quantity* data type to represent arrays with associated units (and is how units are handled in IntrumentKit). \n", "\n", "For now, let's go on and import python-quantities." ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "collapsed": false }, "outputs": [], "source": [ "import numpy as np\n", "import quantities as pq" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can now create quantities by specifying both a magnitude and a unit string." ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "array(2) * m/s" ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "velocity = pq.Quantity(2, 'm / s')\n", "velocity" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Units can also be applied by arithmetically multiplying them with an array from numpy or just a python list. The following two ways of adding units are equivalent:" ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[ 1. 2. 3.] N\n", "[ 1. 2. 3.] N\n" ] } ], "source": [ "vader = np.array([1,2,3]) * pq.N\n", "luke = [1,2,3] * pq.N\n", "print(vader) \n", "print(luke)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Arithmetic operations on quantities respect units." ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "array(2.0) * m/s**2" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "acceleration = velocity / pq.second\n", "acceleration" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Importantly, operations on quantities must be correct for dimensions, but may use different units." ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "array(1.3048) * m" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "pq.Quantity(1, 'meter') + pq.Quantity(1, 'foot')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now, you may ask what happens when we use dimensionally incorrect units. Look at the below raised error:" ] }, { "cell_type": "code", "execution_count": 6, "metadata": { "collapsed": false }, "outputs": [ { "ename": "ValueError", "evalue": "Unable to convert between units of \"s\" and \"m\"", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mAssertionError\u001b[0m Traceback (most recent call last)", "\u001b[0;32m/home/skaiser/anaconda3/lib/python3.5/site-packages/quantities/quantity.py\u001b[0m in \u001b[0;36mrescale\u001b[0;34m(self, units)\u001b[0m\n\u001b[1;32m 191\u001b[0m \u001b[0;32mtry\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 192\u001b[0;31m \u001b[0mcf\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mget_conversion_factor\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mfrom_u\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mto_u\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 193\u001b[0m \u001b[0;32mexcept\u001b[0m \u001b[0mAssertionError\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;32m/home/skaiser/anaconda3/lib/python3.5/site-packages/quantities/quantity.py\u001b[0m in \u001b[0;36mget_conversion_factor\u001b[0;34m(from_u, to_u)\u001b[0m\n\u001b[1;32m 52\u001b[0m \u001b[0mto_u\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mto_u\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_reference\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 53\u001b[0;31m \u001b[0;32massert\u001b[0m \u001b[0mfrom_u\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdimensionality\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0mto_u\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdimensionality\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 54\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mfrom_u\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmagnitude\u001b[0m \u001b[0;34m/\u001b[0m \u001b[0mto_u\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmagnitude\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;31mAssertionError\u001b[0m: ", "\nDuring handling of the above exception, another exception occurred:\n", "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)", "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mpq\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mQuantity\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m'meter'\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0mpq\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mQuantity\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m'second'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[0;32m/home/skaiser/anaconda3/lib/python3.5/site-packages/quantities/quantity.py\u001b[0m in \u001b[0;36mg\u001b[0;34m(self, other, *args)\u001b[0m\n\u001b[1;32m 61\u001b[0m \u001b[0mother\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mother\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mview\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mtype\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mQuantity\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 62\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mother\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_dimensionality\u001b[0m \u001b[0;34m!=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_dimensionality\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 63\u001b[0;31m \u001b[0mother\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mother\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mrescale\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0munits\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 64\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mf\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mother\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m*\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 65\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mg\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;32m/home/skaiser/anaconda3/lib/python3.5/site-packages/quantities/quantity.py\u001b[0m in \u001b[0;36mrescale\u001b[0;34m(self, units)\u001b[0m\n\u001b[1;32m 194\u001b[0m raise ValueError(\n\u001b[1;32m 195\u001b[0m \u001b[0;34m'Unable to convert between units of \"%s\" and \"%s\"'\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 196\u001b[0;31m \u001b[0;34m%\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mfrom_u\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_dimensionality\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mto_u\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_dimensionality\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 197\u001b[0m )\n\u001b[1;32m 198\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mQuantity\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mcf\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmagnitude\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mto_u\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;31mValueError\u001b[0m: Unable to convert between units of \"s\" and \"m\"" ] } ], "source": [ "pq.Quantity(1, 'meter') + pq.Quantity(1, 'second')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This looks quite long, but there is basically two main places to look for what the error was: at the top ``ValueError`` and the bottom: ``ValueError: Unable to convert between units of \"s\" and \"m\"``. The text in the middle is just tracking where the functions were called from and is often not needed to troubleshoot the problem. We can use python statements ``try:`` and ``except`` to help us make errors more readable." ] }, { "cell_type": "code", "execution_count": 7, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Unable to convert between units of \"s\" and \"m\"\n" ] } ], "source": [ "try:\n", " pq.Quantity(1, 'meter') + pq.Quantity(1, 'second')\n", "except ValueError as ex:\n", " print(ex)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Quantities also support transforming between different unit systems. You can do this in one of two ways, you can directly set the attribute ``.units`` of the object or use the rescale method. \n", "\n", "*NB: if you directly set the attribute by specifying a named module variable (``pq.hour``) you must use the singular version of the name of the unit (not ``pq.hours``). If you are specifying a unit as a string" ] }, { "cell_type": "code", "execution_count": 8, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "8760.0 h\n" ] }, { "data": { "text/plain": [ "array(365.0) * d" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "love = pq.Quantity(525600.0, 'minutes')\n", "love.units = pq.hour\n", "print(love)\n", "love.rescale('days')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Such conversions are also checked for consistent dimensions." ] }, { "cell_type": "code", "execution_count": 9, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Unable to convert between units of \"m/s**2\" and \"kg\"\n" ] } ], "source": [ "try:\n", " acceleration.rescale('kg')\n", "except ValueError as ex:\n", " print(ex)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Finally, python-quantities also provides a range of useful physical constants that we can use to quickly construct quantities. " ] }, { "cell_type": "code", "execution_count": 10, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[[ 3.5 0. ]\n", " [ 0. -3.5]] gamma_p*(h/(2*pi))*T\n" ] } ], "source": [ "Sz = pq.constants.hbar * np.array([[1, 0], [0, -1]]) / 2\n", "H = pq.constants.gamma_p * pq.Quantity(7, 'tesla') * Sz\n", "print(H)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To make it easier to compare values, you can use the ``simplified`` attribute to convert all the units to a simplified form, and can use the ``rescale()`` method to specify particular units." ] }, { "cell_type": "code", "execution_count": 11, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[[ 9.87424663e-26 0.00000000e+00]\n", " [ 0.00000000e+00 -9.87424663e-26]] kg*m**2/s**2\n" ] }, { "data": { "text/plain": [ "array([[ 149.02118732, 0. ],\n", " [ 0. , -149.02118732]]) * MHz" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "print(H.simplified)\n", "(H.simplified / (2 * np.pi * pq.constants.hbar)).simplified.rescale('MHz')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Pint Units Package ###" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As we might expect, there is more than one option for a package in Python that can help us handle units. [Pint](https://pint.readthedocs.io/en/0.7.2/) is chronologically a newer package, but with a bit more features than quantities. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "First, let's check to see if the package is installed. Try running: \n", "```bash \n", "pip show pint\n", "```\n", "in your shell to see if pip knows about the package. If it returns nothing, then you do not have it installed and you should run:\n", "```bash \n", "pip install pint\n", "```\n", "If if does print out some stuff to the screen, then it will show you where it has the package installed. Let's look at some examples of using the Pint package. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Pints has this concept of a unit registry, that is a list of all the supported units and their relationship. " ] }, { "cell_type": "code", "execution_count": 12, "metadata": { "collapsed": true }, "outputs": [], "source": [ "import pint \n", "ureg = pint.UnitRegistry()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To use these units you simply multiply the magnitude of the value by an instance of a unit object defined by the unit registry. The variable ``time`` then becomes an instance of a quantity object:" ] }, { "cell_type": "code", "execution_count": 13, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "8.0 second\n" ] }, { "data": { "text/plain": [ "pint.unit.build_quantity_class..Quantity" ] }, "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ "time = 8.0 * ureg.second\n", "print(time)\n", "type(time)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You can also provide the units as a string that Pint will parse:" ] }, { "cell_type": "code", "execution_count": 14, "metadata": { "collapsed": true }, "outputs": [], "source": [ "time = 8.0 * ureg('seconds')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Being an instance of a quantity type means that we can access attributes of ``time``:" ] }, { "cell_type": "code", "execution_count": 15, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "8.0\n", "second\n", "[time]\n" ] } ], "source": [ "print(time.magnitude)\n", "print(time.units)\n", "print(time.dimensionality)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can look at how these ``Quantity`` objects are represented internally:" ] }, { "cell_type": "code", "execution_count": 16, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n" ] } ], "source": [ "print(repr(time))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This then allows us to be more direct about creating an instance of a ``Quantity``:" ] }, { "cell_type": "code", "execution_count": 17, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "3 foot\n" ] } ], "source": [ "distance = ureg.Quantity(3, 'feet')\n", "print(distance)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can perform arithmetic operations with the ``Quantity`` objects with no special syntax:" ] }, { "cell_type": "code", "execution_count": 18, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "0.375 foot / second\n" ] } ], "source": [ "speed = np.absolute( distance / time )\n", "print(speed)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can easily convert quantities with the ``.to`` method, which generates a new object with the new units. Else to change the units associated with the object ``speed`` we can use the ``.ito`` method." ] }, { "cell_type": "code", "execution_count": 19, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "0.41147999999999996 kilometer / hour\n", "0.375 foot / second\n", "0.41147999999999996 kilometer / hour\n" ] } ], "source": [ "print(speed.to(ureg.km / ureg.hour))\n", "print(speed)\n", "speed.ito(ureg.km / ureg.hour)\n", "print(speed)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Similar to quantities, Pint will warn when a particular unit conversion is not possible." ] }, { "cell_type": "code", "execution_count": 20, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Cannot convert from 'kilometer / hour' ([length] / [time]) to 'newton' ([length] * [mass] / [time] ** 2)\n" ] } ], "source": [ "try:\n", " speed.to(ureg.N)\n", "except pint.errors.DimensionalityError as ex:\n", " print(ex)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Lastly, if we want to look up what units are defined in the current registry, we can look at the different unit systems the registry knows about. By default calling ``ureg = UnitRegistry()`` makes units from all systems available." ] }, { "cell_type": "code", "execution_count": 21, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "['US', 'cgs', 'imperial', 'mks']" ] }, "execution_count": 21, "metadata": {}, "output_type": "execute_result" } ], "source": [ "dir(ureg.sys)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To see what units are defined in a specific unit system just look at the attributes of the system:" ] }, { "cell_type": "code", "execution_count": 22, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "['UK_hundredweight',\n", " 'UK_ton',\n", " 'acre_foot',\n", " 'cubic_foot',\n", " 'cubic_inch',\n", " 'cubic_yard',\n", " 'drachm',\n", " 'foot',\n", " 'grain',\n", " 'imperial_barrel',\n", " 'imperial_bushel',\n", " 'imperial_cup',\n", " 'imperial_fluid_drachm',\n", " 'imperial_fluid_ounce',\n", " 'imperial_gallon',\n", " 'imperial_gill',\n", " 'imperial_peck',\n", " 'imperial_pint',\n", " 'imperial_quart',\n", " 'inch',\n", " 'long_hunderweight',\n", " 'long_ton',\n", " 'mile',\n", " 'ounce',\n", " 'pound',\n", " 'quarter',\n", " 'short_hunderdweight',\n", " 'short_ton',\n", " 'square_foot',\n", " 'square_inch',\n", " 'square_mile',\n", " 'square_yard',\n", " 'stone',\n", " 'yard']" ] }, "execution_count": 22, "metadata": {}, "output_type": "execute_result" } ], "source": [ "dir(ureg.sys.imperial)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "## More on Class Inheritance (25 Minutes) ##" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Recall that in Python all types are functions which return values of that type. Classes in Python are a way of defining new types. Let's look a little more in-depth at classes and how they can be useful to us for communicating with instruments." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We will use this ``ExampleClass`` to explore our understanding of classes. It inherits the attributes of the ``object`` class which is the most basic class one can inherit from. Unlike the previous examples we are specifying an ``__init__`` definition. This function is called when an instance of the class is created and allows us to specify input arguments. " ] }, { "cell_type": "code", "execution_count": 23, "metadata": { "collapsed": false }, "outputs": [], "source": [ "class ExampleClass(object):\n", " '''\n", " An example of a basic class with attributes and methods.\n", " '''\n", " \n", " def __init__(self, name, value):\n", " '''\n", " Initialize the class, set the name, and value for the exponent method.\n", " '''\n", " self.name = name\n", " self.value = value\n", " \n", " tally = 0\n", " \n", " def test_exponent(self, x):\n", " self.tally = self.tally + 1\n", " return x ** self.value\n", " \n", " def say_name(self):\n", " print('My name is {name}.'.format(name = self.name))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You might notice the text enclosed with triple quotes (``\"\"\"`` or ``'''``). Triple quotes allow for easily defining a string that includes line breaks, but are otherwise identical to single quotes (``'`` or ``\"``). In any case, if the first line of a class or function is a string, then this is called a *docstring* and is basically documentation attached to that class or function. If you try running the below line, you can see that this text is then displayed in the pop-up." ] }, { "cell_type": "code", "execution_count": 24, "metadata": { "collapsed": true }, "outputs": [], "source": [ "ExampleClass?" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can then create an instance of this class by choosing a name and an exponent value. We can also check on the value of the attribute ``tally``:" ] }, { "cell_type": "code", "execution_count": 25, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "My name is Bob.\n", "27\n", "1\n" ] } ], "source": [ "inst = ExampleClass('Bob', 3)\n", "inst.say_name()\n", "print(inst.test_exponent(3))\n", "print(inst.tally)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can check to see if the attribute ``tally`` is keeping track of how many times we have called the ``test_exponent`` method." ] }, { "cell_type": "code", "execution_count": 26, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "64\n", "2\n" ] } ], "source": [ "print(inst.test_exponent(4))\n", "print(inst.tally)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If we want to look to see what attributes that ``ExampleClass`` is inheriting from ``object`` we can use the ``dir`` function to list attributes:" ] }, { "cell_type": "code", "execution_count": 27, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "['__class__',\n", " '__delattr__',\n", " '__dir__',\n", " '__doc__',\n", " '__eq__',\n", " '__format__',\n", " '__ge__',\n", " '__getattribute__',\n", " '__gt__',\n", " '__hash__',\n", " '__init__',\n", " '__le__',\n", " '__lt__',\n", " '__ne__',\n", " '__new__',\n", " '__reduce__',\n", " '__reduce_ex__',\n", " '__repr__',\n", " '__setattr__',\n", " '__sizeof__',\n", " '__str__',\n", " '__subclasshook__']" ] }, "execution_count": 27, "metadata": {}, "output_type": "execute_result" } ], "source": [ "dir(object)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Inheriting these attributes means that they are also valid attributes of ``ExampleClass``. For example, the ``__doc__`` attribute is used by the ``help()`` function and by IPython's and Jupyter's ``?`` commands to provide help. In our case, ``__doc__`` is defined for an instance of our example class ``cls`` and returns the docstring we set." ] }, { "cell_type": "code", "execution_count": 28, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "32" ] }, "execution_count": 28, "metadata": {}, "output_type": "execute_result" } ], "source": [ "inst.__sizeof__()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can also make a \"subclass\" of ``ExampleClass`` that overrides one of the methods that we defined in ``ExampleClass``:" ] }, { "cell_type": "code", "execution_count": 29, "metadata": { "collapsed": true }, "outputs": [], "source": [ "class MoreSpecificClass(ExampleClass):\n", " def say_name(self):\n", " print('My name is {name}. I raise things to the power of {power}.'.format(name=self.name, power=self.value))" ] }, { "cell_type": "code", "execution_count": 30, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "My name is Alice. I raise things to the power of 2.\n", "25\n", "1\n" ] } ], "source": [ "better_inst = MoreSpecificClass(\"Alice\", 2)\n", "better_inst.say_name()\n", "print(better_inst.test_exponent(5))\n", "print(better_inst.tally)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Having this inheriting structure for classes makes it easy to share definitions for methods, which can reduce copy/pasting of code and make things more readable. Critically, class inheritance also allows for redefining behavior without having to change how a class is used. In the case of instruments, a base class might represent a particular kind of instrument, such that code using subclasses of that class does not need to know which subclass it is using. We can write code for an entire set of different instruments by abstracting away the *interface* that our code uses to talk to each instrument. This interface is sometimes known as an *application-programming interface*, or *API*, as was [made famous recently](https://parkerhiggins.net/wp-content/uploads/2016/05/reimplementanapi.png). \n", "\n", "Python lets us represent this process of abstraction with *abstract classes* (sometimes called *abstract base classes*, in an effort to arrive at *ABCs* as an initialism). Unlike the classes we've seen so far, an abstract class is incomplete in the sense that you can't actually make new instances, but instead must make *concrete* subclasses which fill in the missing parts. Thus, an abstract class lets us write the demand that subclasses fill in and completely specify the behavior for a given interface before they can be considered concrete.\n", "\n", "Let's look at a quick example. In Python, support for ABCs is provided by the ``abc`` package. A class that can be made abstract must have ``abc.ABCMeta`` as its *metaclass*. While metaclasses are fascinating and wonderfully useful, they are also beyond the scope of this workshop, so for now, we'll note that a metaclass lets us add new concepts to the idea of a class itself, rather than to instances of it. Thus, ``ABCMeta`` adds the concept of abstraction to classes. We use the ``future`` package to specify metaclasses, since the syntax for this is different in Python 2 or 3. With that aside, then, our wonderfully compelling and not at all contrived example follows." ] }, { "cell_type": "code", "execution_count": 31, "metadata": { "collapsed": true }, "outputs": [], "source": [ "import abc\n", "from future.utils import with_metaclass" ] }, { "cell_type": "code", "execution_count": 32, "metadata": { "collapsed": true }, "outputs": [], "source": [ "class AbstractFoo(with_metaclass(abc.ABCMeta, object)):\n", " @abc.abstractmethod\n", " def bar(self):\n", " \"\"\"\n", " bar should really do something.\n", " \"\"\"\n", " pass" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We'll get back to what ``@abc.abstractmethod`` is doing in a moment, but for now let's take it for granted that it marks a method as being abstract. Because ``AbstractFoo`` has at least one abstract method, it is an abstract class, and we cannot make new instances yet." ] }, { "cell_type": "code", "execution_count": 33, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Can't instantiate abstract class AbstractFoo with abstract methods bar\n" ] } ], "source": [ "try:\n", " foo = AbstractFoo()\n", "except TypeError as ex:\n", " print(ex)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If we give ``AbstractFoo`` a specification of ``bar()`` by using inheritance, then we're in the clear." ] }, { "cell_type": "code", "execution_count": 34, "metadata": { "collapsed": true }, "outputs": [], "source": [ "class Foo(AbstractFoo):\n", " def bar(self):\n", " print(\"<3\")" ] }, { "cell_type": "code", "execution_count": 35, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "<3\n" ] } ], "source": [ "foo = Foo()\n", "foo.bar()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Below, we'll explore this idea further now by looking at some abstract classes that could describe kinds of instruments we might want to control with Python." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### A Brief Aside About Decorators ###" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "*NB: This part's a bit more advanced. Please don't worry if it doesn't all make sense the first time through, or even the second. It's not that hard in some ways, but it's also a bit different and that takes a lot of getting used to. If you are interested [this](https://realpython.com/blog/python/primer-on-python-decorators/) is a good tutorial*" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "First, though, what was ``abc.abstractmethod`` doing above? Notice that the line started with an at-character (``@``). In Python, this denotes that a function is being used as a *decorator*, a fancy name for a function that takes a function and returns a function. Let's look at a very simple case, in which the decorator does nothing (is the identity decorator), but has a side effect of printing something." ] }, { "cell_type": "code", "execution_count": 36, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Defining ...\n", "4\n", "9\n" ] } ], "source": [ "def loud_identity(fn):\n", " print(\"Defining {}...\".format(fn))\n", " return fn\n", "\n", "@loud_identity\n", "def bar(x):\n", " return x ** 2\n", "\n", "print(bar(2))\n", "print(bar(3))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can make more complicated decorators, too. For instance, we could make a decorator to represent applying a function twice, similar to what we did in [Lecture 2](https://nbviewer.jupyter.org/github/QuinnPhys/PythonWorkshop-science/blob/master/lecture-2-python-general.ipynb) to explore that functions are values." ] }, { "cell_type": "code", "execution_count": 37, "metadata": { "collapsed": true }, "outputs": [], "source": [ "from functools import wraps\n", "\n", "def apply_twice(fn):\n", " @wraps(fn)\n", " def new_fn(x):\n", " return fn(fn(x))\n", " return new_fn\n", "\n", "@apply_twice\n", "def bar(x):\n", " return x * 2 + \"!\"" ] }, { "cell_type": "code", "execution_count": 38, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "'abab!abab!!'" ] }, "execution_count": 38, "metadata": {}, "output_type": "execute_result" } ], "source": [ "bar(\"ab\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here, the ``wraps`` decorator is provided by the Python standard library to copy docstrings, function names and other metadata to a new function; it's a decorator that helps us to write decorators.\n", "\n", "*NB: Note that ``wraps`` takes an argument. This is an example of what's called [currying](https://en.wikipedia.org/wiki/Currying), in that ``wraps`` takes a function and returns a decorator. That is, a function from functions to functions from functions to functions.*\n", "\n", "Back to ``abc.abstractmethod``, then, what does it do?" ] }, { "cell_type": "code", "execution_count": 39, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 39, "metadata": {}, "output_type": "execute_result" } ], "source": [ "AbstractFoo.bar.__isabstractmethod__" ] }, { "cell_type": "code", "execution_count": 40, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "frozenset({'bar'})" ] }, "execution_count": 40, "metadata": {}, "output_type": "execute_result" } ], "source": [ "AbstractFoo.__abstractmethods__" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "That's the crux of it, really. The ``abstractmethod`` decorator sets a new attribute on the method (``bar.__isabstractmethod__``) and adds the method to a special attribute on the class (``AbstractFoo.__abstractmethods__``). Python then uses these internally (hence the double-underscore names) to track that ``bar`` is abstract. In fact, if we take the wholly inadvisable step of modifying ``__abstractmethods__``, we can even trick Python into letting us make instances of abstract classes!\n", "\n", "*NB: Don't do this. Seriously.*" ] }, { "cell_type": "code", "execution_count": 41, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "<__main__.AbstractFoo at 0x7f80774a07b8>" ] }, "execution_count": 41, "metadata": {}, "output_type": "execute_result" } ], "source": [ "AbstractFoo.__abstractmethods__ = {}\n", "AbstractFoo()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "## Let's build up an Instrument and talk to it! (50 Minutes) ##" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true }, "source": [ "To actually put what we have learned now about abstract classes to use, we will use the ``InstrumentKit`` Python package to write a class that can talk to a fake instrument. This instrument is also written in Python with a package that allows you to make GUIs ([PyQt4](http://pyqt.sourceforge.net/Docs/PyQt4/introduction.html#pyqt4-components)).\n", "It will not matter though that it is also written in Python as we will *only* interface with it via text strings, which is the basis for many instrument communication protocols. \n", "\n", "To install our \"instrument\" simply, open a shell and navigate to where you checked out the git repo for this workshop. Once there navigate to the ``epqis16-demos`` folder. In this folder, we have a package that you can install to python by running the following command:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "```bash\n", "$ python setup.py install\n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now to actually run the instrument simply run this command in the shell you have open:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "```bash\n", "$ epqis16 demo_instrument\n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The shell should now show ``Waiting for a connection from InstrumentKit on port 8042.``. We will leave it like this for now." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### The problem ###" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Consider the following situation: your experiment needs constant babysitting to set values and make sure that it functions correctly. You decide that to make your life simpler you will write a program that can set all the measurements needed and save the output data. The device you need to control is a four channel DC power supply. Opening the manual you find the following table for what commands you can send to the device:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "| SPCI command | Example usage | Options | Description |\n", "|---------------------------|--------------------------------------------------------------------|-----------------------------------------------------------------------------------------|-------------------------------------------------------|\n", "| ``*IDN?`` | Query return:
``EPQIS16 Demonstration Instrument`` | | Find out what the name of the instrument is. |\n", "| ``CH VOLTS?`` | ``CH1 VOLTS?``
``CH2 VOLTS?``
Query return:
``{value in mV}`` | `` = {1, 2, 3, 4}`` | Check what the output voltage is set to (returns mV). |\n", "| ``CH VOLTS `` | ``CH1 VOLTS 314``
``CH3 VOLTS 159`` | `` = {1, 2, 3, 4}``
`` = voltage value in mV`` | Set the voltage output (provided value is mV). |\n", "| ``CH ENABLE `` | ``CH2 ENABLE OFF``
``CH4 ENABLE ON`` | `` = {1, 2, 3, 4}``
`` = {ON, OFF}``
Default: ``OFF`` | Toggle the voltage channel output on/off. | \n", "| ``CH ENABLE?`` | ``CH1 ENABLE?``
``CH2 ENABLE?``
Query return:
``{ON, OFF}`` | `` = {1, 2, 3, 4}`` | Check the output status of a voltage channel. | " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can do some initial tests to see if we can send or receive commands from the instrument. Since we saw (intentionally erroneous as you will find in real life) that the manual specified the communication protocol as SPCI. Assuming that is was true, we could consider treating our instrument as a generic SCPI instrument from the ``InstrumentKit`` package. Let's start by importing as ik:" ] }, { "cell_type": "code", "execution_count": 42, "metadata": { "collapsed": true }, "outputs": [], "source": [ "import instruments as ik" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now, since we read elsewhere in the manual for this device that to connect to it we use a tcpip protocol and connect to port 8042. All devices will have some section describing how to do this initial connection, but because we are using a demo \"instrument\" anyway, this is how we have chosen to connect. \n", "\n", "The idea now is that we could use the communication protocols that IK has built in to see if we can find and connect to the device. We start by creating an instance of a generic instrument class that only knows how to communicate via SCPI. " ] }, { "cell_type": "code", "execution_count": 45, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "instruments.generic_scpi.scpi_instrument.SCPIInstrument" ] }, "execution_count": 45, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ins = ik.generic_scpi.SCPIInstrument.open_tcpip(\"localhost\", 8042)\n", "type(ins)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "*NB: The ``open_tcpip`` method above works on classes, not instances of classes, and is hence known as a class method. This is useful to us here, as we don't yet have an instance of a class for ``open_tcpip`` to act on. Rather, it provides a way of creating class instances in the first place.*" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Having done this, a little pop-up window should have appeared on your screen. This is the front panel of our demo device, a 4 channel DC power supply! Now, let's see if we can send/receive sensible things to/from the device using the ``query`` and ``sendcmd`` methods of the SCPIInstrument class. *NB: you could get similar sendcmd/query commands from other python serial communication packages, but we will look at those in IK since we will also use IK for other things.*" ] }, { "cell_type": "code", "execution_count": 46, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "'EPQIS16 Demonstration Instrument'" ] }, "execution_count": 46, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ins.query(\"*IDN?\")" ] }, { "cell_type": "code", "execution_count": 47, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "'0.0'" ] }, "execution_count": 47, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ins.query(\"CH2 VOLTS?\")" ] }, { "cell_type": "code", "execution_count": 48, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "'42.0'" ] }, "execution_count": 48, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ins.sendcmd(\"CH2 VOLTS 42\")\n", "ins.query(\"CH2 VOLTS?\")" ] }, { "cell_type": "code", "execution_count": 49, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "'OFF'" ] }, "execution_count": 49, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ins.query(\"CH2 ENABLE?\")" ] }, { "cell_type": "code", "execution_count": 50, "metadata": { "collapsed": true }, "outputs": [], "source": [ "ins.sendcmd(\"CH1 ENABLE ON\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This is cool! Go ahead and try turning the channels on and off in the GUI and see if that is correctly reported by the instrument. At this point, we have a decision point. We could write all of our code by just constructing the strings that are the commands we want to send to the device. Or we could try to something more *generalized*; in that we abstract the communication details specific to this device to a separate file and just have a generalized interface for other code to use the device. The advantages to the latter approach mean that if for some reason you need to switch to another device, you can just swap out the specific driver file, or perhaps you need to communicate to another similar instrument and don't want to risk having copies of the same code floating around. \n", "\n", "In principal the startup costs for both of these approaches are the same, in that you have to setup code to do the \"translation\" to the instrument. However, as we suggest here there is a lot to be gained by using a package such as IK (others are listed at the end of the lecture) to set up abstract instrument classes as well as classes that handle many types of common communication interfaces (GPIB, USB, VISA, Serial, VXI11, etc). All of these templates set up in these packages you could also write yourself, but the question is: \"wouldn't you rather be doing your experiment and not coding?\" Suffice it to say we will now look at how we could use abstract classes from IK to write a driver for our demo instrument. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### The Solution ###" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now as we have seen with class inheritance, IntrumentKit has a whole host of what are called abstract instruments that set up templates for what attributes and methods would be good to have in a class for a particular kind of instrument. We can take a look at what \"templates\" InstrumentKit has setup by using the ``help`` function:" ] }, { "cell_type": "code", "execution_count": 51, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Help on package instruments.abstract_instruments in instruments:\n", "\n", "NAME\n", " instruments.abstract_instruments - Module containing instrument abstract base classes and communication layers\n", "\n", "PACKAGE CONTENTS\n", " comm (package)\n", " electrometer\n", " function_generator\n", " instrument\n", " multimeter\n", " oscilloscope\n", " power_supply\n", " signal_generator (package)\n", "\n", "DATA\n", " absolute_import = _Feature((2, 5, 0, 'alpha', 1), (3, 0, 0, 'alpha', 0...\n", "\n", "FILE\n", " /home/skaiser/anaconda3/lib/python3.5/site-packages/instruments/abstract_instruments/__init__.py\n", "\n", "\n" ] } ], "source": [ "help(ik.abstract_instruments)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can see that there is an abstract instrument called ``power_supply``, so since we are trying to write a driver for one let's look at that in more detail:" ] }, { "cell_type": "code", "execution_count": 52, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Help on class PowerSupply in module instruments.abstract_instruments.power_supply:\n", "\n", "class PowerSupply(instruments.abstract_instruments.instrument.Instrument)\n", " | Abstract base class for power supply instruments.\n", " | \n", " | All applicable concrete instruments should inherit from this ABC to\n", " | provide a consistent interface to the user.\n", " | \n", " | Method resolution order:\n", " | PowerSupply\n", " | instruments.abstract_instruments.instrument.Instrument\n", " | builtins.object\n", " | \n", " | Data descriptors defined here:\n", " | \n", " | channel\n", " | Gets a channel object for the power supply. This should use\n", " | `~instruments.util_fns.ProxyList` to achieve this.\n", " | \n", " | This is an abstract method.\n", " | \n", " | :rtype: `PowerSupplyChannel`\n", " | \n", " | current\n", " | Gets/sets the output current for all channel on the power supply.\n", " | This is an abstract method.\n", " | \n", " | :type: `~quantities.quantity.Quantity`\n", " | \n", " | voltage\n", " | Gets/sets the output voltage for all channel on the power supply.\n", " | This is an abstract method.\n", " | \n", " | :type: `~quantities.quantity.Quantity`\n", " | \n", " | ----------------------------------------------------------------------\n", " | Data and other attributes defined here:\n", " | \n", " | __abstractmethods__ = frozenset({'channel', 'current', 'voltage'})\n", " | \n", " | ----------------------------------------------------------------------\n", " | Methods inherited from instruments.abstract_instruments.instrument.Instrument:\n", " | \n", " | __init__(self, filelike)\n", " | Initialize self. See help(type(self)) for accurate signature.\n", " | \n", " | binblockread(self, data_width, fmt=None)\n", " | \"\n", " | Read a binary data block from attached instrument.\n", " | This requires that the instrument respond in a particular manner\n", " | as EOL terminators naturally can not be used in binary transfers.\n", " | \n", " | The format is as follows:\n", " | #{number of following digits:1-9}{num of bytes to be read}{data bytes}\n", " | \n", " | :param int data_width: Specify the number of bytes wide each data\n", " | point is. One of [1,2,4].\n", " | \n", " | :param str fmt: Format string as specified by the :mod:`struct` module,\n", " | or `None` to choose a format automatically based on the data\n", " | width. Typically you can just specify `data_width` and leave this\n", " | default.\n", " | \n", " | query(self, cmd, size=-1)\n", " | Executes the given query.\n", " | \n", " | :param str cmd: String containing the query to\n", " | execute.\n", " | :param int size: Number of bytes to be read. Default is read until\n", " | termination character is found.\n", " | :return: The result of the query as returned by the\n", " | connected instrument.\n", " | :rtype: `str`\n", " | \n", " | read(self, size=-1)\n", " | Read the last line.\n", " | \n", " | :param int size: Number of bytes to be read. Default is read until\n", " | termination character is found.\n", " | :return: The result of the read as returned by the\n", " | connected instrument.\n", " | :rtype: `str`\n", " | \n", " | sendcmd(self, cmd)\n", " | Sends a command without waiting for a response.\n", " | \n", " | :param str cmd: String containing the command to\n", " | be sent.\n", " | \n", " | write(self, msg)\n", " | Write data string to the connected instrument. This will call\n", " | the write method for the attached filelike object. This will typically\n", " | bypass attaching any termination characters or other communication\n", " | channel related work.\n", " | \n", " | .. seealso:: `Instrument.sendcmd` if you wish to send a string to the\n", " | instrument, while still having InstrumentKit handle termination\n", " | characters and other communication channel related work.\n", " | \n", " | :param str msg: String that will be written to the filelike object\n", " | (`Instrument._file`) attached to this instrument.\n", " | \n", " | ----------------------------------------------------------------------\n", " | Class methods inherited from instruments.abstract_instruments.instrument.Instrument:\n", " | \n", " | open_file(filename) from abc.ABCMeta\n", " | Given a file, treats that file as a character device file that can\n", " | be read from and written to in order to communicate with the\n", " | instrument. This may be the case, for instance, if the instrument\n", " | is connected by the Linux ``usbtmc`` kernel driver.\n", " | \n", " | :param str filename: Name of the character device to open.\n", " | \n", " | :rtype: `Instrument`\n", " | :return: Object representing the connected instrument.\n", " | \n", " | open_from_uri(uri) from abc.ABCMeta\n", " | Given an instrument URI, opens the instrument named by that URI.\n", " | Instrument URIs are formatted with a scheme, such as ``serial://``,\n", " | followed by a location that is interpreted differently for each\n", " | scheme. The following examples URIs demonstrate the currently supported\n", " | schemes and location formats::\n", " | \n", " | serial://COM3\n", " | serial:///dev/ttyACM0\n", " | tcpip://192.168.0.10:4100\n", " | gpib+usb://COM3/15\n", " | gpib+serial://COM3/15\n", " | gpib+serial:///dev/ttyACM0/15 # Currently non-functional.\n", " | visa://USB::0x0699::0x0401::C0000001::0::INSTR\n", " | usbtmc://USB::0x0699::0x0401::C0000001::0::INSTR\n", " | \n", " | For the ``serial`` URI scheme, baud rates may be explicitly specified\n", " | using the query parameter ``baud=``, as in the example\n", " | ``serial://COM9?baud=115200``. If not specified, the baud rate\n", " | is assumed to be 115200.\n", " | \n", " | :param str uri: URI for the instrument to be loaded.\n", " | :rtype: `Instrument`\n", " | \n", " | .. seealso::\n", " | `PySerial`_ documentation for serial port URI format\n", " | \n", " | .. _PySerial: http://pyserial.sourceforge.net/\n", " | \n", " | open_gpibethernet(host, port, gpib_address) from abc.ABCMeta\n", " | .. warning:: The GPIB-Ethernet adapter that this connection would\n", " | use does not actually exist, and thus this class method should\n", " | not be used.\n", " | \n", " | open_gpibusb(port, gpib_address, timeout=3, write_timeout=3) from abc.ABCMeta\n", " | Opens an instrument, connecting via a\n", " | `Galvant Industries GPIB-USB adapter`_.\n", " | \n", " | :param str port: Name of the the port or device file to open a\n", " | connection on. Note that because the GI GPIB-USB\n", " | adapter identifies as a serial port to the operating system, this\n", " | should be the name of a serial port.\n", " | :param int gpib_address: Address on the connected GPIB bus assigned to\n", " | the instrument.\n", " | :param float timeout: Number of seconds to wait when reading from the\n", " | instrument before timing out.\n", " | :param float write_timeout: Number of seconds to wait when writing to the\n", " | instrument before timing out.\n", " | \n", " | :rtype: `Instrument`\n", " | :return: Object representing the connected instrument.\n", " | \n", " | .. seealso::\n", " | `~serial.Serial` for description of `port` and timeouts.\n", " | \n", " | .. _Galvant Industries GPIB-USB adapter: galvant.ca/#!/store/gpibusb\n", " | \n", " | open_serial(port=None, baud=9600, vid=None, pid=None, serial_number=None, timeout=3, write_timeout=3) from abc.ABCMeta\n", " | Opens an instrument, connecting via a physical or emulated serial port.\n", " | Note that many instruments which connect via USB are exposed to the\n", " | operating system as serial ports, so this method will very commonly\n", " | be used for connecting instruments via USB.\n", " | \n", " | This method can be called by either supplying a port as a string,\n", " | or by specifying vendor and product IDs, and an optional serial\n", " | number (used when more than one device with the same IDs is\n", " | attached). If both the port and IDs are supplied, the port will\n", " | default to the supplied port string, else it will search the\n", " | available com ports for a port matching the defined IDs and serial\n", " | number.\n", " | \n", " | :param str port: Name of the the port or device file to open a\n", " | connection on. For example, ``\"COM10\"`` on Windows or\n", " | ``\"/dev/ttyUSB0\"`` on Linux.\n", " | :param int baud: The baud rate at which instrument communicates.\n", " | :param int vid: the USB port vendor id.\n", " | :param int pid: the USB port product id.\n", " | :param str serial_number: The USB port serial_number.\n", " | :param float timeout: Number of seconds to wait when reading from the\n", " | instrument before timing out.\n", " | :param float write_timeout: Number of seconds to wait when writing to the\n", " | instrument before timing out.\n", " | \n", " | :rtype: `Instrument`\n", " | :return: Object representing the connected instrument.\n", " | \n", " | .. seealso::\n", " | `~serial.Serial` for description of `port`, baud rates and timeouts.\n", " | \n", " | open_tcpip(host, port) from abc.ABCMeta\n", " | Opens an instrument, connecting via TCP/IP to a given host and TCP port.\n", " | \n", " | :param str host: Name or IP address of the instrument.\n", " | :param int port: TCP port on which the insturment is listening.\n", " | \n", " | :rtype: `Instrument`\n", " | :return: Object representing the connected instrument.\n", " | \n", " | .. seealso::\n", " | `~socket.socket.connect` for description of `host` and `port`\n", " | parameters in the TCP/IP address family.\n", " | \n", " | open_test(stdin=None, stdout=None) from abc.ABCMeta\n", " | Opens an instrument using a loopback communicator for a test\n", " | connection. The primary use case of this is to instantiate a specific\n", " | instrument class without requiring an actual physical connection\n", " | of any kind. This is also very useful for creating unit tests through\n", " | the parameters of this class method.\n", " | \n", " | :param stdin: The stream of data coming from the instrument\n", " | :type stdin: `io.BytesIO` or `None`\n", " | :param stdout: Empty data stream that will hold data sent from the\n", " | Python class to the loopback communicator. This can then be checked\n", " | for the contents.\n", " | :type stdout: `io.BytesIO` or `None`\n", " | :return: Object representing the virtually-connected instrument\n", " | \n", " | open_usb(vid, pid) from abc.ABCMeta\n", " | Opens an instrument, connecting via a raw USB stream.\n", " | \n", " | .. note::\n", " | Note that raw USB a very uncommon of connecting to instruments,\n", " | even for those that are connected by USB. Most will identify as\n", " | either serial ports (in which case,\n", " | `~instruments.Instrument.open_serial` should be used), or as\n", " | USB-TMC devices. On Linux, USB-TMC devices can be connected using\n", " | `~instruments.Instrument.open_file`, provided that the ``usbtmc``\n", " | kernel module is loaded. On Windows, some such devices can be opened\n", " | using the VISA library and the `~instruments.Instrument.open_visa`\n", " | method.\n", " | \n", " | :param str vid: Vendor ID of the USB device to open.\n", " | :param int pid: Product ID of the USB device to open.\n", " | \n", " | :rtype: `Instrument`\n", " | :return: Object representing the connected instrument.\n", " | \n", " | open_usbtmc(*args, **kwargs) from abc.ABCMeta\n", " | Opens an instrument, connecting to a USB-TMC device using the Python\n", " | `usbtmc` library.\n", " | \n", " | .. warning:: The operational status of this is unknown. It is suggested\n", " | that you connect via the other provided class methods. For Linux,\n", " | if you have the ``usbtmc`` kernel module, the\n", " | `~instruments.Instrument.open_file` class method will work. On\n", " | Windows, using the `~instruments.Instrument.open_visa` class\n", " | method along with having the VISA libraries installed will work.\n", " | \n", " | :return: Object representing the connected instrument\n", " | \n", " | open_visa(resource_name) from abc.ABCMeta\n", " | Opens an instrument, connecting using the VISA library. Note that\n", " | `PyVISA`_ and a VISA implementation must both be present and installed\n", " | for this method to function.\n", " | \n", " | :param str resource_name: Name of a VISA resource representing the\n", " | given instrument.\n", " | \n", " | :rtype: `Instrument`\n", " | :return: Object representing the connected instrument.\n", " | \n", " | .. seealso::\n", " | `National Instruments help page on VISA resource names\n", " | `_.\n", " | \n", " | .. _PyVISA: http://pyvisa.sourceforge.net/\n", " | \n", " | open_vxi11(*args, **kwargs) from abc.ABCMeta\n", " | Opens a vxi11 enabled instrument, connecting using the python\n", " | library `python-vxi11`_. This package must be present and installed\n", " | for this method to function.\n", " | \n", " | :rtype: `Instrument`\n", " | :return: Object representing the connected instrument.\n", " | \n", " | .. _python-vxi11: https://github.com/python-ivi/python-vxi11\n", " | \n", " | ----------------------------------------------------------------------\n", " | Data descriptors inherited from instruments.abstract_instruments.instrument.Instrument:\n", " | \n", " | __dict__\n", " | dictionary for instance variables (if defined)\n", " | \n", " | __weakref__\n", " | list of weak references to the object (if defined)\n", " | \n", " | address\n", " | Gets/sets the target communication of the instrument.\n", " | \n", " | This is useful for situations when running straight from a Python shell\n", " | and your instrument has enumerated with a different address. An example\n", " | when this can happen is if you are using a USB to Serial adapter and\n", " | you disconnect/reconnect it.\n", " | \n", " | :type: `int` for GPIB address, `str` for other\n", " | \n", " | prompt\n", " | Gets/sets the prompt used for communication.\n", " | \n", " | The prompt refers to a character that is sent back from the instrument\n", " | after it has finished processing your last command. Typically this is\n", " | used to indicate to an end-user that the device is ready for input when\n", " | connected to a serial-terminal interface.\n", " | \n", " | In IK, the prompt is specified that that it (and its associated\n", " | termination character) are read in. The value read in from the device\n", " | is also checked against the stored prompt value to make sure that\n", " | everything is still in sync.\n", " | \n", " | :type: `str`\n", " | \n", " | terminator\n", " | Gets/sets the terminator used for communication.\n", " | \n", " | For communication options where this is applicable, the value\n", " | corresponds to the ASCII character used for termination in decimal\n", " | format. Example: 10 sets the character to NEWLINE.\n", " | \n", " | :type: `int`, or `str` for GPIB adapters.\n", " | \n", " | timeout\n", " | Gets/sets the communication timeout for this instrument. Note that\n", " | setting this value after opening the connection is not supported for\n", " | all connection types.\n", " | \n", " | :type: `int`\n", " | \n", " | ----------------------------------------------------------------------\n", " | Data and other attributes inherited from instruments.abstract_instruments.instrument.Instrument:\n", " | \n", " | URI_SCHEMES = ['serial', 'tcpip', 'gpib+usb', 'gpib+serial', 'visa', '...\n", "\n" ] } ], "source": [ "help(ik.abstract_instruments.PowerSupply)" ] }, { "cell_type": "markdown", "metadata": { "collapsed": false }, "source": [ "Now this is a lot of information, but of interest here is the lines starting at ``class PowerSupply``:" ] }, { "cell_type": "markdown", "metadata": { "collapsed": false }, "source": [ "```\n", "class PowerSupply(instruments.abstract_instruments.instrument.Instrument)\n", " | Abstract base class for power supply instruments.\n", " | \n", " | All applicable concrete instruments should inherit from this ABC to\n", " | provide a consistent interface to the user.\n", " | \n", " | Method resolution order:\n", " | PowerSupply\n", " | instruments.abstract_instruments.instrument.Instrument\n", " | builtins.object\n", " | \n", " | Data descriptors defined here:\n", " | \n", " | channel\n", " | Gets a channel object for the power supply. This should use\n", " | `~instruments.util_fns.ProxyList` to achieve this.\n", " | \n", " | This is an abstract method.\n", " | \n", " | :rtype: `PowerSupplyChannel`\n", " | \n", " | current\n", " | Gets/sets the output current for all channel on the power supply.\n", " | This is an abstract method.\n", " | \n", " | :type: `~quantities.quantity.Quantity`\n", " | \n", " | voltage\n", " | Gets/sets the output voltage for all channel on the power supply.\n", " | This is an abstract method.\n", " | \n", " | :type: `~quantities.quantity.Quantity`\n", " ```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This tells us that the class ``PowerSupply`` has three abstract methods, namely ``channel``, ``current``, and ``voltage``. As we discussed previously, if we want to make a concrete class that inherits from ``PowerSupply`` then we have to override these methods and give them a definition before we can actually make an instance of our concrete class.\n", "\n", "The class ``PowerSupplyChannel`` is mentioned above as the return type for the channel method. Let's look at what the abstract class for a channel of a power supply looks like:" ] }, { "cell_type": "code", "execution_count": 53, "metadata": { "collapsed": false }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Help on class PowerSupplyChannel in module instruments.abstract_instruments.power_supply:\n", "\n", "class PowerSupplyChannel(builtins.object)\n", " | Abstract base class for power supply output channels.\n", " | \n", " | All applicable concrete instruments should inherit from this ABC to\n", " | provide a consistent interface to the user.\n", " | \n", " | Data descriptors defined here:\n", " | \n", " | __dict__\n", " | dictionary for instance variables (if defined)\n", " | \n", " | __weakref__\n", " | list of weak references to the object (if defined)\n", " | \n", " | current\n", " | Gets/sets the output current for the power supply channel. This is an\n", " | abstract method.\n", " | \n", " | :type: `~quantities.quantity.Quantity`\n", " | \n", " | mode\n", " | Gets/sets the output mode for the power supply channel. This is an\n", " | abstract method.\n", " | \n", " | :type: `~enum.Enum`\n", " | \n", " | output\n", " | Gets/sets the output status for the power supply channel. This is an\n", " | abstract method.\n", " | \n", " | :type: `bool`\n", " | \n", " | voltage\n", " | Gets/sets the output voltage for the power supply channel. This is an\n", " | abstract method.\n", " | \n", " | :type: `~quantities.quantity.Quantity`\n", " | \n", " | ----------------------------------------------------------------------\n", " | Data and other attributes defined here:\n", " | \n", " | __abstractmethods__ = frozenset({'current', 'mode', 'output', 'voltage...\n", "\n" ] } ], "source": [ "help(ik.abstract_instruments.PowerSupplyChannel)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As an exercise for now, take some time to look at what some concrete classes for instruments look like [here](https://github.com/Galvant/InstrumentKit/tree/master/instruments/). Notably, look at what abstract classes the real instrument classes are inheriting from and what methods they had provide definitions for because of the abstract class. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "After looking at some of these concrete instrument classes, have a shot at writing your own for our demo instrument! A starting hint is that the class definition should probably start by inheriting from the PowerSupply class. Don't forget to override the required methods!" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": true }, "outputs": [], "source": [ "#class EpqisDemoInstrument(ik.abstract_instruments.PowerSupply):\n", "# class Channel(ik.abstract_instruments.PowerSupplyChannel):\n", "# pass" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You can test your new instrument by using the ``open_test`` class method, which specifies that the \"instrument\" is actually whatever you type into Jupyter Notebook." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": true }, "outputs": [], "source": [ "#ins = EpqisDemoInstrument.open_test()" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "#ins.query(\"*IDN?\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If you want to test with the real fake instrument, use the ``open_tcpip`` class method instead:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": true }, "outputs": [], "source": [ "#ins = EpqisDemoInstrument.open_tcpip('localhost', 8042)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's take a look at a possible way you could write this driver file for the demo instrument. This example driver file was a part of the package you imported earlier, so let's import it explicitly now:" ] }, { "cell_type": "code", "execution_count": 54, "metadata": { "collapsed": false }, "outputs": [], "source": [ "from epqis16_demos import EpqisDemoInstrument" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Just like with the base instrument class, we start by making an instance of our new instrument class called ``EpqisDemoInstrument``. Since ``PowerSupply`` inherits methods from the ``Instrument`` class, and since that class has the definition for the ``open_tcpip`` method we used before, we can use it again to connect to the instrument." ] }, { "cell_type": "code", "execution_count": 55, "metadata": { "collapsed": false }, "outputs": [], "source": [ "ik_ins = EpqisDemoInstrument.open_tcpip(\"localhost\", 8042)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now comes the fun part, just like we have done all along with our trivial example classes, we can just ask for the voltage attribute of a particular channel on our device:" ] }, { "cell_type": "code", "execution_count": 56, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "array(0.0) * V" ] }, "execution_count": 56, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ik_ins.channel[1].voltage" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Similarly, we can set the value of attributes of the channels of the devices, and e:" ] }, { "cell_type": "code", "execution_count": 57, "metadata": { "collapsed": false }, "outputs": [], "source": [ "ik_ins.channel[2].output = True" ] }, { "cell_type": "code", "execution_count": 58, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "array(0.314) * V" ] }, "execution_count": 58, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ik_ins.voltage = 12\n", "ik_ins.channel[1].voltage = pq.Quantity(314, 'mV')\n", "ik_ins.channel[1].voltage" ] }, { "cell_type": "code", "execution_count": 59, "metadata": { "collapsed": false }, "outputs": [ { "ename": "NotImplementedError", "evalue": "This instrument does not support querying or setting the output current.", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mNotImplementedError\u001b[0m Traceback (most recent call last)", "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mik_ins\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcurrent\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[0;32m/home/skaiser/anaconda3/lib/python3.5/site-packages/epqis16_demos-0.1-py3.5.egg/epqis16_demos/ik_driver.py\u001b[0m in \u001b[0;36mcurrent\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 193\u001b[0m \u001b[0mGets\u001b[0m\u001b[0;34m/\u001b[0m\u001b[0msets\u001b[0m \u001b[0mthe\u001b[0m \u001b[0mcurrent\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mthe\u001b[0m \u001b[0mspecified\u001b[0m \u001b[0mchannel\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 194\u001b[0m \"\"\"\n\u001b[0;32m--> 195\u001b[0;31m raise NotImplementedError('This instrument does not support querying '\n\u001b[0m\u001b[1;32m 196\u001b[0m 'or setting the output current.')\n\u001b[1;32m 197\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;31mNotImplementedError\u001b[0m: This instrument does not support querying or setting the output current." ] } ], "source": [ "ik_ins.current" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "**EXAMPLE SOLUTION**\n", "\n", "---\n", "\n", "Let's take a look at the source for this driver file (with some comments removed to shorten it), and look at how the required ``channel``, ``voltage``, and ``current`` methods are set for the ``EpqisDemoInstrument`` class." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "```python\n", "#!/usr/bin/env python\n", "# -*- coding: utf-8 -*-\n", "\"\"\"\n", "Provides support for the epqis16 Demonstration Instrument.\n", "\"\"\"\n", "\n", "# IMPORTS #####################################################################\n", "\n", "from __future__ import absolute_import\n", "from __future__ import division\n", "\n", "import quantities as pq\n", "\n", "from instruments.abstract_instruments import (\n", " PowerSupply,\n", " PowerSupplyChannel,\n", ")\n", "\n", "from instruments.util_fns import assume_units, ProxyList, bool_property\n", "from instruments.generic_scpi import SCPIInstrument\n", "\n", "# CLASSES #####################################################################\n", "\n", "class EpqisDemoInstrument(PowerSupply, SCPIInstrument):\n", "\n", " _channel_count = 4\n", "\n", " def __init__(self, filelike):\n", " super(EpqisDemoInstrument, self).__init__(filelike)\n", "\n", " # PROPERTIES #\n", " @property\n", " def channel(self):\n", " return ProxyList(self, EpqisDemoInstrument.Channel, range(self._channel_count))\n", "\n", " @property\n", " def voltage(self):\n", " return [\n", " self.channel[i].voltage for i in range(self._channel_count)\n", " ]\n", "\n", " @voltage.setter\n", " def voltage(self, newval):\n", " if isinstance(newval, (list, tuple)):\n", " if len(newval) is not self._channel_count:\n", " raise ValueError('When specifying the voltage for all channels '\n", " 'as a list or tuple, it must be of '\n", " 'length {}.'.format(self._channel_count))\n", " for channel, new_voltage in zip(self.channel, newval):\n", " channel.voltage = new_voltage\n", " else:\n", " for channel in self.channel:\n", " channel.voltage = newval\n", "\n", " @property\n", " def current(self):\n", " raise NotImplementedError('This instrument does not support querying '\n", " 'or setting the output current.')\n", "\n", " @current.setter\n", " def current(self, newval):\n", " raise NotImplementedError('This instrument does not support querying '\n", " 'or setting the output current.')\n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can see that the device is defined to have 4 channels with ``_channel_count``. The @property and @____.setter lines are called decorators and basically allow a function to \"wrap\" the one that is defined right below it. That means that the decorator function is called first with the argument of the function that it is wrapping. For example then for the function voltage(self), it is actually called as property(voltage)(self).\n", "\n", "Also defined in the ``EpqisDemoInstrument`` is how to interface to a channel for this instrument." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "```python\n", " class Channel(PowerSupplyChannel):\n", "\n", " def __init__(self, parent, idx):\n", " self._parent = parent\n", " self._idx = idx + 1\n", "\n", " # COMMUNICATION METHODS #\n", "\n", " def sendcmd(self, cmd):\n", " self._parent.sendcmd(\"CH{} {}\".format(self._idx, cmd))\n", "\n", " def query(self, cmd):\n", " return self._parent.query(\"CH{} {}\".format(self._idx, cmd))\n", "\n", " # PROPERTIES #\n", " @property\n", " def mode(self):\n", " raise NotImplementedError('This instrument does not support querying '\n", " 'or setting the output current.')\n", "\n", " @mode.setter\n", " def mode(self, newval):\n", " raise NotImplementedError('This instrument does not support querying '\n", " 'or setting the output current.')\n", "\n", " @property\n", " def voltage(self):\n", " value = self.query(\"VOLTS?\")\n", " return assume_units(float(value), pq.millivolt).rescale(pq.volt)\n", "\n", " @voltage.setter\n", " def voltage(self, newval):\n", " newval = assume_units(newval, pq.volt).rescale(pq.millivolt).magnitude\n", " self.sendcmd(\"VOLTS {}\".format(newval))\n", "\n", " @property\n", " def current(self):\n", " raise NotImplementedError('This instrument does not support querying '\n", " 'or setting the output current.')\n", "\n", " @current.setter\n", " def current(self, newval):\n", " raise NotImplementedError('This instrument does not support querying '\n", " 'or setting the output current.')\n", "\n", " output = bool_property(\n", " \"ENABLE\",\n", " inst_true=\"ON\",\n", " inst_false=\"OFF\",\n", " doc=\"\"\"\n", " Sets the outputting status of the specified channel.\n", "\n", " This is a toggle setting that can be ON or OFF. \n", "\n", " :type: `bool`\n", " \"\"\"\n", " )\n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Lots of options of starting points, don't reinvent the wheel! ## " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We all have no more valuable resource than our own time, so there is little point to reimplementing functionality that exists in other packages that will work in your Python work flow. As usual, google your instrument to see if there is a Python package that already supports it (or if the manufacturer can supply control code samples!). Here are some popular packages that together cover a lot of instruments. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "- [InstrumentKit](http://instrumentkit.readthedocs.io/en/latest/index.html)\n", "- [Instrumental](http://instrumental-lib.readthedocs.io/en/latest/)\n", "- [QuDi](http://qosvn.physik.uni-ulm.de/trac/qudi) \n", "- [Python IVI](http://alexforencich.com/wiki/en/python-ivi/readme)" ] } ], "metadata": { "anaconda-cloud": {}, "kernelspec": { "display_name": "Python [conda root]", "language": "python", "name": "conda-root-py" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.5.2" } }, "nbformat": 4, "nbformat_minor": 1 }