Download
Download this file as Jupyter notebook: sweep_examples.ipynb.
Sweeping program variables
The Running a program on hardware tutorial covers the basic workflow for running experiments on hardware and extracting trace data and I/Q data. Here we expand on that and add sweeps to the programs.
Setting up
As before, we use two AWG channels and two digitizers.
[2]:
import keysight.qcs as qcs
import numpy as np
# set the following to True when connected to hardware:
run_on_hw = False
# instantiate channels representing two AWGs and two digitizers
awgs = qcs.Channels(range(2), "readoutawg")
digs = qcs.Channels(range(2), "readoutreceiver")
# load our channel mapper to connect the virtual channes above to physical ones
mapper = qcs.load("../../assets/channel_map.qcs")
Single variable sweeps
We define a program that plays a single RF waveform on our AWG channels and performs
an acquisition on the digitizer channels with an
IntegrationFilter
to perform I/Q
demodulation of the trace data.
We are sweeping the frequency of the RF pulse but keep the frequency of the integration filter constant.
[3]:
# define the frequency and amplitude of our RF waveform as QCS variables
frequency = qcs.Scalar("rf", dtype=float)
amplitude = qcs.Scalar("amp", dtype=float, value=1)
def make_program():
"""Creates a simple program with a single RF waveform"""
program = qcs.Program()
# define the waveform using this frequency variable & add it to the program
gauss = qcs.RFWaveform(80e-9, qcs.GaussianEnvelope(num_sigma=4), 1, frequency)
program.add_waveform(gauss, awgs)
# define an integration filter using the same RF waveform but with fixed frequency
int_filter = qcs.RFWaveform(80e-9, qcs.GaussianEnvelope(num_sigma=4), 1, 4.15e9)
program.add_acquisition(int_filter, digs)
# specify the number of shots
return program.n_shots(10)
program = make_program()
# specify the frequency values to be swept over
frequencies = qcs.Array(
"frequencies", value=[4.05e9, 4.1e9, 4.15e9, 4.2e9, 4.25e9], dtype=float
)
# add the sweep, targeting the `frequency` variable
program.sweep(frequencies, frequency)
program.draw()
Program
Program
|
||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Layer #0
Layer #0
|
||||||||||||||||||
|
|
RFWaveform on ('readoutawg', 0)
Parameters
|
||||||||||||||||
|
RFWaveform on ('readoutawg', 1)
Parameters
|
|||||||||||||||||
|
|
Acquisition on ('readoutreceiver', 0)
Parameters
|
||||||||||||||||
|
Acquisition on ('readoutreceiver', 1)
Parameters
|
We can inspect the sweep settings on the program visualization by hovering over the
table header. Alternatively, we can retrieve them through the repetitions
attribute:
[4]:
print(program.repetitions.items)
[Sweep(rf=Array(name=frequencies, shape=(5,), dtype=float, unit=none)), Repeat(10)]
Next, we execute the program on hardware.
[5]:
if run_on_hw:
# initialize the backend pass
backend = qcs.HclBackend(channel_mapper=mapper)
# the executor returns the program populated with results
program = qcs.Executor(backend).execute(program)
# (optional) export the data to an HDF5 file
program.to_hdf5("program2.hdf5")
# we are loading a previously run program here for this example
program = qcs.load("program2.hdf5")
The results will now consist of five traces for each sweep point and for each channel.
[6]:
program.plot_trace()
We can retrieve the I/Q data by calling the
get_iq()
method, which returns a dataframe
that has the sweep values in its column header.
[7]:
program.get_iq(stack_channels=True)
[7]:
(rf, 4.05 GHz) | ... | (rf, 4.25 GHz) | |||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ... | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | |
(((Channels(labels=[0], name=readoutreceiver, absolute_phase=False)))) | 0.004758-0.002990j | 0.004799-0.003075j | 0.004757-0.003199j | 0.004858-0.002910j | 0.004812-0.002800j | 0.004616-0.003163j | 0.004664-0.002983j | 0.004873-0.003186j | 0.004779-0.003203j | 0.004773-0.003333j | ... | 0.000008+0.000228j | -0.000033+0.000178j | 0.000004+0.000067j | -0.000068+0.000167j | 0.000089+0.000080j | 0.000005+0.000251j | 0.000127+0.000326j | -0.000157+0.000054j | 0.000100+0.000153j | -0.000043+0.000044j |
(((Channels(labels=[1], name=readoutreceiver, absolute_phase=False)))) | -0.002110-0.003461j | -0.002106-0.003594j | -0.002328-0.003569j | -0.002150-0.003564j | -0.001910-0.003705j | -0.002224-0.003482j | -0.002244-0.003500j | -0.002081-0.003476j | -0.002267-0.003586j | -0.002431-0.003436j | ... | -0.000044-0.000029j | -0.000143-0.000123j | -0.000125-0.000205j | -0.000099+0.000166j | -0.000143-0.000135j | -0.000205-0.000065j | -0.000212-0.000082j | -0.000218-0.000183j | -0.000206-0.000007j | -0.000091-0.000083j |
2 rows × 50 columns
Since this program sweeps the frequency of the RF waveform but keeps the frequency in the integration filter constant, we expect a peak in I/Q magnitude at the center frequency, which can be visually confirmed by plotting the data:
[8]:
program.plot_iq(channel_subplots=False)
In typical applications, we typically have larger sweep ranges and storing all the trace data and performing the I/Q demodulation in software adds timing overhead. Instead, we can perform the demodulation on hardware, and only I/Q data will be stored.
[9]:
new_frequencies = qcs.Array(
"rfs", value=np.linspace(4.05e9, 4.25e9, num=20), dtype=float
)
program = make_program()
program.sweep(new_frequencies, frequency)
if run_on_hw:
# initialize the backend pass
# setting hw_demod=True enables hardware demodulation of trace data
backend = qcs.HclBackend(channel_mapper=mapper, hw_demod=True)
# the executor returns the program populated with results
program = qcs.Executor(backend).execute(program)
# (optional) export the data to an HDF5 file
program.to_hdf5("program3.hdf5")
# we are loading a previously run program here for this example
program = qcs.load("program3.hdf5")
program.plot_iq(channel_subplots=False)
We can retrieve the classified data by calling the
get_classified()
method,
which returns a dataframe that has the sweep values in its column header.
[10]:
program.get_classified()
[10]:
(((Channels(labels=[0], name=readoutreceiver, absolute_phase=False)))) | ... | (((Channels(labels=[1], name=readoutreceiver, absolute_phase=False)))) | |||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
(rf, 4.05 GHz) | ... | (rf, 4.25 GHz) | |||||||||||||||||||
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ... | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | |
0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | ... | 1 | 0 | 1 | 0 | 1 | 1 | 1 | 0 | 1 | 0 |
1 rows × 400 columns
Plot classified data gives the count of occurences of 1’s and 0’s:
[11]:
program.plot_classified(channel_subplots=False)
Note
It is possible to perform variable sweeps both on hardware and software. To enable hardware sweeping, the program’s
sweep()
method needs to be called before then_shots()
method. This will execute both the sweep as well as the repetitions on hardware. Any other sweeps defined after those will be performed in software.
Nested sweeps
Sweeps can be nested by calling the sweep()
method multiple times. For example, we can sweep the frequency and amplitude in our
program from above:
[12]:
program = make_program()
# specify the frequency values to be swept over
frequencies = qcs.Array(
"frequencies", value=[4.05e9, 4.1e9, 4.15e9, 4.2e9, 4.25e9], dtype=float
)
# specify the amplitude values to be swept over
amplitudes = qcs.Array("amplitude", value=[0.2, 0.4, 0.6, 0.8, 1], dtype=float)
program.sweep(frequencies, frequency)
program.sweep(amplitudes, amplitude)
print(program.repetitions.items)
[Sweep(amp=Array(name=amplitude, shape=(5,), dtype=float, unit=none)), Sweep(rf=Array(name=frequencies, shape=(5,), dtype=float, unit=none)), Repeat(10)]
We execute this program as before and load in the results:
[13]:
if run_on_hw:
program = qcs.Executor(qcs.HclBackend(mapper, hw_demod=True)).apply(program)
program.to_hdf5("program4.hdf5")
program = qcs.load("program4.hdf5")
program.get_iq(avg=True, stack_channels=True)
[13]:
(amp, 0.2) | (amp, 0.4) | ... | (amp, 0.8) | (amp, 1) | |||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
(rf, 4.05 GHz) | (rf, 4.1 GHz) | (rf, 4.15 GHz) | (rf, 4.2 GHz) | (rf, 4.25 GHz) | (rf, 4.05 GHz) | (rf, 4.1 GHz) | (rf, 4.15 GHz) | (rf, 4.2 GHz) | (rf, 4.25 GHz) | ... | (rf, 4.05 GHz) | (rf, 4.1 GHz) | (rf, 4.15 GHz) | (rf, 4.2 GHz) | (rf, 4.25 GHz) | (rf, 4.05 GHz) | (rf, 4.1 GHz) | (rf, 4.15 GHz) | (rf, 4.2 GHz) | (rf, 4.25 GHz) | |
(((Channels(labels=[0], name=readoutreceiver, absolute_phase=False)))) | 0.000105-0.000036j | -0.003680-0.002577j | -0.047331-0.024364j | -0.004177-0.001451j | -0.000017-0.000074j | 0.000592-0.000145j | -0.006955-0.004578j | -0.092679-0.047788j | -0.007755-0.002529j | 0.000019-0.000044j | ... | 0.003394-0.001125j | -0.011098-0.004057j | -0.174561-0.087034j | -0.010838-0.003026j | 0.000008-0.000055j | 0.004765-0.003211j | -0.009205-0.001710j | -0.206609-0.095425j | -0.010406+0.000160j | 0.000033+0.000155j |
(((Channels(labels=[1], name=readoutreceiver, absolute_phase=False)))) | 0.000024-0.000026j | 0.002184-0.003385j | 0.020540-0.042693j | 0.001262-0.003685j | 0.000039+0.000001j | -0.000146-0.000342j | 0.004011-0.006360j | 0.040480-0.083915j | 0.002179-0.007069j | 0.000022-0.000000j | ... | -0.000964-0.002324j | 0.004360-0.010822j | 0.074076-0.159659j | 0.002772-0.010767j | 0.000066+0.000091j | -0.002242-0.003489j | 0.002247-0.011192j | 0.084541-0.193357j | 0.001693-0.010974j | -0.000138-0.000019j |
2 rows × 25 columns
We can see that the dataframe now has an additional column index for the second
sweep. When plotting the I/Q data, we can easily switch between sweep axes by
specifying the plot_axis
argument. The default, plot_axis=0
will plot
the data over the first sweep (zeroth index of the dataframe columns). Note that
this can also be used to plot the I/Q data as a function of shots, when the method
is called with avg=False
.
[14]:
# plot over amplitude for each frequency
program.plot_iq(channel_subplots=False)
[15]:
# plot over frequency for each amplitude
program.plot_iq(channel_subplots=False, plot_axis=1)
Simultaneous sweeps
Sweeps can also be performed simultaneously by passing the sweep values and targets as lists:
[16]:
program = make_program()
# specify the frequency values to be swept over
frequencies = qcs.Array(
"frequencies", value=[4.05e9, 4.1e9, 4.15e9, 4.2e9, 4.25e9], dtype=float
)
# specify the amplitude values to be swept over
amplitudes = qcs.Array("amplitude", value=[0.2, 0.4, 0.6, 0.8, 1], dtype=float)
program.sweep([frequencies, amplitudes], [frequency, amplitude])
Program
Program
|
||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Layer #0
Layer #0
|
||||||||||||||||||
|
|
RFWaveform on ('readoutawg', 0)
Parameters
|
||||||||||||||||
|
RFWaveform on ('readoutawg', 1)
Parameters
|
|||||||||||||||||
|
|
Acquisition on ('readoutreceiver', 0)
Parameters
|
||||||||||||||||
|
Acquisition on ('readoutreceiver', 1)
Parameters
|
[16]:
Program([Layer(Channels(labels=[0, 1], name=readoutawg, absolute_phase=False)=[RFWaveform(duration=Scalar(name=_implicit, value=8e-08, dtype=float, unit=s), envelope={GaussianEnvelope(4.0): Scalar(name=_implicit, value=1.0, dtype=float, unit=none)}, rf_frequency=Scalar(name=rf, value=None, dtype=float, unit=Hz), instantaneous_phase={GaussianEnvelope(4.0): Scalar(name=_implicit, value=0.0, dtype=float, unit=rad)}, post_phase=Scalar(name=_implicit, value=0.0, dtype=float, unit=rad))], Channels(labels=[0, 1], name=readoutreceiver, absolute_phase=False)=[Acquisition(IntegrationFilter(RFWaveform(duration=Scalar(name=_implicit, value=8e-08, dtype=float, unit=s), envelope={GaussianEnvelope(4.0): Scalar(name=_implicit, value=1.0, dtype=float, unit=none)}, rf_frequency=Scalar(name=_implicit, value=4150000000.0, dtype=float, unit=Hz), instantaneous_phase={GaussianEnvelope(4.0): Scalar(name=_implicit, value=0.0, dtype=float, unit=rad)}, post_phase=Scalar(name=_implicit, value=0.0, dtype=float, unit=rad))))])])
We execute this program as before and load in the results:
[17]:
if run_on_hw:
program = qcs.Executor(qcs.HclBackend(mapper, hw_demod=True)).apply(program)
program.to_hdf5("program5.hdf5")
program = qcs.load("program5.hdf5")
program.get_iq(avg=True, stack_channels=True)
[17]:
(rf, 4.05 GHz), (amp, 0.2) | (rf, 4.1 GHz), (amp, 0.4) | (rf, 4.15 GHz), (amp, 0.6) | (rf, 4.2 GHz), (amp, 0.8) | (rf, 4.25 GHz), (amp, 1) | |
---|---|---|---|---|---|
(((Channels(labels=[0], name=readoutreceiver, absolute_phase=False)))) | 0.000091-0.000045j | -0.006965-0.004680j | -0.136030-0.067032j | -0.010754-0.003004j | 0.000042+0.000173j |
(((Channels(labels=[1], name=readoutreceiver, absolute_phase=False)))) | -0.000015-0.000042j | 0.004056-0.006367j | 0.056529-0.123557j | 0.002737-0.010782j | -0.000161-0.000117j |
[18]:
# plot the results
program.plot_iq(channel_subplots=False)
Download
Download this file as Jupyter notebook: sweep_examples.ipynb.