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 *
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
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.
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 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']['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']['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.