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 .. code-block:: python 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 .. code-block:: python 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. .. code-block:: python 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: .. code-block:: python 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: .. code-block:: python 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. .. code-block:: python 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: .. code-block:: python 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. .. code-block:: python 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). .. code-block:: python 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: .. code-block:: python keithley.volt.arbsweep([0,0.1,0.5,0.66,0.45,0.456,1.11]) .. code-block:: python 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.