Basic Example#

The following contains the essential building blocks to design your own experiment. Mock experiments made using these building blocks can be found in the Demo Notebooks section.

If you downloaded the pyscan directory from git, you can even run the demo_notebooks yourself in Jupyter.

1. Import libraries#

First you need to import libraries. As a convention, we import pyscan as ps in all of our demo notebooks.

The output tells you which drivers were not imported due to specific requirements not being met. This is not a problem unless you are trying to use one of those instruments. If your instrument doesn’t show up on this list, that means it was imported successfully.

[1]:
import pyscan as ps
Could not load Keysight SD1
Could not load Keysight SD1
pylablib not found, AttocubeANC350 not loaded
Basler Camera software not found, BaserCamera not loaded
Helios Camera not installed
msl not installed, Thorlabs BSC203 driver not loaded
seabreeze module not found, Ocean Optics not imported
Failed to load spinapi library.
spinapi is not installed, PulseBlaster driver not loaded.
Thorlabs Kinesis not found, ThorlabsBSC203 not loaded
Thorlabs Kinesis not found, ThorlabsBPC303 not loaded
Thorlabs Kinesis not found, ThorlabsMFF101 not loaded

2. Setup devices#

Next, create an instance of ps.ItemAttribute in a variable called devices. This is where you will store instances of driver classes which connect to your instruments.

Remember, an ItemAttribute class is just a class which has methods that mimic a dictionary. You can name your devices whatever you like.

[2]:
devices = ps.ItemAttribute()

The first device we will add is a dummy driver called TestVoltage. It has basic functionality to show you what a driver class does without actually requiring you to connect to an instrument.

The TestVoltage instance will allow you to set the voltage property as well as query it.

[3]:
devices.voltagesource = ps.TestVoltage()

2.1. Required parameters for certain drivers#

Some driver classes require parameters such as serial number or the VISA or GPIB address. Check the docs for that driver to find out.

If you’re not sure what the GPIB address is, you can get a list of connected instruments using the pyvisa library.

2.2. See what devices have already been setup#

[4]:
devices.items()
[4]:
dict_items([('voltagesource', <pyscan.drivers.test_voltage.TestVoltage object at 0x11b3a0090>)])

2.3. Test the device to ensure that it is working#

It’s always good practice to both write to the instrument and query it to ensure your connection to the instrument is successful and working as expected.

In the case of TestVoltage, we can read in the documentation that it has a property called voltage, so let’s test that.

[5]:
devices.voltagesource.voltage
[5]:
0.0
[6]:
devices.voltagesource.voltage = 5
devices.voltagesource.voltage
[6]:
5.0

Looks good!

3. Define a measure function#

A measure_function is a required attribute of a RunInfo instance, which in turn is a required parameter when you create an instance of Experiment.

This measure_function is run after every iteration of scans, which define the independent variables of your experiment.

The measure_function is a custom function you create, and its only requirements are that:

  1. It takes an Experiment object as its only parameter

  2. It returns an ItemAttribute containing data attributes (unlimited in number and named anything you like) which represent a single observation.

Note that Experiment saves its runinfo and devices parameters as attributes upon initialization, thus these can also be accessed from within the measure function.

A very simple measure_function is defined below:

[7]:
def get_voltage(expt):
    devices = expt.devices
    runinfo = expt.runinfo

    # setup a new ItemAttribute instance in which to store the collected data
    data = ps.ItemAttribute()

    # collect a measurement and store it in the data object
    data.voltage = devices.voltagesource.voltage

    return data

4. Setup a RunInfo instance#

Next, we will setup a RunInfo isntance and define scans.

[8]:
runinfo = ps.RunInfo()

A RunInfo instance contains a number of default attributes, but here we will focus on the essentials. You must define the measure_function as well as any scans you want, each representing independent variables. You may define between 1 and 4 scans, labelled as scan0, scan1, scan2, and scan3.

Just for education, let’s see what attributes exist inside a runinfo:

[9]:
runinfo.keys()
[9]:
dict_keys(['scan0', 'scan1', 'scan2', 'scan3', 'static', 'measured', 'measure_function', 'trigger_function', 'initial_pause', 'average_d', 'verbose'])

4.1. Setup the measure_function#

Now let’s set the measure function to the get_voltage function we already defined. Do not put parentheses after this function, since we do not want to call the function - rather, we are saving the funciton object itself inside the runinfo.

[10]:
runinfo.measure_function = get_voltage

4.2. Setup the scans#

The simplest type of scan is a PropertyScan. It takes three arguments:

PropertyScan(input_dict, prop, dt=0)

where input_dict is a dictionary containing key-value pairs representing “device name strings and arrays of values representing the new prop values you want to set for each device.” The most common and simplest scenario is to change a single device within a PropertyScan, thus the input_dict will contain only one key-value pair.

The prop is the name of the property on the device that will be iterated through - in this case, we will use voltage. The available properties for a particular device are only known by reading the docs for that driver class.

The dt is the delay time in seconds after one iteration of the scan, and before the measure_function is called. If unset, it defaults to 0s. Sometimes experiments will operate more optimally with a longer dt, for example, a stage may take a certain fraction of a second to reach its destination after you set its position.

[11]:
runinfo.scan0 = ps.PropertyScan({'voltagesource': [0,1,2,3,4,5]}, 'voltage', dt=0.01)

You may also use the built-in drange(start, step, stop) function to create that same array.

[12]:
ps.drange(0, 1, 5)
[12]:
array([0., 1., 2., 3., 4., 5.])

The exact value of the stop parameter is always included, even if the steps don’t fit perfectly into the range.

[13]:
ps.drange(0, 2, 5)
[13]:
[0.0, 2.0, 4.0, 5]

Thus an alternate way that scan0 could have been defined is:

[14]:
runinfo.scan0 = ps.PropertyScan({'voltagesource': ps.drange(0, 1, 5)}, 'voltage', dt=1)

We’ve set the dt to be unreasonably large (1s) just so that we will be able to watch its progress in the live_plot for demonstration purposes.

5. Setup & run Experiment#

Setting up the Experiment is now simple. While there are a few types of experiments that all inherit from the AbstractExperiment class such as SparseExperiment, which does not collect data for every single point defined by the scans, by far the class that will be used for most purposes is the Experiment class.

To setup an Experiment, simply input the runinfo and devices objects which we previously defined.

[15]:
expt = ps.Experiment(runinfo, devices)

Now we run the experiment. Experiment has two run methods: run() and start_thread(). start_thread() calls run() in a separate thread, so it is non-blocking. We will use start_thread() as that enables us to also use live plotting.

[16]:
expt.start_thread()
[17]:
expt.stop()
Stopping Experiment
[ ]: