Source code for qibolab.instruments.qblox.sweeper

from enum import Enum, auto

import numpy as np

from qibolab.instruments.qblox.q1asm import (
    Block,
    Program,
    Register,
    convert_frequency,
    convert_gain,
    convert_offset,
    convert_phase,
)
from qibolab.sweeper import Parameter, Sweeper


[docs]class QbloxSweeperType(Enum): """An enumeration for the different types of sweepers supported by qblox. - frequency: sweeps pulse frequency by adjusting the sequencer `nco_freq` with q1asm command `set_freq`. - gain: sweeps sequencer gain by adjusting the sequencer `gain_awg_path0` and `gain_awg_path1` with q1asm command `set_awg_gain`. Since the gain is a parameter between -1 and 1 that multiplies the samples of the waveforms before they are fed to the DACs, it can be used to sweep the pulse amplitude. - offset: sweeps sequencer offset by adjusting the sequencer `offset_awg_path0` and `offset_awg_path1` with q1asm command `set_awg_offs` - start: sweeps pulse start. - duration: sweeps pulse duration. """ frequency = auto() gain = auto() offset = auto() start = auto() duration = auto() number = auto() # internal relative_phase = auto() # not implemented yet time = auto() # not implemented yet
[docs]class QbloxSweeper: """A custom sweeper object with the data and functionality required by qblox instruments. It is responsible for generating the q1asm code required to execute sweeps in a sequencer. The object can be initialised with either: - a :class:`qibolab.sweepers.Sweeper` using the :func:`qibolab.instruments.qblox.QbloxSweeper.from_sweeper`, or - a range of values and a sweeper type (:class:`qibolab.instruments.qblox.QbloxSweeperType`) Like most FPGAs, qblox FPGAs do not support floating point arithmetics. All parameters that can be manipulated in real time within the FPGA are represented as two's complement integers. Attributes: type (:class:`qibolab.instruments.qblox.QbloxSweeperType`): the type of sweeper name (str): a name given for the sweep that is later used within the q1asm code to identify the loops. register (:class:`qibolab.instruments.qblox_q1asm.Register`): the main Register (q1asm variable) used in the loop. aux_register (:class:`qibolab.instruments.qblox_q1asm.Register`): an auxialiry Register requried in duration sweeps. update_parameters (Bool): a flag to instruct the sweeper to update the paramters or not depending on whether a parameter of the sequencer needs to be swept or not. Methods: block(inner_block: :class:`qibolab.instruments.qblox_q1asm.Block`): generates the block of q1asm code that implements the sweep. """ FREQUENCY_LIMIT = 500e6 def __init__( self, program: Program, rel_values: list, type: QbloxSweeperType = QbloxSweeperType.number, add_to: float = 0, multiply_to: float = 1, name: str = "", ): """Creates an instance from a range of values and a sweeper type (:class:`qibolab.instruments.qblox.QbloxSweeperType`). Args: program (:class:`qibolab.instruments.qblox_q1asm.Program`): a program object representing the q1asm program of a sequencer. rel_values (list): a list of values to iterate over. Currently qblox only supports a list of equally spaced values, like those created with `np.arange(start, stop, step)`. These values are considered relative values. They will later be added to the `add_to` parameter and multiplied to the `multiply_to` parameter. type (:class:`qibolab.instruments.qblox.QbloxSweeperType`): the type of sweeper. add_to (float): a value to be added to each value of the range of values defined in `sweeper.values` or `rel_values`. multiply_to (float): a value to be multiplied by each value of the range of values defined in `sweeper.values` or `rel_values`. name (str): a name given for the sweep that is later used within the q1as m code to identify the loops. """ self.type: QbloxSweeperType = type self.name: str = None self.register: Register = None self.aux_register: Register = None self.update_parameters: bool = False # Number of iterations in the loop self._n: int = None # Absolute values self._abs_start = None self._abs_step = None self._abs_stop = None self._abs_values: np.ndarray = None # Converted values (converted to q1asm values, two's complement) self._con_start: int = None self._con_step: int = None self._con_stop: int = None self._con_values: np.ndarray = None # Validate input parameters if not len(rel_values) > 1: raise ValueError("values must contain at least 2 elements.") elif rel_values[1] == rel_values[0]: raise ValueError("values must contain different elements.") self._n = len(rel_values) - 1 rel_start = rel_values[0] rel_step = rel_values[1] - rel_values[0] if name != "": self.name = name else: self.name = self.type.name # create the registers (variables) to be used in the loop self.register: Register = Register(program, self.name) if type == QbloxSweeperType.duration: self.aux_register: Register = Register(program, self.name + "_aux") # Calculate absolute values self._abs_start = (rel_start + add_to) * multiply_to self._abs_step = rel_step * multiply_to self._abs_stop = self._abs_start + self._abs_step * (self._n) self._abs_values = np.arange(self._abs_start, self._abs_stop, self._abs_step) # Verify that all values are within acceptable ranges check_values = { QbloxSweeperType.frequency: ( lambda v: all( (-self.FREQUENCY_LIMIT <= x and x <= self.FREQUENCY_LIMIT) for x in v ) ), QbloxSweeperType.gain: (lambda v: all((-1 <= x and x <= 1) for x in v)), QbloxSweeperType.offset: ( lambda v: all( (-1.25 * np.sqrt(2) <= x and x <= 1.25 * np.sqrt(2)) for x in v ) ), QbloxSweeperType.relative_phase: (lambda v: True), QbloxSweeperType.start: (lambda v: all((4 <= x and x < 2**16) for x in v)), QbloxSweeperType.duration: ( lambda v: all((0 <= x and x < 2**16) for x in v) ), QbloxSweeperType.number: ( lambda v: all((-(2**16) < x and x < 2**16) for x in v) ), } if not check_values[type](np.append(self._abs_values, [self._abs_stop])): raise ValueError( f"Sweeper {self.name} values are not within the allowed range" ) # Convert absolute values to q1asm values convert = { QbloxSweeperType.frequency: convert_frequency, QbloxSweeperType.gain: convert_gain, QbloxSweeperType.offset: convert_offset, QbloxSweeperType.relative_phase: convert_phase, QbloxSweeperType.start: (lambda x: int(x) % 2**16), QbloxSweeperType.duration: (lambda x: int(x) % 2**16), QbloxSweeperType.number: (lambda x: int(x) % 2**32), } self._con_start = convert[type](self._abs_start) self._con_step = convert[type](self._abs_step) self._con_stop = (self._con_start + self._con_step * (self._n) + 1) % 2**32 self._con_values = np.array( [(self._con_start + self._con_step * m) % 2**32 for m in range(self._n + 1)] ) # log.info(f"Qblox sweeper converted values: {self._con_values}") if not ( isinstance(self._con_start, int) and isinstance(self._con_stop, int) and isinstance(self._con_step, int) ): raise ValueError("start, stop and step must be int")
[docs] @classmethod def from_sweeper( cls, program: Program, sweeper: Sweeper, add_to: float = 0, multiply_to: float = 1, name: str = "", ): """Creates an instance form a :class:`qibolab.sweepers.Sweeper` object. Args: program (:class:`qibolab.instruments.qblox_q1asm.Program`): a program object representing the q1asm program of a sequencer. sweeper (:class:`qibolab.sweepers.Sweeper`): the original qibolab sweeper. associated with the sweep. If no name is provided it uses the sweeper type as name. add_to (float): a value to be added to each value of the range of values defined in `sweeper.values`, `rel_values`. multiply_to (float): a value to be multiplied by each value of the range of values defined in `sweeper.values`, `rel_values`. name (str): a name given for the sweep that is later used within the q1asm code to identify the loops. """ type_c = { Parameter.frequency: QbloxSweeperType.frequency, Parameter.gain: QbloxSweeperType.gain, Parameter.amplitude: QbloxSweeperType.gain, Parameter.bias: QbloxSweeperType.offset, Parameter.start: QbloxSweeperType.start, Parameter.duration: QbloxSweeperType.duration, Parameter.relative_phase: QbloxSweeperType.relative_phase, } if sweeper.parameter in type_c: type = type_c[sweeper.parameter] rel_values = sweeper.values else: raise ValueError( f"Sweeper parameter {sweeper.parameter} is not supported by qblox driver yet." ) return cls( program=program, rel_values=rel_values, type=type, add_to=add_to, multiply_to=multiply_to, name=name, )
[docs] def block(self, inner_block: Block): """Generates the block of q1asm code that implements the sweep. The q1asm code for a sweeper has the following structure: .. code-block:: text # header_block # initialise register with start value move 0, R0 # 0 = start value, R0 = register name nop # wait an instruction cycle (4ns) for the register to be updated with its value loop_R0: # loop label # update_parameter_block # update parameters, in this case pulse frequency set_freq R0 # sets the frequency of the sequencer nco to the value stored in R0 upd_param 100 # makes the change effective and wait 100ns # inner block play 0,1,4 # play waveforms with index 0 and 1 (i and q) and wait 4ns # footer_block # increment or decrement register with step value add R0, 2500, R0 # R0 = R0 + 2500 nop # wait an instruction cycle (4ns) for the register to be updated with its value # check condition and loop jlt R0, 10001, @loop_R0 # while R0 is less than the stop value loop to loop_R0 # in this example it would loop 5 times # with R0 values of 0, 2500, 5000, 7500 and 10000 Args: inner_block (:class:`qibolab.instruments.qblox_q1asm.Block`): the block of q1asm code to be repeated within the loop. """ # Initialisation header_block = Block() header_block.append( f"move {self._con_start}, {self.register}", comment=f"{self.register.name} loop, start: {round(self._abs_start, 6):_}", ) header_block.append("nop") header_block.append(f"loop_{self.register}:") # Parameter update if self.update_parameters: update_parameter_block = Block() update_time = 1000 if self.type == QbloxSweeperType.frequency: update_parameter_block.append( f"set_freq {self.register}" ) # TODO: move to pulse update_parameter_block.append(f"upd_param {update_time}") if self.type == QbloxSweeperType.gain: update_parameter_block.append( f"set_awg_gain {self.register}, {self.register}" ) # TODO: move to pulse update_parameter_block.append(f"upd_param {update_time}") if self.type == QbloxSweeperType.offset: update_parameter_block.append( f"set_awg_offs {self.register}, {self.register}" ) update_parameter_block.append(f"upd_param {update_time}") if self.type == QbloxSweeperType.start: pass if self.type == QbloxSweeperType.duration: update_parameter_block.append( f"add {self.register}, 1, {self.aux_register}" ) if self.type == QbloxSweeperType.time: pass if self.type == QbloxSweeperType.number: pass if self.type == QbloxSweeperType.relative_phase: pass header_block += update_parameter_block header_block.append_spacer() # Main code body_block = Block() body_block.indentation = 1 body_block += inner_block # Loop instructions footer_block = Block() footer_block.append_spacer() footer_block.append( f"add {self.register}, {self._con_step}, {self.register}", comment=f"{self.register.name} loop, step: {round(self._abs_step, 6):_}", ) footer_block.append("nop") # Qblox fpgas implement negative numbers using two's complement however their conditional jump instructions # (jlt and jge) only work with unsigned integers. Negative numbers (from 2**31 to 2**32) are greater than # possitive numbers (0 to 2**31). There is therefore a discontinuity between negative and possitive numbers. # Depending on whether the sweep increases or decreases the register, and on whether it crosses the # discontinuity or not, there are 4 scenarios: if self._abs_step > 0: # increasing if (self._abs_start < 0 and self._abs_stop < 0) or ( self._abs_stop > 0 and self._abs_start >= 0 ): # no crossing 0 footer_block.append( f"jlt {self.register}, {self._con_stop}, @loop_{self.register}", comment=f"{self.register.name} loop, stop: {round(self._abs_stop, 6):_}", ) elif self._abs_start < 0 and self._abs_stop >= 0: # crossing 0 # wait until the register crosses 0 to possitive values footer_block.append( f"jge {self.register}, {2**31}, @loop_{self.register}", ) # loop if the register is less than the stop value footer_block.append( f"jlt {self.register}, {self._con_stop}, @loop_{self.register}", comment=f"{self.register.name} loop, stop: {round(self._abs_stop, 6):_}", ) else: raise ValueError( f"incorrect values for abs_start: {self._abs_start}, abs_stop: {self._abs_stop}, abs_step: {self._abs_step}" ) elif self._abs_step < 0: # decreasing if (self._abs_start < 0 and self._abs_stop < 0) or ( self._abs_stop >= 0 and self._abs_start > 0 ): # no crossing 0 footer_block.append( f"jge {self.register}, {self._con_stop + 1}, @loop_{self.register}", comment=f"{self.register.name} loop, stop: {round(self._abs_stop, 6):_}", ) elif self._abs_start >= 0 and self._abs_stop < 0: # crossing 0 if self._con_stop + 1 != 2**32: # wait until the register crosses 0 to negative values footer_block.append( f"jlt {self.register}, {2**31}, @loop_{self.register}", ) # loop if the register is greater than the stop value footer_block.append( f"jge {self.register}, {self._con_stop + 1}, @loop_{self.register}", comment=f"{self.register.name} loop, stop: {round(self._abs_stop, 6):_}", ) else: # special case when stopping at -1 footer_block.append( f"jlt {self.register}, {2**31}, @loop_{self.register}", comment=f"{self.register.name} loop, stop: {round(self._abs_stop, 6):_}", ) else: raise ValueError( f"incorrect values for abs_start: {self._abs_start}, abs_stop: {self._abs_stop}, abs_step: {self._abs_step}" ) return header_block + body_block + footer_block