Razorbill Instruments sells equipment that forms part of a scientific experiment. Most users want to combine our products with other components from other vendors. This would include cryostat components such as temperature and magnet controllers, and measurement instruments such as lock-in amplifiers.
It’s not practical to provide universal software for every possible combination, so most labs end up writing some degree of bespoke code. There are several popular languages for doing this (e.g. LabView, Matlab, Python), and there are projects in those languages (e.g Qcodes or Pymeasure) that offer toolkits and many advanced functions for lab automation. Razorbill Instrument’s technical director Jack has written a couple of blogs on the topic of lab automation and instrument communication which you can find here.
This example script is for people who have chosen Python as there preferred language, but are looking for something simpler than Qcodes or Pymeasure. This script demonstrates how to send instructions to our RP100 power supply; measure the feedback sensor with a Keysight E4980AL LCR meter (the most popular option for our customers); convert capacitance to displacement; and record the results to a csv file that you can open in Microsoft Excel, python, or any other data analysis system.
The script isn’t the simplest is could possibly be, it includes some extra components that we consider good practice, but it still fits in a single file of some 190 lines. An even simpler example of controlling an RP100 is given in the RP100 manual, but that doesn’t include the LCR meter or saving data to a file.
The simple example in the manual uses Python’s serial module. This is nice and simple, but only works for serial or virtual serial instruments. The LCR meter is a TMC device (Test and Measurement Class). Python’s direct support for TMC is weak, so I’m using VISA to bridge the gap. This has the added advantage that visa makes all connections look the same, so they can be written to or read from using the same commands. This does mean that you have to install a visa implementation in order to use this script.
Throughout this supporting documentation I refer to sections of code by comment numbers e.g ‘# 02’. These are in numerical order and you can search for them (include the # in the search box) to find the section of code I’m referring to. As long as you don’t delete the comments this will work even if you’ve substantially edited the file and line numbers have changed.
Download
You can download the script from the Razorbill Instruments webpage here. Unzip it after downloading.
The script doesn’t have to be installed, you just copy it onto your machine and run it. It does have some requirements though, you will need to install Python and Visa as a minimum
Python
You can download and install python from it’s own website, but you will probably find it easier if you install some kind of development environment. Development environments provide lots of tools to help you develop your code, including colour-coding it, checking it for errors as you type, auto-completing lines; and allowing you to quickly run snippets from your code to test it.
At Razorbill Instruments we use Spyder, but it’s a bit awkward to install extra modules in it. There are lots of other options available. It often makes sense to use the same system as your colleagues.
Python comes with lots of built in functionality, but one key part for communicating with instruments via the VISA system is missing. This functionality is provided by a module called pyvisa, which you will need to install separately. How to install it depends on what development environment you have chosen (e.g. if you decided to use the Anaconda distribution you can install it with conda, or if you just installed basic python, use pip)
Visa
Pyvisa allows python to talk to VISA but doesn’t include VISA itself. There are multiple different implementations of VISA available from different Instrument manufacturers, but you don’t have to use the one that goes with the instruments you are using. Instrument manufacturers know that users will want to use instruments from several brands, so they design VISA systems to work interchangeably.
Razorbill Instruments’ Technical director Jack has written a blog about what Visa is and how it works and has a few suggestions that are free to use. You can read it here.
This example was developed using Nationals Instruments’ VISA implementation, but it shouldn’t matter.
Instrument addresses
The script needs to know where to find the physical hardware. This takes the form of VISA addresses. The easiest way to find both instruments is probably to ask visa for a list of addresses with the following commands:import pyvisa resource_manager = pyvisa.ResourceManager() resource_manager.list_resources()This should produce a list of attached resources, which might look like this:
('USB0::0x2A8D::0x2F01::MY54408716::INSTR', 'ASRL1::INSTR', 'ASRL4::INSTR', 'ASRL5::INSTR',)Different types of instruments have different formats. The LCR meter is a TMC device, so will have an address in the format of the first one in this example. TMC device addresses are unique to the instrument so it shouldn’t change unless you replace the LCR meter. The RP100 will appear as a serial instrument like the later 3 in this example. If you have several results try unplugging one and run the rm.list_resources() command again to see which instrument disappears. The names of serial instruments may change, especially if unplugged and plugged back into a different port, in which case you may need to repeat this process. You can copy and paste the address into the script, they should include the inverted commas at the start and end.
Entering calibration data
ALPHA, D0, and CP are taken from the cell calibration sheet. The units are the same as used on the calibration sheet, there is no need to convert to SI units. If you have an older cell the calibration equation could have a different form, in which case you can alter the equation in section # 09, or ask us to re-issue the calibration sheet with the current form a, b, c, d, e are the constants from the polynomial fit on the temperature calibration. If your cell doesn’t have a temperature calibration or there isn’t a polynomial fit, you can ask Razorbill instruments if we have this information, or set all 5 constants to 0.Choosing voltages
The voltages are entered in the following format:voltages = numpy.array([[0, 0], [10, -10], [20, -20], [10, -10], [0, 0]])Each row corresponds to a voltage set point, with the first value as the voltage for channel 1 and the second one for channel 2. The layout in a vertical column isn’t important, but the brackets and commas are. You can add more rows as needed. The relationship between the voltage and resulting displacement isn’t straightforward as it depends on sample stiffness, temperature, and device variant. In most cases you will need to do an experiment with a small voltage to get a feel for system behaviour before choosing voltages for the main experiment. It is possible to write a closed loop script so that you can specify a displacement and the script will adjust the voltage to achieve that displacement, but closed loop control is beyond the scope of this script. Once you’ve entered the data needed in section # 02, you can run the script and it should do an experiment and save the data.
# 01 Imports
Python imports always go at the top of the file. These are modules we need to run the script. All except pyvisa are built into python so don’t need to be installed separately. For information on installing pyvisa, see the install instructions above.- pyvisa is a module for working with Visa. Visa handles communication with instruments
- time is used to generate timestamps for the lines in the data file
- csv is a helper for writing data to the file
- tkinter draws the pop-up dialogs for temperature and filename
- filedialog for some python modules you can import the module and access all it’s submodules as module.submodule. Because of the way the tkinter module is structured, it’s submodules have to be explicitly imported.
- Numpy extends python’s maths capabilities. It’s used to handle a 2D array of voltages.
# 02 Data entry
This is where you type in information needed for the experiment. For where to get the information, see the “how to do an experiment” section above.# 03 Getting temperature from the user
I use tkinter to pop up a box for the customer to enter the temperature of the strain cell. You could have just typed the temperature in like the calibration data, but I used a pop up box because I want to force the user to keep the temperature data up to date. Out of date temperature information could result in voltages outside the permitted range being applied to the cell, which could damage it. Ideally you might replace this section with code that gets the temperature from your temperature controller. Before we can pop up a box we have to start tkinter by creating a root window, at which point it draws a blank window. I “withdraw” (hide) the blank window just to keep things tidy.# 04 enforce the voltage limits
It’s good to check the voltages early in the script and stop if they are not permitted. At this point no files have been created or opened and neither have connections been made to the instruments. If the script exits at this stage, there is no mess to clear up and no time has been wasted. The initial if/else statements reproduce the voltage limit curves in the strain cell manuals. They are the same for all our current products. If the voltages are outside the permitted range we raise an error, which stops the script and you should see the associated message so that you know you need to change the voltages in section 2.# 05 Open connections to instruments
I’m using Pyvisa to communicate with the instruments. Before I can use it, I need to create a resource manager. I can then use the manager to open the resources. The open resources correspond to connections to an instrument, and I can read or write messages to them.# 06 set up the instruments
Most instruments can be configured through their front panels or start up in a helpful state. It’s a good idea to pull out important settings here and make sure they are as you need them, particularly if you regularly change them or the instrument’s start-up defaults are unsuitable. For the RP100, we need to turn on the outputs before we can use the supply. If the outputs are already on, these output commands will have no effect. I’ve also chosen to set the slew rates. 100 is the power-on default, but it’s important it’s set correctly because later in the code I use time delays to allow the power supply to slew to the correct voltage. If the slew rate is too low, we would end up recording data before the power supply reached the intended voltage. There is a full list of possible commands in the back of the RP100 manual. For the LCR meter, I’m writing our recommended measurement settings, which result in significantly better performance than the power-on defaults when measuring pF capacitances.# 07 Pop up a box to select the file name. Add '.csv' if necessary.
The tkinter pop up box is a very convenient single line of code. I’m reusing the root window from section 4. Adding .csv to the file name if it’s not already there is just for convenience, it means the file will open in Excel and other analysis systems. The rest of this script would work fine without it, or with a different file extension.# 08 open the file and write the column headings
The pop up only gets a file name. The open command opens it. The writer is a helper from the csv module, it handles converting numbers to text and formatting the file.# 09 Data acquisition functions
This is the only place where I’m using functions. I feel the main loop in section 11 would be too complicated otherwise. This also sets up the overall structure of the script to accept more complicated data acquisition functions in future. In get_capacitance and get_exvolt I send a message to the LCR meter asking for a measurement. You can look up the message structure in the user manual. The excitation voltage will be returned as a sting containing only a number, so I can just convert it to a floating point number. I could just leave it as a string and it would get written to the file just fine, but converting to a number makes sure it is written in the same format as other numbers. The capacitance query is more complicated. The LCR meter replies with three numbers, separated by commas, so we unpack them and convert to floats as before. The last number (the instrument status) is discarded. In python you can return two (or more) variables at the same time, without explicitly packing them into a list or other container. I’m using that trick here. get_displacement doesn’t speak to an instrument; it just uses the capacitance from the previous call to get_capacitance. In a way this makes recording both capacitance and displacement redundant, but I consider it good practice to record both. Displacement because it is convenient for a human observer, capacitance because this is the original data and we might need to go back to it if the displacement calculation gets mangled – by a typo in the calibration data for example.# 10 record the start time
We need the start time to calculate the time elapsed for each row in the data file# 11 perform the experiment
With all the preparatory work done, and the data acquisition provided as separate functions, the actual experiment is fairly short. It uses for loops to work through the list of voltages, gets the data and writes it to the file. The experiment is structured to take several measurements at each voltage. This is good practice because a) it gives you a feel for the measurement noise and drift; b) it allows you to spot and ignore single bad measurements, as might occur if a cable is knocked or a nearby motor starts; c) you can improve noise by taking an average of many data points.# 12 shut down the instruments
The script should leave the hardware in a sensible final state, so I’ve set the power supply back to zero and switched off the output relays. This should prevent any surprises if the cell gets plugged or unplugged# 13 close connections and end programs
I also want to tidy up on the software side. Closing the connections to the instruments makes them available to other software, such as our RP100 tool. Having more than one resource manager at once can cause issues, so it’s best to close that too. We have to close the file or you might not be able to move, rename, or open it elsewhere. Everything in this section would happen naturally if/when your python interpreter was closed – but that’s not true for section 12.MUX_ADDR = 'ASRL6::INSTR'Like the RP100 the MP240 is a virtual serial instrument, so the address will have the format above but you need to find the actual address using the same steps as for the RP100 in “how to do an experiment”. Next, we need to open the connection to the instrument. To keep everything tidy, we do this in section # 05. We need to add:
mux = resource_manager.open_resource(MUX_ADDR)And when we open a connection, we should think about when to close it. So add the following to section # 12.
mux.close()Now we need to think about how - and when - to switch the multiplexer. The simplest option is to switch it in the loop at section # 11, which doesn’t require us to make changes to the functions that get capacitance and excitation voltage. The necessary command is
>mux.write('SELECT 1')The message needed can be looked up from the back of the MP240 manual. We want to set one channel then the other, and get the capacitance and excitation voltage for each. So in total we replace
cap, d = get_capacitance() exvolt = get_exvolt()with
mux.write('SELECT 1') time.sleep(0.5) cap1, d1 = get_capacitance() exvolt1 = get_exvolt() mux.write('SELECT 2') time.sleep(0.5) cap2, d2 = get_capacitance(2) exvolt1 = get_exvolt()The time.sleep(0.5) command allows the LCR to take a measurement on the new capacitor – otherwise we might get a stale measurement that was taken before the multiplexer switched. Now we have all the measurements we wanted, but there’s a problem. If we pass these capacitances to the get_displacement function, they will both be subject to the same calibration. So we need to modify get_displacement to handle different calibration constants. The first step is to bundle the calibration constants together so that we can pass them around easily. I’m going to use a ‘dictionary’ to do this. We will have two dictionaries, one for each capacitor. In section # 02 we replace
ALPHA = 53.12 D0 = 52.94 CP = 0.0750 a = -2.22 b = -0.00102 c = 8.64e-5 d = -4.86e-7 e = 6.78e-10With
cal_Fcap = {'ALPHA' : 53.12, 'D0' : 52.94, 'CP' : 0.0750, 'a' : -2.22, 'b' : -0.00102, 'c' : 8.64e-5, 'd' : -4.86e-7, 'e' : 6.78e-10} cal_Dcap = {'ALPHA' : 53.12, 'D0' : 52.94, 'CP' : 0.0750, 'a' : -2.22, 'b' : -0.00102, 'c' : 8.64e-5, 'd' : -4.86e-7, 'e' : 6.78e-10}But don’t forget to change the numbers to match your calibration sheets! The next step is to update get_displacement in section # 09 to use this new format. We will call get_displacement with the required dictionary as an argument, so add cal_data between the brackets to get
def get_displacement(cap, t, cal_data):next, update the calibration function to use the new data format:
disp = cal_data['ALPHA'] / (cap-cal_data['CP']) – cal_data['D0']It’s the same maths as before, it just pulls the calibration data from the dictionary. Likewise, the temperature correction becomes:
t_offset = cal_data['a'] + cal_data['b'] * t + cal_data['c'] * t**2 + cal_data['d'] * t**3 + cal_data['e'] * t**4Now that we’ve updated the get_dispalcement function, we update how we use it. In section # 11, we replace
disp = get_displacement(cap)with
disp = get_displacement(cap1, cal_Dcap) force = get_displacement(cap2, cal_Fcap)Of course, make sure you have the right multiplexer channel (1 or 2) with the right capacitor (D or F). In section # 11 we now have all the data we need, so we just need to write to the file. We add the new data to the writer to get
writer.writerow([time_elapsed, row[0], row[1], cap1, d1, exvolt1, disp, cap2, d2, exvolt2, force])finally, we need to update the column heading on the file to match our new data, so adjust the writer in section # 08 accordingly:
writer.writerow(['Time Elapsed (S)', 'PZ drive voltage CH1 (V)', 'PZ drive voltage CH2 (V)', 'Capacitance 1 (pF)', 'Loss 1 (D)', 'LCR excitation voltage 1 (V)', 'Displacement (um)', 'Capacitance 2 (pF)', 'Loss 2 (D)', 'LCR excitation voltage 2 (V)', 'Force (N)'])And that should be the changes complete.
LOCKIN_ADDR = 'ASRL7::INSTR'Next, we need to open the connection to the instrument. To keep everything tidy, we do this in section # 05. We need to add:
lockin = resource_manager.open_resource(LOCKIN_ADDR)Unlike the RP100, the Stanford 830 is a real serial instrument, so you might need to configure the serial link with the correct baud rate, parity etc. This isn’t required for the RP100 because it’s virtual serial. You can change the settings on the instrument itself (setup key on a Stanford 830) to match the pyvisa defaults, or you can add lines in the script to match the instrument settings. For example you might use:
lockin.baud_rate = 19200to set the baud rate to 19200 instead of the default 9600. When we open a connection, we should think about when to close it. So add the following to section # 12.
lockin.close()If you want to write any settings to the instrument, add them under section # 06. For now, I’ll assume you have set the instrument up the way you want it using the instrument front panel. One line you probably will need is
lockin.write('OUTX 0')Which tells the instrument to direct all communication to the serial port, rather than GPIB. The next step is to get the measurements from the instrument. To keep the structure of the code tidy, we add a function to do this in section # 09. In python it’s good practice to include a description of what the function does (known as a docstring), so you might add:
Def get_xy(): """Get a measurement from the Stanford 830 in X-Y format""" reply = lockin.query('SNAP? 1,2') chunks = reply.split(',') x = float(chunks[0]) y = float(chunks[1]) Return x,yI’ve used the SNAP? command rather than OUTP? To make sure the X and Y measurements are simultaneous. This causes the instrument to respond with X and Y values in a single message, separated by a comma. I break the message into chunks before I convert them from text to a number with the float function. The next step is to incorporate this function into the loop that does the actual experiment in section # 11. Before the writer line, we can add
x,y = get_xy()And we also want to write our new data to the file, so add x and y to the writer line, it becomes:
writer.writerow([time_elapsed, row[0], row[1], cap, d, exvolt, disp, x, y])finally, don’t forget to update the column headings in section # 08. The writer line here becomes:
writer.writerow(['Time Elapsed (S)', 'PZ drive voltage CH1 (V)', 'PZ drive voltage CH2 (V)', 'Capacitance (pF)', 'Loss (D)', 'LCR excitation voltage (V)', 'Displacement (um)’, ‘In Phase (V)’, ‘Quadrature (V)'])