Dynamic Plotting of HF2 Lock-in Data with Python

The Matplotlib is a library for creating 2D plots of arrays in Python. Matplotlib.pyplot and Matplotlib.pylab have a collection of commands that make Matplotlib work like Matlab. The designers of the Matplotlib library made two assumptions concerning plotting with Python. First, when plots are created by scripting, there is no need to update the figure every time a single property is changed, only once after all the properties are changed. Thus, Matplotlib defers plotting until the end of the Python script because drawing can be an expensive operation. Second, when working in the Python shell, it is desirable to update the figure with each command. The purpose of this blog entry is to depict a general approach to enable continuous updating of figure data during the Python script execution.

The installation of the Zurich Instruments Python interface is discussed in Controlling the HF2LI Lock-in with Python.

Simple Plotting Example with Python

Find below a simple plot example that uses the Matplotlib.pyplot module:

import matplotlib.pyplot as plt
from numpy import *

a=arange(0,10,0.1)
plt.figure()
plt.plot(a,sin(2*pi*a/10))

plt.figure()
plt.plot(a,cos(2*pi*a/10))
plt.show()
print 'Done!'

The show() command tells the Matplotlib to render all figure windows and start the GUI mainloop. As the mainloop is blocking, this command should be called only once at the end of the script. The figure will contain the final result of all preceding plot commands. Script commands following show() will be blocked until all figures are closed by the user.
One solution to plot continuous oscilloscope data using the show() command is the creation of a new figure for each oscilloscope shot.
However, it is more convenient to update the same figure for each oscilloscope shot as it is for example done for the oscilloscope tab within ziControl (Zurich Instruments graphics user interface). Therefore, continuous updating of the figure with each pyplot command has to be enabled. Command draw() is suitable to force re-plotting. In that case, the program execution after plotting will also be possible, even without closing the figure.

Python Settings for Dynamic Plotting

There are interactive Python distributions such as iPython that are automatically working in the mode where every pyplot command triggers a figure update. The subsequently presented approach is not limited to a specific distribution and explains a general method to enable the interactive mode.
Matplotlib uses matplotlibrc configuration file to customize all kinds of properties such as default figure size, line width, colors and style, grid properties, text font. To find out the location of matplotlibrc file that is currently loaded use the command matplotlib.matplolib_frame(). To support updating the figure on every pyplot command the backend should be set to TkAgg and the interactive flag should be set to True in the matplotlibrc file. If matplotlib is installed again, the settings will be overwritten.
Alternatively, the settings can also be applied in the python script directly. Before importing pyplot, backend should be set with command matplotlib.use('TkAgg'). After importing pyplot, interactive mode should be turned on with the command matplolib.pyplot.ion().

Dynamic Plotting of HF2 Lock-in Oscilloscope Data

The Python script presented in this section will plot the sum of two sine waves with frequency difference of 1 Hz. The result is a signal with beating amplitude. Before running the script scope_example.py, Signal Output 2 Out should be connected to Signal Output 1 Add and Signal Output 1 Out should be connected to Signal Input 1 +In. The HF2-LI Lock-in should be connected to the computer with a USB 2.0 cable. The ziServer should be running on the computer where the instrument is connected to.
There are several ways how to run scope_example.py script because the code in the script is structured so that can be used as a standalone program and as a module. As a standalone program, the script can be run from the command prompt with python -i scope_example.py. This is possible if you are located in folder that contains file scope_example.py and the environment variable path contains path to the executable Python file in C:\Python26.
For the users who are using IDLE (Python GUI), it’s recommended to open file scope_example.py with the right click on file and selecting ‘Edit with IDLE’. In this case the IDLE shell will write a message ==== No Subprocess ====. To run the script, press F5 or select Run module from the Run menu. It’s important to run Python shell with no sub-processes because otherwise the script execution will crash if you for example only try to move the figure window.
The scope_example.py script can be imported in a Python shell as a module with import scope_example. It contains class called Oscilloscope. Class instantiation automatically invokes the __init__() method to obtain initialized instance of the class. Arguments that must be supplied at class instantiation are the reference to the server, the device name string and the input channel number of which data should be analyzed. The class defines methods set_parameters and update. Method set_parameters sets the oscilloscope parameters (see ZI oscilloscope tab in the ziControl) source channel, sampling rate, configuring the trigger, and time between to oscilloscope shots. The method update creates an oscilloscope window and plots new oscilloscope data after each call.

Download the file scope_example.py.

import time, math, random
import zhinst.ziPython, zhinst.utils
import matplotlib
matplotlib.use('TkAgg')
import matplotlib.pyplot as plt
from numpy import *
plt.ion()

class Oscilloscope(object):
   def __init__(self,device,daq,channel):
       self.device=device
       self.daq=daq
       self.channel=channel
       self.updated=0
       
   def set_parameters(self, bwlimit=0,trigedge=1,triglevel=0, \
                      trigholdoff=0.01,trigchannel=-2,t=1):
       settings = [
           [['/', self.device, '/scopes/0/channel'], self.channel-1],
           [['/', self.device, '/scopes/0/bwlimit'], bwlimit],
           [['/', self.device, '/scopes/0/trigedge'], trigedge],
           [['/', self.device, '/scopes/0/triglevel'], triglevel],
           [['/', self.device, '/scopes/0/trigholdoff'], trigholdoff],
           [['/', self.device, '/scopes/0/trigchannel'], trigchannel],
           [['/', self.device, '/scopes/0/time'], t]
       ]
       self.daq.set(settings)
       # Wait 1s
       time.sleep(1)
       # Clean queue
       self.daq.flush()
 
   def update(self):
           rng=self.daq.getDouble('/' + self.device + '/sigins/' \
                                  + str(self.channel-1) + '/range')
           # Poll data 0.05s with a timeout of 500ms
           dataDict = self.daq.poll(0.05,500)
           # For first oscilloscope shot use plot function and after is
           # used draw function to update figure with new oscilloscope data
           if self.device in dataDict:
               data=dataDict[self.device]['scopes']['0']['wave'][0]['wave']
               datay=data*rng/float(2**15)
               if plt.get_fignums() and self.updated==1:
                   self.line.set_ydata(datay)
                   plt.draw()
               else:
                   # Create oscilloscope window
                   plt.figure()
                   plt.grid(True)
                   plt.title('Osciloscope data')
                   plt.xlabel('Time(us)')
                   plt.ylabel('Amplitude(V)')
                   
		   # Plot first oscilloscope shot
                   dt=dataDict[self.device]['scopes']['0']['wave'][0]['dt']
                   datax=[x*1e6 for x in arange(0,dt*2048,dt)]
                   plt.axis([0,datax[-1],-2*rng,2*rng])
                   self.line, = plt.plot(datax,datay)
                   self.updated=1


if __name__ == '__main__':

   # Open connection to ziServer
   daq = zhinst.ziPython.ziDAQServer('localhost', 8005)

   # Detect device
   device = zhinst.utils.autoDetect()

   # Set channels parameters
   channel_settings = [
       [['/', device, '/sigins/0/diff'], 0],
       [['/', device, '/sigins/0/imp50'], 0],
       [['/', device, '/sigins/0/ac'], 0],
       [['/', device, '/sigins/0/range'], 1],

       [['/', device, '/sigins/1/diff'], 0],
       [['/', device, '/sigins/1/imp50'], 0],
       [['/', device, '/sigins/1/ac'], 0],
       [['/', device, '/sigins/1/range'], 1],

       [['/', device, '/oscs/0/freq'], 100e3],
       [['/', device, '/oscs/1/freq'], 100e3+1],

       [['/', device, '/sigouts/0/add'], 1],
       [['/', device, '/sigouts/0/on'], 1],
       [['/', device, '/sigouts/0/range'], 1],
       [['/', device, '/sigouts/0/amplitudes/6'],0.5],

       [['/', device, '/sigouts/1/add'], 0],
       [['/', device, '/sigouts/1/on'], 1],
       [['/', device, '/sigouts/1/range'],1],
       [['/', device, '/sigouts/1/amplitudes/7'],0.5],
       ]
   daq.set(channel_settings)
   time.sleep(1)

   # Select lock-in channel
   channel=1

   # Create object Oscilloscope
   a=Oscilloscope(device,daq,channel)

   # Set parameters for the scope
   a.set_parameters(bwlimit=0,trigedge=1,triglevel=0, \
                    trigholdoff=0.01,trigchannel=0,t=1)

   # Subscribe to scope data
   path0 = '/' + device + '/scopes/0/wave'
   daq.subscribe(path0)

   # N is number of oscilloscope shots
   N=random.randint(50,100)

   for i in range(0, N):
       a.update()
   
   # Unsubscribe     
   daq.unsubscribe(path0)
   print 'Done!'

Here is a figure with only 8 oscilloscope shots plotted in the same figure. The darker the color the later the oscilloscope data was recorded.

Categories