Skip to content

Instantly share code, notes, and snippets.

@jesteve
Last active July 13, 2016 17:01
Show Gist options
  • Save jesteve/8851946 to your computer and use it in GitHub Desktop.
Save jesteve/8851946 to your computer and use it in GitHub Desktop.
An Introduction to Experiment Control in Python
Display the source blob
Display the rendered blob
Raw
{
"metadata": {
"name": "",
"signature": "sha256:21ddef4df27114bd495459b12ed7d5a9ccaa8c984827f3d0296acb8c601f18cc"
},
"nbformat": 3,
"nbformat_minor": 0,
"worksheets": [
{
"cells": [
{
"cell_type": "heading",
"level": 1,
"metadata": {},
"source": [
"An Introduction to Experiment Control in Python"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"*This document was written for a one day course to PhD students starting their thesis in a physics lab. It aims at introducing some basic concepts about how to write an experiment control program in Python. It is NOT exhaustive NOR for advanced programmers...*"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The goal of this course is to review some of the main difficulties that one usually faces when writing an experiment control program and how they can be solved in Python. The outline is the following:\n",
"\n",
"* Controling a single device\n",
"* Controling many devices via a graphical user interface\n",
"* Saving the data\n",
"\n",
"It is far from being exhaustive, but it should hopefully put you on the right tracks to rapidly write your program without reinventing the wheel. A glossary of the abbreviations appearing in the text can be found at the end. Finally, I have attached as appendices the code of the Lecroy scope interface program that you will develop during the course and of a PyQt GUI fetching and plotting waveforms from the scope."
]
},
{
"cell_type": "heading",
"level": 2,
"metadata": {},
"source": [
"Controling a single device"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"When trying to interface common lab equipment, you will usually face two situations :\n",
"\n",
"* *The instrument follows standard specifications (VISA, IVI, ...)* \n",
" Many measurement devices, such as scopes, signal generators, spectrum analyzers, power supplies, ..., enter this category. Writing a Python module to control the device is raher straightforward. If you're lucky, you may not even have to write a single line of code. \n",
"\n",
"\n",
"* *The instruments is controlled via a proprietary interface* \n",
" This is typically the case for CCD cameras or acquisition boards. The manufacturer provides libraries that you must call from your program. In Python, this can be done using the `ctypes` module. This requires good programming skills (C knowledge) and can be time consuming. Some libraries that have already been wrapped into Python can be found on the web (e.g. Ni-DAQmx). Some thorough googling can be a good time investment..."
]
},
{
"cell_type": "heading",
"level": 3,
"metadata": {},
"source": [
"VISA compatible and similar devices (most of the oscilloscopes, signal generators, ...)"
]
},
{
"cell_type": "heading",
"level": 4,
"metadata": {},
"source": [
"Low level communication"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"You have a scope or a signal generator physically connected to your computer via RS232, USB, GPIB or Ethernet. \n",
"The first thing is to establish a connection to the device. One of the following modules should do the trick :\n",
"\n",
"* [PyVISA](http://pyvisa.readthedocs.org/en/latest/) (mostly windows, but can also work on Linux or OS X). Interface any VISA compatible device over RS232, GPIB, USB or LAN. It requires a VISA driver to be installed on your computer, typically NI-VISA which comes with almost any NI product. Search for \"NI-VISA runtime engine\" on the National Instrument website.\n",
"\n",
"* [python-vxi11](http://alexforencich.com/wiki/en/python-vxi11/start) (multiplatform). Interface a VXI or LXI compatible device over LAN.\n",
"\n",
"* [python-usbtmc](http://alexforencich.com/wiki/en/python-usbtmc/start) (multiplatform). Interface a USBTMC compatible device over USB.\n",
"\n",
"* [pySerial](http://pyserial.sourceforge.net/) (multiplatform). General serial interface. Interface any USB or RS232 device. You have to take care of the communication details (speed, parity, stop bit, ...) by yourself.\n",
"\n",
"The first three modules have very similar syntax. Here is an example using python-vxi11 to check that our Lecroy scope with IP address 10.118.16.94 can be remotely controlled via its LXI interface :"
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"from vxi11 import Instrument\n",
"instr = Instrument('10.118.16.94')\n",
"print instr.ask('*IDN?')[:22]"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "stream",
"stream": "stdout",
"text": [
"*IDN LECROY,WS104MXS-B\n"
]
}
],
"prompt_number": 4
},
{
"cell_type": "heading",
"level": 4,
"metadata": {},
"source": [
"High level interface"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Once the low level communication works, one usually wants a Python module defining a class allowing high level access to the device via methods. Many devices follow the IVI specifications, which means that the method names and signatures to control them are standardized (e.g. a waveform acquired by any IVI scope can be fetched via the `FetchWaveform` method). The two following modules implement the IVI standard in Python : \n",
"\n",
"* [python-ivi](http://alexforencich.com/wiki/en/python-ivi/start) (multiplatform). Requires specific Python code for each instrument. Many drivers have already been written and come with the module.\n",
"* [pyivi](https://pypi.python.org/pypi/pyivi) (windows only). Automatically wrap the drivers of all IVI devices connected to your computer.\n",
"\n",
"If this does not work with your device, you have to write you own Python code using SCPI commands (like the `*IDN?` command we used above). For example, we can write a fetch waveform method for our Lecroy scope using :"
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"class LecroyScope(Instrument):\n",
" def fetchwaveform(self, channel):\n",
" self.write('C{0}:WF?'.format(channel))\n",
" return self.read_raw()\n",
"\n",
"instr = LecroyScope('10.118.16.94')\n",
"print instr.fetchwaveform(1)[:200]"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "stream",
"stream": "stdout",
"text": [
"C1:WF ALL,#9000010348WAVEDESC\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000LECROY_2_3\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0001\u0000Z\u0001\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0012'\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000LECROYWS104MXs-B\ufffd\ufffd\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0012'\u0000\u0000\u0012'\u0000\u0000\u0010'\u0000\u0000\u0000\u0000\u0000\u0000\u0011'\u0000\u0000\u0000\u0000\u0000\u0000\u0001\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0001\u0000\u0000\u0000\u0001\u0000\u0000\u0000\u0000\u0000\u0000\u0000\ufffd\u0006\ufffd=\u0000\u0000\u0000\ufffd\u0000\u0000\ufffdB\u0000\u0000\ufffd\ufffd\b\u0000\u0001\u0000\u0017\ufffd\ufffd\n"
]
}
],
"prompt_number": 6
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The answer of the scope is a string containing a header followed by binary data. A common task when interfacing an instrument is to decode and encode binary strings. In Python, this can be done using the [struct](http://docs.python.org/2/library/struct.html) module and/or numpy using [`fromstring`](http://docs.scipy.org/doc/numpy/reference/generated/numpy.fromstring.html) and [`tostring`](http://docs.scipy.org/doc/numpy/reference/generated/numpy.ndarray.tostring.html). In the case of our Lecroy scope, the full header parsing is a tedious task, because many informations are passed using different variable types. Here's a quick and dirty improvement of the `fetchwaveform` method :"
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"import numpy as np\n",
"\n",
"class LecroyScope(Instrument):\n",
" def fetchwaveform(self, channel):\n",
" self.write('C{0}:WF?'.format(channel))\n",
" msg = self.read_raw()\n",
" start = msg.find('WAVEDESC')\n",
" msg = msg[start:]\n",
" nb_of_points = np.fromstring(msg[60 :64 ], dtype=np.uint32)\n",
" voltage_gain = np.fromstring(msg[156:160], dtype=np.float32)\n",
" voltage_offset = np.fromstring(msg[160:164], dtype=np.float32)\n",
" horiz_interval = np.fromstring(msg[176:180], dtype=np.float32)\n",
" horiz_offset = np.fromstring(msg[180:188], dtype=np.float64)\n",
" voltage = np.fromstring(msg[346:], dtype=np.int8, count=nb_of_points).astype(np.float)\n",
" voltage *= voltage_gain\n",
" voltage += voltage_offset\n",
" time = np.arange(nb_of_points, dtype=np.float)\n",
" time *= horiz_interval\n",
" time += horiz_offset\n",
" return time, voltage\n",
"\n",
"% pylab inline\n",
"instr = LecroyScope('10.118.16.94')\n",
"plot(*instr.fetchwaveform(1))"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "stream",
"stream": "stdout",
"text": [
"Populating the interactive namespace from numpy and matplotlib\n"
]
},
{
"metadata": {},
"output_type": "pyout",
"prompt_number": 10,
"text": [
"[<matplotlib.lines.Line2D at 0x10724af90>]"
]
},
{
"metadata": {},
"output_type": "display_data",
"png": "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\n",
"text": [
"<matplotlib.figure.Figure at 0x107269e50>"
]
}
],
"prompt_number": 10
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"In the first appendix at the end of this document, you will find the code of a function that fully decodes the Lecroy format. When writing and reading binary data, you usually have to use the `read_raw` and `write_raw` methods instead of `read` and `write`. You should also be careful with the definition of data types (endianness, number of bytes), which is machine dependent. "
]
},
{
"cell_type": "heading",
"level": 3,
"metadata": {},
"source": [
"Instruments with proprietary interface : calling a DLL from Python with `ctypes`"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"You have just received your brand new CCD camera and want to acquire images in Python. For the moment, it is unlikely that the manufacturer provides you with a Python module. You will rather get a DLL and its corresponding C header file. Python allows you to call functions in a DLL via the [ctypes](http://docs.python.org/2/library/ctypes.html) module. For example, let's have a look at the `ATMCD32D.dll` from Andor and how to retrieve an image via the `GetMostRecentImage16` function. The header file says :\n",
"```C\n",
" EXPNETTYPE unsigned int WINAPI GetMostRecentImage16 (WORD* array, unsigned long size);\n",
" ```\n",
"The function expects a pointer to an array of 16 bit elements, an unsigned long integer and returns an unsigned integer. In our Python module for the camera, we simply have to make Python aware of the signature of the function using the following code: \n",
"```python\n",
"from ctypes import * \n",
"import numpy as np\n",
"dll = windll.LoadLibrary('ATMCD32D.dll')\n",
"\n",
"GetMostRecentImage16 = dll.GetMostRecentImage16\n",
"GetMostRecentImage16.argtypes = [np.ctypeslib.ndpointer(dtype = np.uint16), c_ulong] \n",
"GetMostRecentImage16.restype = c_uint\n",
"```\n",
"You can now fetch an image directly into a numpy array :\n",
"```python\n",
"last_image = np.empty((1024, 1024), dtype = np.uint16)\n",
"GetMostRecentImage16(last_image, 1024*1024)\n",
"```\n",
"Python will automatically transform your variable into a pointer and/or do type conversion when necessary. It is not very difficult to parse the C header file and automatically generate the corresponding Python code, but this is beyond the scope of this course. Some common problems that you may encounter are :\n",
"\n",
"* You're not loading the `DLL` via the proper `ctypes` class. Try `windll` and `cdll`. \n",
"* Your signature is incorrect. For example, this can happen when the header file redefines some types for your platform in an unexpected way.\n",
"\n",
"Wrapping a library is not always as easy as the previous example. So before going into coding, you may want to find out if someone has not already wrapped the `DLL` and posted it on the web... Here is a non exhaustive list of some already wrapped libraries : \n",
"\n",
"* [PyDAQmx](http://pythonhosted.org/PyDAQmx/) : the PyDAQmx module is a full interface to the NIDAQmx C driver. It imports all the functions from the driver and imports all the predefined constants. This provides an almost one-to-one match between C and Python code. "
]
},
{
"cell_type": "heading",
"level": 2,
"metadata": {},
"source": [
"Controlling many devices through a graphical interface"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Let's suppose that you have managed to control all your instruments separately with Python. Now, you want to write a single application through which you will control all the devices, possibly with a graphical interface. Here, we will use [PyQt](http://www.riverbankcomputing.co.uk/software/pyqt/intro) that works very well on any platform. You can look [here](https://wiki.python.org/moin/GuiProgramming) for a list of the different GUI modules developed for Python. Designing the application can be done using `qtdesigner` which creates a `ui` file. This file must then be transformed into Python code using the `pyuic` tool.\n",
"\n",
"\n",
"We will create a simple application to view the data from our Lecroy scope with one plotting area and one button to start and stop the acquisition:\n",
"\n",
"![UI](files/simpleUI.png)\n",
"\n",
"We will suppose that the output of the `pyuic` is in the `simpleUI.py` file and that the button was renamed as `pushButton_Start`. Let's consider the following minimal code:\n",
"```python\n",
"#!/usr/bin/env python\n",
"import sys\n",
"from PyQt4 import Qwt5, QtCore, QtGui, Qt\n",
"from simpleUI import Ui_MainWindow\n",
"\n",
"class MainWindow(QtGui.QMainWindow):\n",
" def __init__(self):\n",
" # Create the main window\n",
" super(MainWindow, self).__init__()\n",
" self.ui = Ui_MainWindow()\n",
" self.ui.setupUi(self)\n",
"\n",
" @QtCore.pyqtSignature(\"\")\n",
" def on_pushButton_Start_clicked(self):\n",
" print 'Clic !'\n",
"\n",
"if __name__ == '__main__':\n",
" app = QtGui.QApplication(sys.argv)\n",
" mainWin = MainWindow()\n",
" mainWin.show()\n",
" sys.exit(app.exec_())\n",
"```\n",
"Save it as `livescope.py` and run it. You see that clicking on the button actually prints \"`Clic !`\" as expected. Writing code for a GUI boils down to adding methods to the `MainWindow` class that is instantiated when the program starts. Everytime a button is clicked or any other event happens, Qt will look for the appropriate method to handle the event and call it. Here, the decorator `@QtCore.pyqtSignature(\"\")` together with the standardized name `on_pushButton_Start_clicked` connects the event \"the user clicked on the button named `pushButton_Start`\" to the `on_pushButton_Start_clicked` method. You can have a look [here](http://pyqt.sourceforge.net/Docs/PyQt4/new_style_signals_slots.html) for more details."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Let's now incorporate our previous LecroyScope class and modify the previous code so that pressing on start fetches one waveform and plots it :\n",
"```python\n",
"from lecroy.scope import LecroyScope\n",
"\n",
"class MainWindow(QtGui.QMainWindow):\n",
" def __init__(self):\n",
" # Creates the main window\n",
" super(MainWindow, self).__init__()\n",
" self.ui = Ui_MainWindow()\n",
" self.ui.setupUi(self)\n",
" # Setups the plot\n",
" self.curve = Qwt5.QwtPlotCurve()\n",
" self.curve.attach(self.ui.qwtPlot)\n",
" # Connects to the scope\n",
" self.scope = LecroyScope('10.118.16.94')\n",
"\n",
" @QtCore.pyqtSignature(\"\")\n",
" def on_pushButton_Start_clicked(self):\n",
" self.acquire()\n",
" self.plot(*self.last_trace)\n",
" \n",
" def acquire(self):\n",
" self.last_trace = self.scope.fetchwaveform(1)\n",
" \n",
" def plot(self, x, y): \n",
" self.curve.setData(x, y)\n",
" self.ui.qwtPlot.replot()\n",
"```"
]
},
{
"cell_type": "heading",
"level": 3,
"metadata": {},
"source": [
"Multithreading"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Now, let's try to fetch one waveform every second and refresh the plot until the user presses the button again.\n",
"After a bit of thinking, you will realize that this cannot be done with \"normal\" linear programming.\n",
"In order to solve this problem, one has to delegate the scope acquisition to another thread. A new thread is a new task that the computer will run in parallel to the main thread. All threads share the same memory space (and thus variables).\n",
"Python implements [multithreading](http://docs.python.org/2/library/threading.html), but here we will rather use `QThread`, the Qt implementation of threads, which can trigger a Qt event when the thread finishes. The code changes to :\n",
"```python\n",
"class AcquisitionThread(QtCore.QThread):\n",
" def __init__(self, fun):\n",
" super(AcquisitionThread, self).__init__()\n",
" self.fun = fun\n",
" def run(self):\n",
" self.fun()\n",
" \n",
"class MainWindow(QtGui.QMainWindow):\n",
" def __init__(self):\n",
" # Create the main window\n",
" super(MainWindow, self).__init__()\n",
" self.ui = Ui_MainWindow()\n",
" self.ui.setupUi(self)\n",
" # Setup the plot\n",
" self.curve = Qwt5.QwtPlotCurve()\n",
" self.curve.attach(self.ui.qwtPlot)\n",
" # Connects to the scope\n",
" self.scope = LecroyScope('10.118.16.94')\n",
" # Acquisition thread\n",
" self.acquisition_thread = AcquisitionThread(self.acquire)\n",
" self.acquiring = False\n",
"\n",
" @QtCore.pyqtSignature(\"\")\n",
" def on_pushButton_Start_clicked(self):\n",
" if not self.acquiring:\n",
" # Start acquisition\n",
" self.acquisition_thread.finished.connect(self.callback)\n",
" self.acquisition_thread.start()\n",
" self.ui.pushButton_Start.setText('Stop')\n",
" self.stop_request = False\n",
" self.acquiring = True\n",
" else:\n",
" # Request to stop\n",
" self.stop_request = True\n",
"\n",
" def acquire(self):\n",
" time.sleep(1) # In a real program, this is replaced by a function that blocks until the acquisition terminates\n",
" self.last_trace = self.scope.fetchwaveform(1)\n",
"\n",
" def callback(self):\n",
" # Plot the last trace\n",
" self.plot(*self.last_trace)\n",
" if self.stop_request :\n",
" # Stop acquisition\n",
" self.acquisition_thread.finished.disconnect(self.callback)\n",
" self.acquiring = False\n",
" self.ui.pushButton_Start.setText('Start')\n",
" else:\n",
" # Continue acquisition\n",
" self.acquisition_thread.start()\n",
"\n",
" def plot(self, x, y): \n",
" self.curve.setData(x, y)\n",
" self.ui.qwtPlot.replot()\n",
"```\n",
"The waiting and the waveform fetching is done in the `acquisition_thread`. By connecting the `finished` event of this thread to our `callback` method, we can refresh the plot and decide whether the acquisition should go on or stop every time the acquisition finishes. Pressing stop\n",
"disconnects the event from the `callback` slot and the refreshing stops. Here, the last acquired data are lost (or will eventually be part of the next acquisition). You can easily change this behavior.\n",
"\n",
"In a real experiment control program, all the blocking functions could go in the acquisition thread. The thread finishes as soon as all devices have terminated their tasks. You can also use one thread to perform data analysis in parallel of data acquisition without blocking the main thread. Delegating time consuming tasks away from the main thread will allow you to keep the application responsive. However, you cannot use \n",
"multithreading to do parallel calculations in Python, e.g. to use the two cores of your processor (you should use the [multiprocessing](http://docs.python.org/2/library/multiprocessing.html) module for this purpose). Only one thread at a time should process heavy tasks, while the others wait. At best, you can have the main thread waiting for user input, the acquisition thread waiting for the blocking function to return and one working thread doing data analysis."
]
},
{
"cell_type": "heading",
"level": 3,
"metadata": {},
"source": [
"Multiprocessing and Interprocess communication"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Multithreading can handle many situations but is limited to a single application and thus to a single computer. Having applications running in parallel, eventually on different computers can be very helpful. In order to communicate between each other, the different applications or processes, which do not share memory, communicate via so-called inter process communication (IPC) channels. Depending on the platform, different sort of channels can be used. TCP sockets exist on all platforms and can link two processes on distant machines or on the same machine in a transparent way. Python offers builtin solutions to work with sockets in the multiprocessing module (see [here](http://docs.python.org/2/library/multiprocessing.html#using-a-remote-manager) for example) and there exists also a [Python binding](http://zeromq.github.io/pyzmq/) of the widely used [ZeroMQ](http://zeromq.org/) library (e.g. IPython uses ZeroMQ to connect consoles to the kernel). IPC is out of the scope of this course and we will not give any programming examples.\n",
"\n",
"If you consider that controlling your experiment requires to synchronize many devices on distant machines. You should maybe consider to use [Tango](http://www.tango-controls.org/) and the associated Python bindings [pyTango](http://www.tango-controls.org/static/PyTango/latest/doc/html/)."
]
},
{
"cell_type": "heading",
"level": 2,
"metadata": {},
"source": [
"Saving your data"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"One important task that your experiment control program will have to handle is data saving. Many options are possible depending on the type and amount of data you want to save and how you want to be able to access them. Here, we will compare three solutions: two using local files and one using a server database. Before going on, we briefly give pros and cons for these two options:\n",
"\n",
"* Local files : easy to implement, easy to copy from one machine to another, backup is not built in, concurrent access is not allowed\n",
"* Database : harder to install, copying part of the data is difficult, backup is built in (if the server is well configured), allows concurrent access, searchable\n",
"\n",
"The main difference is concurrent access. If you want many applications to write to the same dataset, then you should consider a real database. Otherwise, file based solutions with a good backup solution are easier to handle. A second advantage of using a database is that you can easily perform searches through all your data. Of course, you can combine the two approaches and get the advantages of both. "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"For local file solutions, we will consider :\n",
"\n",
"* Pickling via the [cPickle](http://docs.python.org/2/library/pickle.html) module in conjunction with the [zipfile](http://docs.python.org/2/library/zipfile) module \n",
"* HDF5 files with [pyTables](http://www.pytables.org/moin)\n",
"\n",
"For the database approach, we will use [MongoDB](http://www.mongodb.org/) and [PyMongo](http://api.mongodb.org/python/current/)."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Let's first create a dataset consisting of 100 images : "
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"dataset = [ random.rand(512,512) for i in range(100) ]"
],
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 1
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We also want to save some acquisition parameters associated to each image:"
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"import time\n",
"import os\n",
"acquisition_parameters = [ dict(acquisition_time_ms = 50, \n",
" CCD_temperature = -80., \n",
" date_time = time.time(), \n",
" run_number = i) for i in range(100)]"
],
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 2
},
{
"cell_type": "heading",
"level": 4,
"metadata": {},
"source": [
"Pickling and ZIP file"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Pickling is the standard way to save a Python object to a file. Almost any object can be saved in this way, but it is strongly recommended to only use standard objects like dictionaries or lists, because the definition of the object class is not saved during the pickling. It means that if you loose or strongly modify custom objects, you may encounter problems when unpickling the object.\n",
"\n",
"A very simple way to save data is to create a pickle file per experimental run and to gather the different runs forming a dataset into a zip file. Here's a short list of pros and cons for this method:\n",
"\n",
"* Simple, relies only on built-in Python modules\n",
"* The zip file can be used to store all important files that produced the data : experiment script, configuration file, ...\n",
"* Easy storage of any Python object\n",
"* Data can only be read in Python\n",
"* Standard zip file is limited to 2 GB"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Preparing the file :"
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"import cPickle\n",
"import zipfile\n",
"pickleprotocol = cPickle.HIGHEST_PROTOCOL\n",
"zipf = zipfile.ZipFile('dataset_test_zip.zip', 'w')"
],
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 20
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Writing the data :"
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"%%time\n",
"for i in range(100): \n",
" result = dict(data = dataset[i], acquisition_parameters = acquisition_parameters[i] )\n",
" zipf.writestr( 'run{0:06d}.p'.format(i), cPickle.dumps(result, pickleprotocol) )"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "stream",
"stream": "stdout",
"text": [
"CPU times: user 614 ms, sys: 809 ms, total: 1.42 s\n",
"Wall time: 3.2 s\n"
]
}
],
"prompt_number": 21
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"del dataset\n",
"zipf.close()\n",
"os.path.getsize('dataset_test_zip.zip')"
],
"language": "python",
"metadata": {},
"outputs": [
{
"metadata": {},
"output_type": "pyout",
"prompt_number": 22,
"text": [
"209752122"
]
}
],
"prompt_number": 22
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Reading the data :"
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"with zipfile.ZipFile('dataset_test_zip.zip', 'r') as zip:\n",
" %time dataset = [ cPickle.loads(zip.read(x))['data'] for x in zip.namelist() ]"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "stream",
"stream": "stdout",
"text": [
"CPU times: user 506 ms, sys: 358 ms, total: 864 ms\n",
"Wall time: 872 ms\n"
]
}
],
"prompt_number": 23
},
{
"cell_type": "heading",
"level": 4,
"metadata": {},
"source": [
"HDF5 file"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"HDF5 is a standard format for large data files that is widely used by many scientific applications. It is a powerful solution that can handle many situations. Pros and cons are :\n",
"\n",
"* Powerful but complex\n",
"* PyTables 3 and 2 compatibility issues for your code \n",
"* Handle well very large amount of data\n",
"* Fully compatible with numpy\n",
"* Data can be opened by other applications (Matlab, R, ...)\n",
"* Efficient extracion of partial data"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Preparing the file :"
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"import tables\n",
"class Image(tables.IsDescription):\n",
" run_number = tables.Int64Col() \n",
" CCD_temperature = tables.Float32Col()\n",
" acquisition_time_ms = tables.Int32Col()\n",
" date_time = tables.Float64Col()\n",
" data = tables.Float64Col( shape=(512,512) )\n",
"h5file = tables.open_file('dataset_test_hdf5.h5', mode = 'w')\n",
"table = h5file.create_table('/', 'dataset', Image)\n",
"image = table.row"
],
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 24
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Writing the data :"
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"%%time\n",
"for i in range(100):\n",
" for k,v in acquisition_parameters[i].iteritems():\n",
" image[k] = v\n",
" image['data'] = dataset[i]\n",
" image.append()\n",
"table.flush()"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "stream",
"stream": "stdout",
"text": [
"CPU times: user 426 ms, sys: 239 ms, total: 666 ms\n",
"Wall time: 3.15 s\n"
]
}
],
"prompt_number": 25
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"del dataset\n",
"h5file.close()\n",
"os.path.getsize('dataset_test_hdf5.h5')"
],
"language": "python",
"metadata": {},
"outputs": [
{
"metadata": {},
"output_type": "pyout",
"prompt_number": 26,
"text": [
"209726488"
]
}
],
"prompt_number": 26
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Reading the data :"
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"h5file = t.open_file('dataset_test_hdf5.h5', mode = 'r')\n",
"table = h5file.root.dataset\n",
"%time dataset = [ x['data'] for x in table.iterrows() ]\n",
"h5file.close()"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "stream",
"stream": "stdout",
"text": [
"CPU times: user 188 ms, sys: 285 ms, total: 473 ms\n",
"Wall time: 495 ms\n"
]
}
],
"prompt_number": 27
},
{
"cell_type": "heading",
"level": 4,
"metadata": {},
"source": [
"MongoDB database"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The database approach can be very efficient but is more complex to handle. Many database are available in Python, here we have \n",
"chosen a non SQL approach that is becoming more and more common. MongoDB is one of the leading non SQL database scheme. Pros and cons are :\n",
" \n",
"* Concurrent writing : different programs can write data at the same time\n",
"* Data can be automatically duplicated (instantaneous backup)\n",
"* Efficient search algorithm\n",
"* Server must be installed and run separately\n",
"* Read/write time is hard to predict\n",
"* Data are hard to copy as files\n",
"* Data have to be converted : no native support of numpy arrays\n",
"* One item is limited to 16 MB (possible workaround)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Preparing the connection :"
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"import pymongo\n",
"import bson\n",
"client = pymongo.MongoClient()\n",
"db = client['test-database']\n",
"db.drop_collection('dataset')\n",
"collection = db.create_collection('dataset', size=300000000)"
],
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 22
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Writing to the database :"
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"%%time\n",
"data = ( {'data' : bson.binary.Binary(dataset[i].tostring()) , 'acquisition_parameters':acquisition_parameters[i] } for i in range(100) )\n",
"collection.insert(data)"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "stream",
"stream": "stdout",
"text": [
"CPU times: user 684 ms, sys: 805 ms, total: 1.49 s\n",
"Wall time: 5.45 s\n"
]
}
],
"prompt_number": 23
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"client.close()\n",
"del dataset"
],
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 24
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Reading from the database :"
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"client = pymongo.MongoClient()\n",
"db = client['test-database']\n",
"collection = db['dataset']"
],
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 25
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"%time dataset = [ fromstring(x['data']) for x in collection.find() ]\n",
"client.close()"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "stream",
"stream": "stdout",
"text": [
"CPU times: user 612 ms, sys: 822 ms, total: 1.43 s\n",
"Wall time: 1.77 s\n"
]
}
],
"prompt_number": 27
},
{
"cell_type": "heading",
"level": 2,
"metadata": {},
"source": [
"Glossary"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"* HDF5 : [Hierarchical Data Format 5](http://www.hdfgroup.org/HDF5/)\n",
"* GPIB : [General Purpose Interface Bus](http://en.wikipedia.org/wiki/IEEE-488)\n",
"* GUI : Graphical User Interface\n",
"* IVI : [Interchangeable Virtual Instrument](http://www.ivifoundation.org/)\n",
"* IPC : [Inter-Process Communication](http://en.wikipedia.org/wiki/Inter-process_communication)\n",
"* LXI : [LAN eXtensions for Instrumentation](http://www.lxistandard.org/About/VXI-11-and-LXI.aspx)\n",
"* SCPI : [Standard Commands for Programmable Instruments](http://fr.wikipedia.org/wiki/Standard_Commands_for_Programmable_Instruments)\n",
"* USBTMC : [USB Test & Measurement Class](http://digital.ni.com/public.nsf/allkb/044FA220F32774ED86256DB3005850CA)\n",
"* VISA : [Virtual Instrument Software Architecture](http://en.wikipedia.org/wiki/Virtual_Instrument_Software_Architecture)\n",
"* VXI11 : [VME eXtensions for Instrumentation](http://www.lxistandard.org/About/VXI-11-and-LXI.aspx)\n",
"\n"
]
},
{
"cell_type": "heading",
"level": 2,
"metadata": {},
"source": [
"Appendix A : Lecroy module"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Content of the lecroy/interpreter.py file\n",
"\n",
"```python\n",
"\"\"\"Interpreter for the Lecroy file format.\n",
"See lecroy_decode function.\"\"\"\n",
"\n",
"# This Lecroy waveform interpreter is adapted from :\n",
"# LeCrunch\n",
"# Copyright (C) 2010 Anthony LaTorre\n",
"\n",
"import numpy as np\n",
"import struct\n",
"\n",
"\n",
"# data types in lecroy binary blocks, where:\n",
"# length -- byte length of type\n",
"# packfmt -- format string for struct.unpack()\n",
"class String:\n",
" length = 16\n",
"class Byte:\n",
" length = 1\n",
" packfmt = 'b'\n",
"class Word:\n",
" length = 2\n",
" packfmt = 'h'\n",
"class Long:\n",
" length = 4\n",
" packfmt = 'l'\n",
"class Enum:\n",
" length = 2\n",
" packfmt = 'h'\n",
"class Float:\n",
" length = 4\n",
" packfmt = 'f'\n",
"class Double:\n",
" length = 8\n",
" packfmt = 'd'\n",
"class TimeStamp:\n",
" length = 16\n",
" packfmt = 'dbbbbhh'\n",
"class UnitDefinition:\n",
" length = 48\n",
"\n",
"# template of wavedesc block, where each entry in tuple is:\n",
"# (variable name, byte position from beginning of block, datatype)\n",
"wavedesc = ( ('descriptor_name' , 0 , String),\n",
" ('template_name' , 16 , String),\n",
" ('comm_type' , 32 , Enum),\n",
" ('comm_order' , 34 , Enum),\n",
" ('wave_descriptor' , 36 , Long),\n",
" ('user_text' , 40 , Long),\n",
" ('res_desc1' , 44 , Long),\n",
" ('trigtime_array' , 48 , Long),\n",
" ('ris_time_array' , 52 , Long),\n",
" ('res_array1' , 56 , Long),\n",
" ('wave_array_1' , 60 , Long),\n",
" ('wave_array_2' , 64 , Long),\n",
" ('res_array_2' , 68 , Long),\n",
" ('res_array_3' , 72 , Long),\n",
" ('instrument_name' , 76 , String),\n",
" ('instrument_number' , 92 , Long),\n",
" ('trace_label' , 96 , String),\n",
" ('reserved1' , 112 , Word),\n",
" ('reserved2' , 114 , Word),\n",
" ('wave_array_count' , 116 , Long),\n",
" ('pnts_per_screen' , 120 , Long),\n",
" ('first_valid_pnt' , 124 , Long),\n",
" ('last_valid_pnt' , 128 , Long),\n",
" ('first_point' , 132 , Long),\n",
" ('sparsing_factor' , 136 , Long),\n",
" ('segment_index' , 140 , Long),\n",
" ('subarray_count' , 144 , Long),\n",
" ('sweeps_per_acq' , 148 , Long),\n",
" ('points_per_pair' , 152 , Word),\n",
" ('pair_offset' , 154 , Word),\n",
" ('vertical_gain' , 156 , Float),\n",
" ('vertical_offset' , 160 , Float),\n",
" ('max_value' , 164 , Float),\n",
" ('min_value' , 168 , Float),\n",
" ('nominal_bits' , 172 , Word),\n",
" ('nom_subarray_count' , 174 , Word),\n",
" ('horiz_interval' , 176 , Float),\n",
" ('horiz_offset' , 180 , Double),\n",
" ('pixel_offset' , 188 , Double),\n",
" ('vertunit' , 196 , UnitDefinition),\n",
" ('horunit' , 244 , UnitDefinition),\n",
" ('horiz_uncertainty' , 292 , Float),\n",
" ('trigger_time' , 296 , TimeStamp),\n",
" ('acq_duration' , 312 , Float),\n",
" ('record_type' , 316 , Enum),\n",
" ('processing_done' , 318 , Enum),\n",
" ('reserved5' , 320 , Word),\n",
" ('ris_sweeps' , 322 , Word),\n",
" ('timebase' , 324 , Enum),\n",
" ('vert_coupling' , 326 , Enum),\n",
" ('probe_att' , 328 , Float),\n",
" ('fixed_vert_gain' , 332 , Enum),\n",
" ('bandwidth_limit' , 334 , Enum),\n",
" ('vertical_vernier' , 336 , Float),\n",
" ('acq_vert_offset' , 340 , Float),\n",
" ('wave_source' , 344 , Enum) )\n",
"\n",
"\n",
"def lecroy_decode(trc, full_output=False):\n",
" \"\"\" Decode the string `trc` returned by a Lecroy scope or read from a Lecroy TRC file.\n",
" Return two numpy arrays x and y corresponding to time in s and voltage in V.\n",
" If `full_output` is True then a dictionnary with all the additional\n",
" parameters contained in the string is returned.\"\"\"\n",
" \n",
" # Parse the WAVEDESC block\n",
" startpos = trc.find('WAVEDESC')\n",
" var = dict(endian = '<')\n",
" for name, pos, datatype in wavedesc:\n",
" pos += startpos\n",
" raw = trc[pos : pos + datatype.length]\n",
" if datatype in (String, UnitDefinition):\n",
" var[name] = raw.rstrip('\\x00')\n",
" elif datatype in (TimeStamp,):\n",
" var[name] = struct.unpack( var['endian'] + datatype.packfmt, raw)\n",
" else:\n",
" var[name] = struct.unpack( var['endian'] + datatype.packfmt, raw)[0]\n",
"\n",
" # Read binary data block\n",
" datatype = Word if var['comm_type'] else Byte\n",
" y = np.fromstring(trc[startpos + var['wave_descriptor'] + var['user_text'] : ],\n",
" dtype = var['endian'] + datatype.packfmt,\n",
" count = var['wave_array_1'] ).astype(np.float)\n",
"\n",
" # Convert to volt\n",
" y *= var['vertical_gain']\n",
" y += var['vertical_offset']\n",
"\n",
" # Create horizontal scale\n",
" x = np.arange( len(y) )*var['horiz_interval']\n",
" x += var['horiz_offset']\n",
"\n",
" if full_output:\n",
" return x, y, var\n",
" else:\n",
" return x, y\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Content of the lecroy/scope.py file\n",
"\n",
"```python\n",
"\"\"\"Interface to Lecroy oscilloscopes via LAN\n",
"Tested so far with : WaveSurfer 104, WaveRunner \"\"\"\n",
"\n",
"import vxi11 \n",
"from interpreter import lecroy_decode\n",
"import time\n",
"\n",
"# For testing only\n",
"# import numpy as np\n",
"# \n",
"# class LecroyScope_test:\n",
"# def __init__(self, *args, **kwargs):\n",
"# pass\n",
"# def wait_for_acquisition(self, polling_interval=0.1):\n",
"# time.sleep(0.1) \n",
"# def fetchwaveform(self, channel, full_output=False):\n",
"# x = np.linspace(-1,1,1000)*2*np.pi\n",
"# y = np.sin(x) + np.random.rand(1000)*0.1\n",
"# return x,y\n",
"\n",
"class LecroyScope(vxi11.Instrument):\n",
" \"\"\"Inherits from the python-vxi11 Instrument class.\n",
" Add a few high level function to retieve data from a Lecroy scope.\"\"\"\n",
" def wait_for_acquisition(self, polling_interval=0.1):\n",
" \"\"\"Waits until a new trace is acquired.\n",
" The state of the corresponding scope register is polled every `polling_interval`.\"\"\"\n",
" finished = 0\n",
" while not finished:\n",
" time.sleep(polling_interval)\n",
" msg = self.ask('INR?')\n",
" finished = int(msg[4:]) & 1\n",
"\n",
" def fetchwaveform(self, channel, full_output=False):\n",
" \"\"\"Return the content of the waveform specified by `channel` as two vectors x,y.\n",
" If `full_output` is True then a dictionnary with acquisition parameters is also returned.\"\"\"\n",
" # Request data from the scope\n",
" self.write(\"C{0}:WF?\".format(channel))\n",
" trc = self.read_raw()\n",
" return lecroy_decode(trc, full_output)\n",
"```\n"
]
},
{
"cell_type": "heading",
"level": 2,
"metadata": {},
"source": [
"Appendix B : Viewscope application"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Content of viewscope.py\n",
"```python\n",
"#!/usr/bin/env python\n",
"import sys\n",
"from PyQt4 import Qwt5, QtCore, QtGui, Qt\n",
"import tables \n",
"\n",
"from ui.viewscope import Ui_MainWindow # Graphical user interface\n",
"from lecroy.scope import LecroyScope # Lecroy scope interface\n",
"\n",
"class AcquisitionThread(QtCore.QThread):\n",
" def __init__(self, fun):\n",
" super(AcquisitionThread, self).__init__()\n",
" self.fun = fun\n",
" def run(self):\n",
" self.fun()\n",
"\n",
"class MainWindow(QtGui.QMainWindow):\n",
" def __init__(self):\n",
" # Create the main window\n",
" super(MainWindow, self).__init__()\n",
" self.ui = Ui_MainWindow()\n",
" self.ui.setupUi(self)\n",
" # Update button state\n",
" self.update_buttons(connected=False)\n",
" # Some global variables\n",
" self.live = False\n",
" self.acquisitions = []\n",
" self.keeprunning = False\n",
" # Create the acquisition thread\n",
" self.acquisition_thread = AcquisitionThread( self.acquire )\n",
" # Prepare plot\n",
" self.ui.plot.setCanvasBackground(Qt.Qt.white)\n",
" self.ui.plot.setAxisTitle(Qwt5.QwtPlot.xBottom, 'Time (s)')\n",
" self.ui.plot.setAxisTitle(Qwt5.QwtPlot.yLeft, 'Voltage (V)')\n",
" self.pen = Qt.QPen(Qt.Qt.blue)\n",
" self.pen.setWidth(1)\n",
" self.curve = Qwt5.QwtPlotCurve()\n",
" self.curve.attach(self.ui.plot)\n",
" self.curve.setPen(self.pen)\n",
"\n",
" def update_buttons(self, connected=True, live=False, acquiring = False):\n",
" # Enable or disable buttons according to the state of the application\n",
" self.ui.lineEdit_IP.setEnabled( not connected )\n",
" self.ui.pushButton_Live.setEnabled(connected and not acquiring)\n",
" self.ui.pushButton_Disconnect.setEnabled( connected )\n",
" self.ui.pushButton_Connect.setEnabled( not connected )\n",
" self.ui.pushButton_Reset.setEnabled( not live )\n",
" self.ui.pushButton_Single.setEnabled( connected and not live and not acquiring)\n",
" self.ui.pushButton_Start.setEnabled( connected and not live and not acquiring )\n",
" self.ui.pushButton_Stop.setEnabled( connected and not live and acquiring )\n",
" self.ui.pushButton_SaveTo.setEnabled( not live and not acquiring )\n",
"\n",
" @QtCore.pyqtSignature(\"\")\n",
" def on_pushButton_Connect_clicked(self):\n",
" # Connect to the scope at the specified IP address\n",
" self.scope = LecroyScope( str(self.ui.lineEdit_IP.text()) )\n",
" self.update_buttons()\n",
"\n",
" @QtCore.pyqtSignature(\"\")\n",
" def on_pushButton_Disconnect_clicked(self):\n",
" # Disconnect the scope\n",
" if self.acq.isRunning() :\n",
" self.acq.terminate()\n",
" self.scope.close()\n",
" self.update_buttons(connected = False)\n",
"\n",
" @QtCore.pyqtSignature(\"\")\n",
" def on_pushButton_Live_clicked(self):\n",
" if not self.live:\n",
" # Start live acquisition\n",
" self.acquisition_thread.finished.connect( self.liveupdate )\n",
" self.acquisition_thread.start()\n",
" self.live = True\n",
" self.ui.pushButton_Live.setText('Stop')\n",
" self.update_buttons(live=True)\n",
" else:\n",
" # Stop live acquisition\n",
" self.acq.finished.disconnect( self.live_finished )\n",
" self.live = False\n",
" self.ui.pushButton_Live.setText('Live')\n",
" self.update_buttons(live=False)\n",
"\n",
" def acquire(self):\n",
" # Acquire one waveform from channel 1\n",
" self.scope.wait_for_acquisition()\n",
" self.last_trace = self.scope.fetchwaveform(1)\n",
"\n",
" def live_finished(self):\n",
" # Callback function during \"live\" acquisition.\n",
" self.plot(*self.last_trace)\n",
" self.acquisition_thread.start()\n",
"\n",
" def plot(self, x, y):\n",
" self.curve.setData(x, y)\n",
" self.ui.plot.setAxisScale(Qwt5.QwtPlot.xBottom, x[0], x[-1])\n",
" self.ui.plot.replot()\n",
"\n",
" @QtCore.pyqtSignature(\"\")\n",
" def on_pushButton_Reset_clicked(self):\n",
" # Empty the acquired trace list\n",
" self.acquisitions = []\n",
" self.update_counter()\n",
"\n",
" @QtCore.pyqtSignature(\"\")\n",
" def on_pushButton_Single_clicked(self):\n",
" # Acquires a single trace and store it\n",
" if self.buffer_is_full : return\n",
" self.keeprunning = False\n",
" self.start_acquisition()\n",
"\n",
" @QtCore.pyqtSignature(\"\")\n",
" def on_pushButton_Start_clicked(self):\n",
" # Acquire many traces and store them\n",
" if self.buffer_is_full : return\n",
" self.keeprunning = True\n",
" self.start_acquisition()\n",
"\n",
" def start_acquisition(self):\n",
" # Start acquisition \n",
" self.acquisition_thread.finished.connect( self.acquisition_finished )\n",
" self.acquisition_thread.start()\n",
" self.update_buttons(acquiring=True)\n",
"\n",
" @QtCore.pyqtSignature(\"\")\n",
" def on_pushButton_Stop_clicked(self):\n",
" # Stop acquisition\n",
" self.acquisition_thread.finished.disconnect( self.acqupdate )\n",
" self.update_buttons(acquiring=False)\n",
"\n",
" def acquisition_finished(self):\n",
" # Callback function during \"normal\" acquisition\n",
" self.plot(*self.last_trace)\n",
" self.acquisitions.append( self.last_trace )\n",
" self.update_counter()\n",
" if self.keeprunning and not self.buffer_is_full:\n",
" self.acquisition_thread.start()\n",
" else:\n",
" self.on_pushButton_Stop_clicked()\n",
"\n",
" def update_counter(self):\n",
" self.ui.label_Acquisitions.setText( str( len(self.acquisitions) ))\n",
"\n",
" @property\n",
" def buffer_is_full(self):\n",
" # Return True if the required number of acquired traces is reached\n",
" max = self.ui.spinBox_MaxNumberOfAcquisitions.value()\n",
" if max==0 : return False\n",
" return len(self.acquisitions)>=max\n",
"\n",
" @QtCore.pyqtSignature(\"\")\n",
" def on_pushButton_SaveTo_clicked(self):\n",
" # Save acquired traces to HDF5 format\n",
" if len(self.acquisitions)==0 : return\n",
" filename = QtGui.QFileDialog.getSaveFileName(filter=\"HDF5 file (*.h5)\")\n",
" if filename:\n",
" if tables.__version__[0]=='3':\n",
" # Code for PyTables v3\n",
" h5file = tables.open_file( str(filename), mode = \"w\", title = \"Viewscope Data\")\n",
" filters = tables.Filters(complevel=5, complib='zlib')\n",
" arr = h5file.create_carray(h5file.root, 'Waveforms', obj = self.acquisitions, filters=filters)\n",
" arr.attrs.description = \"(Time (s), Voltage(V) )\"\n",
" h5file.close()\n",
" else:\n",
" # Code for PyTables v2\n",
" h5file = tables.openFile( str(filename), mode = \"w\", title = \"Viewscope Data\")\n",
" arr = h5file.createArray(h5file.root, 'Waveforms', self.acquisitions)\n",
" arr.attrs.description = \"(Time (s), Voltage(V) )\"\n",
" h5file.close()\n",
"\n",
"\n",
"if __name__ == '__main__':\n",
" app = QtGui.QApplication(sys.argv)\n",
" mainWin = MainWindow()\n",
" mainWin.show()\n",
" sys.exit(app.exec_())\n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Content of ui/viewscope.py\n",
"```python\n",
"# -*- coding: utf-8 -*-\n",
"\n",
"# Form implementation generated from reading ui file 'viewscope.ui'\n",
"#\n",
"# Created: Fri Jan 10 10:32:35 2014\n",
"# by: PyQt4 UI code generator 4.8.4\n",
"#\n",
"# WARNING! All changes made in this file will be lost!\n",
"\n",
"from PyQt4 import QtCore, QtGui\n",
"\n",
"try:\n",
" _fromUtf8 = QtCore.QString.fromUtf8\n",
"except AttributeError:\n",
" _fromUtf8 = lambda s: s\n",
"\n",
"class Ui_MainWindow(object):\n",
" def setupUi(self, MainWindow):\n",
" MainWindow.setObjectName(_fromUtf8(\"MainWindow\"))\n",
" MainWindow.resize(727, 487)\n",
" self.centralwidget = QtGui.QWidget(MainWindow)\n",
" self.centralwidget.setObjectName(_fromUtf8(\"centralwidget\"))\n",
" self.verticalLayout = QtGui.QVBoxLayout(self.centralwidget)\n",
" self.verticalLayout.setObjectName(_fromUtf8(\"verticalLayout\"))\n",
" self.horizontalLayout_4 = QtGui.QHBoxLayout()\n",
" self.horizontalLayout_4.setObjectName(_fromUtf8(\"horizontalLayout_4\"))\n",
" self.plot = Qwt5.QwtPlot(self.centralwidget)\n",
" self.plot.setMaximumSize(QtCore.QSize(16777215, 16777215))\n",
" self.plot.setFocusPolicy(QtCore.Qt.StrongFocus)\n",
" self.plot.setProperty(_fromUtf8(\"propertiesDocument\"), _fromUtf8(\"\"))\n",
" self.plot.setObjectName(_fromUtf8(\"plot\"))\n",
" self.horizontalLayout_4.addWidget(self.plot)\n",
" self.verticalLayout.addLayout(self.horizontalLayout_4)\n",
" self.horizontalLayout_2 = QtGui.QHBoxLayout()\n",
" self.horizontalLayout_2.setObjectName(_fromUtf8(\"horizontalLayout_2\"))\n",
" self.label = QtGui.QLabel(self.centralwidget)\n",
" self.label.setObjectName(_fromUtf8(\"label\"))\n",
" self.horizontalLayout_2.addWidget(self.label)\n",
" self.lineEdit_IP = QtGui.QLineEdit(self.centralwidget)\n",
" self.lineEdit_IP.setMaximumSize(QtCore.QSize(121, 16777215))\n",
" self.lineEdit_IP.setObjectName(_fromUtf8(\"lineEdit_IP\"))\n",
" self.horizontalLayout_2.addWidget(self.lineEdit_IP)\n",
" self.pushButton_Connect = QtGui.QPushButton(self.centralwidget)\n",
" self.pushButton_Connect.setObjectName(_fromUtf8(\"pushButton_Connect\"))\n",
" self.horizontalLayout_2.addWidget(self.pushButton_Connect)\n",
" self.pushButton_Disconnect = QtGui.QPushButton(self.centralwidget)\n",
" self.pushButton_Disconnect.setObjectName(_fromUtf8(\"pushButton_Disconnect\"))\n",
" self.horizontalLayout_2.addWidget(self.pushButton_Disconnect)\n",
" spacerItem = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum)\n",
" self.horizontalLayout_2.addItem(spacerItem)\n",
" self.pushButton_Live = QtGui.QPushButton(self.centralwidget)\n",
" self.pushButton_Live.setObjectName(_fromUtf8(\"pushButton_Live\"))\n",
" self.horizontalLayout_2.addWidget(self.pushButton_Live)\n",
" self.verticalLayout.addLayout(self.horizontalLayout_2)\n",
" self.GoupBoxAcquisition = QtGui.QGroupBox(self.centralwidget)\n",
" self.GoupBoxAcquisition.setObjectName(_fromUtf8(\"GoupBoxAcquisition\"))\n",
" self.horizontalLayout = QtGui.QHBoxLayout(self.GoupBoxAcquisition)\n",
" self.horizontalLayout.setObjectName(_fromUtf8(\"horizontalLayout\"))\n",
" self.pushButton_Reset = QtGui.QPushButton(self.GoupBoxAcquisition)\n",
" self.pushButton_Reset.setObjectName(_fromUtf8(\"pushButton_Reset\"))\n",
" self.horizontalLayout.addWidget(self.pushButton_Reset)\n",
" self.pushButton_Single = QtGui.QPushButton(self.GoupBoxAcquisition)\n",
" self.pushButton_Single.setObjectName(_fromUtf8(\"pushButton_Single\"))\n",
" self.horizontalLayout.addWidget(self.pushButton_Single)\n",
" self.pushButton_Start = QtGui.QPushButton(self.GoupBoxAcquisition)\n",
" self.pushButton_Start.setFocusPolicy(QtCore.Qt.NoFocus)\n",
" self.pushButton_Start.setText(_fromUtf8(\"Start\"))\n",
" self.pushButton_Start.setObjectName(_fromUtf8(\"pushButton_Start\"))\n",
" self.horizontalLayout.addWidget(self.pushButton_Start)\n",
" self.pushButton_Stop = QtGui.QPushButton(self.GoupBoxAcquisition)\n",
" self.pushButton_Stop.setFocusPolicy(QtCore.Qt.NoFocus)\n",
" self.pushButton_Stop.setObjectName(_fromUtf8(\"pushButton_Stop\"))\n",
" self.horizontalLayout.addWidget(self.pushButton_Stop)\n",
" self.label_2 = QtGui.QLabel(self.GoupBoxAcquisition)\n",
" self.label_2.setObjectName(_fromUtf8(\"label_2\"))\n",
" self.horizontalLayout.addWidget(self.label_2)\n",
" self.label_Acquisitions = QtGui.QLabel(self.GoupBoxAcquisition)\n",
" self.label_Acquisitions.setObjectName(_fromUtf8(\"label_Acquisitions\"))\n",
" self.horizontalLayout.addWidget(self.label_Acquisitions)\n",
" self.label_3 = QtGui.QLabel(self.GoupBoxAcquisition)\n",
" self.label_3.setObjectName(_fromUtf8(\"label_3\"))\n",
" self.horizontalLayout.addWidget(self.label_3)\n",
" self.spinBox_MaxNumberOfAcquisitions = QtGui.QSpinBox(self.GoupBoxAcquisition)\n",
" self.spinBox_MaxNumberOfAcquisitions.setObjectName(_fromUtf8(\"spinBox_MaxNumberOfAcquisitions\"))\n",
" self.horizontalLayout.addWidget(self.spinBox_MaxNumberOfAcquisitions)\n",
" spacerItem1 = QtGui.QSpacerItem(77, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum)\n",
" self.horizontalLayout.addItem(spacerItem1)\n",
" self.pushButton_SaveTo = QtGui.QPushButton(self.GoupBoxAcquisition)\n",
" self.pushButton_SaveTo.setFocusPolicy(QtCore.Qt.NoFocus)\n",
" self.pushButton_SaveTo.setText(_fromUtf8(\"Save To\"))\n",
" self.pushButton_SaveTo.setObjectName(_fromUtf8(\"pushButton_SaveTo\"))\n",
" self.horizontalLayout.addWidget(self.pushButton_SaveTo)\n",
" self.verticalLayout.addWidget(self.GoupBoxAcquisition)\n",
" MainWindow.setCentralWidget(self.centralwidget)\n",
" self.statusbar = QtGui.QStatusBar(MainWindow)\n",
" self.statusbar.setObjectName(_fromUtf8(\"statusbar\"))\n",
" MainWindow.setStatusBar(self.statusbar)\n",
" self.actionTransmission = QtGui.QAction(MainWindow)\n",
" self.actionTransmission.setCheckable(True)\n",
" self.actionTransmission.setChecked(True)\n",
" self.actionTransmission.setObjectName(_fromUtf8(\"actionTransmission\"))\n",
" self.actionReflection = QtGui.QAction(MainWindow)\n",
" self.actionReflection.setCheckable(True)\n",
" self.actionReflection.setObjectName(_fromUtf8(\"actionReflection\"))\n",
" self.actionTrigger = QtGui.QAction(MainWindow)\n",
" self.actionTrigger.setCheckable(True)\n",
" self.actionTrigger.setChecked(True)\n",
" self.actionTrigger.setObjectName(_fromUtf8(\"actionTrigger\"))\n",
" self.actionLoad = QtGui.QAction(MainWindow)\n",
" self.actionLoad.setObjectName(_fromUtf8(\"actionLoad\"))\n",
"\n",
" self.retranslateUi(MainWindow)\n",
" QtCore.QMetaObject.connectSlotsByName(MainWindow)\n",
"\n",
" def retranslateUi(self, MainWindow):\n",
" MainWindow.setWindowTitle(QtGui.QApplication.translate(\"MainWindow\", \"Viewcav\", None, QtGui.QApplication.UnicodeUTF8))\n",
" self.label.setText(QtGui.QApplication.translate(\"MainWindow\", \"Scope IP\", None, QtGui.QApplication.UnicodeUTF8))\n",
" self.lineEdit_IP.setText(QtGui.QApplication.translate(\"MainWindow\", \"10.118.16.94\", None, QtGui.QApplication.UnicodeUTF8))\n",
" self.pushButton_Connect.setText(QtGui.QApplication.translate(\"MainWindow\", \"Connect\", None, QtGui.QApplication.UnicodeUTF8))\n",
" self.pushButton_Disconnect.setText(QtGui.QApplication.translate(\"MainWindow\", \"Disconnect\", None, QtGui.QApplication.UnicodeUTF8))\n",
" self.pushButton_Live.setText(QtGui.QApplication.translate(\"MainWindow\", \"Live\", None, QtGui.QApplication.UnicodeUTF8))\n",
" self.GoupBoxAcquisition.setTitle(QtGui.QApplication.translate(\"MainWindow\", \"Acquisition\", None, QtGui.QApplication.UnicodeUTF8))\n",
" self.pushButton_Reset.setText(QtGui.QApplication.translate(\"MainWindow\", \"Reset\", None, QtGui.QApplication.UnicodeUTF8))\n",
" self.pushButton_Single.setText(QtGui.QApplication.translate(\"MainWindow\", \"Single\", None, QtGui.QApplication.UnicodeUTF8))\n",
" self.pushButton_Stop.setText(QtGui.QApplication.translate(\"MainWindow\", \"Stop\", None, QtGui.QApplication.UnicodeUTF8))\n",
" self.label_2.setText(QtGui.QApplication.translate(\"MainWindow\", \"Acquired :\", None, QtGui.QApplication.UnicodeUTF8))\n",
" self.label_Acquisitions.setText(QtGui.QApplication.translate(\"MainWindow\", \"0\", None, QtGui.QApplication.UnicodeUTF8))\n",
" self.label_3.setText(QtGui.QApplication.translate(\"MainWindow\", \"Max\", None, QtGui.QApplication.UnicodeUTF8))\n",
" self.actionTransmission.setText(QtGui.QApplication.translate(\"MainWindow\", \"Transmission\", None, QtGui.QApplication.UnicodeUTF8))\n",
" self.actionReflection.setText(QtGui.QApplication.translate(\"MainWindow\", \"Reflection\", None, QtGui.QApplication.UnicodeUTF8))\n",
" self.actionTrigger.setText(QtGui.QApplication.translate(\"MainWindow\", \"Trigger\", None, QtGui.QApplication.UnicodeUTF8))\n",
" self.actionLoad.setText(QtGui.QApplication.translate(\"MainWindow\", \"Load\", None, QtGui.QApplication.UnicodeUTF8))\n",
"\n",
"from PyQt4 import Qwt5\n",
"```"
]
}
],
"metadata": {}
}
]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment