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 typing import Any, Dict, Optional

import numpy as np

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) 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)}." ) non_null = lambda amp: abs(amp) >= 2 / constants.IMMEDIATE_SZ_GAIN idx_real = ( helpers.add_to_wf_dict_if_unique( wf_dict=wf_dict, waveform=waveform_data.real ) if non_null(amp_real) else None ) idx_imag = ( helpers.add_to_wf_dict_if_unique( wf_dict=wf_dict, waveform=waveform_data.imag ) if non_null(amp_imag) else None ) # Update self._waveform_index and self._amplitude_path 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() # Only emit play command if at least one path has a signal # else auto-generate wait command index0 = self._waveform_index0 index1 = self._waveform_index1 if (index0 is not None) or (index1 is not None): qasm_program.set_gain_from_amplitude( self._amplitude_path0, self._amplitude_path1, self.operation_info ) # If a channel doesn't have an index (index0 or index1 is None) means, # that for that channel we do not want to play any waveform; # it's also ensured in this case, that the gain is set to 0 for that channel; # but, the Q1ASM program needs a waveform index for both channels, # so we set the other waveform's index in this case as a dummy qasm_program.emit( q1asm_instructions.PLAY, index0 if (index0 is not None) else index1, index1 if (index1 is not None) else index0, 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
[docs]class MarkerPulseStrategy(PulseStrategyPartial): """ If this strategy is used a digital pulse is played on the corresponding marker. """
[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): """ Inserts the QASM instructions to play the marker pulse. Note that for RF modules the first two bits of set_mrk are used as switches for the RF outputs. Parameters ---------- qasm_program The QASMProgram to add the assembly instructions to. """ if self.io_mode != "digital": raise ValueError( f"MarkerPulseStrategy can only be used with digital IO, not {self.io_mode}. " f"Operation causing exception: {self.operation_info}" ) duration = round(self.operation_info.duration * 1e9) output = int(self.operation_info.data["output"]) default_marker = qasm_program.static_hw_properties.default_marker # RF modules use first 2 bits of marker string as output/input switch. if qasm_program.static_hw_properties.instrument_type in ("QRM-RF", "QCM-RF"): output += 2 # QRM-RF has swapped addressing of outputs, TODO: change when fixed in firmware if qasm_program.static_hw_properties.instrument_type == "QRM-RF": output = self._fix_output_addressing(output) qasm_program.set_marker((1 << output) | default_marker) qasm_program.emit(q1asm_instructions.UPDATE_PARAMETERS, constants.GRID_TIME) qasm_program.elapsed_time += constants.GRID_TIME # Wait for the duration of the pulse minus 2 times grid time, one for each upd_param. qasm_program.auto_wait(duration - constants.GRID_TIME - constants.GRID_TIME) qasm_program.set_marker(default_marker) qasm_program.emit(q1asm_instructions.UPDATE_PARAMETERS, constants.GRID_TIME) qasm_program.elapsed_time += constants.GRID_TIME
@staticmethod
[docs] def _fix_output_addressing(output): """ Temporary fix for the marker output addressing of the QRM-RF. QRM-RF has swapped addressing of outputs. TODO: change when fixed in firmware """ if output == 3: output = 4 elif output == 4: output = 3 return output