Instrumental¶
Instrumental is a Python-based library for controlling lab hardware like cameras, DAQs, oscilloscopes, spectrometers, and more. It has high-level drivers for instruments from NI, Tektronix, Thorlabs, PCO, Photometrics, Burleigh, and others.
Note
As of version 0.7, Instrumental has dropped support for Python 2 and now requires Python 3.7+.
Instrumental’s goal is to make common tasks simple to perform, while still providing the flexibility to perform complex tasks with relative ease. It also makes it easy to mess around with instruments in the shell. For example, to list the available instruments and open one of them:
>>> from instrumental import instrument, list_instruments
>>> paramsets = list_instruments()
>>> paramsets
[<ParamSet[TSI_Camera] serial='05478' number=0>,
<ParamSet[K10CR1] serial='55000247'>
<ParamSet[NIDAQ] model='USB-6221 (BNC)' name='Dev1'>]
>>> daq = instrument(paramsets[2])
>>> daq.ai0.read()
<Quantity(5.04241962841, 'volt')>
If you’re going to be using an instrument repeatedly, save it for later:
>>> daq.save_instrument('myDAQ')
Then you can simply open it by name:
>>> daq = instrument('myDAQ')
You can even access and control instruments on remote machines. Check out Working with Instruments for more detailed info.
Instrumental also bundles in some additional support code, including:
- Plotting and curve fitting utilities
- Utilities for acquiring and organizing data
Instrumental makes use of NumPy, SciPy, Matplotlib, and Pint, a Python units library. It optionally uses PyVISA/VISA and other drivers for interfacing with lab equipment.
To download Instrumental or browse its source, see our GitHub page.
You can cite Instrumental through Zenodo (DOI: 10.5281/zenodo.2556398).
Note
Instrumental is currently still under heavy development, so its interfaces are subject to change. Contributions are greatly appreciated, see Writing Drivers and Developer’s Guide for more info.
User Guide¶
Installation¶
Brief Install Instructions¶
Starting with version 0.2.1, you can install Instrumental using pip:
$ pip install instrumental-lib
This will install the latest release version along with the core dependencies if they aren’t already installed. It’s recommended that you use the the Anaconda distribution so you don’t have to compile numpy and scipy (see the detailed install instructions below).
Installing the Development Version from GitHub¶
Download and extract a zip of Instrumental from the Github page or clone it using git. Now install:
$ cd /path/to/Instrumental
$ python setup.py install
Detailed Install Instructions¶
Instrumental should install any core dependencies it requires, but if you’re having problems, you may want to read this section over. Note that many per-driver dependencies are not installed automatically, so you can install them as-needed.
Python Sci-Comp Stack¶
To install the standard scientific computing stack, we recommend using Anaconda. Download the appropriate installer from the download page and run it to install Anaconda. The default installation will include NumPy, SciPy, and Matplotlib as well as lots of other useful stuff.
Pint¶
Next, install Pint for units support:
$ pip install pint
For more information, or to get a more recent version, check out the Pint install page.
Instrumental¶
If you’re using git, you can clone the Instrumental repository to get the source code. If you don’t know git or don’t want to set up a local repo yet, you can just download a zip file by clicking the ‘Download ZIP’ button on the right hand side of the Instrumental Github page. Unzip the code wherever you’d like, then open a command prompt to that directory and run:
$ python setup.py install
to install Instrumental to your Python site-packages directory. You’re all set! Now go check out
some of the examples in the examples
directory contained in the files you downloaded!
Optional Driver Libraries¶
Package Overview¶
Drivers¶
The drivers
subpackage is the primary focus of Instrumental, and its purpose is to provide relatively high-level ‘drivers’ for interfacing with lab equipment. Currently it supports:
Drivers¶
Instrumental drivers allow you to control and read data from various hardware devices.
Some devices (e.g. Thorlabs cameras) have drivers that act as wrappers to their drivers’ C
bindings, using ctypes
or cffi
. Others (e.g. Tektronix scopes and AFGs) utilize VISA and
PyVISA
, its Python wrapper. PyVISA
requires a local installation of the VISA library (e.g.
NI-VISA) to interface with connected devices.
Cameras¶
Create Camera
objects using instrument()
.
This module is for controlling PCO cameras that use the PCO.camera SDK. Note that not all PCO cameras use this SDK, e.g. older Pixelfly cameras have their own SDK.
This module requires the PCO SDK and the cffi
package.
You should install the PCO SDK provided on PCO’s website. Specifically, this module requires
SC2_Cam.dll
to be available in your PATH, as well as any interface-specific DLLs. Firewire
requires SC2_1394.dll
, and each type of Camera Link grabber requires its own DLL, e.g.
sc2_cl_me4.dll
for a Silicon Software microEnable IV grabber card.
This module is for controlling PCO Pixelfly cameras.
This module requires the Pixelfly SDK and the cffi
package.
You should install the Pixelfly SDK provided on PCO’s website. Specifically, this module requires
pf_cam.dll
to be available in your PATH.
This module is for controlling Thorlabs cameras that use the TSI SDK. Note that Thorlabs DCx cameras use a separate SDK.
This module requires the TSI SDK and the NiceLib
package.
- Install the uc480 API provided by Thorlabs.
- Add the DLL to your PATH environment variable.
- Run
pip install pywin32 nicelib
. - Call
list_instruments()
, which will auto-build the API bindings.
- Download and install ThorCam from the Thorlabs website, which comes with the uc480 API libraries. (Since these cameras are rebranded IDS cameras, you may instead install the IDS uEye software)
- Make sure the path to the shared library (
uc480.dll
,uc480_64.dll
,ueye_api.dll
, orueye_api_64.dll
) is added to your PATH. The library will usually be located in the Thorlabs or IDS folder inside your Program Files folder. On my system they are located withinC:\Program Files\Thorlabs\Scientific Imaging\DCx Camera Support\Develop\Lib
. - Run
pip install pywin32 nicelib
on the command line to install thepywin32
andnicelib
packages. - Use
list_instruments()
to see if your camera shows up. This will automatically build the bindings to the DLL. If this doesn’t work (and your camera is plugged in an works with the ThorCam software), try to import the driver module directly:from instrumental.drivers.cameras import uc480
. If this fails, the error should give you information about what went wrong. Be sure to check out the FAQs page for more information, and you can use the mailing list or GitHub if you need additional help.
- Added
gain_boost
,master_gain
,gamma
,blacklevel
, and manyauto-x
Facets/properties - Made sure framerate is set before exposure time
- Fixed AOI-related error on calling
start_live_video()
- Converted to use new-style Instrument initialization
- Added error code to UC480 errors
- Converted to use new-style Params
- Removed deprecated usage of ‘is_SetImageSize’
- Ported driver to use
NiceLib
instead ofctypes
- Added subsampling support
- Added gain setting
- Added triggering support
- Added support for using IDS library
- Initial driver release
This module requires the Picam SDK and the NiceLib
package. Tested to work on Windows and Linux.
On Linux, you must set the GENICAM_ROOT_V2_4
environment variable to the path to genicam (probably /opt/pleora/ebus_sdk/x86_64/lib/genicam
) and ensure that Picam’s lockfile directory exists (the Picam SDK installer isn’t good about doing this).
On Windows, the DLLs Picam.dll
, Picc.dll
, Pida.dll
, and Pidi.dll
must be copied to a
directory on the system path. Note that the DLLs found first on the system path must match the
version of the headers installed with the Picam SDK.
In addition to the documented methods, instances of PicamCamera
have a params
attribute which contains the camera’s Picam parameters. Each parameter implements get_value()
, set_value()
, can_set()
, and get_default()
methods that call the underlying Picam SDK functions. For example,
>>> cam.params.ShutterTimingMode.get_value() # => gives ShutterTimingMode.AlwaysOpen
>>> cam.params.ShutterTimingMode.set_value(PicamEnums.ShutterTimingMode.AlwaysClosed)
>>> cam.params.ShutterTimingMode.get_value() # verify the change
These data types are returned by the API and are not meant to be created directly by users. They provide a wrapped interface to Picam’s data types and automatically handle memory cleanup.
All Picam Parameters accessible through PicamCamera.params
are instances of one of these classes.
The NicePicamLib
class provides a more direct wrapping of the Picam SDK’s C interface—what the NiceLib package calls a “Mid-level” interface. See the NiceLib documentation for more information on how to use this kind of interface.
DAQs¶
Create DAQ
objects using instrument()
.
This module has been developed using an NI USB-6221 – the code should generally work for all DAQmx boards, but I’m sure there are plenty of compatibility bugs just waiting for you wonderful users to find and fix.
First, make sure you have NI’s DAQmx software installed. Instrumental will then use NiceLib to generate bindings from the header it finds.
The NIDAQ
class lets you interact with your board and all its various inputs
and outputs in a fairly simple way. Let’s say you’ve hooked up digital I/O P1.0
to analog input AI0, and your analog out AO1 to analog input AI1:
>>> from instrumental.drivers.daq.ni import NIDAQ, list_instruments
>>> list_instruments()
[<NIDAQ 'Dev0'>]
>>> daq = NIDAQ('Dev0')
>>> daq.ai0.read()
<Quantity(0.0154385786803, 'volt)>
>>> daq.port1[0].write(True)
>>> daq.ai0.read()
<Quantity(5.04241962841, 'volt')>
>>> daq.ao1.write('2.1V')
>>> daq.ai1.read()
<Quantity(2.10033320744, 'volt')>
Now let’s try using digital input. Assume P1.1 is attached to P1.2:
>>> daq.port1[1].write(False)
>>> daq.port1[2].read()
False
>>> daq.port1[1].write(True)
>>> daq.port1[2].read()
True
Let’s read and write more than one bit at a time. To write to multiple lines
simultaneously, pass an unsigned int to write()
. The line with the lowest
index corresponds to the lowest bit, and so on. If you read from multiple
lines, read() returns an int. Connect P1.0-3 to P1.4-7:
>>> daq.port1[0:3].write(5) # 0101 = decimal 5
>>> daq.port1[4:7].read() # Note that the last index IS included
5
>>> daq.port1[7:4].read() # This flips the ordering of the bits
10 # 1010 = decimal 10
>>> daq.port1[0].write(False) # Zero the ones bit individually
>>> daq.port1[4:7].read() # 0100 = decimal 4
4
You can also read and write arrays of buffered data. Use the same read()
and
write()
methods, just include your timing info (and pass in the data as an
array if writing). When writing, you must provide either freq
or fsamp
, and
may provide either duration
or reps
to specify for how long the waveform is
output. For example, there are many ways to output the same sinusoid:
>>> from instrumental import u
>>> from numpy import pi, sin, linspace
>>> data = sin( 2*pi * linspace(0, 1, 100, endpoint=False) )*5*u.V + 5*u.V
>>> daq.ao0.write(data, duration='1s', freq='500Hz')
>>> daq.ao0.write(data, duration='1s', fsamp='50kHz')
>>> daq.ao0.write(data, reps=500, freq='500Hz')
>>> daq.ao0.write(data, reps=500, fsamp='50kHz')
Note the use of endpoint=False
in linspace
. This ensures we don’t
repeat the start/end point (0V) of our sine waveform when outputting more than
one period.
All this stuff is great for simple tasks, but sometimes you may want to perform input and output on multiple channels simultaneously. To accomplish this we need to use Tasks.
Note
Tasks in the ni
module are similar, but not the same as Tasks in DAQmx
(and PyDAQmx). Our Tasks allow you to quickly and easily perform simultaneous
input and output with one Task without the hassle of having to create multiple
and hook their timing and triggers up.
Here’s an example of how to perform simultaneous input and output:
>>> from instrumental.drivers.daq.ni import NIDAQ, Task
>>> from instrumental import u
>>> from numpy import linspace
>>> daq = NIDAQ('Dev0')
>>> task = Task(daq.ao0, daq.ai0)
>>> task.set_timing(duration='1s', fsamp='10Hz')
>>> write_data = {'ao0': linspace(0, 9, 10) * u.V}
>>> task.run(write_data)
{u'ai0': <Quantity([ 1.00000094e+01 1.89578724e-04 9.99485542e-01 2.00007917e+00
3.00034866e+00 3.99964556e+00 4.99991698e+00 5.99954114e+00
6.99981625e+00 7.99976941e+00], 'volt')>,
u't': <Quantity([ 0. 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9], 'second')>}
As you can see, we create a dict as input to the run()
method. Its keys are
the names of the input channels, and its values are the corresponding array
Quantities that we want to write. Similarly, the run()
returns a dict that
contains the input that was read. This dict also contains the time data under
key ‘t’. Note that the read and write happen concurrently, so each voltage read
has not yet moved to its new setpoint.
There may be applications where data needs to be acquired continuously. This can be achieved by setting up a Task
object in ‘continuous’ mode.
Here is an example of how to perform continuous sampling:
from instrumental.drivers.daq.ni import NIDAQ, Task
daq = NIDAQ('Dev0') # Or whatever is shown from instrumental.list_instruments()
task = daq.Task(daq.ai0, daq.ai1)
task.set_timing(fsamp='100 Hz', n_samples=1000, mode='continuous')
task.start()
done = False
while not done:
data = task.read()
# do something with the data
# do something to eventually set done = True
task.stop()
Once set, data
will contain a dictionary. Its keys are the names of input channels, and values are the corresponding array Quantities. The dictionary also contains time data under key ‘t’. The length of each of the arrays in this dictionary will be between 0 and n_samples
elements. Therefore, you do not need to worry about syncronizing the timing of your read()
calls, as each read()
call will only return the data returned since the last call to read()
, or since the task started. To avoid unexpected behavior, ensure that your code calls task.read()
frequently enough so that the daq never completely fills the n_samples
-sized buffer.
Function Generators¶
Create FunctionGenerator
objects using instrument()
.
Lasers¶
Create Laser
objects using instrument()
.
Driver for Tobtica FemtoFiber Lasers.
The femtofiber drivers, which among other things make the usb connection appear as a serial port, must be installed (available from http://www.toptica.com/products/ultrafast_fiber_lasers/femtofiber_smart/femtosecond_erbium_fiber_laser_1560_nm_femtoferb)
-
class
instrumental.drivers.lasers.femto_ferb.
FemtoFiber
¶ A femtoFiber laser.
Lasers can only be accessed by their serial port address.
-
close
()¶ Closes the connection to the laser.
-
is_control_on
()¶ Returns the status of the hardware input control.
Hardware input control must be on in order for the laser to be controlled by usb connection.
Returns: message – If True, hardware input conrol is on. Return type: bool
-
is_on
()¶ Indicates if the laser is on (True) or off (False).
-
set_control
(control)¶ Sets the status of the hardware input control.
Hardware input control must be on in order for the laser to be controlled by usb connection.
Parameters: control (bool) – If True, hardware input conrol is turned on. Returns: error – Zero is returned if the hardware input control status was set correctly. Otherwise, the error string returned by the laser is returned. Return type: int or str
-
Motion Control¶
Create Motion
objects using instrument()
.
Driver for controlling Thorlabs Flipper Filters using the Kinesis SDK.
One must place Thorlabs.MotionControl.DeviceManager.dll and Thorlabs.MotionControl.FilterFlipper.dll in the path
-
exception
instrumental.drivers.motion.filter_flipper.
FilterFlipperError
¶
-
class
instrumental.drivers.motion.filter_flipper.
Filter_Flipper
¶ Driver for controlling Thorlabs Filter Flippers
The polling period, which is how often the device updates its status, is passed as a pint quantity with units of time and is optional argument, with a default of 200ms
-
close
()¶
-
flip
()¶ Flips the position of the filter.
-
get_position
()¶ Get the position of the flipper.
Returns an instance of Position. Note that this represents the position at the most recent polling event.
-
get_transit_time
()¶ Returns the transit time.
The transit time is the time to transition from one filter position to the next.
-
home
()¶ Homes the device
-
isValidPosition
(position)¶ Indicates if it is possible to move to the given position.
Parameters: position (instance of Position) –
-
move_and_wait
(position, delay='100ms')¶ Moves to the indicated position and waits until that position is reached.
Parameters: - position (instance of Position) – should not be ‘Position.moving’
- delay (pint quantity with units of time) – the period with which the position of the flipper is checked.
-
move_to
(position)¶ Moves the flipper to the indicated position.
Returns immediatley.
Parameters: position (instance of Position) – should not be ‘Position.moving’
-
set_transit_time
(transit_time='500ms')¶ Sets the transit time. The transit time is the time to transition from one filter position to the next.
Parameters: transit_time (pint quantity with units of time) –
-
-
class
instrumental.drivers.motion.filter_flipper.
Position
¶ The position of the flipper.
-
moving
= 0¶
-
one
= 1¶
-
two
= 2¶
-
-
instrumental.drivers.motion.filter_flipper.
list_instruments
()¶
Multimeters¶
Create Multimeter
objects using instrument()
.
Driver module for HP/Agilent 34401A multimeters.
-
exception
instrumental.drivers.multimeters.hp.
MultimeterError
¶
-
class
instrumental.drivers.multimeters.hp.
HPMultimeter
¶ -
clear
()¶
-
config_voltage_dc
(range=None, resolution=None)¶ Configure the multimeter to perform a DC voltage measurement.
Parameters: - range (Quantity, 'min', 'max', or 'def') – Expected value of the input signal
- resolution (Quantity, 'min', 'max', or 'def') – Resolution of the measurement, in measurement units
-
fetch
()¶
-
initiate
()¶
-
trigger
()¶ Emit the software trigger
To use this function, the device must be in the ‘wait-for-trigger’ state and its
trigger_source
must be set to ‘bus’.
-
npl_cycles
¶
-
trigger_source
¶
-
Power Meters¶
Create PowerMeter
objects using instrument()
.
Driver module for Newport power meters. Supports:
- 1830-C
For example, suppose a power meter is connected on port COM1. One can then connect and measure the power using the following sequence:
>>> from instrumental import instrument
>>> newport_power_meter = instrument(visa_address='COM1',
classname='Newport_1830_C',
module='powermeters.newport')
>>> newport_power_meter.power
<Quantity(3.003776, 'W')>
-
class
instrumental.drivers.powermeters.newport.
Newport_1830_C
¶ A Newport 1830-C power meter
-
attenuator_enabled
(**kwds)¶
-
close
()¶
-
disable_attenuator
(**kwds)¶
-
disable_auto_range
()¶ Disable auto-range
Leaves the signal range at its current position.
-
disable_hold
()¶ Disable hold mode
-
disable_zero
()¶ Disable the zero function
-
enable_attenuator
(**kwds)¶
-
enable_auto_range
()¶ Enable auto-range
-
enable_hold
(enable=True)¶ Enable hold mode
-
enable_zero
(enable=True)¶ Enable the zero function
When enabled, the next power reading is stored as a background value and is subtracted off of all subsequent power readings.
-
get_filter
()¶ Get the current setting for the averaging filter
Returns: the current averaging filter Return type: SLOW_FILTER, MEDIUM_FILTER, NO_FILTER
-
get_power
(**kwds)¶
-
get_range
(**kwds)¶
-
get_status_byte
(**kwds)¶
-
get_units
()¶ Get the units used for displaying power measurements
Returns: units – ‘watts’, ‘db’, ‘dbm’, or ‘rel’ Return type: str
-
get_valid_power
(max_attempts=10, polling_interval=<Quantity(0.1, 'second')>)¶ Returns a valid power reading
This convience function will try to measure a valid power up to a maximum of
max_attempts
times, pausing for timepolling_interval
between each attempt. If a power reading is taken when the power meter is over-range, saturated, or busy, the reading will be invalid. In practice, this function also seems to mitigate the fact that about 1 in 500 power readings mysteriously fails.Parameters: - max_attempts (integer) – maximum number of attempts to measure a valid power
- polling_interval (Quantity) – time to wait between measurement attemps, in units of time
Returns: power – Power in units of watts, regardless of the power meter’s current ‘units’ setting.
Return type: Quantity
-
get_wavelength
(**kwds)¶
-
hold_enabled
()¶ Whether hold mode is enabled
Returns: enabled – True if in hold mode, False if in run mode Return type: bool
-
is_measurement_valid
()¶ Whether the current measurement is valid
The measurement is considered invalid if the power meter is saturated, over-range or busy.
-
set_medium_filter
()¶ Set the averaging filter to medium mode
The medium filter uses a 4-measurement running average.
-
set_no_filter
()¶ Set the averaging filter to fast mode, i.e. no averaging
-
set_range
(**kwds)¶
-
set_slow_filter
()¶ Set the averaging filter to slow mode
The slow filter uses a 16-measurement running average.
-
set_units
(units)¶ Set the units for displaying power measurements
The different unit modes are watts, dB, dBm, and REL. Each displays the power in a different way.
‘watts’ displays absolute power in watts
‘dBm’ displays power in dBm (i.e. dBm = 10 * log(P / 1mW))
‘dB’ displays power in dB relative to the current reference power (i.e. dB = 10 * log(P / Pref). At power-up, the reference power is set to 1mW.
‘REL’ displays power relative to the current reference power (i.e. REL = P / Pref)
The current reference power can be set using
store_reference()
.Parameters: units ('watts', 'dBm', 'dB', or 'REL') – Case-insensitive str indicating which units mode to enter.
-
set_wavelength
(**kwds)¶
-
store_reference
()¶ Store the current power input as a reference
Sets the current power measurement as the reference power for future dB or relative measurements.
-
zero_enabled
()¶ Whether the zero function is enabled
-
MEDIUM_FILTER
= 2¶
-
NO_FILTER
= 3¶
-
SLOW_FILTER
= 1¶
-
attenuator
¶ Whether the attenuator is enabled
-
local_lockout
¶ Whether local-lockout is enabled
-
power
¶ Get the current power measurement
Returns: power – Power in units of watts, regardless of the power meter’s current ‘units’ setting. Return type: Quantity
-
range
¶ The current input range, [1-8], where 1 is lowest signal.
-
status_byte
¶
-
wavelength
¶
-
-
instrumental.drivers.powermeters.newport.
MyFacet
(msg, readonly=False, **kwds)¶ Like SCPI_Facet, but without a space before the set-value
Spectrometers¶
Create Spectrometer
objects using instrument()
.
Wavemeters¶
Create Wavemeter
objects using instrument()
.
Driver module for Burleigh wavemeters. Supports:
- WA-1000/1500
-
class
instrumental.drivers.wavemeters.burleigh.
WA_1000
¶ A Burleigh WA-1000/1500 wavemeter
-
averaging_enabled
()¶ Whether averaging mode is enabled
-
disable_averaging
()¶ Disable averaging mode
-
enable_averaging
(enable=True)¶ Enable averaging mode
-
get_deviation
()¶ Get the current deviation
Returns: deviation – The wavelength difference between the current input wavelength and the fixed setpoint. Return type: Quantity
-
get_num_averaged
()¶ Get the number of samples used in averaging mode
-
get_pressure
()¶ Get the barometric pressure inside the wavemeter
Returns: pressure – The barometric pressure inside the wavemeter Return type: Quantity
-
get_setpoint
()¶ Get the wavelength setpoint
Returns: setpoint – the wavelength setpoint Return type: Quantity
-
get_temperature
()¶ Get the temperature inside the wavemeter
Returns: temperature – The temperature inside the wavemeter Return type: Quantity
-
get_wavelength
()¶ Get the wavelength
Returns: wavelength – The current input wavelength measurement Return type: Quantity
-
is_locked
()¶ Whether the front panel is locked or not
-
lock
(lock=True)¶ Lock the front panel of the wavemeter, preventing manual input
When locked, the wavemeter can only be controlled remotely by a computer. To unlock, use
unlock()
or hit the ‘Remote’ button on the wavemeter’s front panel.
-
set_num_averaged
(num)¶ Set the number of samples used in averaging mode
When averaging mode is enabled, the wavemeter calculates a running average of the last
num
samples.Parameters: num (int) – Number of samples to average. Must be between 2 and 50.
-
set_setpoint
(setpoint)¶ Set the wavelength setpoint
The setpoint is a fixed wavelength used to compute the deviation. It is used for display and to determine the analog output voltage.
Parameters: setpoint (Quantity) – Wavelength of the setpoint, in units of [length]
-
unlock
()¶ Unlock the front panel of the wavemeter, allowing manual input
-
Functions¶
-
class
instrumental.drivers.
Instrument
¶ Base class for all instruments.
-
observe
(name, callback)¶ Add a callback to observe changes in a facet’s value
The callback should be a callable accepting a
ChangeEvent
as its only argument. ThisChangeEvent
is a namedtuple withname
,old
, andnew
fields.name
is the facet’s name,old
is the old value, andnew
is the new value.
-
save_instrument
(name, force=False)¶ Save an entry for this instrument in the config file.
Parameters: - name (str) – The name to give the instrument, e.g. ‘myCam’
- force (bool, optional) – Force overwrite of the old entry for instrument
name
. By default, Instrumental will raise an exception if you try to write to a name that’s already taken. Ifforce
is True, the old entry will be commented out (with a warning given) and a new entry will be written.
-
-
instrumental.drivers.
instrument
(inst=None, **kwargs)¶ Create any Instrumental instrument object from an alias, parameters, or an existing instrument.
- reopen_policy : str of (‘strict’, ‘reuse’, ‘new’), optional
- How to handle the reopening of an existing instrument. ‘strict’ - disallow reopening an instrument with an existing instance which hasn’t been cleaned up yet. ‘reuse’ - if an instrument is being reopened, return the existing instance ‘new’ - always create a new instance of the instrument class. Not recommended unless you know exactly what you’re doing. The instrument objects are not synchronized with one another. By default, follows the ‘reuse’ policy.
>>> inst1 = instrument('MYAFG') >>> inst2 = instrument(visa_address='TCPIP::192.168.1.34::INSTR') >>> inst3 = instrument({'visa_address': 'TCPIP:192.168.1.35::INSTR'}) >>> inst4 = instrument(inst1)
-
instrumental.drivers.
list_instruments
(server=None, module=None, blacklist=None)¶ Returns a list of info about available instruments.
May take a few seconds because it must poll hardware devices.
It actually returns a list of specialized dict objects that contain parameters needed to create an instance of the given instrument. You can then get the actual instrument by passing the dict to
instrument()
.>>> inst_list = get_instruments() >>> print(inst_list) [<NIDAQ 'Dev1'>, <TEKTRONIX 'TDS 3032'>, <TEKTRONIX 'AFG3021B'>] >>> inst = instrument(inst_list[0])
Parameters: - server (str, optional) – The remote Instrumental server to query. It can be an alias from your instrumental.conf
file, or a str of the form
(hostname|ip-address)[:port]
, e.g. ‘192.168.1.10:12345’. Is None by default, meaning search on the local machine. - blacklist (list or str, optional) – A str or list of strs indicating driver modules which should not be queried for instruments.
Strings should be in the format
'subpackage.module'
, e.g.'cameras.pco'
. This is useful for very slow-loading drivers whose instruments no longer need to be listed (but may still be in use otherwise). This can be set permanently in yourinstrumental.conf
. - module (str, optional) – A str to filter what driver modules are checked. A driver module gets checked only if it
contains the substring
module
in its full name. The full name includes both the driver group and the module, e.g.'cameras.pco'
.
- server (str, optional) – The remote Instrumental server to query. It can be an alias from your instrumental.conf
file, or a str of the form
-
instrumental.drivers.
list_visa_instruments
()¶ Returns a list of info about available VISA instruments.
May take a few seconds because it must poll the network.
It actually returns a list of specialized dict objects that contain parameters needed to create an instance of the given instrument. You can then get the actual instrument by passing the dict to
instrument()
.>>> inst_list = get_visa_instruments() >>> print(inst_list) [<TEKTRONIX 'TDS 3032'>, <TEKTRONIX 'AFG3021B'>] >>> inst = instrument(inst_list[0])
>>> from instrumental import instrument
>>> scope = instrument('my_scope_alias')
>>> x, y = scope.get_data()
It should be pretty easy to write drivers for other VISA-compatible devices by using VisaMixin
and Facets
. Check out Writing Drivers for more info. Driver submissions are greatly appreciated!
Other Subpackages¶
There are several other subpackages within Instrumental. These may eventually be moved to their own separate packages.
Plotting¶
The plotting
module provides or aims to provide
- Unit-aware plotting functions as a drop-in replacement for pyplot
- Easy slider-plots
Fitting¶
The fitting
module is a good place for curating ‘standard’ fitting tools
for common cases like
- Triple-lorentzian cavity scans
- Ringdown traces (exponential decay)
It should also provide optional unit-awareness.
Tools¶
The tools
module is used for full-fledged scripts and programs that may
make use of all of the other modules above. A good example would be a script
that pulls a trace from the scope, auto-fits a ringdown curve, and saves both
the raw data and fit parameters to files in a well-organized directory
structure.
Working with Instruments¶
Getting Started¶
Instrumental tries to make it easy to find and open all the instruments
available to your computer. This is primarily accomplished using
list_instruments()
and instrument()
:
>>> from instrumental import instrument, list_instruments
>>> paramsets = list_instruments()
>>> paramsets
[<ParamSet[TSI_Camera] serial='05478' number=0>,
<ParamSet[K10CR1] serial='55000247'>
<ParamSet[NIDAQ] model='USB-6221 (BNC)' name='Dev1'>]
You can then use the output of list_instruments()
to open the instrument you
want:
>>> daq = instrument(paramsets[2])
>>> daq
<instrumental.drivers.daq.ni.NIDAQ at 0xb61...>
Or you can enter the parameters directly:
>>> instrument(ni_daq_name='Dev1')
<instrumental.drivers.daq.ni.NIDAQ at 0xb61...>
If you’re going to be using an instrument repeatedly, save it for later:
>>> daq.save_instrument('myDAQ')
Then you can simply open it by name:
>>> daq = instrument('myDAQ')
Using Units¶
pint
units are used heavily by Instrumental, so you should familiarize yourself with them. Many methods only accept unitful quantities, as a way to add clarity and prevent errors. In most cases you can use a string as shorthand and it will be converted automatically:
>>> daq.ao1.write('3.14 V')
If you need to create your own quantities directly, you can use the u
and Q_
objects provided by Instrumental:
>>> from instrumental import u, Q_
u
is a pint.UnitRegistry
, while Q_
is a shorhand for the registry’s Quantity
class. There are several ways you can use them:
>>> u.m # Access units as attributes
<Unit('meter')>
>>> 3 * u.s
<Quantity(3, 'second')>
>>> u('2.54 inches') # Parse a string into a quantity using u()
<Quantity(2.54, 'inch')>
>>> Q('852 nm') # ...or Q_()
<Quantity(852, 'nanometer')>
>>> Q(32.89, 'MHz') # Specify magnitude and units separately
<Quantity(32.89, 'megahertz')>
pint
also supports many physical constants (e.g. )
Note that it can be tricky to create offset units—e.g. by Q_('20 degC')
— because pint
treats this as a multiplication and will raise an OffsetUnitCalculusError
. You can get around this by separating the magnitude and units, e.g. Q_(20, 'degC')
. Note that Facets
as well as the check_units
and unit_mag
decorators can properly parse strings like '20 degC'
.
Advanced Usage¶
An Even Quicker Way¶
Here’s a shortcut for opening an instrument that means you don’t have to assign the instrument list to a variable, or even know how to count—just use part of the instrument’s string:
>>> list_instruments()
[<ParamSet[TSI_Camera] serial='05478' number=0>,
<ParamSet[K10CR1] serial='55000247'>
<ParamSet[NIDAQ] model='USB-6221 (BNC)' name='Dev1'>]
>>> instrument('TSI') # Opens the TSI_Camera
>>> instrument('NIDAQ') # Opens the NIDAQ
This will work as long as the string you use isn’t saved as an instrument alias. If you use a string that matches multiple instruments, it just picks the first in the list.
Filtering Results¶
If you’re only interested in a specific driver or category of instrument, you can use the module
argument to filter your results. This will also speed up the search for the instruments:
>>> list_instruments(module='cameras')
[<ParamSet[TSI_Camera] serial='05478' number=0>]
>>> list_instruments(module='cameras.tsi')
[<ParamSet[TSI_Camera] serial='05478' number=0>]
list_instruments()
checks if module
is a substring of each driver module’s name. Only modules whose names match are queried for available instruments.
Remote Instruments¶
You can even control instruments that are attached to a remote computer:
>>> list_instruments(server='192.168.1.10')
This lists only the instruments located on the remote machine, not any local ones.
The remote PC must be running as an Instrumental server (and its firewall configured to allow
inbound connections on this port). To do this, run the script tools/instr_server.py
that comes packaged
with Instrumental. The client needs to specify the server’s IP address (or hostname), and port
number (if differs from the default of 28265). Alternatively, you may save an alias for this server
in the [servers]
section of you instrumental.conf
file (see Saved Instruments for
more information about instrumental.conf
). Then you can list the remote instruments like this:
>>> list_instruments(server='myServer')
You can then open your instrument using instrument()
as usual, but now you’ll get a
RemoteInstrument
, which you can control just like a regular Instrument
.
How Does it All Work?¶
Listing Instruments¶
What exactly is list_instruments()
doing? Basically it walks through all the driver modules,
trying to import them one by one. If import fails (perhaps the DLL isn’t available because the user
doesn’t have this instrument), that module is skipped. Each module is responsible for returning a
list of its available instruments, e.g. the drivers.daqs.ni
module returns a list of all the NI
DAQs that are accessible. list_instruments()
combines all these instruments into one big list
and returns it.
There’s an unfortunate side-effect of this: if a module fails to import due to a bug, the exception is caught and ignored, so you don’t get a helpful traceback. To diagnose issues with a driver module, you can import the module directly:
>>> import instrumental.drivers.daq.ni
or enable logging before calling list_instruments()
:
>>> from instrumental.log import log_to_screen
>>> log_to_screen()
list_instruments()
doesn’t open instruments directly, but instead returns a list of dict-like ParamSet
objects that contain info about how to open each instrument. For example, for our DAQ:
>>> dict(paramsets[2])
{'classname': 'NIDAQ',
'model': 'USB-6221 (BNC)',
'module': 'daq.ni',
'name': 'Dev1',
'serial': 20229473L}
We could also open it with keyword arguments:
>>> instrument(name='Dev1')
<instrumental.drivers.daq.ni.NIDAQ at 0xb69...>
or a dictionary:
>>> instrument({'name': 'Dev1'})
<instrumental.drivers.daq.ni.NIDAQ at 0xb69...>
Behind the scenes, instrument()
uses the keywords to figure out what type of instrument you’re talking about, and what class should be instantiated. If you don’t give it much information to use, it may take awhile scanning through the available instruments. You can speed this up by providing the model and/or classname:
>>> instrument(module='daq.ni', classname='NIDAQ', name='Dev1')
<instrumental.drivers.daq.ni.NIDAQ at 0xb69...>
In addition, a convenient shorthand exists for specifying the module (or category of module) when you pass a parameter. For example:
>>> instrument(ni_daq_name='Dev1')
<instrumental.drivers.daq.ni.NIDAQ at 0xb69...>
only looks at instrument types in the daq.ni
module that have a name
parameter. These special parameter names support the format <module>_<category>_<parameter>
, <module>_<parameter>
, and <category>_<parameter>
. The parameter name is split by underscores, then used to filter which modules are checked. Note that each segment can be abbreviated, so e.g. cam_serial
will match all drivers in the cameras
category having a serial
parameter (this works because ‘cam’ is a substring of ‘cameras’).
Saved Instruments¶
Opening instruments using list_instruments()
is really helpful when you’re messing around in the
shell and don’t quite know what info you need yet, or you’re checking what devices are available to
you. But if you’ve found your device and want to write a script that reuses it constantly, it’s
convenient (and more efficient) to have it saved under an alias, which you can do easily with save_instrument()
as we showed
above.
When you do this, the instrument’s info gets saved in your instrumental.conf
config file. To find
where the file is located on your system, run:
>>> from instrumental.conf import user_conf_dir
>>> user_conf_dir
u'C:\\Users\\Lab\\AppData\\Local\\MabuchiLab\\Instrumental'
To save your instrument manually, you can add its parameters to the [instruments]
section of instrumental.conf
. For our DAQ, that would look like:
# NI-DAQ device
myDAQ = {'module': 'daq.ni', 'classname': 'NIDAQ', 'name': 'Dev1'}
This gives our DAQ the alias myDAQ
, which can then be used to open it easily:
>>> instrument('myDAQ')
<instrumental.drivers.daq.ni.NIDAQ at 0xb71...>
The default version of instrumental.conf
also provides some commented-out example entries to help make things clear.
Reopen Policy¶
By default, Instrumental will prevent you from double-opening an instrument:
>>> cam1 = instrument('myCamera')
>>> cam2 = instrument('myCamera') # results in an InstrumentExistsError
Usually it makes the most sense to simply reuse a previously-opened instrument rather than re-creating it. So, by default an exception is raised in if double-creation is attempted. However, this behavior is configurable via the reopen_policy
parameter upon instrument creation.
>>> cam1 = instrument('myCamera')
>>> cam2 = instrument('myCamera', reopen_policy='reuse')
>>> cam1 is cam2
True
>>> cam1 = instrument('myCamera')
>>> cam2 = instrument('myCamera', reopen_policy='new') # Might cause a driver error
>>> cam1 is cam2
False
The available policies are:
strict
- The default policy; raises an
InstrumentExistsError
if anInstrument
object already exists that matches the given paramset. reuse
- Returns the previously-created instrument object if it exists.
new
- Simply create the instrument as usual. Not recommended; only use this if you really know what you’re doing, as the instrument driver is unlikely to support two objects controlling a single device.
For these purposes, an instrument “already exists” if the given paramset matches that of any Instrument
which hasn’t yet been garbage collected. Thus, an instrument can be re-created even under the strict
policy as long as the old instrument has been fully deleted, either manually via del
or automatically by it going out of scope.
FAQs¶
My instrument isn’t showing up in list_instruments()
. What now?¶
If you’re using this particular driver for the first time, make sure you’ve followed the install directions fully. You should also check that the device works with any vendor-provided software (e.g. a camera viewer GUI). If the device still isn’t showing up, you should import the driver module directly to reveal any errors (see Why isn’t list_instruments() producing any errors?).
Why isn’t list_instruments()
producing any errors?¶
list_instruments()
is designed to check all Instrumental drivers that are available, importing each driver in turn. If a driver fails to import, this is often because you haven’t installed its requirements (because you’re not using it), so list_instruments()
simply ignores the error and moves on.
Where is the instrumental.conf
configuration file stored?¶
The location of instrumental.conf
is platform-dependent. To find where the file is located on
your system, run:
>>> from instrumental.conf import user_conf_dir
>>> user_conf_dir
u'C:\\Users\\Lab\\AppData\\Local\\MabuchiLab\\Instrumental'
Contributing¶
Contributions are highly encouraged, and can be as small or big as you like. Don’t worry if you think your code is incomplete or not “pretty” enough—an incomplete driver is more useful than no driver at all, and we can help you improve your code and integrate it within the framework of Instrumental.
Feedback is also much appreciated. Suspected bugs should be submitted as GitHub issues, and you can use the mailing list to get troubleshooting help, provide general feedback, or discuss feature requests and development.
Submitting a New Driver¶
If you’re considering submitting a driver—thank you! We suggest that you submit it on GitHub via a Pull Request[1] that contains only changes that relate to this driver. Sometimes we may ask you to make a few minor changes before the driver is merged in. After merging, we’ll create an issue on GitHub to discuss any additional changes that should be made before the driver is included in a release of Instrumental. This way we get code merged in quickly, while also ensuring high-quality releases.
You should check out Writing Drivers for more info related to driver development. There’s also the Developer’s Guide if you’re the kind of person who enjoys reading about guidelines, coding conventions, and project philosophies.
[1] | You can ask for help if you’re unfamiliar with git/GitHub and aren’t sure how to make a Pull Request. |
Writing Drivers¶
Examples¶
VISA Driver Example¶
In this guide, we’ll run through the process of making a VISA-based driver using the example of a Thorlabs PM100D power meter.
If you’re following along, it may be helpful to look at the PM100D’s commands, listed in the “SCPI Commands” section of its manual [PDF].
First, let’s open the device and play around with it in an ipython shell using pyvisa:
In [1]: import visa
In [2]: rm = visa.ResourceManager()
In [4]: rm.list_resources()
Out[4]:
(u'USB0::0x1313::0x8078::P0009084::INSTR',
u'USB0::0x0699::0x0362::C101689::INSTR',
u'ASRL1::INSTR',
u'ASRL3::INSTR')
Which resource is our power meter? Well, like all well-behaved SCPI instruments, the PM100D supports the *IDN?
command, which asks the device to identify itself. Let’s query the IDN of each of these resources:
In [5]: for addr in rm.list_resources():
...: try:
...: print(addr, '-->', rm.open_resource(addr).query('*IDN?').strip())
...: except visa.VisaIOError:
...: pass
...:
USB0::0x1313::0x8078::P0009084::INSTR --> Thorlabs,PM100D,P0009084,2.4.0
USB0::0x0699::0x0362::C101689::INSTR --> TEKTRONIX,TDS 1001B,C101689,CF:91.1CT FV:v22.11
We’ve used a try-except block here to catch errors from any devices that don’t support the *IDN?
command. We can now see which device is our power meter. Let’s open it and try some of the commands listed in the manual:
In [5]: rsrc = rm.open_resource('USB0::0x1313::0x8078::P0009084::INSTR',
read_termination='\n')
In [6]: rsrc.query('measure:power?')
Out[6]: '4.20021615E-06'
In [7]: rsrc.query('power:dc:unit?')
Out[7]: 'W'
In [8]: rsrc.query('sense:corr:wav?')
Out[8]: '8.520000E+02'
Here we’ve set the resource’s read termination to automatically strip off the newline at the end of each message, to make the output clearer. We can see that our power meter is measuring 4.2 microwatts of optical power and its operation wavelength is set to 852 nm. Let’s change the wavelength:
In [9]: rsrc.write('sense:corr:wav 532')
Out[9]: (20, <StatusCode.success: 0>)
In [10]: rsrc.query('sense:corr:wav?')
Out[10]: '5.320000E+02'
Now let’s get start writing our driver:
from instrumental.drivers.powermeters import PowerMeter
from instrumental.drivers import VisaMixin
class PM100D(PowerMeter, VisaMixin):
"""A Thorlabs PM100D series power meter"""
_INST_PARAMS_ = ['visa_address']
We inherit from PowerMeter
, a subclass of Instrument
, and use the _INST_PARAMS_
class attribute to declare what parameters our instrument needs. We also inherit from VisaMixin
, a mixin class which provides us some useful VISA-related features:
In [1]: from mydriver import *
In [2]: pm = PM100D(visa_address='USB0::0x1313::0x8078::P0009084::INSTR')
In [3]: pm.resource
Out[3]: <'USBInstrument'(u'USB0::0x1313::0x8078::P0009084::INSTR')>
In [4]: pm.query('*IDN?')
Out[4]: 'Thorlabs,PM100D,P0009084,2.4.0\n'
VisaMixin
allows us to construct an instance by providing only a visa_address
parameter, and provides our class with a resource
attribute as well as query
and write
convenience methods. Notice that the message termination isn’t being stripped. We can enable this by setting read_termination
inside the _initialize
method, which is called just after the instance is created:
from instrumental.drivers.powermeters import PowerMeter
from instrumental.drivers import VisaMixin
class PM100D(PowerMeter, VisaMixin):
"""A Thorlabs PM100D series power meter"""
def _initialize(self):
self.resource.read_termination = '\n'
Now let’s add a method to return the measured optical power:
from instrumental import Q_
[...]
class PM100D(PowerMeter, VisaMixin):
def power(self):
"""The measured optical power"""
self.write('power:dc:unit W')
power_W = float(self.query('measure:power?'))
return Q_(power_W, 'W')
This will sets the measurement units to watts, queries the power, and returns it as a unitful Quantity
. Let’s try it out:
In [3]: pm.power()
Out[3]: <Quantity(1.03476232e-05, 'watt')>
Now let’s add a way to get and set the wavelength, but let’s use the SCPI_Facet
convenience function, which allows us to concisely wrap well-behaving SCPI commands:
from instrumental.drivers import VisaMixin, SCPI_Facet
[...]
class PM100D(PowerMeter, VisaMixin):
[...]
wavelength = SCPI_Facet('sense:corr:wav', units='nm', type=float,
doc="Input signal wavelength")
wavelength
here is a Facet
, which is like a suped-up python property
. We’ve told SCPI_Facet
the command to use, and noted that it refers to a float with units of nanometers. Now we let’s see how our new wavelength attribute behaves:
In [4]: pm.wavelength
Out[4]: <Quantity(532.0, 'nanometer')>
In [5]: pm.wavelength = 1064
[...]
DimensionalityError: Cannot convert from 'dimensionless' (dimensionless) to 'nanometer' ([length])
What happened? The Facet ensures that we set the wavelength in units of length, to keep us from making unit conversion errors. We can use either a Quantity or a string that can be parsed by Q_()
In [6]: pm.wavelength = Q_(1064, 'nm')
In [7]: pm.wavelength
Out[7]: <Quantity(1064.0, 'nanometer')>
In [8]: pm.wavelength = Q_(0.633, 'um')
In [9]: pm.wavelength
Out[9]: <Quantity(633.0, 'nanometer')>
In [10]: pm.wavelength = '852 nm'
In [11]: pm.wavelength
Out[11]: <Quantity(852.0, 'nanometer')>
That’s better. Now that we have a basic driver, we need to make sure everything is cleaned up when we close our instrument. For most VISA-based instruments, this isn’t necessary, but the PM100D enters a special REMOTE mode, which locks out the front panel, when you start sending it commands. We use the control_ren()
method of pyvisa.resources.USBInstrument
to disable remote mode:
[...]
class PM100D(PowerMeter, VisaMixin):
[...]
def close(self):
self.resource.control_ren(False) # Disable remote mode
The close()
method can be called explicitly, and it is automatically called when the instrument is cleaned up or the interpreter exits. This way, the power meter will exit remote mode even if our program exits due to an exception.
@Facet(units='W', cached=False)
def power(self):
"""The measured optical power"""
self.write('power:dc:unit W')
return float(self.query('measure:power?'))
from instrumental.drivers.powermeters import PowerMeter
from instrumental.drivers import Facet, SCPI_Facet, VisaMixin, deprecated
class PM100D(PowerMeter, VisaMixin):
"""A Thorlabs PM100D series power meter"""
range = SCPI_Facet('power:dc:range', units='W', convert=float, readonly=True,
doc="The current input range's max power")
auto_range = SCPI_Facet('power:dc:range:auto', convert=int, value={False:0, True:1},
doc="Whether auto-ranging is enabled")
wavelength = SCPI_Facet('sense:corr:wav', units='nm', type=float,
doc="Input signal wavelength")
num_averaged = SCPI_Facet('sense:average:count', type=int,
doc="Number of samples to average")
def close(self):
self._rsrc.control_ren(False) # Disable remote mode
NiceLib Driver Example¶
In this guide, we’ll run through the process of writing a driver that uses NiceLib to wrap a C library, using the example of an NI DAQ. A NiceLib-based driver consists of three parts: low-level, mid-level, and high-level interfaces.
- Low-level
- Mimics the C interface directly
- Mid-level
- Has the same functions as the low-level interface, but with a more convenient interface
- High-level
- Is nice and pythonic, often doesn’t necessarily mimic the C interface’s structure
Low-Level Interface¶
NiceLib semi-automatically generates a low-level interface for us. To tell it how to do so, we create a build file, which contains a build()
. This function invokes build_lib()
with arguments that tell it what library (.dll/.so file) we’re wrapping and what header(s) to use.
For our ni
library, we name our file _build_ni.py
:
# _build_ni.py
from nicelib import build_lib
header_info = r'C:\Program Files (x86)\National Instruments\NI-DAQ\DAQmx ANSI C Dev\include\NIDAQmx.h'
lib_name = 'nicaiu'
def build():
build_lib(header_info, lib_name, '_nilib', __file__)
if __name__ == '__main__':
from instrumental.log import log_to_screen
log_to_screen(fmt='%(message)s')
build()
You can see we’ve indicated the path to the header NIDAQmx.h
and the library nicaiu.dll
that are used by the Windows version of NI-DAQmx.
Now let’s manually invoke a build:
$ python _build_ni.py
Module _nilib does not yet exist, building it now. This may take a minute...
Searching for headers...
Found C:\Program Files (x86)\National Instruments\NI-DAQ\DAQmx ANSI C Dev\include\NIDAQmx.h
Parsing and cleaning headers...
Successfully parsed input headers
Compiling cffi module...
Writing macros...
Done building _nilib
Nice, it worked! We now have a freshly-generated _nilib.py
module, which is our low-level interface. You can then call load_lib('foo', __package__)
to load the LibInfo
object, as we’ll see in the mid-level interface section.
We won’t always be this fortunate, since headers sometimes include nonstandard syntax which nicelib’s parsing system can’t handle. In that case, there are two basic approaches:
- Manually include only the necessary snippets of the header, cleaning up any unparseable syntax.
- Use
build_lib
’s options, includingtoken_hooks
andast_hooks
to avoid or programmatically clean up the problem syntax.
Option 1 can be good for quickly moving on and starting to write your mid-level interface. You can include just a few functions that you want to test out, and perhaps later come back and pursue option 2. For example, we could do the following for our DAQmx driver:
from nicelib import build_lib
source = """
#define __CFUNC __stdcall
typedef signed int int32;
typedef unsigned int uInt32;
int32 __CFUNC DAQmxGetSysDevNames(char *data, uInt32 bufferSize);
int32 __CFUNC DAQmxGetDevProductNum(const char device[], uInt32 *data);
int32 __CFUNC DAQmxGetDevSerialNum(const char device[], uInt32 *data);
"""
lib_name = 'nicaiu'
def build():
build_lib(None, lib_name, '_nilib', __file__, preamble=source)
We use header_info=None
to skip loading any external header files, and pass in our source via the preamble
parameter.
Option 2 is more complete, but can sometimes be tricky, as it requires some extra knowledge of C and why some section of code may not be parsing correctly (e.g. because it’s actually C++, which happens commonly in some libs written only for Windows). In the simplest case, you may just need to exclude a problematic header that’s being included, or use one of the pre-written token hooks or ast hooks that nicelib provides. In other cases, you may need to write your own hook to clean up the header. See the nicelib docs for a more detailed account of how to use token/ast hooks and the other paramters of build_lib()
.
Mid-Level Interface¶
Once we have a low-level interface that can be loaded via load_lib()
, we can start to work on the mid-level bindings. What’s the point of these bindings? Well, they make the functions a lot more hospitable to work with. Take for example int32 DAQmxGetSysDevNames(char *data, uInt32 bufferSize)
. This function takes a preallocated char
buffer and its length, returning its string within the buffer, and an error code as the int32
return value. Using the low-level binding looks like this:
buflen = 1024
data = ffi.new('char[]', buflen)
retval = DAQmxGetSysDevNames(data, buflen)
handle_daq_retval(retval)
result = ffi.string(data)
Seems kinda verbose—and this function only takes two arguments! Write too much code like this and your code’s intent will drown in a sea of bookkeeping. In contrast, using a mid-level binding looks like this:
result = NiceNI.GetSysDevNames()
Better, right? So how do you write these mid-level bindings? Here’s a simple start:
from nicelib import load_lib, NiceLib, Sig
class NiceNI(NiceLib):
_info_ = load_lib('ni', __package__)
_prefix_ = 'DAQmx'
GetErrorString = Sig('in', 'buf', 'len')
GetSysDevNames = Sig('buf', 'len')
CreateTask = Sig('in', 'out')
We define a subclass of NiceLib
that specifies some general info about the library, as well as some signature (Sig
) definitions for the functions we want to wrap. _info_
specifies the lib we’re wrapping, and _prefix_
is a prefix that will be removed from the names of the functions. A Sig
specifies the purpose of each of its function’s parameters, e.g. whether it’s an input, an output, or something more special.
For instance, CreateTask
was was matched with Sig('in', 'out')
, reflecting that int32 DAQmxCreateTask(const char taskName[], TaskHandle *taskHandle)
uses taskName
as an input, and taskHandle
as an output. This tells nicelib that CreateTask
takes only one argument and returns one value, and nicelib creates a function accordingly:
In [1]: NiceNI.CreateTask('myTask')
Out[1]: (<cdata 'void *' 0x000000000AD49250>, 0)
But wait, there are two values here, what’s going on? The first part makes sense, that’s our taskHandle
(of type TaskHandle
, an alias for void*
), but what’s the zero from? It’s the actual return value, the error-code of type int32
. What if we want to ignore this value, or do something else with it? That’s where RetHandler
s come in. We’ll talk more about these later, but nicelib comes with two builtin return handlers, ret_return
and ret_ignore
. ret_return
is used by default, and it tacks the C return value on the the end of the Python return values. ret_ignore
simply ignores the return value. There are a few levels at which we can specify the return handler, but to apply it to all functions within the lib we use the _ret_
attribute:
from nicelib import load_lib, NiceLib, Sig, ret_ignore
class NiceNI(NiceLib):
_info_ = load_lib('ni', __package__)
_prefix_ = 'DAQmx'
_ret_ = ret_ignore
GetErrorString = Sig('in', 'buf', 'len')
GetSysDevNames = Sig('buf', 'len')
CreateTask = Sig('in', 'out')
Now let’s try again:
In [1]: NiceNI.CreateTask('myTask')
Out[1]: <cdata 'void *' 0x0000000009169250>
For now let’s ignore the return codes; we’ll handle them properly later. Now that we’ve explained 'in'
and 'out'
, what do 'buf'
and 'len'
do? Recall that DAQmxGetSysDevNames(char *data, uInt32 bufferSize)
takes a char
buffer and its length, writing a C-string into the buffer. The pair of 'buf'
and 'len'
are made for exactly such a situation—nicelib will create a char
array, passing it in for the 'buf'
parameter, and its length in as the 'len'
parameter, then extracting a bytes
object using ffi.string()
and returning it:
In [2]: NiceNI.GetSysDevNames()
Out[2]: b'Dev1'
You can check out the nicelib docs to find a listing of all the possible Sig
string codes and what they do.
TODO:
- NiceObject classdefs
- RetHandlers
High-Level Interface¶
Now let’s get start writing our driver:
from instrumental.drivers.daq import DAQ
class NIDAQ(DAQ):
_INST_PARAMS_ = ['name', 'serial', 'model']
def _initialize(self):
self.name = self._paramset['name']
self._dev = self.mx.Device(self.name)
We inherit from DAQ
, a subclass of Instrument
, and use the _INST_PARAMS_
class attribute to declare what parameters our instrument can use to construct itself.
Overview¶
An Instrumental driver is a high-level Python interface to a hardware device. These can be implemented in a number of ways, but usually fall into one of two categories: message-based drivers and foreign function interface (FFI)-based drivers.
Many lab instruments—whether they use GPIB, RS-232, TCPIP, or USB—communicate using text-based messaging protocols. In this case, we can use PyVISA to interface with the hardware, and focus on providing a high-level pythonic API. See Writing VISA-based Drivers for more details.
Otherwise, the instrument is likely controlled via a library (DLL) which is designed to be used by an application written in C. In this case, we can use NiceLib to greatly simplify the wrapping of the library. See Writing NiceLib-Based Drivers for more details.
Generally a driver module should correspond to a single API/library which is being wrapped. For example, there are two separate drivers for Thorlabs cameras, instrumental.drivers.cameras.uc480
and instrumental.drivers.cameras.tsi
, each corresponding to a separate library.
By subclassing Instrument
, your class gets a number of features for free:
- Auto-closing on program exit (just provide a
close()
method) - A context manager which automatically closes the instrument
- Saving of instruments via
save_instrument()
- Integration with
ParamSet
- Integration with
Facet
Writing VISA-based Drivers¶
To control instruments using message-based protocols, you should use PyVISA, by making your driver class inherit from VisaMixin
. You can then use MessageFacet()
or SCPI_Facet()
to easily implement a lot of common functionality (see Facets for more information). VisaMixin
provides a resource
property as well as write()
and query()
methods for your class.
If you’re implementing _instrument()
and need to open/access the VISA instrument/resource, you should use instrumental.drivers._get_visa_instrument()
to take advantage of caching.
For a walkthough of writing a VISA-based driver, check out the VISA Driver Example.
Writing NiceLib-Based Drivers¶
If you need to wrap a library or SDK with a C-style interface (most DLLs), you will probably want to use NiceLib, which simplifies the process. You’ll first write some code to generate mid-level bindings for the library, then write your high-level bindings as a separate class which inherits from the appropriate Instrument
subclass. See the NiceLib documentation for details on how to use it, and check out other NiceLib-based drivers to see how to integrate with Instrumental.
For a walkthough of writing a NiceLib-based driver, check out the NiceLib Driver Example.
Integrating Your Driver with Instrumental¶
To make your driver module integrate nicely with Instrumental, there are a few patterns that you should follow.
Special Driver Variables¶
Note
Some old drivers use special variables that are defined on the module level, just below the imports. This method is now deprecated in favor of class-level variables. See Special Driver Variables (deprecated method) for info on the old variables.
When an instrument is created via list_instruments()
, Instrumental must find the proper driver to use. To avoid importing every driver module to check for the instrument, we use the statically generated file driver_info.py
. To register your driver in this file, do not edit it directly, but instead define special class attributes within your driver class:
_INST_PARAMS_
- A list of strings indicating the parameter names which can be used to construct the instruments that this driver class provides. The
ParamSet
objects returned bylist_instruments()
should provide each of these parameters. Usually VISA instruments just set_INST_PARAMS_ = ['visa_address']
. _INST_VISA_INFO_
- (Optional, only used for VISA instruments) A tuple
(manufac, models)
, to be checked against the result of an*IDN?
query.manufac
is the manufacturer string, andmodels
is a list of model strings. _INST_PRIORITY_
- (Optional) An int (nominally 0-9) denoting the driver’s priority. Lower-numbered drivers will be tried first. This is useful because some drivers are either slower, less reliable, or less commonly used than others, and should therefore be tried only after all other options are exhausted.
To re-generate driver_info.py
, run python -m instrumental.parse_modules
. This will parse all of the driver code and look for classes defining the class attribute _INST_PARAMS_
, adding them to its list of known drivers. The generated file contains all driver modules, driver classes, parameters, and required imports. For instanc:
driver_info = OrderedDict([
('motion._kinesis.isc', {
'params': ['serial'],
'classes': ['K10CR1'],
'imports': ['cffi', 'nicelib'],
}),
('scopes.tektronix', {
'params': ['visa_address'],
'classes': ['MSO_DPO_2000', 'MSO_DPO_3000', 'MSO_DPO_4000', 'TDS_1000', 'TDS_200', 'TDS_2000', 'TDS_3000', 'TDS_7000'],
'imports': ['pyvisa', 'visa'],
'visa_info': {
'MSO_DPO_2000': ('TEKTRONIX', ['MSO2012', 'MSO2014', 'MSO2024', 'DPO2012', 'DPO2014', 'DPO2024']),
},
}),
])
Special Driver Variables (deprecated method)¶
Note that these old variable names lacked the trailing underscore.
_INST_PARAMS
- A list of strings indicating the parameter names which can be used to construct the instruments that this driver provides. The
ParamSet
objects returned bylist_instruments()
should provide each of these parameters. _INST_CLASSES
- (Not required for VISA-based drivers) A list of strings indicating the names of all
Instrument
subclasses the driver module provides (typically only one). This allows you to avoid writing a driver-specific_instrument()
function in most cases. _INST_VISA_INFO
(Optional, only used for VISA instruments) A dict mapping instrument class names to a tuple
(manufac, models)
, to be checked against the result of an*IDN?
query.manufac
is the manufacturer string, andmodels
is a list of model strings.For instruments that support the
*IDN?
query, this allows us to directly find the correct driver and class to use._INST_PRIORITY
- (Optional) An int (nominally 0-9) denoting the driver’s priority. Lower-numbered drivers will be tried first. This is useful because some drivers are either slower, less reliable, or less commonly used than others, and should therefore be tried only after all other options are exhausted.
Special Driver Functions¶
These functions, if implemented, should be defined at the module level.
list_instruments()
- (Optional for VISA-based drivers) This must return a list of
ParamSet
s which correspond to each available device that this driver sees attached. EachParamSet
should contain all of the params listed in this driver’s_INST_PARAMS
. _instrument(paramset)
- (Optional) Must find and return the device corresponding to
paramset
. If this function is defined,instrumental.instrument()
will use it to open instruments. Otherwise, the appropriate driver class is instantiated directly. _check_visa_support(visa_rsrc)
- (Optional, only applies to VISA-based drivers) Must return the name of the
Instrument
subclass to use ifvisa_rsrc
is a device that is supported by this driver, andNone
if it is not supported.visa_rsrc
is apyvisa.resources.Resource
object. This function is only needed for VISA-based drivers where the device does not support the*IDN?
query, and instead implements its own message-based protocol.
Writing Your Instrument
Subclass¶
Each driver subpackage (e.g. instrumental.drivers.motion
) defines its own subclass of Instrument
, which you should use as the base class of your new instrument. For instance, all motion control instruments should inherit from instrumental.drivers.motion.Motion
.
Writing _initialize()
¶
Instrument
subclasses should implement an _initialize()
method to perform any required initialization (instead of __init__
). For convenience, the special settings parameter is unpacked (using **
) into this initializer. Any optional settings you support should be given default values in the function signature. No other arguments are passed to _initialize()
.
_paramset
and other mixin-related attributes (e.g. resource
for subclasses of :class`~instrumental.drivers.VisaMixin`) are already set before _initialize()
is called, so you may access them if you need to.
Special Methods¶
There are also some special methods you may provide, all of which are optional.
close()
- Close the instrument. Useful for cleaning up per-instrument resources. This automatically gets called for each instrument upon program exit. The default implementation does nothing.
_fill_out_paramset()
Flesh out the
ParamSet
that the user provided. Usually you’d only reimplement this to provide a more efficient implementation than the default. The input params can be accessed and modified viaself._paramset
.The default implementation first checks which parameters were provided. If the user provided all parameters listed in the module’s
_INST_PARAMS
, the params are considered complete. Otherwise, the driver’sinstrumental.drivers.list_instruments()
is called, and the the first matching set of params is used to fill in any missing entries in the input params.
Driver Parameters¶
A ParamSet
is a set of identifying information, like serial number or name, that is used to find and identify an instrument. These ParamSet
s are used heavily by instrument()
and list_instruments()
. There are some specially-handled parameters in addition to the ordinary ones, as described below.
You can customize how an Instrument
’s paramset is filled out by overriding the _fill_out_paramset()
method. The default implementation uses instrumental.drivers.list_instruments()
to find a matching paramset, and updates the original paramset with any fields that are missing.
Special params¶
There are a few parameters that are treated specially. These include:
- module
- The name of the driver module, relative to the
drivers
package, e.g.scopes.tektronix
. - classname
- The name of the class to which these parameters apply.
- server
- The address of an instrument server which should be used to open the remote instrument.
- settings
- A dict of extra settings which get passed as arguments to the instrument’s constructor. These settings are separated from the other parameters because they are not considered identifying information, but simply configuration information. More specifically, changing the
settings
should never change which instrument the givenParamSet
will open. - visa_address
- The address string of a VISA instrument. If this is given, Instrumental will assume the parameters refer to a VISA instrument, and will try to open it with one of the VISA-based drivers.
Common params¶
Driver-defined parameters can be named pretty much anything (other than the special names given above). However, they should typically fall into a small set of commonly shared names to make the user’s life easier. Some commonly-used names you should consider using include:
- serial
- model
- number
- id
- name
- port
In general, don’t use vendor-specific names like newport_id
(also avoid including underscores, for reasons that will become clear). Convenient vendor-specific parameters are automatically supported by instrument()
. Say for example that the driver instrumental.drivers.cameras.tsi
supports a :param:`serial` parameter. Then you can use any of the parameters serial
, tsi_serial
, tsi_cam_serial
, and cam_serial
to open the camera. The parameter name is split by underscores, then used to filter which modules are checked.
Note that cam_serial
(vs cameras_serial
) is not a typo. Each section is matched by substring, so you can even use something like tsi_cam_ser
.
Useful Utilities¶
Instrumental provides some commonly-used utilities for helping you to write drivers, including decorators and functions for helping to handle unitful arguments and enums.
-
instrumental.drivers.util.
check_units
(*pos, **named) Decorator to enforce the dimensionality of input args and return values.
Allows strings and anything that can be passed as a single arg to
pint.Quantity
.@check_units(value='V') def set_voltage(value): pass # `value` will be a pint.Quantity with Volt-like units
-
instrumental.drivers.util.
unit_mag
(*pos, **named) Decorator to extract the magnitudes of input args and return values.
Allows strings and anything that can be passed as a single arg to
pint.Quantity
.@unit_mag(value='V') def set_voltage(value): pass # The input must be in Volt-like units and `value` will be a raw number # expressing the magnitude in Volts
-
instrumental.drivers.util.
check_enums
(**kw_args)¶ Decorator to type-check input arguments as enums.
Allows strings and anything that can be passed to
as_enum
.@check_enums(mode=SampleMode) def set_mode(mode): pass # `mode` will be of type SampleMode
Noindex:
-
instrumental.drivers.util.
as_enum
(enum_type, arg) Check if arg is an instance or key of enum_type, and return that enum
-
instrumental.drivers.util.
visa_timeout_context
(*args, **kwds) Context manager for temporarily setting a visa resource’s timeout.
with visa_timeout_context(rsrc, 100): ... # `rsrc` will have a timeout of 100 ms within this block
Driver-Writing Checklist¶
There are a few things that should be done to make a driver integrate really nicely with Instrumental:
- Add any Special Driver Variables your driver needs at the top of the driver module
- Implement any Special Driver Functions you need
- Implement a
close()
method if appropriate - Implement any required methods from the base class
Some other important things to keep in mind:
- Use Pint Units in your API
- Ensure Python 3 compatibility
- Add documentation
- Add supported device(s) to the list in Package Overview
- Document methods using numpy-style docstrings
- Add extra docs to show common usage patterns, if applicable
- List dependencies following a template (both Python packages and external libraries)
Facets¶
Note
Facets are new in version 0.4
Introduction¶
A Facet
represents a property of an instrument—for example, the wavelength setting on an optical power meter. Facets exist to ease driver development and help to provide a consistent driver interface with features like unit conversion and bounds-checking. They also make driver code more declarative, and hence easier to read. Take, for example, this snippet from the Thorlabs PM100D driver:
class PM100D(PowerMeter, VisaMixin):
"""A Thorlabs PM100D series power meter"""
[...]
wavelength = SCPI_Facet('sense:corr:wav', units='nm', type=float,
doc="Input signal wavelength")
This is not much code, yet it already gives us something pretty useful. If we open such a power meter, we can see how its wavelength
facet behaves:
>>> pm.wavelength
<Quantity(852.0, 'nanometer')
>>> pm.wavelength = '532 nm'
>>> pm.wavelength
<Quantity(532.0, 'nanometer')
>>> pm.wavelength = 1064
DimensionalityError: Cannot convert from 'dimensionless' to 'nanometer'
As you can see, the facet automatically parses and checks units, and converts any ints to floats. We could also specify the allowed wavelength range if we wanted to.
You’ll notice the code above uses SCPI_Facet
, which is a helper function for creating facets that use SCPI messaging standards. When we enter pm.wavelength
, it sends the message "sense:corr:wav?"
to the device, reads its response, and converts it into the proper type and units. Similarly, when we enter pm.wavelength = '532 nm'
, it sends the message "sense:corr:wav 532"
to the device.
If you’re using a message-based device with slightly different message format, it’s easy to write your own wrapper function that calls MessageFacet
. Check out the source of SCPI_Facet
to see how this is done. It’s frequently useful to write a helper function like this for a given driver, even if it’s not message-based.
Facets are partially inspired by the Lantz concept of Features (or ‘Feats’).
API¶
-
class
instrumental.drivers.
Facet
(fget=None, fset=None, doc=None, cached=False, type=None, units=None, value=None, limits=None, name=None)¶ Property-like class representing an attribute of an instrument.
Parameters: - fget (callable, optional) – A function to be used for getting the facet’s value.
- fset (callable, optional) – A function to be used for setting the facet’s value.
- doc (str, optional) – Docstring to describe the facet
- cached (bool, optional) – Whether the facet should use caching. If True, the repeated writes of the same value will
only write to the instrument once, while repeated reads of a value will only query the
instrument once. Therefore, one should be careful to use caching only when it makes sense.
Caching can be disabled on a per-get or per-set basis by using the
use_cache
parameter toget_value()
orset_value()
. - type (callable, optional) – Type of the outward-facing value of the facet. Typically an actual type like
int
, but can be any callable that converts a value to the proper type. - units (pint.Units or corresponding str) – Physical units of the facet’s value. Used for converting both user input (when setting) and the output of fget (when getting).
- value (dict-like, optional) – A map from ‘external’ values to ‘internal’ facet values. Used internally to convert input values for use with fset and to convert values returned by fget into ‘nice’ values fit force user consumption.
- limits (sequence, optional) – Limits specified in
[stop]
,[start, stop]
, or[start, stop, step]
format. When given, raises aValueError
if a user tries to set a value that is out of range.step
, if given, is used to round an in-range value before passing it to fset.
-
instrumental.drivers.
MessageFacet
(get_msg=None, set_msg=None, convert=None, **kwds)¶ Convenience function for creating message-based Facets.
Creates
fget
andfset
functions that are passed toFacet
, based on message templates. This is primarily used for writing your own Facet-creating helper functions for message-based drivers that have a unique message format. For standard SCPI-style messages, you can useSCPI_Facet()
directly.This is for use with
VisaMixin
, as it assumes the instrument haswrite
andquery
methods.Parameters: - get_msg (str, optional) – Message used to query the facet’s value. If omitted, getting is unsupported.
- set_msg (str, optional) – Message used to set the facet’s value. This string is filled in with
set_msg.format(value)
, where value is the user-given value being set. - convert (function or callable) – Function that converts both the string returned by querying the instrument and the set-value
before it is passed to
str.format()
. Usually something likeint
orfloat
. - **kwds – Any other keywords are passed along to the
Facet
constructor
-
instrumental.drivers.
SCPI_Facet
(msg, convert=None, readonly=False, **kwds)¶ Facet factory for use in VisaMixin subclasses that use SCPI messages
Parameters: - msg (str) – Base message used to create SCPI get- and set-messages. For example, if
msg='voltage'
, the get-message is'voltage?'
and the set-message becomes'voltage {}'
, where{}
gets filled in by the value being set. - convert (function or callable) – Function that converts both the string returned by querying the instrument and the set-value
before it is passed to
str.format()
. Usually something likeint
orfloat
. - readonly (bool, optional) – Whether the Facet should be read-only.
- **kwds – Any other keywords are passed along to the
Facet
constructor
- msg (str) – Base message used to create SCPI get- and set-messages. For example, if
API Documentation¶
Driver Utils¶
-
class
instrumental.drivers.
Instrument
¶ Base class for all instruments.
-
_after_init
()¶ Called just after _initialize
-
_before_init
()¶ Called just before _initialize
-
classmethod
_create
(paramset, **other_attrs)¶ Factory method meant to be used by
instrument()
-
_fill_out_paramset
()¶
-
_initialize
(**settings)¶
-
_load_state
(state_path=None)¶ Load instrument state from a pickle file
-
_save_state
(state_path=None)¶ Save instrument state to a pickle file
-
close
()¶
-
get
(facet_name, use_cache=False)¶
-
observe
(name, callback)¶ Add a callback to observe changes in a facet’s value
The callback should be a callable accepting a
ChangeEvent
as its only argument. ThisChangeEvent
is a namedtuple withname
,old
, andnew
fields.name
is the facet’s name,old
is the old value, andnew
is the new value.
-
save_instrument
(name, force=False)¶ Save an entry for this instrument in the config file.
Parameters: - name (str) – The name to give the instrument, e.g. ‘myCam’
- force (bool, optional) – Force overwrite of the old entry for instrument
name
. By default, Instrumental will raise an exception if you try to write to a name that’s already taken. Ifforce
is True, the old entry will be commented out (with a warning given) and a new entry will be written.
-
_abc_cache
= <_weakrefset.WeakSet object>¶
-
_abc_negative_cache
= <_weakrefset.WeakSet object>¶
-
_abc_negative_cache_version
= 39¶
-
_abc_registry
= <_weakrefset.WeakSet object>¶
-
_all_instances
= {}¶
-
_instances
= <_weakrefset.WeakSet object>¶
-
_prop_funcs
= {}¶
-
_props
= []¶
-
_state_path
¶
-
-
instrumental.drivers.
instrument
(inst=None, **kwargs)¶ Create any Instrumental instrument object from an alias, parameters, or an existing instrument.
- reopen_policy : str of (‘strict’, ‘reuse’, ‘new’), optional
- How to handle the reopening of an existing instrument. ‘strict’ - disallow reopening an instrument with an existing instance which hasn’t been cleaned up yet. ‘reuse’ - if an instrument is being reopened, return the existing instance ‘new’ - always create a new instance of the instrument class. Not recommended unless you know exactly what you’re doing. The instrument objects are not synchronized with one another. By default, follows the ‘reuse’ policy.
>>> inst1 = instrument('MYAFG') >>> inst2 = instrument(visa_address='TCPIP::192.168.1.34::INSTR') >>> inst3 = instrument({'visa_address': 'TCPIP:192.168.1.35::INSTR'}) >>> inst4 = instrument(inst1)
-
instrumental.drivers.
list_instruments
(server=None, module=None, blacklist=None)¶ Returns a list of info about available instruments.
May take a few seconds because it must poll hardware devices.
It actually returns a list of specialized dict objects that contain parameters needed to create an instance of the given instrument. You can then get the actual instrument by passing the dict to
instrument()
.>>> inst_list = get_instruments() >>> print(inst_list) [<NIDAQ 'Dev1'>, <TEKTRONIX 'TDS 3032'>, <TEKTRONIX 'AFG3021B'>] >>> inst = instrument(inst_list[0])
Parameters: - server (str, optional) – The remote Instrumental server to query. It can be an alias from your instrumental.conf
file, or a str of the form
(hostname|ip-address)[:port]
, e.g. ‘192.168.1.10:12345’. Is None by default, meaning search on the local machine. - blacklist (list or str, optional) – A str or list of strs indicating driver modules which should not be queried for instruments.
Strings should be in the format
'subpackage.module'
, e.g.'cameras.pco'
. This is useful for very slow-loading drivers whose instruments no longer need to be listed (but may still be in use otherwise). This can be set permanently in yourinstrumental.conf
. - module (str, optional) – A str to filter what driver modules are checked. A driver module gets checked only if it
contains the substring
module
in its full name. The full name includes both the driver group and the module, e.g.'cameras.pco'
.
- server (str, optional) – The remote Instrumental server to query. It can be an alias from your instrumental.conf
file, or a str of the form
-
instrumental.drivers.
list_visa_instruments
()¶ Returns a list of info about available VISA instruments.
May take a few seconds because it must poll the network.
It actually returns a list of specialized dict objects that contain parameters needed to create an instance of the given instrument. You can then get the actual instrument by passing the dict to
instrument()
.>>> inst_list = get_visa_instruments() >>> print(inst_list) [<TEKTRONIX 'TDS 3032'>, <TEKTRONIX 'AFG3021B'>] >>> inst = instrument(inst_list[0])
-
class
instrumental.drivers.
ParamSet
(cls=None, **params)¶ -
__init__
(cls=None, **params)¶ x.__init__(…) initializes x; see help(type(x)) for signature
-
create
(**settings)¶
-
get
(key, default=None)¶
-
items
()¶
-
keys
()¶
-
lazyupdate
(other)¶ Add values from
other
for keys that are missing
-
matches
(other)¶ True iff all common keys have matching values
-
to_ini
(name)¶
-
update
(other)¶
-
values
()¶
-
-
class
instrumental.drivers.
VisaMixin
¶ -
_end_transaction
()¶
-
_flush_message_queue
()¶ Write all queued messages at once
-
_start_transaction
()¶
-
query
(message, *args, **kwds)¶ Query the instrument’s VISA resource with
message
Flushes the message queue if called within a transaction.
-
transaction
(**kwds)¶ Transaction context manager to auto-chain VISA messages
Queues individual messages written with the
write()
method and sends them all at once, joined by ‘;’. Messages are actually sent (1) when a call toquery()
is made and (2) upon the end of transaction.This is especially useful when using higher-level functions that call
write()
, as it lets you combine multiple logical operations into a single message (if only using writes), which can be faster than sending lots of little messages.Be cognizant that a visa resource’s write and query methods are not transaction-aware, only VisaMixin’s are. If you need to call one of these methods (e.g. write_raw), make sure you flush the message queue manually with
_flush_message_queue()
.As an example:
>>> with myinst.transaction(): ... myinst.write('A') ... myinst.write('B') ... myinst.query('C?') # Query forces flush. Writes "A;B" and queries "C?" ... myinst.write('D') ... myinst.write('E') # End of transaction block, writes "D;E"
-
write
(message, *args, **kwds)¶ Write a string message to the instrument’s VISA resource
Calls
format(*args, **kwds)
to format the message. This allows for clean inclusion of parameters. For example:>>> inst.write('source{}:value {}', channel, value)
-
_abc_cache
= <_weakrefset.WeakSet object>¶
-
_abc_negative_cache
= <_weakrefset.WeakSet object>¶
-
_abc_negative_cache_version
= 39¶
-
_abc_registry
= <_weakrefset.WeakSet object>¶
-
_in_transaction
¶
-
_instances
= <_weakrefset.WeakSet object>¶
-
_prop_funcs
= {}¶
-
_props
= []¶
-
resource
¶ VISA resource
-
-
instrumental.drivers.
_get_visa_instrument
(params)¶ Returns the VISA instrument corresponding to ‘visa_address’. Uses caching to avoid multiple network accesses.
Helpful utilities for writing drivers.
-
instrumental.drivers.util.
check_units
(*pos, **named)¶ Decorator to enforce the dimensionality of input args and return values.
Allows strings and anything that can be passed as a single arg to
pint.Quantity
.@check_units(value='V') def set_voltage(value): pass # `value` will be a pint.Quantity with Volt-like units
-
instrumental.drivers.util.
unit_mag
(*pos, **named)¶ Decorator to extract the magnitudes of input args and return values.
Allows strings and anything that can be passed as a single arg to
pint.Quantity
.@unit_mag(value='V') def set_voltage(value): pass # The input must be in Volt-like units and `value` will be a raw number # expressing the magnitude in Volts
-
instrumental.drivers.util.
check_enums
(**kw_args)¶ Decorator to type-check input arguments as enums.
Allows strings and anything that can be passed to
as_enum
.@check_enums(mode=SampleMode) def set_mode(mode): pass # `mode` will be of type SampleMode
-
instrumental.drivers.util.
as_enum
(enum_type, arg)¶ Check if arg is an instance or key of enum_type, and return that enum
-
instrumental.drivers.util.
visa_timeout_context
(*args, **kwds)¶ Context manager for temporarily setting a visa resource’s timeout.
with visa_timeout_context(rsrc, 100): ... # `rsrc` will have a timeout of 100 ms within this block
Fitting¶
Plotting¶
Tools¶
Developer’s Guide¶
This page is for those of you who enjoy diving into guidelines, coding conventions, and project philosophies. If you’re looking to get started more quickly, instead check out the Contributing and Writing Drivers pages.
Driver Status¶
Category | Driver | Documentation | instrument() | Python 3 Support |
---|---|---|---|---|
Cameras | PCO | |||
Pixelfly | ||||
PVCam | ||||
UC480 | ||||
TSI | ||||
DAQ | NI | Yes | ||
Function Generators | Tektronix | |||
Lasers | Femto Ferb | |||
Lock-in Amplifiers | SR850 | |||
Motion | Kinesis | |||
Multimeters | HP | |||
Power Meters | Newport | |||
Thorlabs | ||||
Oscilloscopes | Tektronix | |||
Spectrometers | Thorlabs | |||
Bristol | ||||
Wavemeters | Burleigh |
Release Instructions¶
Here’s some info on what needs to be done before each release:
- Update the CHANGELOG (add any needed entries and update the version heading)
- Update the version number in
__about__.py
- Run
python -m instrumental.parse_modules
from the Instrumental directory to regeneratedriver_info.py
- [Locally build and review the documentation]
- Verify the PyPI description (generated from
README.rst
) is valid:python setup.py sdist
twine check dist/*
- Commit and push these changes
- Wait to verify that the builds, tests, and documentation builds all succeed
- Tag the commit with the version number and push the tag
- Set up the release info on GitHub
- Verify that Travis CI and AppVeyor have successfully deployed to PyPI
The Instrumental Manifesto¶
A major goal of Instrumental is to try to unify and simplify a lot of common, useful operations. Essential to that is a consistent and coherent interface.
- Simple, common tasks should be simple to perform
- Options should be provided to enable more complex tasks
- Documentation is essential
- Use of physical units should be standard and widespread
Simple, common tasks should be simple to perform¶
Tasks that are conceptually simple or commonly performed should be made easy. This means having sane defaults.
Options should be provided to enable more complex tasks¶
Along with sane defaults, provide options. Typically, this means providing optional parameters in functions and methods.
Documentation is essential¶
Providing Documentation can be tiring or boring, but, without it, your carefully crafted interfaces can be opaque to others (including future-you). In particular, all functions and methods should have brief summary sentences, detailed explanations, and descriptions of their parameters and return values.
This also includes providing useful error messages and warnings that the average user can actually understand and do something with.
Use of physical units should be standard and widespread¶
Units in scientific code can be a big issue. Instrumental incorporates unitful quantities using the very nice Pint package. While units are great, it can seem like extra work to start using them. Instrumental strives to use units everywhere to encourage their widespread use.
Coding Conventions¶
As with most Python projects, you should be keeping in mind the style suggestions in PEP8. In particular:
- Use 4 spaces per indent (not tabs!)
- Classes should be named using
CapWords
capitalization - Functions and methods should be named using
lower_case_with_underscores
- As an exception, python wrapper (e.g. cffi/ctypes) code used as a _thin_ wrapper to an underlying library may stick with its naming convention for functions/methods. (See the docs for Attocube stages for an example of this)
- Modules and packages should have short, all-lowercase names, e.g.
drivers
- Use a
_leading_underscore
for non-public functions, methods, variables, etc. - Module-level constants are written in
ALL_CAPS
Strongly consider using a plugin for your text editor (e.g. vim-flake8) to check your PEP8 compliance.
It is OK to have lines over 80 characters, though they should almost always be 100 characters or less.
Docstrings¶
Code in Instrumental is primarily documented using python docstrings, following the numpydoc conventions. In general, you should also follow the guidelines of pep 257.
- No spaces after the opening triple-quote
- One-line docstrings should be on a single line, e.g.
"""Does good stuff."""
- Multi-liners have a summary line, followed by a blank line, followed by the rest of the doc. The closing quote should be on its own line
Python 2/3 Compatibility¶
Instrumental was originally developed for Python 2.7 and long maintained Python 2/3 cross compatibility. As of release 0.7, we haved dropped that support and now require Python 3.7+. This means all future development should target Python 3.
Python 2 support may be removed from existing code as a part of future development, though this is not currently a priority.
Developing Drivers¶
If you’re considering writing a driver, thank you! Check out Writing Drivers for details.