Skip to content

Power-Time Rabi

Greetings! In this module, we'll dive into the following topics:

  1. QUA variables
  2. Parametric Waveform Generation:
    • Amplitude and Duration
  3. Real-Time Control Flow:
    • for-loop
  4. Out-Bound Data Flow
    • Data Processing in the SPU
    • Live Data Fetching

Can we jump in?

The QUA program

In QUA, the PPU handles variables. These can be fed into instructions and adjusted in real-time based on quantum experiment outcomes or PPU computations. Here we cover the integer t and fixed a variables.

power_time_rabi.py
# Go to lines 17 and 23-26
import qm.qua as qua
from qm import QuantumMachinesManager
from OPX1000configuration import config, qop_ip, cluster_name, cloud_username, cloud_password # or OPXplusconfiguration, depending on your hardware
from qualang_tools.results import progress_counter, fetching_tool
from qualang_tools.plot import interrupt_on_close
from qm import SimulationConfig, LoopbackInterface
from qualang_tools.loops import from_array
import warnings

warnings.filterwarnings("ignore")

###################
# The QUA program #
###################

n_avg = 1  # (1)!
amplitudes = np.linspace(0, 1.0, 5)
durations = np.arange(10, 14, 1)

with qua.program() as power_rabi:
    n = qua.declare(int)
    a = qua.declare(qua.fixed)  # (2)!
    t = qua.declare(int)
    I = qua.declare(qua.fixed)
    Q = qua.declare(qua.fixed) 
    I_st = qua.declare_stream()
    Q_st = qua.declare_stream()
    n_st = qua.declare_stream()

    with qua.for_(n, 0, n < n_avg, n + 1):
        with qua.for_(*from_array(a, amplitudes)): 
            with qua.for_(*from_array(t, durations)):
                qua.play("x180" * qua.amp(a), "qubit", duration=t)
                qua.align("qubit", "resonator")
                qua.measure(
                    "readout",
                    "resonator",
                    None,
                    qua.dual_demod.full("cos", "sin", I),
                    qua.dual_demod.full("minus_sin", "cos", Q),
                )
                qua.wait(thermalization_time //4, "resonator")
                qua.save(I, I_st)
                qua.save(Q, Q_st)
        qua.save(n, n_st)

    with qua.stream_processing():
        I_st.buffer(len(durations))
            .buffer(len(amplitudes))
            .average()
            .save("I")
        Q_st.buffer(len(durations))
            .buffer(len(amplitudes))
            .average()
            .save("Q")
        n_st.save("iteration")

#####################################
#  Open Communication with the QOP  #
#####################################
simulate = True
simulation_in_cloud = True

if simulate: 

    if simulation_in_cloud:
        client = QmSaas(email=cloud_username, password=cloud_password)
        instance = client.simulator(QoPVersion.v3_2_0)
        instance.spawn()
        qmm = QuantumMachinesManager(host=instance.host,
                                    port=instance.port,
                                    connection_headers=instance.default_connection_headers)
    else:
        qmm = QuantumMachinesManager(host=qop_ip, 
                                    cluster_name=cluster_name)

    simulation_config = SimulationConfig(duration=1_000) # duration is in units of clock cycles, i.e., 4 nanoseconds
    job = qmm.simulate(config, power_rabi, simulation_config)
    job.get_simulated_samples().con1.plot()
    if simulation_in_cloud:
        instance.close()
    plt.show()

else:

    qm = qmm.open_qm(config)

    job = qm.execute(power_rabi)

    results = fetching_tool(job, data_list=["I", "Q", "iteration"], mode="live")
    fig = plt.figure()
    interrupt_on_close(fig, job)
    while results.is_processing():
        I, Q, iteration = results.fetch_all()
        progress_counter(iteration, n_avg, start_time=results.get_start_time())
        plt.suptitle("Power Rabi")
        plt.subplot(211)
        plt.cla()
        plt.pcolor(amplitudes * x180_amp, 4 * durations, I)
        plt.ylabel("I quadrature [V]")
        plt.subplot(212)
        plt.cla()
        plt.pcolor(amplitudes * x180_amp, 4 * durations, Q)
        plt.xlabel("Rabi pulse amplitude [V]")
        plt.ylabel("Q quadrature [V]")
        plt.pause(1)
        plt.tight_layout()
    plt.show()

plt.figure()
results = fetching_tool(job, data_list=["I", "Q", "iteration"])
I, Q, iteration = results.fetch_all()
S = u.demod2volts(I + 1j * Q, readout_len)
R = np.abs(S)
phase = np.angle(S)
plt.suptitle("Power Rabi")
plt.subplot(211)
plt.pcolor(amplitudes * x180_amp, 4 * durations, I)
plt.ylabel("I quadrature [V]")
plt.subplot(212)
plt.pcolor(amplitudes * x180_amp, 4 * durations, Q)
plt.xlabel("Rabi pulse amplitude [V]")
plt.ylabel("Q quadrature [V]")
plt.tight_layout()
plt.show()
  1. This Python variable is used in the QUA program on line 30, and it used by the PPU to set up the QUA for-loop.
  2. The declaration of variables follows this syntax. As a good practice, we declare them at the top of the indented context manager block.

The specially designed PPU allows for real-time parametric changes to a pulse during its synthesis. In this context, we're focusing on real-time updates pulse's amplitude and duration for a power-time Rabi experiment.

power_time_rabi.py
# Go to line 36
import qm.qua as qua
from qm import QuantumMachinesManager
from OPX1000configuration import config, qop_ip, cluster_name, cloud_username, cloud_password # or OPXplusconfiguration, depending on your hardware
import matplotlib.pyplot as plt
import numpy as np
from qualang_tools.results import progress_counter, fetching_tool
from qualang_tools.plot import interrupt_on_close
from qm import SimulationConfig, LoopbackInterface
from qualang_tools.loops import from_array
import warnings

warnings.filterwarnings("ignore")

###################
# The QUA program #
###################

n_avg = 1 
amplitudes = np.linspace(0, 1.0, 5)
durations = np.arange(10, 14, 1)

with qua.program() as power_rabi:
    n = qua.declare(int)
    a = qua.declare(qua.fixed)
    t = qua.declare(int)
    I = qua.declare(qua.fixed)
    Q = qua.declare(qua.fixed) 
    I_st = qua.declare_stream()
    Q_st = qua.declare_stream()
    n_st = qua.declare_stream()

    with qua.for_(n, 0, n < n_avg, n + 1):
        with qua.for_(*from_array(a, amplitudes)): 
            with qua.for_(*from_array(t, durations)):
                qua.play("x180" * qua.amp(a), "qubit", duration=t)  # (1)!
                qua.align("qubit", "resonator")
                qua.measure(
                    "readout",
                    "resonator",
                    None,
                    qua.dual_demod.full("cos", "sin", I),
                    qua.dual_demod.full("minus_sin", "cos", Q),
                )
                qua.wait(thermalization_time // 4, "resonator")
                qua.save(I, I_st)
                qua.save(Q, Q_st)
        qua.save(n, n_st)

    with qua.stream_processing():
        I_st.buffer(len(durations))
            .buffer(len(amplitudes))
            .average()
            .save("I")
        Q_st.buffer(len(durations))
            .buffer(len(amplitudes))
            .average()
            .save("Q")
        n_st.save("iteration")

#####################################
#  Open Communication with the QOP  #
#####################################
simulate = True
simulation_in_cloud = True

if simulate: 

    if simulation_in_cloud:
        client = QmSaas(email=cloud_username, password=cloud_password)
        instance = client.simulator(QoPVersion.v3_2_0)
        instance.spawn()
        qmm = QuantumMachinesManager(host=instance.host,
                                    port=instance.port,
                                    connection_headers=instance.default_connection_headers)
    else:
        qmm = QuantumMachinesManager(host=qop_ip, 
                                    cluster_name=cluster_name)

    simulation_config = SimulationConfig(duration=1_000) # duration is in units of clock cycles, i.e., 4 nanoseconds
    job = qmm.simulate(config, power_rabi, simulation_config)
    job.get_simulated_samples().con1.plot()
    if simulation_in_cloud:
        instance.close()
    plt.show()

else:

    qm = qmm.open_qm(config)

    job = qm.execute(power_rabi)

    results = fetching_tool(job, data_list=["I", "Q", "iteration"], mode="live")
    fig = plt.figure()
    interrupt_on_close(fig, job)
    while results.is_processing():
        I, Q, iteration = results.fetch_all()
        progress_counter(iteration, n_avg, start_time=results.get_start_time())
        plt.suptitle("Power Rabi")
        plt.subplot(211)
        plt.cla()
        plt.pcolor(amplitudes * x180_amp, 4 * durations, I)
        plt.ylabel("I quadrature [V]")
        plt.subplot(212)
        plt.cla()
        plt.pcolor(amplitudes * x180_amp, 4 * durations, Q)
        plt.xlabel("Rabi pulse amplitude [V]")
        plt.ylabel("Q quadrature [V]")
        plt.pause(1)
        plt.tight_layout()
    plt.show()

plt.figure()
results = fetching_tool(job, data_list=["I", "Q", "iteration"])
I, Q, iteration = results.fetch_all()
S = u.demod2volts(I + 1j * Q, readout_len)
R = np.abs(S)
phase = np.angle(S)
plt.suptitle("Power Rabi")
plt.subplot(211)
plt.pcolor(amplitudes * x180_amp, 4 * durations, I)
plt.ylabel("I quadrature [V]")
plt.subplot(212)
plt.pcolor(amplitudes * x180_amp, 4 * durations, Q)
plt.xlabel("Rabi pulse amplitude [V]")
plt.ylabel("Q quadrature [V]")
plt.tight_layout()
plt.show()
  1. In the provided code, you can see the syntax for modifying pulse amplitude and duration using *qua.amp(a) and duration=t, respectively. When *qua.amp(a) is used, the PPU scales the waveform's amplitude based on the QUA variable a. On the other hand, specifying duration=t prompts the PPU to adjust the pulse's duration through real-time interpolation. Crucially, these adjustments are tied to QUA variables, which means that the scaling of amplitude and stretching of duration can change from one iteration to another. They can even be influenced by quantum measurement outcomes, given that the measure() function can update QUA variables with new data. It's also important to note that the PPU applies these modifications each time it encounters the play() command in the thread. Lastly, regardless of the number of for-loops used or the specific values you're adjusting, the utilized waveform and instruction memory remains consistent and minute indeed.

The for-loop in the QM platform allows efficient instruction repetition without consuming extensive waveform and instruction memory. In this code, three nested loops can produce hundreds of thousands of instruction repetitions.

power_time_rabi.py
# Go to lines 33-35
import qm.qua as qua
from qm import QuantumMachinesManager
from OPX1000configuration import config, qop_ip, cluster_name, cloud_username, cloud_password # or OPXplusconfiguration, depending on your hardware
import matplotlib.pyplot as plt
import numpy as np
from qualang_tools.results import progress_counter, fetching_tool
from qualang_tools.plot import interrupt_on_close
from qm import SimulationConfig, LoopbackInterface
from qualang_tools.loops import from_array
import warnings

warnings.filterwarnings("ignore")

###################
# The QUA program #
###################

n_avg = 1 
amplitudes = np.linspace(0, 1.0, 5)
durations = np.arange(10, 14, 1)

with qua.program() as power_rabi:
    n = qua.declare(int)
    a = qua.declare(qua.fixed)
    t = qua.declare(int)
    I = qua.declare(qua.fixed)
    Q = qua.declare(qua.fixed) 
    I_st = qua.declare_stream()
    Q_st = qua.declare_stream()
    n_st = qua.declare_stream()

    with qua.for_(n, 0, n < n_avg, n + 1):  # (1)!
        with qua.for_(*from_array(a, amplitudes)):  # (2)!
            with qua.for_(*from_array(t, durations)):
                play("x180" * qua.amp(a), "qubit", duration=t)
                qua.align("qubit", "resonator")
                measure(
                    "readout",
                    "resonator",
                    None,
                    qua.dual_demod.full("cos", "sin", I),
                    qua.dual_demod.full("minus_sin", "cos", Q),
                )
                qua.wait(thermalization_time // 4, "resonator")
                qua.save(I, I_st)
                qua.save(Q, Q_st)
        qua.save(n, n_st)

    with qua.stream_processing():
        I_st.buffer(len(durations))
            .buffer(len(amplitudes))
            .average()
            .qua.save("I")
        Q_st.buffer(len(durations))
            .buffer(len(amplitudes))
            .average()
            .qua.save("Q")
        n_st.qua.save("iteration")

#####################################
#  Open Communication with the QOP  #
#####################################
simulate = True
simulation_in_cloud = True

if simulate: 

    if simulation_in_cloud:
        client = QmSaas(email=cloud_username, password=cloud_password)
        instance = client.simulator(QoPVersion.v3_2_0)
        instance.spawn()
        qmm = QuantumMachinesManager(host=instance.host,
                                    port=instance.port,
                                    connection_headers=instance.default_connection_headers)
    else:
        qmm = QuantumMachinesManager(host=qop_ip, 
                                    cluster_name=cluster_name)

    simulation_config = SimulationConfig(duration=1_000) # duration is in units of clock cycles, i.e., 4 nanoseconds
    job = qmm.simulate(config, power_rabi, simulation_config)
    job.get_simulated_samples().con1.plot()
    if simulation_in_cloud:
        instance.close()
    plt.show()

else:

    qm = qmm.open_qm(config)

    job = qm.execute(power_rabi)

    results = fetching_tool(job, data_list=["I", "Q", "iteration"], mode="live")
    fig = plt.figure()
    interrupt_on_close(fig, job)
    while results.is_processing():
        I, Q, iteration = results.fetch_all()
        progress_counter(iteration, n_avg, start_time=results.get_start_time())
        plt.suptitle("Power Rabi")
        plt.subplot(211)
        plt.cla()
        plt.pcolor(amplitudes * x180_amp, 4 * durations, I)
        plt.ylabel("I quadrature [V]")
        plt.subplot(212)
        plt.cla()
        plt.pcolor(amplitudes * x180_amp, 4 * durations, Q)
        plt.xlabel("Rabi pulse amplitude [V]")
        plt.ylabel("Q quadrature [V]")
        plt.pause(1)
        plt.tight_layout()
    plt.show()

plt.figure()
results = fetching_tool(job, data_list=["I", "Q", "iteration"])
I, Q, iteration = results.fetch_all()
S = u.demod2volts(I + 1j * Q, readout_len)
R = np.abs(S)
phase = np.angle(S)
plt.suptitle("Power Rabi")
plt.subplot(211)
plt.pcolor(amplitudes * x180_amp, 4 * durations, I)
plt.ylabel("I quadrature [V]")
plt.subplot(212)
plt.pcolor(amplitudes * x180_amp, 4 * durations, Q)
plt.xlabel("Rabi pulse amplitude [V]")
plt.ylabel("Q quadrature [V]")
plt.tight_layout()
plt.show()
  1. Thanks to the PPU executing pulses very closely to the timing of a pulse sequence, we place the averaging loop as the outermost one which help us to overcome slow frequency drift as you perform your measurement.
  2. *from_array() belongs to our py-qua-tools helper function. The QUA instruction without the helper function looks like with qua.for_(a, 0.0, a < 1.01, a + 0.01):.

The SPU's operations are designed to minimize the post-processing needed on your computer. As data moves through the SPU pipelines, it's shaped and averaged before being saved to disk and accessed by your computer.

power_time_rabi.py
# Go to lines 46, 47, and 50-54
import qm.qua as qua
from qm import QuantumMachinesManager
from OPX1000configuration import config, qop_ip, cluster_name, cloud_username, cloud_password # or OPXplusconfiguration, depending on your hardware
import matplotlib.pyplot as plt
import numpy as np
from qualang_tools.results import progress_counter, fetching_tool
from qualang_tools.plot import interrupt_on_close
from qm import SimulationConfig, LoopbackInterface
from qualang_tools.loops import from_array
import warnings

warnings.filterwarnings("ignore")

###################
# The QUA program #
###################

n_avg = 1 
amplitudes = np.linspace(0, 1.0, 5)
durations = np.arange(10, 14, 1)

with qua.program() as power_rabi:
    n = qua.declare(int)
    a = qua.declare(qua.fixed)
    t = qua.declare(int)
    I = qua.declare(qua.fixed)
    Q = qua.declare(qua.fixed) 
    I_st = qua.declare_stream()
    Q_st = qua.declare_stream()
    n_st = qua.declare_stream()

    with qua.for_(n, 0, n < n_avg, n + 1):
        with qua.for_(*from_array(a, amplitudes)): 
            with qua.for_(*from_array(t, durations)):
                play("x180" * qua.amp(a), "qubit", duration=t)
                qua.align("qubit", "resonator")
                measure(
                    "readout",
                    "resonator",
                    None,
                    qua.dual_demod.full("cos", "sin", I),
                    qua.dual_demod.full("minus_sin", "cos", Q),
                )
                qua.wait(thermalization_time // 4, "resonator")
                qua.save(I, I_st)  # (1)!
                qua.save(Q, Q_st)
        qua.save(n, n_st)

    with qua.stream_processing():
        I_st.buffer(len(durations))  # (2)!
            .buffer(len(amplitudes))
            .average()
            .qua.save("I")
        Q_st.buffer(len(durations))
            .buffer(len(amplitudes))
            .average()
            .qua.save("Q")
        n_st.qua.save("iteration")

#####################################
#  Open Communication with the QOP  #
#####################################
simulate = True
simulation_in_cloud = True

if simulate: 

    if simulation_in_cloud:
        client = QmSaas(email=cloud_username, password=cloud_password)
        instance = client.simulator(QoPVersion.v3_2_0)
        instance.spawn()
        qmm = QuantumMachinesManager(host=instance.host,
                                    port=instance.port,
                                    connection_headers=instance.default_connection_headers)
    else:
        qmm = QuantumMachinesManager(host=qop_ip, 
                                    cluster_name=cluster_name)

    simulation_config = SimulationConfig(duration=1_000) # duration is in units of clock cycles, i.e., 4 nanoseconds
    job = qmm.simulate(config, power_rabi, simulation_config)
    job.get_simulated_samples().con1.plot()
    if simulation_in_cloud:
        instance.close()
    plt.show()

else:

    qm = qmm.open_qm(config)

    job = qm.execute(power_rabi)

    results = fetching_tool(job, data_list=["I", "Q", "iteration"], mode="live")
    fig = plt.figure()
    interrupt_on_close(fig, job)
    while results.is_processing():
        I, Q, iteration = results.fetch_all()
        progress_counter(iteration, n_avg, start_time=results.get_start_time())
        plt.suptitle("Power Rabi")
        plt.subplot(211)
        plt.cla()
        plt.pcolor(amplitudes * x180_amp, 4 * durations, I)
        plt.ylabel("I quadrature [V]")
        plt.subplot(212)
        plt.cla()
        plt.pcolor(amplitudes * x180_amp, 4 * durations, Q)
        plt.xlabel("Rabi pulse amplitude [V]")
        plt.ylabel("Q quadrature [V]")
        plt.pause(1)
        plt.tight_layout()
    plt.show()

plt.figure()
results = fetching_tool(job, data_list=["I", "Q", "iteration"])
I, Q, iteration = results.fetch_all()
S = u.demod2volts(I + 1j * Q, readout_len)
R = np.abs(S)
phase = np.angle(S)
plt.suptitle("Power Rabi")
plt.subplot(211)
plt.pcolor(amplitudes * x180_amp, 4 * durations, I)
plt.ylabel("I quadrature [V]")
plt.subplot(212)
plt.pcolor(amplitudes * x180_amp, 4 * durations, Q)
plt.xlabel("Rabi pulse amplitude [V]")
plt.ylabel("Q quadrature [V]")
plt.tight_layout()
plt.show()
  1. The I and Q variables live in the PPU and to pass them to the SPU we use qua.save("I", "I_st").

  2. To append SPU operations to the pipeline, you can use the . followed by the instruction name. Here you find that we are creating a 2D data structure by using two consecutive .buffer(), as well as performing a running-average as the 2D data structures are filled with .average(). Additional instructions, including arithmetic operations, can be found in the SP documentation.

Recall that the SPU and PPU work concurrently, and it is possible to fetch data from the SPU's hard disk concurrently as pipelines continue to be processed.

power_time_rabi.py
# Go to lines 93-102
import qm.qua as qua
from qm import QuantumMachinesManager
from OPX1000configuration import config, qop_ip, cluster_name, cloud_username, cloud_password # or OPXplusconfiguration, depending on your hardware
import matplotlib.pyplot as plt
import numpy as np
from qualang_tools.results import progress_counter, fetching_tool
from qualang_tools.plot import interrupt_on_close
from qm import SimulationConfig, LoopbackInterface
from qualang_tools.loops import from_array
import warnings

warnings.filterwarnings("ignore")

###################
# The QUA program #
###################

n_avg = 1 
amplitudes = np.linspace(0, 1.0, 5)
durations = np.arange(10, 14, 1)

with qua.program() as power_rabi:
    n = qua.declare(int)
    a = qua.declare(qua.fixed)
    t = qua.declare(int)
    I = qua.declare(qua.fixed)
    Q = qua.declare(qua.fixed) 
    I_st = qua.declare_stream()
    Q_st = qua.declare_stream()
    n_st = qua.declare_stream()

    with qua.for_(n, 0, n < n_avg, n + 1):
        with qua.for_(*from_array(a, amplitudes)): 
            with qua.for_(*from_array(t, durations)):
                play("x180" * qua.amp(a), "qubit", duration=t)
                qua.align("qubit", "resonator")
                measure(
                    "readout",
                    "resonator",
                    None,
                    qua.dual_demod.full("cos", "sin", I),
                    qua.dual_demod.full("minus_sin", "cos", Q),
                )
                qua.wait(thermalization_time // 4, "resonator")
                qua.save(I, I_st)
                qua.save(Q, Q_st)
        qua.save(n, n_st)

    with qua.stream_processing():
        I_st.buffer(len(durations))
            .buffer(len(amplitudes))
            .average()
            .qua.save("I")
        Q_st.buffer(len(durations))
            .buffer(len(amplitudes))
            .average()
            .qua.save("Q")
        n_st.qua.save("iteration")

#####################################
#  Open Communication with the QOP  #
#####################################
simulate = True
simulation_in_cloud = True

if simulate: 

    if simulation_in_cloud:
        client = QmSaas(email=cloud_username, password=cloud_password)
        instance = client.simulator(QoPVersion.v3_2_0)
        instance.spawn()
        qmm = QuantumMachinesManager(host=instance.host,
                                    port=instance.port,
                                    connection_headers=instance.default_connection_headers)
    else:
        qmm = QuantumMachinesManager(host=qop_ip, 
                                    cluster_name=cluster_name)

    simulation_config = SimulationConfig(duration=1_000) # duration is in units of clock cycles, i.e., 4 nanoseconds
    job = qmm.simulate(config, power_rabi, simulation_config)
    job.get_simulated_samples().con1.plot()
    if simulation_in_cloud:
        instance.close()
    plt.show()

else:

    qm = qmm.open_qm(config)

    job = qm.execute(power_rabi)

    results = fetching_tool(job, data_list=["I", "Q", "iteration"], mode="live") # (1)!
    fig = plt.figure()
    interrupt_on_close(fig, job) # (2)!
    while results.is_processing(): # (3)!
        I, Q, iteration = results.fetch_all() # (4)!
        progress_counter(iteration, n_avg, start_time=results.get_start_time())
        plt.suptitle("Power Rabi")
        plt.subplot(211)
        plt.cla()
        plt.pcolor(amplitudes * x180_amp, 4 * durations, I)
        plt.ylabel("I quadrature [V]")
        plt.subplot(212)
        plt.cla()
        plt.pcolor(amplitudes * x180_amp, 4 * durations, Q)
        plt.xlabel("Rabi pulse amplitude [V]")
        plt.ylabel("Q quadrature [V]")
        plt.pause(1)
        plt.tight_layout()
    plt.show()

plt.figure()
results = fetching_tool(job, data_list=["I", "Q", "iteration"])
I, Q, iteration = results.fetch_all()
S = u.demod2volts(I + 1j * Q, readout_len)
R = np.abs(S)
phase = np.angle(S)
plt.suptitle("Power Rabi")
plt.subplot(211)
plt.pcolor(amplitudes * x180_amp, 4 * durations, I)
plt.ylabel("I quadrature [V]")
plt.subplot(212)
plt.pcolor(amplitudes * x180_amp, 4 * durations, Q)
plt.xlabel("Rabi pulse amplitude [V]")
plt.ylabel("Q quadrature [V]")
plt.tight_layout()
plt.show()
  1. We're utilizing a helper function from py-qua-tools called fetching_tool to manage the data retrieval. By specifying mode="live", the function first waits for at least one item to be saved to the SPU's hard drive using .wait_for_value(1) before it proceeds with fetching. This ensures that we don't fetch a value of None.
  2. The helper function facilitates live-plotting. Even as the PPU continues to perform quantum experiments in the background, you have the option to halt() the process by closing the pyplot figure.
  3. By using results.is_processing(), you can gauge the SPU's processing status, which will either be True or False. When this is combined with a while loop, it makes live-plotting possible.
  4. This line of code retrieves the data stored on the SPU's hard drive and provides it as Python variables for subsequent plotting and analysis.

QUA-libs

You can find the full program in our qua-libs github repo in the following link and link.

Other Parametric Waveform Generation Features

The PPU also allows for other waveform modifications such as frequency chirp, truncation, and dynamic port allocation. We emphasize these because they can be executed using the qua.play() command in QUA.

Python for-loop instead of QUA for-loop

When using QUA in Python, it's easy to mistakenly use Python loops like for n in range(n_avg) instead of the QUA loop with qua.for_(n, 0, n < n_avg, n+1):. The Python loop unrolls and sends repeated instructions to the compiler, increasing memory usage. While this might work to some extent, it quickly consumes instruction memory. Using the QUA loop is more memory-efficient as it properly repeats instructions without the extra overhead.

Suggested usage of Python for-loop

Python simplifies the process of writing repetitive QUA instructions that might not be efficiently represented directly in QUA. For instance, to execute qua.play('x180'*qua.amp(a), 'qubit', duration=t) ten times, you can use for i in range(10) instead of copying and pasting. Think of Python as a helper for crafting QUA code. While you're editing on your computer, the actual processing is done in the PPU within your system.

Linking QUA program to Configuration

Check the highlighted lines below to see how power_time_rabi.py links to the configuration dictionary. Specifically, line 32 the QUA code qua.play("x180" * qua.amp(a), "qubit", duration=t) directly use the dictionary, and from the analysis of rabi oscillations you will get to calibrate the pulse's amplitude and duration.

The Configuration
configuration.py
...
x180_len = 40  # (1)!
x180_sigma = x180_len / 5
x180_amp = 0.35
x180_wf, x180_der_wf = np.array(
    drag_gaussian_pulse_waveforms(x180_amp, x180_len, x180_sigma, drag_coef, anharmonicity, AC_stark_detuning)
)  # (2)!
x180_I_wf = x180_wf
x180_Q_wf = x180_der_wf
# No DRAG when alpha=0, it's just a gaussian.

...

config = {
    ...
    "waveforms": {
    "const_wf": {"type": "constant", "sample": const_amp},
    "saturation_drive_wf": {"type": "constant", "sample": saturation_amp},
    "pi_wf": {"type": "constant", "sample": square_pi_amp},
    "pi_half_wf": {"type": "constant", "sample": square_pi_amp / 2},
    "const_flux_wf": {"type": "constant", "sample": const_flux_amp},
    "zero_wf": {"type": "constant", "sample": 0.0},
    "x90_I_wf": {"type": "arbitrary", "samples": x90_I_wf.tolist()},
    "x90_Q_wf": {"type": "arbitrary", "samples": x90_Q_wf.tolist()},
    "x180_I_wf": {"type": "arbitrary", "samples": x180_I_wf.tolist()}, # (3)!
    "x180_Q_wf": {"type": "arbitrary", "samples": x180_Q_wf.tolist()},
    "minus_x90_I_wf": {"type": "arbitrary", "samples": minus_x90_I_wf.tolist()},
    "minus_x90_Q_wf": {"type": "arbitrary", "samples": minus_x90_Q_wf.tolist()},
    "y90_Q_wf": {"type": "arbitrary", "samples": y90_Q_wf.tolist()},
    "y90_I_wf": {"type": "arbitrary", "samples": y90_I_wf.tolist()},
    "y180_Q_wf": {"type": "arbitrary", "samples": y180_Q_wf.tolist()},
    "y180_I_wf": {"type": "arbitrary", "samples": y180_I_wf.tolist()},
    "minus_y90_Q_wf": {"type": "arbitrary", "samples": minus_y90_Q_wf.tolist()},
    "minus_y90_I_wf": {"type": "arbitrary", "samples": minus_y90_I_wf.tolist()},
    "readout_wf": {"type": "constant", "sample": readout_amp},
},
}
  1. We associate the x180 operation with a DRAG-gaussian pulse. The parameters for these waveforms are detailed in lines 61 to 63, like the pulse amplitude in volts, the pulse duration in nanoseconds, and the gaussian sigma.

  2. We first generate a numpy array that outlines the waveform's shape point by point. The sampling rate is set at 1 GSa/sec, matching the DAC's rate.

  3. Lines 317 and 318 show how to load custom waveforms. We first set the type as arbitrary. Then, we convert the DRAG-gaussian numpy arrays into Python lists to provide the samples.

Work with any arbitrary waveform

In the Rabi experiment, we used a DRAG-gaussian envelope. However, because you define it in Python and then load it into the quantum machine through configuration, you can actually use any desired arbitrary waveform, such as a flat-top gaussian, as a pulse.

Now it is your turn

  1. Reconfigure the Rabi sequence, place the amplitude sweep as the outer loop, the duration as the middle, and the shots as the innermost loop; then, how should the SPU pipelines be set up to retrieve averaged 2D arrays? Hint: use the information in here.
  2. Modify the update condition to: with qua.for_(a, 0, a < a_max, a): (line 31); note that we disabled the update condition. To implement the iterations once again, which QUA code line would you add to the indented instructions? Implement it.
  3. Generate a simplified program to test the following. Instead of using qua.assign(a, a + da), use the Python command a += da, and then fetch the results using qua.save(a, a_st) to your computer.
  4. Instead of using a QUA for-loop for generating 100k shots in the outer averaging loop (line 30), use a Python loop -> for n in range(100_000).
  5. What occurs if you have a=7.8 and do assign(a, a+1)? Similarly, what happens with a=-7.8 when you apply assign(a, a-1)?
  6. What occurs if you have n=4_294_966_296 and do assign(n, n+10_000)? Similarly, what happens with n=-4_294_966_296 when you do assign(n, n-10_000)?

Test your knowledge

  1. Consider using flat-top Gaussian pulse instead of a standard Gaussian pulse. In a time-Rabi sequence, if you wish to scan only the flat-top section while maintaining a constant rise and fall, how would you use duration to achieve it?
  2. If the Gaussian pulse's amplitude is to 0.17 V in the configuration dictionary, and during a power-Rabi you sweep the a parameter up to 1.5, what is the highest voltage measured at the analog outputs?
  3. What would occur if you set the Gaussian pulse amplitude to 0.4 V, and sweep the a parameter up to 1.5?
  4. Why do you need to multiply amplitudes * x180_amp and 4 * durations when plotting your results in line 109 of the QUA program?

Coming up

In the upcoming module, you'll gain a clear understanding of a unique real-time feature of the QM platform: phase updates. This will be demonstrated through the implementation of the Ramsey experiment, where you'll modify the phase of the second x90 pulse while still operating near the qubit resonance frequency.