Controlling the HF2LI Lock-in with Python

The Zurich Instruments device can be controlled with any programming language that can interface to a dynamically loaded (on Windows) or shared (on Linux) library. This blog entry contains a basic example how to communicate with HF2LI Lock-in using the Python programming language. This is achieved with an interface library of Zurich Instruments that transforms the ziAPI into a native Python interface, subsequently named ziPython.

Communication with HF2LI Lock-in using Matlab is explained in Controlling the HF2LI Lock in with Matlab.. These two libraries share most of the code and are thus very similar in usage.

Zurich Instruments Python Interface Installation

Currently, Python 2.6x is supported. It’s recommend to install this Python version from Python programming language official website. The ziPython package containing the interface to control the HF2LI Lock-in from Python can be downloaded from the Zurich Instruments download page. Please use your Zurich Instruments login to get access to the download area.

To install the package on Windows execute the installer. It will guide you through the installation process as displayed in the following screen shots.

After clicking on the Next button, the existing Python installation can be selected. The package will be installed in the folder …\Python26\Lib\site-packages\zhinst.

The interface will use numpy for fast data transfer. Therefore, the package NumPy is required. To run the example code the matplotlib library needs to be installed as well. Unofficial 64bit precompiled packages of both modules can be found on the page of Christoph Gohlke.

After installation, we are ready to run the first Python script controlling the HF2 Lock-in amplifier.

Simple Loop-back Measurement with the HF2LI Lock-in

During a loop-back measurement we generate a signal and measure the signal with the HF2LI Lock-in. Thus, connect the Signal Output 1 Out to Signal Input 1 +In with a BNC cable. The HF2LI Lock-in is connected to the computer with a USB 2.0 cable. The ziServer should be running on the computer where the instrument is connected to. It is automatically started if the Zurich Instruments driver ziBase was installed beforehand. To check whether a server is running, check in the Windows Task Manager for the processes ziSrv01.exe and ziStrt01.exe. On Linux the process name is ziServer64 or ziServer32 for 64bit or 32bit systems, respectively.

The package ziPython contains two modules zhinst.ziPython and zhinst.utils. The module zhinst.ziPython provides all classes for interfacing to the ziServer which is communicating with the HF2LI Lock-in. The class ziDAQServer provides methods that access the device with synchronous calls. This methods block during execution.
The module zhinst.utils contains helper functions to provide code for often performed tasks. In the example below we use the method autoDetect to read the name of the HF2LI Lock-in device currently connected to the ziServer.
Download the file simple_example.py.

import zhinst.ziPython, zhinst.utils
from numpy import *
import time
# Open connection to ziServer
daq = zhinst.ziPython.ziDAQServer('localhost', 8005)
# Detect device
device = zhinst.utils.autoDetect()
#Record one demodulator sample from the specified node
sample = daq.getSample('/'+device+'/demods/0/sample')
r = sqrt(sample['x']**2+sample['y']**2)
print 'Measured rms amplitude is %fV.' % r

After running this script, measured rms amplitude is zero because the output channel is not enabled.

Changing Device Settings

Switching the output channel on and setting the amplitude of the output signal to 100 mV using the ziControl interface, the measured rms amplitude after running the same example will be around 70.7 mV. Because the ziServer manages all communication between the HF2LI amplifier and all programs (clients) that access the HF2LI, it’s possible to control HF2LI device with user programs and ziControl simultaneously. The ziServer keeps track of all the setting changes and sends update changes to the other applications.

There are two methods for changing device settings with Python. The first approach is using setInt(...), setDouble(...), setByte(...) methods. This methods receive as argument a path string of the node and the node value. In a multi-threaded environment the Python interpreter uses a global lock, called Global Interpreter Lock (GIL), that synchronizes the code execution of the different Python threads. Only one thread that has acquired the GIL may operate on Python objects. This serialization process may slow down program execution in the multi-thread environment if many set commands are executed in sequence. For performance reasons it is thus an advantage to collect all settings in a list and the set all at once. During that set command execution the GIL is released by ziPython. This allows the Python interpreter to execute code from other threads simultaneously.
The following code fragment gives an example of single setInt(...), setDouble(...), and setByte(...) commands. The ziPython interface performs an implicit conversion between double and integer if needed. Thus, for most settings the precision is accurate enough to use either of them.

import zhinst.ziPython, zhinst.utils
from numpy import *
import time
# Open connection to ziServer
daq = zhinst.ziPython.ziDAQServer('localhost', 8005)
# Detect device
device = zhinst.utils.autoDetect()
#set the amplitude of output channel 1 to 100mV
daq.setDouble('/' + device + '/sigouts/0/amplitudes/0', 0.1);
#set the enable output channel
daq.setDouble('/' + device + '/sigouts/0/enables/0', 1);
#turn on output channel
daq.setInt('/' + device + '/sigouts/0/on', 1);
# wait 1s
time.sleep(1)
#Record one demodulator sample from the specified node
sample = daq.getSample('/'+device+'/demods/0/sample')
r = sqrt(sample['x']**2+sample['y']**2)
print 'Measured rms amplitude is %fV.' % r

From the point of view of program execution time if it’s needed to change more settings, it’s better to use set command. That method receives as argument a list with elements [[node_path], set_value]. The next example uses the set command to change settings of the HF2LI Lock-in amplifier.

It’s convenient to use the ziControl to find out path strings of the nodes. If you change a setting using the ziControl, the command line display (located at the bottom of the ziControl interface) will show the command that was sent to the instrument in a format node_path : set_value.

Polling Demodulator Data

If continuous data recording is performed polling functions are best suited. To select the nodes from which data should be polled, the subscribe / unsubscribe commands are used. The following example demonstrates the synchronous poll usage. Thus, the poll command will block during the specified recording time. Due to the blocking behavior, too long recording times should be avoided as the interface is not responsive within that time.

For recoding times longer than 10s it is recommended to used the poll command inside a loop. With this method continuous data can be recorded over a longer time frame. Internal data buffering on the ziServer ensures that no data is lost between the poll commands. As these buffers are limited in size, the time between poll commands must not be too large for high sampling rates. If extensive calculations should be performed during the poll commands, the asynchronous poll interface might be better suited.
Download the file synchronous_example.py.

import time, math
import zhinst.ziPython, zhinst.utils
import matplotlib
import matplotlib.pyplot as plt
from numpy import *
def measureSynchronousFeedback(daq, device, channel, frequency):
   c=str(channel-1)
   amplitude=1
   rate=200
   tc=0.01
   # Disable all outputs and all demods
   general_setting = [
        [['/', device, '/demods/0/trigger'], 0],
        [['/', device, '/demods/1/trigger'], 0],
        [['/', device, '/demods/2/trigger'], 0],
        [['/', device, '/demods/3/trigger'], 0],
        [['/', device, '/demods/4/trigger'], 0],
        [['/', device, '/demods/5/trigger'], 0],
        [['/', device, '/sigouts/0/enables/*'], 0],
        [['/', device, '/sigouts/1/enables/*'], 0]
   ]
   daq.set(general_setting)
   # Set test settings
   t1_sigOutIn_setting = [
       [['/', device, '/sigins/',c,'/diff'], 0],
       [['/', device, '/sigins/',c,'/imp50'], 1],
       [['/', device, '/sigins/',c,'/ac'], 0],
       [['/', device, '/sigins/',c,'/range'], 2],
       [['/', device, '/demods/',c,'/order'], 8],
       [['/', device, '/demods/',c,'/timeconstant'], tc],
       [['/', device, '/demods/',c,'/rate'], rate],
       [['/', device, '/demods/',c,'/adcselect'], channel-1],
       [['/', device, '/demods/',c,'/oscselect'], channel-1],
       [['/', device, '/demods/',c,'/harmonic'], 1],
       [['/', device, '/oscs/',c,'/freq'], frequency],
       [['/', device, '/sigouts/',c,'/add'], 0],
       [['/', device, '/sigouts/',c,'/on'], 1],
       [['/', device, '/sigouts/',c,'/enables/',c], 1],
       [['/', device, '/sigouts/',c,'/range'], 1],
       [['/', device, '/sigouts/',c,'/amplitudes/',c], amplitude],
   ]
   daq.set(t1_sigOutIn_setting);
   # wait 1s to get a settled lowpass filter
   time.sleep(1)
   #clean queue
   daq.flush()
   # Subscribe to scope
   path0 = '/' + device + '/demods/',c,'/sample'
   daq.subscribe(path0)
   # Poll data 1s, second parameter is poll timeout in [ms]  
   # (recomended value is 500ms) 
   dataDict = daq.poll(1,500);
   # Unsubscribe to scope
   daq.unsubscribe(path0)
   # Recreate data
   if device in dataDict:
       if dataDict[device]['demods'][c]['sample'][0]['time']['dataloss']:
           print 'Sample loss detected.'
       else:
           e=0.5*amplitude/sqrt(2)
           data = dataDict[device]['demods'][c]['sample'][0]
           rdata = sqrt(data['x']**2+data['y']**2)
           print 'Measured rms amplitude is %.5fV (expected: %.5fV).' %(mean(rdata),e)
           tdata= (data['timestamp']-data['timestamp'][0])/210e6
           # Create plot 
           plt.figure(channel)
           plt.grid(True)
           plt.plot(tdata,rdata)
           plt.title('Demodulator data')
           plt.xlabel('Time (s)')
           plt.ylabel(' R component (V)')
           plt.axis([tdata[0],tdata[-1],0.97*e,1.03*e])
           plt.show()
# Open connection to ziServer
daq = zhinst.ziPython.ziDAQServer('localhost', 8005)
# Detect device
device = zhinst.utils.autoDetect()
measureSynchronousFeedback(daq, device, 1, 1e5);

The method ziDAQServer creates an instance of the ziServer you are connecting to. The poll method of this object returns data of all subscribed nodes in the Python dict data type. In the previous example values of the following nodes have been received.

data['timestamp'] # Time stamp data [uint64]. Divide value by 210e6 to
# calculate the time stamp in seconds.
data['x'] # Demodulator x value in volt [double]
data['y'] # Demodulator y value in volt [double]
data['frequency'] # Current demodulator frequency in hertz [double].
data['phase'] # Phase value [double].
data['dio'] # Digital IO data [uint32].
data['auxin0'] # Auxiliary input value of channel 0 in volt [double].
data['auxin1'] # Auxiliary input value of channel 1 in volt [double].
data['time']['dataloss '] # Indication of sample loss (including block loss).
data['time']['blockloss '] # Indication of data block loss over the socket
# connection. This may be the result of a too long break between subsequent
# poll commands.
data['time']['invalidtimestamp '] # Indication of invalid time stamp data
# as a result of a sampling rate change during the measurement.

Categories