Source code for quantify_scheduler.backends.qblox.operation_handling.pulses

# Repository: https://gitlab.com/quantify-os/quantify-scheduler
# Licensed according to the LICENCE file on the main branch
"""Classes for handling pulses."""

from __future__ import annotations

import logging
from collections import namedtuple
from typing import Any, Dict, Optional

import numpy as np

from quantify_core.utilities import deprecated
from quantify_scheduler.backends.qblox import constants, helpers, q1asm_instructions
from quantify_scheduler.backends.qblox.operation_handling.base import IOperationStrategy
from quantify_scheduler.backends.qblox.qasm_program import QASMProgram
from quantify_scheduler.backends.types import qblox as types
from quantify_scheduler.helpers.waveforms import normalize_waveform_data

[docs]logger = logging.getLogger(__name__)
[docs]class PulseStrategyPartial(IOperationStrategy): """Contains the logic shared between all the pulses.""" def __init__(self, operation_info: types.OpInfo, io_mode: str): """ Constructor. Parameters ---------- operation_info The operation info that corresponds to this pulse. io_mode Either "real", "imag" or complex depending on whether the signal affects only path0, path1 or both. """ self._pulse_info: types.OpInfo = operation_info self.io_mode = io_mode @property
[docs] def operation_info(self) -> types.OpInfo: """Property for retrieving the operation info.""" return self._pulse_info
[docs] def _check_amplitudes_set(self): if self.amplitude_path0 is None: raise ValueError("Amplitude for path0 is None.") if self.amplitude_path1 is None: raise ValueError("Amplitude for path1 is None.")
[docs]class GenericPulseStrategy(PulseStrategyPartial): """ Default class for handling pulses. No assumptions are made with regards to the pulse shape and no optimizations are done. """ def __init__(self, operation_info: types.OpInfo, io_mode: str): """ Constructor for this strategy. Parameters ---------- operation_info The operation info that corresponds to this pulse. io_mode Either "real", "imag" or "complex" depending on whether the signal affects only path0, path1 or both, respectively. """ super().__init__(operation_info, io_mode) self.amplitude_path0: Optional[float] = None self.amplitude_path1: Optional[float] = None self.waveform_index0: Optional[int] = None self.waveform_index1: Optional[int] = None self.waveform_len: Optional[int] = None
[docs] def generate_data(self, wf_dict: Dict[str, Any]): """ Generates the data and adds them to the ``wf_dict`` (if not already present). In complex mode, real-valued data is produced on sequencer path0 (:math:`I_\\text{IF}`) and imaginary data on sequencer path1 (:math:`Q_\\text{IF}`) after the NCO mixing. .. math:: \\underbrace{\\begin{bmatrix} \\cos\\omega t & -\\sin\\omega t \\\\ \\sin\\omega t & \\phantom{-}\\cos\\omega t \\end{bmatrix}}_\\text{NCO} \\begin{bmatrix} I \\\\ Q \\end{bmatrix} = \\begin{matrix} \\overbrace{ I \\cdot \\cos\\omega t - Q \\cdot\\sin\\omega t}^{\\small \\textbf{real} \\Rightarrow \\text{path0}} \\\\ \\underbrace{I \\cdot \\sin\\omega t + Q \\cdot\\cos\\omega t}_{\\small \\textbf{imag} \\Rightarrow \\text{path1}} \\end{matrix} = \\begin{bmatrix} I_\\text{IF} \\\\ Q_\\text{IF} \\end{bmatrix} In real mode, :math:`I_\\text{IF}` can be produced on either path0 (``io_mode == "real"``) or path1 (``io_mode == "imag"``). For ``io_mode == imag``, the real-valued input (:math:`I`) on path0 is swapped with imaginary input (:math:`Q`) on path1. We multiply :math:`Q` by -1 (via ``amp_imag``) to undo the 90-degree phase shift resulting from swapping the NCO input paths. .. math:: \\underbrace{\\begin{bmatrix} \\cos\\omega t & -\\sin\\omega t \\\\ \\sin\\omega t & \\phantom{-}\\cos\\omega t \\end{bmatrix}}_\\text{NCO} \\begin{bmatrix} -Q \\\\ I \\end{bmatrix} = \\begin{matrix} \\\\ \\underbrace{-Q \\cdot \\sin\\omega t + I \\cdot\\cos\\omega t}_{\\small \\textbf{real} \\Rightarrow \\text{path1}} \\end{matrix}= \\begin{bmatrix} - \\\\ I_\\text{IF} \\end{bmatrix} Parameters ---------- wf_dict The dictionary to add the waveform to. N.B. the dictionary is modified in function. Raises ------ ValueError Data is complex (has an imaginary component), but the io_mode is not set to "complex". """ # pylint: disable=line-too-long op_info = self.operation_info waveform_data = helpers.generate_waveform_data( op_info.data, sampling_rate=constants.SAMPLING_RATE ) waveform_data, amp_real, amp_imag = normalize_waveform_data(waveform_data) self.waveform_len = len(waveform_data) _, _, idx_real = helpers.add_to_wf_dict_if_unique(wf_dict, waveform_data.real) _, _, idx_imag = helpers.add_to_wf_dict_if_unique(wf_dict, waveform_data.imag) if np.any(np.iscomplex(waveform_data)) and not self.io_mode == "complex": raise ValueError( f"Complex valued {str(op_info)} detected but the sequencer" f" is not expecting complex input. This can be caused by " f"attempting to play complex valued waveforms on an output" f" marked as real.\n\nException caused by {repr(op_info)}." ) if self.io_mode == "imag": self.waveform_index0, self.waveform_index1 = idx_imag, idx_real self.amplitude_path0, self.amplitude_path1 = ( -amp_imag, # Multiply by -1 to undo 90-degree shift amp_real, ) else: self.waveform_index0, self.waveform_index1 = idx_real, idx_imag self.amplitude_path0, self.amplitude_path1 = amp_real, amp_imag
[docs] def insert_qasm(self, qasm_program: QASMProgram): """ Add the assembly instructions for the Q1 sequence processor that corresponds to this pulse. Parameters ---------- qasm_program The QASMProgram to add the assembly instructions to. """ self._check_amplitudes_set() qasm_program.set_gain_from_amplitude( self.amplitude_path0, self.amplitude_path1, self.operation_info ) qasm_program.emit( q1asm_instructions.PLAY, self.waveform_index0, self.waveform_index1, constants.GRID_TIME, # N.B. the waveform keeps playing comment=f"play {self.operation_info.name} ({self.waveform_len} ns)", ) qasm_program.elapsed_time += constants.GRID_TIME
@deprecated( "0.15.0", "Support for the `StitchedSquarePulseStrategy` will be dropped in a future " "version. Please use `long_square_pulse` or `StitchedPulseBuilder` " "directly instead.", )
[docs]class StitchedSquarePulseStrategy(PulseStrategyPartial): """ If this strategy is used, a (long) square pulse is generated by stitching shorter square pulses together. """ def __init__(self, operation_info: types.OpInfo, io_mode: str): """ Parameters ---------- operation_info The operation info that corresponds to this pulse. io_mode Either "real", "imag" or complex depending on whether the signal affects only path0, path1 or both. """ super().__init__(operation_info, io_mode) self.amplitude_path0: Optional[float] = None self.amplitude_path1: Optional[float] = None self.waveform_index0: Optional[int] = None self.waveform_index1: Optional[int] = None
[docs] def generate_data(self, wf_dict: Dict[str, Any]): """ Produces the waveform data for the stitched square pulse. This will be of a fixed duration. If the output mode is set to "complex", path1 will play all zeros. Otherwise both paths will play ones, but the amplitude will be set to 0 on one of them. Parameters ---------- wf_dict The dictionary to add the waveform to. N.B. the dictionary is modified in function. """ op_info = self.operation_info amplitude = op_info.data["amp"] array_with_ones = np.ones( int(constants.PULSE_STITCHING_DURATION * constants.SAMPLING_RATE) ) _, _, idx_ones = helpers.add_to_wf_dict_if_unique(wf_dict, array_with_ones.real) if self.io_mode == "complex": _, _, idx_zeros = helpers.add_to_wf_dict_if_unique( wf_dict, array_with_ones.imag ) self.waveform_index0, self.waveform_index1 = idx_ones, idx_zeros self.amplitude_path0, self.amplitude_path1 = amplitude, 0 else: self.waveform_index0, self.waveform_index1 = idx_ones, idx_ones if self.io_mode == "imag": self.amplitude_path0, self.amplitude_path1 = 0, amplitude else: self.amplitude_path0, self.amplitude_path1 = amplitude, 0
[docs] def insert_qasm(self, qasm_program: QASMProgram): """ Add the assembly instructions for the Q1 sequence processor that corresponds to this pulse. If the pulse is at least twice the stitching duration, a loop will be used. Parameters ---------- qasm_program The QASMProgram to add the assembly instructions to. """ duration = self.operation_info.duration repetitions = int(duration // constants.PULSE_STITCHING_DURATION) self._check_amplitudes_set() qasm_program.set_gain_from_amplitude( self.amplitude_path0, self.amplitude_path1, self.operation_info ) if repetitions > 1: with qasm_program.loop( label=f"stitch{len(qasm_program.instructions)}", repetitions=repetitions, ): qasm_program.emit( q1asm_instructions.PLAY, self.waveform_index0, self.waveform_index1, helpers.to_grid_time(constants.PULSE_STITCHING_DURATION), ) qasm_program.elapsed_time += repetitions * helpers.to_grid_time( constants.PULSE_STITCHING_DURATION ) elif repetitions == 1: qasm_program.emit( q1asm_instructions.PLAY, self.waveform_index0, self.waveform_index1, helpers.to_grid_time(constants.PULSE_STITCHING_DURATION), ) qasm_program.elapsed_time += helpers.to_grid_time( constants.PULSE_STITCHING_DURATION ) pulse_time_remaining = helpers.to_grid_time( duration % constants.PULSE_STITCHING_DURATION ) if pulse_time_remaining > 0: logger.warning( f"Using pulse stitching with pulse duration that is not a multiple of " f"{constants.PULSE_STITCHING_DURATION} s. This can cause unexpected " f"behavior to occur.\n\n{repr(self.operation_info)}" ) qasm_program.emit( q1asm_instructions.PLAY, self.waveform_index0, self.waveform_index1, pulse_time_remaining, ) qasm_program.emit( q1asm_instructions.SET_AWG_GAIN, 0, 0, comment="set to 0 at end of pulse", ) qasm_program.elapsed_time += pulse_time_remaining
[docs]_StaircaseParameters = namedtuple( "_StaircaseParameters", ["start_amp_immediate", "amp_step_immediate", "step_duration_ns", "num_steps"], )
"""Used to keep track of all the parameters that are to be used for generating the assembly for the staircase.""" @deprecated( "0.15.0", "Support for the `StaircasePulseStrategy` will be dropped in a future " "version. Please use `staircase_pulse` or `StitchedPulseBuilder` " "directly instead.", )
[docs]class StaircasePulseStrategy(PulseStrategyPartial): """ If this strategy is used, a staircase is generated through offset instructions, without using waveform memory. """
[docs] def generate_data(self, wf_dict: Dict[str, Any]): """Returns None as no waveforms are generated in this strategy.""" return None
[docs] def insert_qasm(self, qasm_program: QASMProgram): """ Add the assembly instructions for the Q1 sequence processor that corresponds to this pulse. Steps are generated using offset instructions. Using io_mode "real" or "complex" will cause the signal to appear on path0, "imag" on path1. Parameters ---------- qasm_program The QASMProgram to add the assembly instructions to. """ pulse = self.operation_info num_steps = pulse.data["num_steps"] start_amp = pulse.data["start_amp"] final_amp = pulse.data["final_amp"] step_duration_ns = helpers.to_grid_time(pulse.duration / num_steps) offset_param_label = ( "offset_awg_path1" if self.io_mode == "imag" else "offset_awg_path0" ) start_amp_immediate = qasm_program.expand_from_normalised_range( start_amp / qasm_program.static_hw_properties.max_awg_output_voltage, constants.IMMEDIATE_SZ_OFFSET, offset_param_label, pulse, ) amp_step = (final_amp - start_amp) / (num_steps - 1) amp_step_immediate = qasm_program.expand_from_normalised_range( amp_step / qasm_program.static_hw_properties.max_awg_output_voltage, constants.IMMEDIATE_SZ_OFFSET, offset_param_label, pulse, ) if start_amp_immediate < 0: start_amp_immediate += constants.REGISTER_SIZE # registers are unsigned staircase_params = _StaircaseParameters( start_amp_immediate=start_amp_immediate, amp_step_immediate=amp_step_immediate, step_duration_ns=step_duration_ns, num_steps=num_steps, ) self._generate_staircase(qasm_program, staircase_params)
[docs] def _generate_staircase( self, qasm_program: QASMProgram, staircase_params: _StaircaseParameters ): """Generates the actual staircase.""" with qasm_program.temp_registers(2) as (offs_reg, offs_reg_zero): qasm_program.emit( q1asm_instructions.SET_AWG_GAIN, constants.IMMEDIATE_SZ_GAIN // 2, constants.IMMEDIATE_SZ_GAIN // 2, comment="set gain to known value", ) # Initialize registers qasm_program.emit( q1asm_instructions.MOVE, staircase_params.start_amp_immediate, offs_reg, comment="keeps track of the offsets", ) qasm_program.emit( q1asm_instructions.MOVE, 0, offs_reg_zero, comment="zero for unused output path", ) qasm_program.emit(q1asm_instructions.NEW_LINE) with qasm_program.loop( f"ramp{len(qasm_program.instructions)}", repetitions=staircase_params.num_steps, ): self._generate_step( qasm_program, offs_reg, offs_reg_zero, staircase_params.amp_step_immediate, ) qasm_program.auto_wait( staircase_params.step_duration_ns - constants.GRID_TIME ) qasm_program.elapsed_time += ( staircase_params.step_duration_ns * (staircase_params.num_steps - 1) if staircase_params.num_steps > 1 else 0 ) qasm_program.emit( q1asm_instructions.SET_AWG_OFFSET, 0, 0, comment="return offset to 0 after staircase", ) qasm_program.emit(q1asm_instructions.NEW_LINE)
[docs] def _generate_step( self, qasm_program: QASMProgram, offs_reg: str, offs_reg_zero: str, amp_step_immediate: int, ): """Generates the inner part of the loop.""" if self.io_mode == "imag": qasm_program.emit( q1asm_instructions.SET_AWG_OFFSET, offs_reg_zero, offs_reg ) else: qasm_program.emit( q1asm_instructions.SET_AWG_OFFSET, offs_reg, offs_reg_zero ) qasm_program.emit( q1asm_instructions.UPDATE_PARAMETERS, constants.GRID_TIME, ) qasm_program.elapsed_time += constants.GRID_TIME if amp_step_immediate >= 0: qasm_program.emit( q1asm_instructions.ADD, offs_reg, amp_step_immediate, offs_reg, comment=f"next incr offs by {amp_step_immediate}", ) else: # if negative, we want to subtract a positive number, since we cannot add a # negative one. qasm_program.emit( q1asm_instructions.SUB, offs_reg, abs(amp_step_immediate), offs_reg, comment=f"next decr offs by {abs(amp_step_immediate)}", )