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 IntegrationFilterto 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()
keysight-logo-svg
Program
Program
Duration 80 ns
Layers 1
Targets 4
Repetitions Sweep with 5 repetitions
Associations
rf Array(name=frequencies, shape=(5,), dtype=float, unit=none, value=[4.05 GHz, 4.1 GHz, 4.15 GHz, 4.2 GHz, 4.25 GHz])
Repeat with 10 repetitions
Layer #0
Layer #0
Duration 80 ns
readoutawg 0
RFWaveform on ('readoutawg', 0)

Parameters
Duration 80 ns
Amplitude 1
Frequency Scalar(name=rf, value=None, dtype=float, unit=Hz)
Envelope GaussianEnvelope(4.0)
Instantaneous Phase 0 rad
Post-phase 0 rad
1
RFWaveform on ('readoutawg', 1)

Parameters
Duration 80 ns
Amplitude 1
Frequency Scalar(name=rf, value=None, dtype=float, unit=Hz)
Envelope GaussianEnvelope(4.0)
Instantaneous Phase 0 rad
Post-phase 0 rad
readoutreceiver 0
Acquisition on ('readoutreceiver', 0)

Parameters
Duration 80 ns
Integration Filter
RFWaveform

Parameters
Duration 80 ns
Amplitude 1
Frequency 4.15 GHz
Envelope GaussianEnvelope(4.0)
Instantaneous Phase 0 rad
Post-phase 0 rad
1
Acquisition on ('readoutreceiver', 1)

Parameters
Duration 80 ns
Integration Filter
RFWaveform

Parameters
Duration 80 ns
Amplitude 1
Frequency 4.15 GHz
Envelope GaussianEnvelope(4.0)
Instantaneous Phase 0 rad
Post-phase 0 rad

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 the n_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])
keysight-logo-svg
Program
Program
Duration 80 ns
Layers 1
Targets 4
Repetitions Sweep with 5 repetitions
Associations
rf Array(name=frequencies, shape=(5,), dtype=float, unit=none, value=[4.05 GHz, 4.1 GHz, 4.15 GHz, 4.2 GHz, 4.25 GHz])
amp Array(name=amplitude, shape=(5,), dtype=float, unit=none, value=[0.2, 0.4, 0.6, 0.8, 1])
Repeat with 10 repetitions
Layer #0
Layer #0
Duration 80 ns
readoutawg 0
RFWaveform on ('readoutawg', 0)

Parameters
Duration 80 ns
Amplitude 1
Frequency Scalar(name=rf, value=None, dtype=float, unit=Hz)
Envelope GaussianEnvelope(4.0)
Instantaneous Phase 0 rad
Post-phase 0 rad
1
RFWaveform on ('readoutawg', 1)

Parameters
Duration 80 ns
Amplitude 1
Frequency Scalar(name=rf, value=None, dtype=float, unit=Hz)
Envelope GaussianEnvelope(4.0)
Instantaneous Phase 0 rad
Post-phase 0 rad
readoutreceiver 0
Acquisition on ('readoutreceiver', 0)

Parameters
Duration 80 ns
Integration Filter
RFWaveform

Parameters
Duration 80 ns
Amplitude 1
Frequency 4.15 GHz
Envelope GaussianEnvelope(4.0)
Instantaneous Phase 0 rad
Post-phase 0 rad
1
Acquisition on ('readoutreceiver', 1)

Parameters
Duration 80 ns
Integration Filter
RFWaveform

Parameters
Duration 80 ns
Amplitude 1
Frequency 4.15 GHz
Envelope GaussianEnvelope(4.0)
Instantaneous Phase 0 rad
Post-phase 0 rad
[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.

On this page