Advanced use of Loop

Throughout, we have used the loop1d and loop2d functions to create loops for 1D and 2D measurements. However, you can also create loops manually using the qc.Loop class, which gives you more control over the measurement process and allows for e.g. multidimensional loops, non-linear sweeps, etc.

The general scheme is:

  1. create a (potentially nested) qc.Loop, which defines the sweep setpoints and delays

  2. activate the Loop (which changes it to an ActiveLoop object), by attaching one or more actions to it, using the .each method. Actions can be: parameters to measure, tasks to run, waits, or other loops.

  3. Associate the ActiveLoop with a DataSetPP, which will hold the data collected, using the get_data_set method.

  4. Run the ActiveLoop with the .run method, which additionally can be passed parameters to be plotted using live_plot.

The most basic examples, which are equivalent to the loop1d and loop2d functions, are:

1D loop, specifying parameters to measure and plot

loop=qc.Loop(sweep_parameter.sweep(0,1,num=101), delay=0.1).each(measure_param1, measure_param2)
data=loop.get_data_set(name='My 1D loop')
loop.run([measure_param1, measure_param2])

2D loop, using station.set_measurement to set the default measurement

station.set_measurement(measure_param1, measure_param2)
loop=qc.Loop(sweep_parameter1.sweep(0,1,num=11), delay=0.1).loop(sweep_parameter2.sweep(-1,0,num=101),delay=0.1).each(*station.measure())
data=loop.get_data_set(name='My 2D loop')
loop.run([measure_param1, measure_param2])

2D (and higher dimension) loops can (also) be constructed by nesting loops in .each, e.g.

loop1d=qc.Loop(sweep_parameter2.sweep(-1,0,num=101), delay=0.1).each(*station.measure())
loop=qc.Loop(sweep_parameter1.sweep(0,1,num=11), delay=0.1).each(loop1d)
data=loop.get_data_set(name='My nested 2D loop')
loop.run([measure_param1, measure_param2])

Some examples: Sweeping the inner loop up and down many times at each point of the outer loop:

loopup=qc.Loop(instrument.output1.sweep(-1,0,num=101), delay=0.1).each(*station.measure())
loopdown=qc.Loop(instrument.output1.sweep(0,-1,num=101), delay=0.1).each(*station.measure())
actions=[]
for i in range(10):
    actions.append(loopup)
    actions.append(loopdown)
loop=qc.Loop(instrument.output2.sweep(0,1,num=11), delay=0.1).each(*actions)

A 3d loop would look something like the below:

loop1d=qc.Loop(sweep_parameter3.sweep(0,1,num=11), delay=0.1).each(*station.measure())
loop2d=qc.Loop(sweep_parameter2.sweep(-1,0,num=101), delay=0.1).each(loop3d)
loop=qc.Loop(sweep_parameter1.sweep(0,1,num=11), delay=0.1).each(loop2d)
data=loop.get_data_set(name='My 3D loop')
loop.run()

The data generated by the last example is unfortunately beyond the bounds of both live_plot and offline_plotting, which both only can interpret 1D or 2D rectangular arrays, so you will have to plot the data manually after the measurement is done. Usually I would recommend to actually embed a series of 2d Loop s inside a python loop, and create a series of 2D DataSetPP s, which can then be plotted together using live_plot. Let’s pretend we want to checkout how the lock in frequency affects the conductance of a device in a series of 2d loops.

station.set_measurement(lockin.input1,lockin.input2,current,conductance)

pp=qc.live_plot()

lockin_freqs=[33,73,133,223,228]

for i, freq in enumerate(lockin_freqs):
    lockin.frequency(freq)
    loop=qc.Loop(voltage_source.output1.sweep(-1,1,num=101),delay=0.03).each(*station.measure())
    outerloop=qc.Loop(voltage_source.output2.sweep(-0.5,-0.3,num=21),delay=0.1).each(loop)
    data=outerloop.get_data_set(name=f'Sample XYZ Vout1(-1 1)V Vout2(-0.5 -0.3) freq={freq}')
    data.publisher=pp
    pp.add(data.conductance,subplot=i)
    outerloop.run()

or, completely equivalently, but using the built-in qc.loop2d to help automatically name the data files:

station.set_measurement(lockin.input1,lockin.input2,current,conductance)

pp=qc.live_plot()

lockin_freqs=[33,73,133,223,228]

for i, freq in enumerate(lockin_freqs):
    lockin.frequency(freq)
    loop=qc.loop2d(sweep_parameter=voltage_source.output1
                start=-1,stop=1,num=101,delay=0.01,
                step_parameter=voltage_source.output2
                step_start=-0.5,step_stop=-0.3,step_num=21,step_delay=0.03,
                device_info='Sample XYZ', instrument_info=f'freq={freq}Hz')
    loop.data_set.publisher=pp
    pp.add(loop.data_set.conductance,subplot=i)
    loop.run()

Sweep types

The setpoints of each Loop have so far been generated by the parameter.sweep method, which generates a linear list of values. There are three more sweep types. Firstly, returnsweep offers the possibility to return to the start value once the stop value is reached.

loopdownup=qc.Loop(keithley.volt.returnloop(0,1,num=101),delay=0.1).each(*station.measure())

The above will return a loop with 201 steps, as the stop value is not repeated.

Secondly, there is logsweep, which uses numpy’s geomspace to generate points evenly spaced on a log scale (useful for things such as frequency and in some cases temperature).

freqloop=qc.Loop(lockin.frequency.logsweep(0.01,1000,num=1001),delay=0.1).each(*station.measure())

For everything else, there is arbsweep, where you can pass an arbitrary list of values for the instrument to sweep. For example:

keithley.volt.arbsweep([0,0.1,0.5,0.66,0.45,0.456,1.11])
keithley.volt.arbsweep([i**2 for i in range(101)])

Another usecase for arbsweep could be sweeps with regions of low and high data point densities. For example if you know the location of narrow peaks, you might want to create a list with a high density of data points around those points, while moving quickly between them.