Source code for qibolab.instruments.qblox.sequencer
import numpy as np
from qblox_instruments.qcodes_drivers.sequencer import Sequencer as QbloxSequencer
from qibolab.instruments.qblox.q1asm import Program
from qibolab.pulses import Pulse, PulseSequence, PulseType
from qibolab.sweeper import Parameter, Sweeper
SAMPLING_RATE = 1
"""Sampling rate for qblox instruments in GSps."""
[docs]class WaveformsBuffer:
"""A class to represent a buffer that holds the unique waveforms used by a
sequencer.
Attributes:
unique_waveforms (list): A list of unique Waveform objects.
available_memory (int): The amount of memory available expressed in numbers of samples.
"""
SIZE: int = 16383
[docs] class NotEnoughMemory(Exception):
"""An error raised when there is not enough memory left to add more
waveforms."""
[docs] class NotEnoughMemoryForBaking(Exception):
"""An error raised when there is not enough memory left to bake
pulses."""
def __init__(self):
"""Initialises the buffer with an empty list of unique waveforms."""
self.unique_waveforms: list = [] # Waveform
self.available_memory: int = WaveformsBuffer.SIZE
[docs] def add_waveforms(
self, pulse: Pulse, hardware_mod_en: bool, sweepers: list[Sweeper]
):
"""Adds a pair of i and q waveforms to the list of unique waveforms.
Waveforms are added to the list if they were not there before.
Each of the waveforms (i and q) is processed individually.
Args:
waveform_i (Waveform): A Waveform object containing the samples of the real component of the pulse wave.
waveform_q (Waveform): A Waveform object containing the samples of the imaginary component of the pulse wave.
Raises:
NotEnoughMemory: If the memory needed to store the waveforms in more than the memory avalible.
"""
pulse_copy = pulse.copy()
for sweeper in sweepers:
if sweeper.pulses and sweeper.parameter == Parameter.amplitude:
if pulse in sweeper.pulses:
pulse_copy.amplitude = 1
baking_required = False
for sweeper in sweepers:
if sweeper.pulses and sweeper.parameter == Parameter.duration:
if pulse in sweeper.pulses:
baking_required = True
values = sweeper.get_values(pulse.duration)
if not baking_required:
if hardware_mod_en:
waveform_i, waveform_q = pulse_copy.envelope_waveforms(SAMPLING_RATE)
else:
waveform_i, waveform_q = pulse_copy.modulated_waveforms(SAMPLING_RATE)
pulse.waveform_i = waveform_i
pulse.waveform_q = waveform_q
if (
waveform_i not in self.unique_waveforms
or waveform_q not in self.unique_waveforms
):
memory_needed = 0
if not waveform_i in self.unique_waveforms:
memory_needed += len(waveform_i)
if not waveform_q in self.unique_waveforms:
memory_needed += len(waveform_q)
if self.available_memory >= memory_needed:
if not waveform_i in self.unique_waveforms:
self.unique_waveforms.append(waveform_i)
if not waveform_q in self.unique_waveforms:
self.unique_waveforms.append(waveform_q)
self.available_memory -= memory_needed
else:
raise WaveformsBuffer.NotEnoughMemory
else:
pulse.idx_range = self.bake_pulse_waveforms(
pulse_copy, values, hardware_mod_en
)
[docs] def bake_pulse_waveforms(
self, pulse: Pulse, values: list(), hardware_mod_en: bool
): # bake_pulse_waveforms(self, pulse: Pulse, values: list(int), hardware_mod_en: bool):
"""Generates and stores a set of i and q waveforms required for a pulse
duration sweep.
These waveforms are generated and stored in a predefined order so that they can later be retrieved within the
sweeper q1asm code. It bakes pulses from as short as 1ns, padding them at the end with 0s if required so that
their length is a multiple of 4ns. It also supports the modulation of the pulse both in hardware (default)
or software.
With no other pulses stored in the sequencer memory, it supports values up to range(1, 126) for regular pulses and
range(1, 180) for flux pulses.
Args:
pulse (:class:`qibolab.pulses.Pulse`): The pulse to be swept.
values (list(int)): The list of values to sweep the pulse duration with.
hardware_mod_en (bool): If set to True the pulses are assumed to be modulated in hardware and their
envelope waveforms are uploaded; if False, software modulated waveforms are uploaded.
Returns:
idx_range (numpy.ndarray): An array with the indices of the set of pulses. For each pulse duration in
`values` the i component is saved in the next avalable index, followed by the q component. For flux
pulses, since both i and q components are equal, they are only saved once.
Raises:
NotEnoughMemory: If the memory needed to store the waveforms in more than the memory avalible.
"""
# In order to generate waveforms for each duration value, the pulse will need to be modified.
# To avoid any conflicts, make a copy of the pulse first.
pulse_copy = pulse.copy()
# there may be other waveforms stored already, set first index as the next available
first_idx = len(self.unique_waveforms)
if pulse.type == PulseType.FLUX:
# for flux pulses, store i waveforms
idx_range = np.arange(first_idx, first_idx + len(values), 1)
for duration in values:
pulse_copy.duration = duration
if hardware_mod_en:
waveform = pulse_copy.envelope_waveform_i(SAMPLING_RATE)
else:
waveform = pulse_copy.modulated_waveform_i(SAMPLING_RATE)
padded_duration = int(np.ceil(duration / 4)) * 4
memory_needed = padded_duration
padding = np.zeros(padded_duration - duration)
waveform.data = np.append(waveform.data, padding)
if self.available_memory >= memory_needed:
self.unique_waveforms.append(waveform)
self.available_memory -= memory_needed
else:
raise WaveformsBuffer.NotEnoughMemoryForBaking
else:
# for any other pulse type, store both i and q waveforms
idx_range = np.arange(first_idx, first_idx + len(values) * 2, 2)
for duration in values:
pulse_copy.duration = duration
if hardware_mod_en:
waveform_i, waveform_q = pulse_copy.envelope_waveforms(
SAMPLING_RATE
)
else:
waveform_i, waveform_q = pulse_copy.modulated_waveforms(
SAMPLING_RATE
)
padded_duration = int(np.ceil(duration / 4)) * 4
memory_needed = padded_duration * 2
padding = np.zeros(padded_duration - duration)
waveform_i.data = np.append(waveform_i.data, padding)
waveform_q.data = np.append(waveform_q.data, padding)
if self.available_memory >= memory_needed:
self.unique_waveforms.append(waveform_i)
self.unique_waveforms.append(waveform_q)
self.available_memory -= memory_needed
else:
raise WaveformsBuffer.NotEnoughMemoryForBaking
return idx_range
[docs]class Sequencer:
"""A class to extend the functionality of qblox_instruments Sequencer.
A sequencer is a hardware component synthesised in the instrument FPGA, responsible for fetching waveforms from
memory, pre-processing them, sending them to the DACs, and processing the acquisitions from the ADCs (QRM modules).
https://qblox-qblox-instruments.readthedocs-hosted.com/en/master/documentation/sequencer.html
This class extends the sequencer functionality by holding additional data required when
processing a pulse sequence:
- the sequencer number,
- the sequence of pulses to be played,
- a buffer of unique waveforms, and
- the four components of the sequence file:
- waveforms dictionary
- acquisition dictionary
- weights dictionary
- program
Attributes:
device (QbloxSequencer): A reference to the underlying `qblox_instruments.qcodes_drivers.sequencer.Sequencer`
object. It can be used to access other features not directly exposed by this wrapper.
https://qblox-qblox-instruments.readthedocs-hosted.com/en/master/documentation/sequencer.html
number (int): An integer between 0 and 5 that identifies the number of the sequencer.
pulses (PulseSequence): The sequence of pulses to be played by the sequencer.
waveforms_buffer (WaveformsBuffer): A buffer of unique waveforms to be played by the sequencer.
waveforms (dict): A dictionary containing the waveforms to be played by the sequencer in qblox format.
acquisitions (dict): A dictionary containing the list of acquisitions to be made by the sequencer in qblox
format.
weights (dict): A dictionary containing the list of weights to be used by the sequencer when demodulating
and integrating the response, in qblox format.
program (str): The pseudo assembly (q1asm) program to be executed by the sequencer.
https://qblox-qblox-instruments.readthedocs-hosted.com/en/master/documentation/sequencer.html#instructions
qubit (str): The id of the qubit associated with the sequencer, if there is only one.
"""
def __init__(self, number: int):
"""Initialises the sequencer.
All class attributes are defined and initialised.
"""
self.device: QbloxSequencer = None
self.number: int = number
self.pulses: PulseSequence = PulseSequence()
self.waveforms_buffer: WaveformsBuffer = WaveformsBuffer()
self.waveforms: dict = {}
self.acquisitions: dict = {}
self.weights: dict = {}
self.program: Program = Program()
self.qubit = None # self.qubit: int | str = None