Qibolab provides support to different quantum laboratories.

Each lab configuration is implemented using a qibolab.platform.Platform object which orchestrates instruments, qubits and channels and provides the basic features for executing pulses. Therefore, the Platform enables the user to interface with all the required lab instruments at the same time with minimum effort.

The API reference section provides a description of all the attributes and methods of the Platform. Here, let’s focus on the main elements.

In the platform, the main methods can be divided in different sections:

  • functions save and change qubit parameters (dump, update)

  • functions to coordinate the instruments (connect, setup, start, stop, disconnect)

  • functions to execute experiments (execute_pulse_sequence, execute_pulse_sequences, sweep)

  • functions to initialize gates (create_RX90_pulse, create_RX_pulse, create_CZ_pulse, create_MZ_pulse, create_qubit_drive_pulse, create_qubit_readout_pulse, create_RX90_drag_pulse, create_RX_drag_pulse)

  • setters and getters of channel/qubit parameters (local oscillator parameters, attenuations, gain and biases)

The idea of the Platform is to serve as the only object exposed to the user, so that we can deploy experiments, without any need of going into the low-level instrument-specific code.

For example, let’s first define a platform (that we consider to be a single qubit platform) using the create method presented in How to connect Qibolab to your lab?:

from qibolab import create_platform

platform = create_platform("my_platform")

Now we connect and start the instruments (note that we, the user, do not need to know which instruments are connected).


We can easily print some of the parameters of the channels (similarly we can set those, if needed):


If the get_method does not apply to the platform (for example there is no local oscillator, to TWPA or no flux tunability…) a NotImplementedError will be raised.

print(f"Drive LO frequency: {platform.qubits[0].drive.lo_frequency}")
print(f"Readout LO frequency: {platform.qubits[0].readout.lo_frequency}")
print(f"TWPA LO frequency: {platform.qubits[0].twpa.lo_frequency}")

print(f"Qubit bias: {platform.get_bias(0)}")
print(f"Qubit attenuation: {platform.get_attenuation(0)}")

Now we can create a simple sequence (again, without explicitly giving any qubit specific parameter, as these are loaded automatically from the platform, as defined in the runcard):

from qibolab.pulses import PulseSequence

ps = PulseSequence()
ps.add(platform.create_RX_pulse(qubit=0, start=0))   # start time is in ns
ps.add(platform.create_RX_pulse(qubit=0, start=100))
ps.add(platform.create_MZ_pulse(qubit=0, start=200))

Now we can execute the sequence on hardware:

results = platform.execute_pulse_sequence(ps)

Finally, we can stop instruments and close connections.


Dummy platform#

In addition to the real instruments presented in the Instruments section, Qibolab provides the qibolab.instruments.dummy.DummyInstrument. This instrument represents a controller that returns random numbers of the proper shape when executing any pulse sequence. This instrument is also part of the dummy platform which is defined in qibolab.dummy and can be initialized as

from qibolab import create_platform

platform = create_platform("dummy")

This platform is equivalent to real platforms in terms of attributes and functions, but returns just random numbers. It is useful for testing parts of the code that do not necessarily require access to an actual quantum hardware platform.


The qibolab.qubits.Qubit class serves as a comprehensive representation of a physical qubit within the Qibolab framework. It encapsulates three fundamental elements crucial to qubit control and operation:

Channels play a pivotal role in connecting the quantum system to the control infrastructure. They are optional and encompass distinct types, each serving a specific purpose:

  • readout (from controller device to the qubits)

  • feedback (from qubits to controller)

  • twpa (pump to the TWPA)

  • drive

  • flux

  • flux_coupler

The Qubit class allows you to set and manage several key parameters that influence qubit behavior. These parameters are typically extracted from the runcard during platform initialization.


In Qibolab, channels serve as abstractions for physical wires within a laboratory setup. Each qibolab.channels.Channel object corresponds to a specific type of connection, simplifying the process of controlling quantum pulses across the experimental setup.

Various types of channels are typically present in a quantum laboratory setup, including:

  • the drive line

  • the readout line (from device to qubit)

  • the feedback line (from qubit to device)

  • the flux line

  • the TWPA pump line

A channel is typically associated with a specific port on a control instrument, with port-specific properties like “attenuation” and “gain” that can be managed using provided getter and setter methods.

The idea of channels is to streamline the pulse execution process. When initiating a pulse, the platform identifies the corresponding channel for the pulse type and directs it to the appropriate port on the control instrument. For instance, to deliver a drive pulse to a qubit, the platform references the qubit’s associated channel and delivers the pulse to the designated port.

In setups involving frequency-specific pulses, a local oscillator (LO) might be required for up-conversion. Although logically distinct from the qubit, the LO’s frequency must align with the pulse requirements. Qibolab accommodates this by enabling the assignment of a qibolab.instruments.oscillator.LocalOscillator object to the relevant channel. The controller’s driver ensures the correct pulse frequency is set based on the LO’s configuration.

Let’s explore an example using an RFSoC controller. Note that while channels are defined in a device-independent manner, the port parameter varies based on the specific instrument.

from qibolab.channels import Channel, ChannelMap
from qibolab.instruments.rfsoc import RFSoC

controller = RFSoC()
channel1 = Channel("my_channel_name_1", port=controller[1])
channel2 = Channel("my_channel_name_2", port=controller[2])
channel3 = Channel("my_channel_name_3", port=controller[3])

Channels are then organized in qibolab.channels.ChannelMap to be passed as a single argument to the platform. Following the tutorial in How to connect Qibolab to your lab?, we can continue the initialization:

from qibolab.serialize import load_qubits

ch_map = ChannelMap()
ch_map |= channel1
ch_map |= channel2
ch_map |= channel3

qubits, pairs = load_qubits, load_runcard
runcard = load_runcard(runcard_path)
qubits, pairs = load_qubits(runcard)

qubits[0].drive = channel1
qubits[0].readout = channel2
qubits[0].feedback = channel3

Where, in the last lines, we assign the channels to the qubits.

To assign local oscillators, the procedure is simple:

from qibolab.instruments.erasynth import ERA as LocalOscillator

local_oscillator = LocalOscillator("NameLO", LO_ADDRESS)
local_oscillator.frequency = 6e9  # Hz
local_oscillator.power = 5  # dB
channel2.local_oscillator = local_oscillator


In Qibolab, an extensive API is available for working with pulses and pulse sequences, a fundamental aspect of quantum experiments. At the heart of this API is the qibolab.pulses.Pulse object, which empowers users to define and customize pulses with specific parameters.

The API provides specialized subclasses tailored to the main types of pulses typically used in quantum experiments:

Each pulse is associated with a channel and a qubit. Additionally, pulses are defined by a shape, represented by a subclass of qibolab.pulses.PulseShape. Qibolab offers a range of pre-defined pulse shapes:

To illustrate, here are some examples of single pulses using the Qibolab API:

from qibolab.pulses import Pulse, Rectangular

pulse = Pulse(
    start=0,  # Timing, always in nanoseconds (ns)
    duration=40,  # Pulse duration in ns
    amplitude=0.5,  # Amplitude relative to instrument range
    frequency=1e8,  # Frequency in Hz
    relative_phase=0,  # Phase in radians
    type="qd",  # Enum type: :class:`qibolab.pulses.PulseType`

In this way, we defined a rectangular drive pulse using the generic Pulse object. Alternatively, you can achieve the same result using the dedicated qibolab.pulses.DrivePulse object:

from qibolab.pulses import DrivePulse, Rectangular

pulse = DrivePulse(
    start=0,  # timing, in all qibolab, is expressed in ns
    amplitude=0.5,  # this amplitude is relative to the range of the instrument
    frequency=1e8,  # frequency are in Hz
    relative_phase=0,  # phases are in radians

Both the Pulses objects and the PulseShape object have useful plot functions and several different various helper methods.

To organize pulses into sequences, Qibolab provides the qibolab.pulses.PulseSequence object. Here’s an example of how you can create and manipulate a pulse sequence:

from qibolab.pulses import PulseSequence

sequence = PulseSequence()


print(f"Total duration: {sequence.duration}")

sequence_ch1 = sequence.get_channel_pulses("channel1")  # Selecting pulses on channel 1
print(f"We have {sequence_ch1.count} pulses on channel 1.")


Pulses in PulseSequences are ordered automatically following the start time (and the channel if needed). Not by the definition order.

When conducting experiments on quantum hardware, pulse sequences are vital. Assuming you have already initialized a platform, executing an experiment is as simple as:

result = my_platform.execute_pulse_sequence(sequence)

Lastly, when conducting an experiment, it is not always required to define a pulse from scratch. Usual pulses, such as pi-pulses or measurements, are already defined in the platform runcard and can be easily initialized with platform methods. These are relying on parameters held in the Native data structures. Typical experiments may include both pre-defined pulses and new ones:

sequence = PulseSequence()

results = my_platform.execute_pulse_sequence(sequence, options=options)


options is an qibolab.execution_parameters.ExecutionParameters object, detailed in a separate section.


Sweeper objects, represented by the qibolab.sweeper.Sweeper class, stand as a crucial component in experiments and calibration tasks within the Qibolab framework.

Consider a scenario where a resonator spectroscopy experiment is performed. This process involves a sequence of steps:

  1. Define a pulse sequence.

  2. Define a readout pulse with frequency A.

  3. Execute the sequence.

  4. Define a new readout pulse with frequency \(A + \epsilon\).

  5. Execute the sequence again.

  6. Repeat for increasing frequencies \(A + 2 \epsilon\), \(A + 3 \epsilon\), and so on.

This approach is suboptimal and time-consuming, mainly due to the frequent communication between the control device and the Qibolab user after each execution. Such communication overhead significantly extends experiment duration.

In supported control devices, an efficient technique involves defining a “sweeper” or a parameter scan directly on the device. This scan, applied to specific parameters, allows multiple variations to be executed in a single communication round, drastically reducing experiment time.

To address the inefficiency, Qibolab introduces the concept of Sweeper objects.

Sweeper objects in Qibolab are characterized by a qibolab.sweeper.Parameter. This parameter, crucial to the sweeping process, can be one of several types:

  • Frequency

  • Amplitude

  • Duration

  • Relative_phase

  • Start

  • Attenuation

  • Gain

  • Bias

The first group includes parameters of the pulses, while the second group include parameters of a different type that, in qibolab, are linked to a qubit object.

To designate the qubit or pulse to which a sweeper is applied, you can utilize the pulses or qubits parameter within the Sweeper object.


It is possible to simultaneously execute the same sweeper on different pulses or qubits. The pulses or qubits attribute is designed as a list, allowing for this flexibility.

To effectively specify the sweeping behavior, Qibolab provides the values attribute along with the type attribute.

The values attribute comprises an array of numerical values that define the sweeper’s progression. To facilitate multi-qubit execution, these numbers can be interpreted in three ways:

  • Absolute Values: Represented by qibolab.sweeper.PulseType.ABSOLUTE, these values are used directly.

  • Relative Values with Offset: Utilizing qibolab.sweeper.PulseType.OFFSET, these values are relative to a designated base value, corresponding to the pulse or qubit value.

  • Relative Values with Factor: Employing qibolab.sweeper.PulseType.FACTOR, these values are scaled by a factor from the base value, akin to a multiplier.

For offset and factor sweepers, the base value is determined by the respective pulse or qubit value.

Let’s see some examples. Consider now a system with three qubits (qubit 0, qubit 1, qubit 2) with resonator frequency at 4 GHz, 5 GHz and 6 GHz. A tipical resonator spectroscopy experiment could be defined with:

sequence = PulseSequence()
sequence.add(my_platform.create_MZ_pulse(0))  # readout pulse for qubit 0 at 4 GHz
sequence.add(my_platform.create_MZ_pulse(1))  # readout pulse for qubit 1 at 5 GHz
sequence.add(my_platform.create_MZ_pulse(2))  # readout pulse for qubit 2 at 6 GHz

sweeper = Sweeper(
    values=np.arange(-200_000, +200_000, 1),  # define an interval of swept values
    pulses=[sequence[0], sequence[1], sequence[2]],

results = my_platform.sweep(sequence, options, sweeper)


options is an qibolab.execution_parameters.ExecutionParameters object, detailed in a separate section.

In this way, we first define a sweeper with an interval of 400 MHz (-200 MHz — 200 MHz), assigning it to all three readout pulses and setting is as an offset sweeper. The resulting probed frequency will then be:
  • for qubit 0: [3.8 GHz, 4.2 GHz]

  • for qubit 1: [4.8 GHz, 5.2 GHz]

  • for qubit 2: [5.8 GHz, 6.2 GHz]

If we had used the qibolab.sweeper.SweeperType absolute, we would have probed for all qubits the same frequencies [-200 MHz, 200 MHz].


The default qibolab.sweeper.SweeperType is absolute!

For factor sweepers, usually useful when dealing with amplitudes, the base value is multipled by the values set.

It is possible to define and executes multiple sweepers at the same time. For example:

sequence = PulseSequence()


sweeper_freq = Sweeper(
    values=np.arange(-100_000, +100_000, 10_000),
sweeper_amp = Sweeper(
    values=np.arange(0, 1.5, 0.1),

results = my_platform.sweep(sequence, options, sweeper_freq, sweeper_amp)

Let’s say that the RX pulse has, from the runcard, a frequency of 4.5 GHz and an amplitude of 0.3, the parameter space probed will be:

  • amplitudes: [0, 0.03, 0.06, 0.09, 0.12, …, 0.39, 0.42]

  • frequencies: [4.4999, 4.49991, 4.49992, …., 4.50008, 4.50009] (GHz)


Different control devices may have different limitations on the sweepers. It is possible that the sweeper will raise an error, if not supported, or that it will be automatically converted as a list of pulse sequences to perform sequentially.

Execution Parameters#

In the course of several examples, you’ve encountered the options argument in function calls like:

res = my_platform.execute_pulse_sequence(..., options=options)
res = my_platform.sweep(..., options=options)

Let’s now delve into the details of the options parameter and understand its components.

The options parameter, represented by the qibolab.execution_parameters.ExecutionParameters class, is a vital element for every hardware execution. It encompasses essential information that tailors the execution to specific requirements:

  • nshots: Specifies the number of experiment repetitions.

  • relaxation_time: Introduces a wait time between repetitions, measured in nanoseconds (ns).

  • fast_reset: Enables or disables fast reset functionality, if supported; raises an error if not supported.

  • acquisition_type: Determines the acquisition mode for results.

  • averaging_mode: Defines the mode for result averaging.

The first three parameters are straightforward in their purpose. However, let’s take a closer look at the last two parameters.

Supported acquisition types, accessible via the qibolab.execution_parameters.AcquisitionType enumeration, include:

  • Discrimination: Distinguishes states based on acquired voltages.

  • Integration: Returns demodulated and integrated waveforms.

  • Raw: Offers demodulated, yet unintegrated waveforms.

Supported averaging modes, available through the qibolab.execution_parameters.AveragingMode enumeration, consist of:

  • Cyclic: Provides averaged results, yielding a single IQ point per measurement.

  • Singleshot: Supplies non-averaged results.


Two averaging modes actually exists: cyclic and sequential. In sequential mode, a sweeper is executed with the repetition loop nested inside, while cyclic mode places the sweeper as the outermost loop. Cyclic execution generally offers better noise resistance. Ideally, use the cyclic mode. However, some devices lack support for it and will automatically convert it to sequential execution.


Within the Qibolab API, a variety of result types are available, contingent upon the chosen acquisition options. These results can be broadly classified into three main categories, based on the AcquisitionType:

Furthermore, depending on whether results are averaged or not, they can be presented in an averaged version (as seen in qibolab.results.AveragedIntegratedResults).

The result categories align as follows:

  • AveragingMode: cyclic or sequential ->
    • AcquisitionType: integration -> qibolab.results.AveragedIntegratedResults

    • AcquisitionType: raw -> qibolab.results.AveragedRawWaveformResults

    • AcquisitionType: discrimination -> qibolab.results.AveragedSampleResults

  • AveragingMode: singleshot ->
    • AcquisitionType: integration -> qibolab.results.IntegratedResults

    • AcquisitionType: raw -> qibolab.results.RawWaveformResults

    • AcquisitionType: discrimination -> qibolab.results.SampleResults

Let’s now delve into a typical use case for result objects within the qibolab framework:

sequence = PulseSequence()

options = ExecutionParameters(

res = my_platform.execute_pulse_sequence(sequence, options=options)

The res object will manifest as a dictionary, mapping the measurement pulse serial to its corresponding results.

The values related to the results will be find in the voltages attribute for IntegratedResults and RawWaveformResults, while for SampleResults the values are in samples.

While for execution of sequences the results represent single measurements, but what happens for sweepers? the results will be upgraded: from values to arrays and from arrays to matrices.

The shape of the values of an integreted acquisition with 2 sweepers will be:

shape = (options.nshots, len(sweeper1.values), len(sweeper2.values))

Transpiler and Compiler#

While pulse sequences can be directly deployed using a platform, circuits need to first be transpiled and compiled to the equivalent pulse sequence. This procedure typically involves the following steps:

  1. The circuit needs to respect the chip topology, that is, two-qubit gates can only target qubits that share a physical connection. To satisfy this constraint SWAP gates may need to be added to rearrange the logical qubits.

  2. All gates are transpiled to native gates, which represent the universal set of gates that can be implemented (via pulses) in the chip.

  3. Native gates are compiled to a pulse sequence.

The transpilation and compilation process is taken care of automatically by the qibolab.backends.QibolabBackend when a circuit is executed, using qibolab.transpilers.abstract.Transpiler and qibolab.compilers.compiler.Compiler. The transpiler is responsible for steps 1 and 2, while the compiler for step 3 of the list above. In order to accomplish this, several transpilers are provided, some of which are listed below:

Custom transpilers can be added by inheriting the abstract qibolab.transpilers.abstract.Transpiler class.

Once a circuit has been transpiled, it is converted to a qibolab.pulses.PulseSequence by the qibolab.compilers.compiler.Compiler. This is a container of rules which define how each native gate can be translated to pulses. A rule is a Python function that accepts a Qibo gate and a platform object and returns the qibolab.pulses.PulseSequence implementing this gate and a dictionary with potential virtual-Z phases that need to be applied in later pulses. Examples of rules can be found on qibolab.compilers.default, which defines the default rules used by Qibolab.


Rules return a qibolab.pulses.PulseSequence for each gate, instead of a single pulse, because some gates such as the U3 or two-qubit gates, require more than one pulses to be implemented.


Each quantum platform supports a specific set of native gates, which are the quantum operations that have been calibrated. If this set is universal any circuit can be transpiled and compiled to a pulse sequence which is then deployed in the given platform.

qibolab.native provides data containers for holding the pulse parameters required for implementing every native gate. Every qibolab.qubits.Qubit object contains a qibolab.native.SingleQubitNatives object which holds the parameters of its native single-qubit gates, while each qibolab.qubits.QubitPair objects contains a qibolab.native.TwoQubitNatives object which holds the parameters of the native two-qubit gates acting on the pair.

Each native gate is represented by a qibolab.native.NativePulse or qibolab.native.NativeSequence which contain all the calibrated parameters and can be converted to an actual qibolab.pulses.PulseSequence that is then executed in the platform. Typical single-qubit native gates are the Pauli-X gate, implemented via a pi-pulse which is calibrated using Rabi oscillations and the measurement gate, implemented via a pulse sent in the readout line followed by an acquisition. For a universal set of single-qubit gates, the RX90 (pi/2-pulse) gate is required, which is implemented by halving the amplitude of the calibrated pi-pulse. U3, the most general single-qubit gate can be implemented using two RX90 pi-pulses and some virtual Z-phases which are included in the phase of later pulses.

Typical two-qubit native gates are the CZ and iSWAP, with their availability being platform dependent. These are implemented with a sequence of flux pulses, potentially to multiple qubits, and virtual Z-phases. Depending on the platform and the quantum chip architecture, two-qubit gates may require pulses acting on qubits that are not targeted by the gate. The qibolab.native.NativeType flag is used for communicating the set of available native two-qubit gates to the transpiler.


One the key features of qibolab is its support for multiple different instruments. A list of all the supported instruments follows:

Controllers (subclasses of qibolab.instruments.abstract.Controller):
Other Instruments (subclasses of qibolab.instruments.abstract.Instrument):

Instruments all implement a set of methods:

  • connect

  • setup

  • start

  • stop

  • disconnect

While the controllers, the main instruments in a typical setup, add other two methods:

  • execute_pulse_sequence

  • sweep

Some more detail on the interal functionalities of instruments is given in How to add a new instrument in Qibolab?

The most important instruments are the controller, the following is a table of the current supported (or not supported) features, dev stands for under development:

Supported features#






Arbitrary pulse sequence





Arbitrary waveforms





Multiplexed readout





Hardware classification





Fast reset





Device simulation





RTS frequency





RTS amplitude





RTS duration





RTS start





RTS relative phase





RTS 2D any combination





Sequence unrolling





Hardware averaging





Singleshot (no averaging)





Integrated acquisition





Classified acquisition





Raw waveform acquisition





Zurich Instruments#

Qibolab has been tested with the following instrument cluster:

  • 1 SHFQC (Superconducting Hybrid Frequency Converter)

  • 2 HDAWGs (High-Density Arbitrary Waveform Generators)

  • 1 PQSC (Programmable Quantum System Controller)

The integration of Qibolab with the instrument cluster is facilitated through the LabOneQ Python library that handles communication and coordination with the instruments.

Quantum Machines#

Tested with a cluster of nine OPX+ controllers, using QOP213 and QOP220.

Qibolab is communicating with the instruments using the QUA language, via the qm-qua and qualang-tools Python libraries.


Supports the following Instruments:

  • Cluster

  • Cluster QRM-RF

  • Cluster QCM-RF

  • Cluster QCM

Compatible with qblox-instruments driver 0.9.0 (28/2/2023).


Compatible and tested with:

  • Xilinx RFSoC4x2

  • Xilinx ZCU111

  • Xilinx ZCU216

Technically compatible with any board running qibosoq.