Source code for qibolab.pulses

"""Pulse and PulseSequence classes."""

import copy
import re
from abc import ABC, abstractmethod
from dataclasses import dataclass
from enum import Enum
from typing import Optional

import numpy as np
from qibo.config import log
from scipy.signal import lfilter

SAMPLING_RATE = 1
"""Default sampling rate in gigasamples per second (GSps).

Used for generating waveform envelopes if the instruments do not provide
a different value.
"""


[docs]class PulseType(Enum): """An enumeration to distinguish different types of pulses. READOUT pulses triger acquisitions. DRIVE pulses are used to control qubit states. FLUX pulses are used to shift the frequency of flux tunable qubits and with it implement two-qubit gates. """ READOUT = "ro" DRIVE = "qd" FLUX = "qf" COUPLERFLUX = "cf"
[docs]class Waveform: """A class to save pulse waveforms. A waveform is a list of samples, or discrete data points, used by the digital to analogue converters (DACs) to synthesise pulses. Attributes: data (np.ndarray): a numpy array containing the samples. serial (str): a string that can be used as a lable to identify the waveform. It is not automatically generated, it must be set by the user. """ DECIMALS = 5 def __init__(self, data): """Initialises the waveform with a of samples.""" self.data: np.ndarray = np.array(data) self.serial: str = "" def __len__(self): """Returns the length of the waveform, the number of samples.""" return len(self.data) def __eq__(self, other): """Compares two waveforms. Two waveforms are considered equal if their samples, rounded to `Waveform.DECIMALS` decimal places, are all equal. """ return self.__hash__() == other.__hash__() def __hash__(self): """Returns a hash of the array of data, after rounding each sample to `Waveform.DECIMALS` decimal places.""" return hash(str(np.around(self.data, Waveform.DECIMALS) + 0)) def __repr__(self): """Returns the waveform serial as its string representation.""" return self.serial
[docs] def plot(self, savefig_filename=None): """Plots the waveform. Args: savefig_filename (str): a file path. If provided the plot is save to a file. """ import matplotlib.pyplot as plt plt.figure(figsize=(14, 5), dpi=200) plt.plot(self.data, c="C0", linestyle="dashed") plt.xlabel("Sample Number") plt.ylabel("Amplitude") plt.grid( visible=True, which="both", axis="both", color="#888888", linestyle="-" ) plt.suptitle(self.serial) if savefig_filename: plt.savefig(savefig_filename) else: plt.show() plt.close()
[docs]class ShapeInitError(RuntimeError): """Error raised when a pulse has not been fully defined.""" default_msg = "PulseShape attribute pulse must be initialised in order to be able to generate pulse waveforms" def __init__(self, msg=None, *args): if msg is None: msg = self.default_msg super().__init__(msg, *args)
[docs]class PulseShape(ABC): """Abstract class for pulse shapes. This object is responsible for generating envelope and modulated waveforms from a set of pulse parameters and its type. Generates both i (in-phase) and q (quadrature) components. """ pulse = None """Pulse (Pulse): the pulse associated with it. Its parameters are used to generate pulse waveforms. """
[docs] @abstractmethod def envelope_waveform_i( self, sampling_rate=SAMPLING_RATE ) -> Waveform: # pragma: no cover raise NotImplementedError
[docs] @abstractmethod def envelope_waveform_q( self, sampling_rate=SAMPLING_RATE ) -> Waveform: # pragma: no cover raise NotImplementedError
[docs] def envelope_waveforms( self, sampling_rate=SAMPLING_RATE ): # -> tuple[Waveform, Waveform]: # pragma: no cover """A tuple with the i and q envelope waveforms of the pulse.""" return ( self.envelope_waveform_i(sampling_rate), self.envelope_waveform_q(sampling_rate), )
[docs] def modulated_waveform_i(self, sampling_rate=SAMPLING_RATE) -> Waveform: """The waveform of the i component of the pulse, modulated with its frequency.""" return self.modulated_waveforms(sampling_rate)[0]
[docs] def modulated_waveform_q(self, sampling_rate=SAMPLING_RATE) -> Waveform: """The waveform of the q component of the pulse, modulated with its frequency.""" return self.modulated_waveforms(sampling_rate)[1]
[docs] def modulated_waveforms(self, sampling_rate=SAMPLING_RATE): """A tuple with the i and q waveforms of the pulse, modulated with its frequency.""" pulse = self.pulse if abs(pulse._if) * 2 > sampling_rate: log.info( f"WARNING: The frequency of pulse {pulse.serial} is higher than the nyqusit frequency ({int(sampling_rate // 2)}) for the device sampling rate: {int(sampling_rate)}" ) num_samples = int(np.rint(pulse.duration * sampling_rate)) time = np.arange(num_samples) / sampling_rate global_phase = pulse.global_phase cosalpha = np.cos( 2 * np.pi * pulse._if * time + global_phase + pulse.relative_phase ) sinalpha = np.sin( 2 * np.pi * pulse._if * time + global_phase + pulse.relative_phase ) mod_matrix = np.array([[cosalpha, -sinalpha], [sinalpha, cosalpha]]) / np.sqrt( 2 ) (envelope_waveform_i, envelope_waveform_q) = self.envelope_waveforms( sampling_rate ) result = [] for n, t, ii, qq in zip( np.arange(num_samples), time, envelope_waveform_i.data, envelope_waveform_q.data, ): result.append(mod_matrix[:, :, n] @ np.array([ii, qq])) mod_signals = np.array(result) modulated_waveform_i = Waveform(mod_signals[:, 0]) modulated_waveform_i.serial = f"Modulated_Waveform_I(num_samples = {num_samples}, amplitude = {format(pulse.amplitude, '.6f').rstrip('0').rstrip('.')}, shape = {str(pulse.shape)}, frequency = {format(pulse._if, '_')}, phase = {format(global_phase + pulse.relative_phase, '.6f').rstrip('0').rstrip('.')})" modulated_waveform_q = Waveform(mod_signals[:, 1]) modulated_waveform_q.serial = f"Modulated_Waveform_Q(num_samples = {num_samples}, amplitude = {format(pulse.amplitude, '.6f').rstrip('0').rstrip('.')}, shape = {str(pulse.shape)}, frequency = {format(pulse._if, '_')}, phase = {format(global_phase + pulse.relative_phase, '.6f').rstrip('0').rstrip('.')})" return (modulated_waveform_i, modulated_waveform_q)
def __eq__(self, item) -> bool: """Overloads == operator.""" return isinstance(item, type(self))
[docs] @staticmethod def eval(value: str) -> "PulseShape": """Deserialize string representation. .. todo:: To be replaced by proper serialization. """ shape_name = re.findall(r"(\w+)", value)[0] if shape_name not in globals(): raise ValueError(f"shape {value} not found") shape_parameters = re.findall(r"[-\w+\d\.\d]+", value)[1:] # TODO: create multiple tests to prove regex working correctly return globals()[shape_name](*shape_parameters)
[docs]class Rectangular(PulseShape): """Rectangular pulse shape.""" def __init__(self): self.name = "Rectangular" self.pulse: Pulse = None
[docs] def envelope_waveform_i(self, sampling_rate=SAMPLING_RATE) -> Waveform: """The envelope waveform of the i component of the pulse.""" if self.pulse: num_samples = int(np.rint(self.pulse.duration * sampling_rate)) waveform = Waveform(self.pulse.amplitude * np.ones(num_samples)) waveform.serial = f"Envelope_Waveform_I(num_samples = {num_samples}, amplitude = {format(self.pulse.amplitude, '.6f').rstrip('0').rstrip('.')}, shape = {repr(self)})" return waveform raise ShapeInitError
[docs] def envelope_waveform_q(self, sampling_rate=SAMPLING_RATE) -> Waveform: """The envelope waveform of the q component of the pulse.""" if self.pulse: num_samples = int(np.rint(self.pulse.duration * sampling_rate)) waveform = Waveform(np.zeros(num_samples)) waveform.serial = f"Envelope_Waveform_Q(num_samples = {num_samples}, amplitude = {format(self.pulse.amplitude, '.6f').rstrip('0').rstrip('.')}, shape = {repr(self)})" return waveform raise ShapeInitError
def __repr__(self): return f"{self.name}()"
[docs]class Exponential(PulseShape): r"""Exponential pulse shape (Square pulse with an exponential decay). Args: tau (float): Parameter that controls the decay of the first exponential function upsilon (float): Parameter that controls the decay of the second exponential function g (float): Parameter that weights the second exponential function .. math:: A\frac{\exp\left(-\frac{x}{\text{upsilon}}\right) + g \exp\left(-\frac{x}{\text{tau}}\right)}{1 + g} """ def __init__(self, tau: float, upsilon: float, g: float = 0.1): self.name = "Exponential" self.pulse: Pulse = None self.tau: float = float(tau) self.upsilon: float = float(upsilon) self.g: float = float(g)
[docs] def envelope_waveform_i(self, sampling_rate=SAMPLING_RATE) -> Waveform: """The envelope waveform of the i component of the pulse.""" if self.pulse: num_samples = int(np.rint(self.pulse.duration * sampling_rate)) x = np.arange(0, num_samples, 1) waveform = Waveform( self.pulse.amplitude * ( (np.ones(num_samples) * np.exp(-x / self.upsilon)) + self.g * np.exp(-x / self.tau) ) / (1 + self.g) ) waveform.serial = f"Envelope_Waveform_I(num_samples = {num_samples}, amplitude = {format(self.pulse.amplitude, '.6f').rstrip('0').rstrip('.')}, shape = {repr(self)})" return waveform raise ShapeInitError
[docs] def envelope_waveform_q(self, sampling_rate=SAMPLING_RATE) -> Waveform: """The envelope waveform of the q component of the pulse.""" if self.pulse: num_samples = int(np.rint(self.pulse.duration * sampling_rate)) waveform = Waveform(np.zeros(num_samples)) waveform.serial = f"Envelope_Waveform_Q(num_samples = {num_samples}, amplitude = {format(self.pulse.amplitude, '.6f').rstrip('0').rstrip('.')}, shape = {repr(self)})" return waveform raise ShapeInitError
def __repr__(self): return f"{self.name}({format(self.tau, '.3f').rstrip('0').rstrip('.')}, {format(self.upsilon, '.3f').rstrip('0').rstrip('.')}, {format(self.g, '.3f').rstrip('0').rstrip('.')})"
[docs]class Gaussian(PulseShape): r"""Gaussian pulse shape. Args: rel_sigma (float): relative sigma so that the pulse standard deviation (sigma) = duration / rel_sigma .. math:: A\exp^{-\frac{1}{2}\frac{(t-\mu)^2}{\sigma^2}} """ def __init__(self, rel_sigma: float): self.name = "Gaussian" self.pulse: Pulse = None self.rel_sigma: float = float(rel_sigma) def __eq__(self, item) -> bool: """Overloads == operator.""" if super().__eq__(item): return self.rel_sigma == item.rel_sigma return False
[docs] def envelope_waveform_i(self, sampling_rate=SAMPLING_RATE) -> Waveform: """The envelope waveform of the i component of the pulse.""" if self.pulse: num_samples = int(np.rint(self.pulse.duration * sampling_rate)) x = np.arange(0, num_samples, 1) waveform = Waveform( self.pulse.amplitude * np.exp( -(1 / 2) * ( ((x - (num_samples - 1) / 2) ** 2) / (((num_samples) / self.rel_sigma) ** 2) ) ) ) waveform.serial = f"Envelope_Waveform_I(num_samples = {num_samples}, amplitude = {format(self.pulse.amplitude, '.6f').rstrip('0').rstrip('.')}, shape = {repr(self)})" return waveform raise ShapeInitError
[docs] def envelope_waveform_q(self, sampling_rate=SAMPLING_RATE) -> Waveform: """The envelope waveform of the q component of the pulse.""" if self.pulse: num_samples = int(np.rint(self.pulse.duration * sampling_rate)) waveform = Waveform(np.zeros(num_samples)) waveform.serial = f"Envelope_Waveform_Q(num_samples = {num_samples}, amplitude = {format(self.pulse.amplitude, '.6f').rstrip('0').rstrip('.')}, shape = {repr(self)})" return waveform raise ShapeInitError
def __repr__(self): return f"{self.name}({format(self.rel_sigma, '.6f').rstrip('0').rstrip('.')})"
[docs]class GaussianSquare(PulseShape): r"""GaussianSquare pulse shape. Args: rel_sigma (float): relative sigma so that the pulse standard deviation (sigma) = duration / rel_sigma width (float): Percentage of the pulse that is flat .. math:: A\exp^{-\frac{1}{2}\frac{(t-\mu)^2}{\sigma^2}}[Rise] + Flat + A\exp^{-\frac{1}{2}\frac{(t-\mu)^2}{\sigma^2}}[Decay] """ def __init__(self, rel_sigma: float, width: float): self.name = "GaussianSquare" self.pulse: Pulse = None self.rel_sigma: float = float(rel_sigma) self.width: float = float(width) def __eq__(self, item) -> bool: """Overloads == operator.""" if super().__eq__(item): return self.rel_sigma == item.rel_sigma and self.width == item.width return False
[docs] def envelope_waveform_i(self, sampling_rate=SAMPLING_RATE) -> Waveform: """The envelope waveform of the i component of the pulse.""" if self.pulse: def gaussian(t, rel_sigma, gaussian_samples): mu = (2 * gaussian_samples - 1) / 2 sigma = (2 * gaussian_samples) / rel_sigma return np.exp(-0.5 * ((t - mu) / sigma) ** 2) def fvec(t, gaussian_samples, rel_sigma, length=None): if length is None: length = t.shape[0] pulse = np.ones_like(t, dtype=float) rise = t < gaussian_samples fall = t > length - gaussian_samples - 1 pulse[rise] = gaussian(t[rise], rel_sigma, gaussian_samples) pulse[fall] = gaussian(t[rise], rel_sigma, gaussian_samples)[::-1] return pulse num_samples = int(np.rint(self.pulse.duration * sampling_rate)) gaussian_samples = num_samples * (1 - self.width) // 2 t = np.arange(0, num_samples) pulse = fvec(t, gaussian_samples, rel_sigma=self.rel_sigma) waveform = Waveform(self.pulse.amplitude * pulse) waveform.serial = f"Envelope_Waveform_I(num_samples = {num_samples}, amplitude = {format(self.pulse.amplitude, '.6f').rstrip('0').rstrip('.')}, shape = {repr(self)})" return waveform raise ShapeInitError
[docs] def envelope_waveform_q(self, sampling_rate=SAMPLING_RATE) -> Waveform: """The envelope waveform of the q component of the pulse.""" if self.pulse: num_samples = int(np.rint(self.pulse.duration * sampling_rate)) waveform = Waveform(np.zeros(num_samples)) waveform.serial = f"Envelope_Waveform_Q(num_samples = {num_samples}, amplitude = {format(self.pulse.amplitude, '.6f').rstrip('0').rstrip('.')}, shape = {repr(self)})" return waveform raise ShapeInitError
def __repr__(self): return f"{self.name}({format(self.rel_sigma, '.6f').rstrip('0').rstrip('.')}, {format(self.width, '.6f').rstrip('0').rstrip('.')})"
[docs]class Drag(PulseShape): """Derivative Removal by Adiabatic Gate (DRAG) pulse shape. Args: rel_sigma (float): relative sigma so that the pulse standard deviation (sigma) = duration / rel_sigma beta (float): relative sigma so that the pulse standard deviation (sigma) = duration / rel_sigma .. math:: """ def __init__(self, rel_sigma, beta): self.name = "Drag" self.pulse: Pulse = None self.rel_sigma = float(rel_sigma) self.beta = float(beta) def __eq__(self, item) -> bool: """Overloads == operator.""" if super().__eq__(item): return self.rel_sigma == item.rel_sigma and self.beta == item.beta return False
[docs] def envelope_waveform_i(self, sampling_rate=SAMPLING_RATE) -> Waveform: """The envelope waveform of the i component of the pulse.""" if self.pulse: num_samples = int(np.rint(self.pulse.duration * sampling_rate)) x = np.arange(0, num_samples, 1) i = self.pulse.amplitude * np.exp( -(1 / 2) * ( ((x - (num_samples - 1) / 2) ** 2) / (((num_samples) / self.rel_sigma) ** 2) ) ) waveform = Waveform(i) waveform.serial = f"Envelope_Waveform_I(num_samples = {num_samples}, amplitude = {format(self.pulse.amplitude, '.6f').rstrip('0').rstrip('.')}, shape = {repr(self)})" return waveform raise ShapeInitError
[docs] def envelope_waveform_q(self, sampling_rate=SAMPLING_RATE) -> Waveform: """The envelope waveform of the q component of the pulse.""" if self.pulse: num_samples = int(np.rint(self.pulse.duration * sampling_rate)) x = np.arange(0, num_samples, 1) i = self.pulse.amplitude * np.exp( -(1 / 2) * ( ((x - (num_samples - 1) / 2) ** 2) / (((num_samples) / self.rel_sigma) ** 2) ) ) q = ( self.beta * (-(x - (num_samples - 1) / 2) / ((num_samples / self.rel_sigma) ** 2)) * i ) waveform = Waveform(q) waveform.serial = f"Envelope_Waveform_Q(num_samples = {num_samples}, amplitude = {format(self.pulse.amplitude, '.6f').rstrip('0').rstrip('.')}, shape = {repr(self)})" return waveform raise ShapeInitError
def __repr__(self): return f"{self.name}({format(self.rel_sigma, '.6f').rstrip('0').rstrip('.')}, {format(self.beta, '.6f').rstrip('0').rstrip('.')})"
[docs]class IIR(PulseShape): """IIR Filter using scipy.signal lfilter.""" # https://arxiv.org/pdf/1907.04818.pdf (page 11 - filter formula S22) # p = [A, tau_iir] # p = [b0 = 1−k +k ·α, b1 = −(1−k)·(1−α),a0 = 1 and a1 = −(1−α)] # p = [b0, b1, a0, a1] def __init__(self, b, a, target: PulseShape): self.name = "IIR" self.target: PulseShape = target self._pulse: Pulse = None self.a: np.ndarray = np.array(a) self.b: np.ndarray = np.array(b) # Check len(a) = len(b) = 2 def __eq__(self, item) -> bool: """Overloads == operator.""" if super().__eq__(item): return ( self.target == item.target and (self.a == item.a).all() and (self.b == item.b).all() ) return False @property def pulse(self): return self._pulse @pulse.setter def pulse(self, value): self._pulse = value self.target.pulse = value
[docs] def envelope_waveform_i(self, sampling_rate=SAMPLING_RATE) -> Waveform: """The envelope waveform of the i component of the pulse.""" if self.pulse: num_samples = int(np.rint(self.pulse.duration * sampling_rate)) self.a = self.a / self.a[0] gain = np.sum(self.b) / np.sum(self.a) if not gain == 0: self.b = self.b / gain data = lfilter( b=self.b, a=self.a, x=self.target.envelope_waveform_i(sampling_rate).data, ) if not np.max(np.abs(data)) == 0: data = data / np.max(np.abs(data)) data = np.abs(self.pulse.amplitude) * data waveform = Waveform(data) waveform.serial = f"Envelope_Waveform_I(num_samples = {num_samples}, amplitude = {format(self.pulse.amplitude, '.6f').rstrip('0').rstrip('.')}, shape = {repr(self)})" return waveform raise ShapeInitError
[docs] def envelope_waveform_q(self, sampling_rate=SAMPLING_RATE) -> Waveform: """The envelope waveform of the q component of the pulse.""" if self.pulse: num_samples = int(np.rint(self.pulse.duration * sampling_rate)) self.a = self.a / self.a[0] gain = np.sum(self.b) / np.sum(self.a) if not gain == 0: self.b = self.b / gain data = lfilter( b=self.b, a=self.a, x=self.target.envelope_waveform_q(sampling_rate).data, ) if not np.max(np.abs(data)) == 0: data = data / np.max(np.abs(data)) data = np.abs(self.pulse.amplitude) * data waveform = Waveform(data) waveform.serial = f"Envelope_Waveform_Q(num_samples = {num_samples}, amplitude = {format(self.pulse.amplitude, '.6f').rstrip('0').rstrip('.')}, shape = {repr(self)})" return waveform raise ShapeInitError
def __repr__(self): formatted_b = [round(b, 3) for b in self.b] formatted_a = [round(a, 3) for a in self.a] return f"{self.name}({formatted_b}, {formatted_a}, {self.target})"
[docs]class SNZ(PulseShape): """Sudden variant Net Zero. https://arxiv.org/abs/2008.07411 (Supplementary materials: FIG. S1.) """ def __init__(self, t_idling, b_amplitude=None): self.name = "SNZ" self.pulse: Pulse = None self.t_idling: float = t_idling self.b_amplitude = b_amplitude def __eq__(self, item) -> bool: """Overloads == operator.""" if super().__eq__(item): return ( self.t_idling == item.t_idling and self.b_amplitude == item.b_amplitude ) return False
[docs] def envelope_waveform_i(self, sampling_rate=SAMPLING_RATE) -> Waveform: """The envelope waveform of the i component of the pulse.""" if self.pulse: if self.t_idling > self.pulse.duration: raise ValueError( f"Cannot put idling time {self.t_idling} higher than duration {self.pulse.duration}." ) if self.b_amplitude is None: self.b_amplitude = self.pulse.amplitude / 2 num_samples = int(np.rint(self.pulse.duration * sampling_rate)) half_pulse_duration = (self.pulse.duration - self.t_idling) / 2 half_flux_pulse_samples = int( np.rint(num_samples * half_pulse_duration / self.pulse.duration) ) idling_samples = num_samples - 2 * half_flux_pulse_samples waveform = Waveform( np.concatenate( ( self.pulse.amplitude * np.ones(half_flux_pulse_samples - 1), np.array([self.b_amplitude]), np.zeros(idling_samples), -np.array([self.b_amplitude]), -self.pulse.amplitude * np.ones(half_flux_pulse_samples - 1), ) ) ) waveform.serial = f"Envelope_Waveform_I(num_samples = {num_samples}, amplitude = {format(self.pulse.amplitude, '.6f').rstrip('0').rstrip('.')}, shape = {repr(self)})" return waveform raise ShapeInitError
[docs] def envelope_waveform_q(self, sampling_rate=SAMPLING_RATE) -> Waveform: """The envelope waveform of the q component of the pulse.""" if self.pulse: num_samples = int(np.rint(self.pulse.duration * sampling_rate)) waveform = Waveform(np.zeros(num_samples)) waveform.serial = f"Envelope_Waveform_Q(num_samples = {num_samples}, amplitude = {format(self.pulse.amplitude, '.6f').rstrip('0').rstrip('.')}, shape = {repr(self)})" return waveform raise ShapeInitError
def __repr__(self): return f"{self.name}({self.t_idling})"
[docs]class eCap(PulseShape): r"""ECap pulse shape. Args: alpha (float): .. math:: e_{\cap(t,\alpha)} &=& A[1 + \tanh(\alpha t/t_\theta)][1 + \tanh(\alpha (1 - t/t_\theta))]\\ &\times& [1 + \tanh(\alpha/2)]^{-2} """ def __init__(self, alpha: float): self.name = "eCap" self.pulse: Pulse = None self.alpha: float = float(alpha) def __eq__(self, item) -> bool: """Overloads == operator.""" if super().__eq__(item): return self.alpha == item.alpha return False
[docs] def envelope_waveform_i(self, sampling_rate=SAMPLING_RATE) -> Waveform: if self.pulse: num_samples = int(self.pulse.duration * sampling_rate) x = np.arange(0, num_samples, 1) waveform = Waveform( self.pulse.amplitude * (1 + np.tanh(self.alpha * x / num_samples)) * (1 + np.tanh(self.alpha * (1 - x / num_samples))) / (1 + np.tanh(self.alpha / 2)) ** 2 ) waveform.serial = f"Envelope_Waveform_I(num_samples = {num_samples}, amplitude = {format(self.pulse.amplitude, '.6f').rstrip('0').rstrip('.')}, shape = {repr(self)})" return waveform raise ShapeInitError
[docs] def envelope_waveform_q(self, sampling_rate=SAMPLING_RATE) -> Waveform: if self.pulse: num_samples = int(self.pulse.duration * sampling_rate) waveform = Waveform(np.zeros(num_samples)) waveform.serial = f"Envelope_Waveform_Q(num_samples = {num_samples}, amplitude = {format(self.pulse.amplitude, '.6f').rstrip('0').rstrip('.')}, shape = {repr(self)})" return waveform raise ShapeInitError
def __repr__(self): return f"{self.name}({format(self.alpha, '.6f').rstrip('0').rstrip('.')})"
[docs]class Custom(PulseShape): """Arbitrary shape.""" def __init__(self, envelope_i, envelope_q=None): self.name = "Custom" self.pulse: Pulse = None self.envelope_i: np.ndarray = np.array(envelope_i) if envelope_q is not None: self.envelope_q: np.ndarray = np.array(envelope_q) else: self.envelope_q = self.envelope_i
[docs] def envelope_waveform_i(self, sampling_rate=SAMPLING_RATE) -> Waveform: """The envelope waveform of the i component of the pulse.""" if self.pulse: if self.pulse.duration != len(self.envelope_i): raise ValueError("Length of envelope_i must be equal to pulse duration") num_samples = int(np.rint(self.pulse.duration * sampling_rate)) waveform = Waveform(self.envelope_i * self.pulse.amplitude) waveform.serial = f"Envelope_Waveform_I(num_samples = {num_samples}, amplitude = {format(self.pulse.amplitude, '.6f').rstrip('0').rstrip('.')}, shape = {repr(self)})" return waveform raise ShapeInitError
[docs] def envelope_waveform_q(self, sampling_rate=SAMPLING_RATE) -> Waveform: """The envelope waveform of the q component of the pulse.""" if self.pulse: if self.pulse.duration != len(self.envelope_q): raise ValueError("Length of envelope_q must be equal to pulse duration") num_samples = int(np.rint(self.pulse.duration * sampling_rate)) waveform = Waveform(self.envelope_q * self.pulse.amplitude) waveform.serial = f"Envelope_Waveform_Q(num_samples = {num_samples}, amplitude = {format(self.pulse.amplitude, '.6f').rstrip('0').rstrip('.')}, shape = {repr(self)})" return waveform raise ShapeInitError
def __repr__(self): return f"{self.name}({self.envelope_i[:3]}, ..., {self.envelope_q[:3]}, ...)"
[docs]@dataclass class Pulse: """A class to represent a pulse to be sent to the QPU.""" start: int """Start time of pulse in ns.""" duration: int """Pulse duration in ns.""" amplitude: float """Pulse digital amplitude (unitless). Pulse amplitudes are normalised between -1 and 1. """ frequency: int """Pulse Intermediate Frequency in Hz. The value has to be in the range [10e6 to 300e6]. """ relative_phase: float """Relative phase of the pulse, in radians.""" shape: PulseShape """Pulse shape, as a PulseShape object. See :py: mod:`qibolab.pulses` for list of available shapes. """ channel: Optional[str] = None """Channel on which the pulse should be played. When a sequence of pulses is sent to the platform for execution, each pulse is sent to the instrument responsible for playing pulses the pulse channel. The connection of instruments with channels is defined in the platform runcard. """ type: PulseType = PulseType.DRIVE """Pulse type, as an element of PulseType enumeration.""" qubit: int = 0 """Qubit or coupler addressed by the pulse.""" _if: int = 0 def __post_init__(self): if isinstance(self.type, str): self.type = PulseType(self.type) if isinstance(self.shape, str): self.shape = PulseShape.eval(self.shape) # TODO: drop the cyclic reference self.shape.pulse = self @property def finish(self) -> Optional[int]: """Time when the pulse is scheduled to finish.""" if None in {self.start, self.duration}: return None return self.start + self.duration @property def global_phase(self): """Global phase of the pulse, in radians. This phase is calculated from the pulse start time and frequency as `2 * pi * frequency * start`. """ # pulse start, duration and finish are in ns return 2 * np.pi * self.frequency * self.start / 1e9 @property def phase(self) -> float: """Total phase of the pulse, in radians. The total phase is computed as the sum of the global and relative phases. """ return self.global_phase + self.relative_phase @property def serial(self) -> str: """Returns a string representation of the pulse.""" return f"Pulse({self.start}, {self.duration}, {format(self.amplitude, '.6f').rstrip('0').rstrip('.')}, {format(self.frequency, '_')}, {format(self.relative_phase, '.6f').rstrip('0').rstrip('.')}, {self.shape}, {self.channel}, {self.type}, {self.qubit})" @property def id(self) -> int: return id(self)
[docs] def envelope_waveform_i(self, sampling_rate=SAMPLING_RATE) -> Waveform: """The envelope waveform of the i component of the pulse.""" return self.shape.envelope_waveform_i(sampling_rate)
[docs] def envelope_waveform_q(self, sampling_rate=SAMPLING_RATE) -> Waveform: """The envelope waveform of the q component of the pulse.""" return self.shape.envelope_waveform_q(sampling_rate)
[docs] def envelope_waveforms( self, sampling_rate=SAMPLING_RATE ): # -> tuple[Waveform, Waveform]: """A tuple with the i and q envelope waveforms of the pulse.""" return ( self.shape.envelope_waveform_i(sampling_rate), self.shape.envelope_waveform_q(sampling_rate), )
[docs] def modulated_waveform_i(self, sampling_rate=SAMPLING_RATE) -> Waveform: """The waveform of the i component of the pulse, modulated with its frequency.""" return self.shape.modulated_waveform_i(sampling_rate)
[docs] def modulated_waveform_q(self, sampling_rate=SAMPLING_RATE) -> Waveform: """The waveform of the q component of the pulse, modulated with its frequency.""" return self.shape.modulated_waveform_q(sampling_rate)
[docs] def modulated_waveforms(self, sampling_rate): # -> tuple[Waveform, Waveform]: """A tuple with the i and q waveforms of the pulse, modulated with its frequency.""" return self.shape.modulated_waveforms(sampling_rate)
def __repr__(self): return self.serial def __hash__(self): return hash(self.serial) def __eq__(self, other): if isinstance(other, Pulse): return self.serial == other.serial return False def __add__(self, other): if isinstance(other, Pulse): return PulseSequence(self, other) if isinstance(other, PulseSequence): return PulseSequence(self, *other.pulses) raise TypeError(f"Expected Pulse or PulseSequence; got {type(other).__name__}") def __mul__(self, n): if not isinstance(n, int): raise TypeError(f"Expected int; got {type(n).__name__}") if n < 0: raise TypeError(f"argument n should be >=0, got {n}") return PulseSequence(*([self.copy()] * n)) def __rmul__(self, n): return self.__mul__(n)
[docs] def copy(self): # -> Pulse|ReadoutPulse|DrivePulse|FluxPulse: """Returns a new Pulse object with the same attributes.""" if type(self) == ReadoutPulse: return ReadoutPulse( self.start, self.duration, self.amplitude, self.frequency, self.relative_phase, repr(self.shape), # self.shape, self.channel, self.qubit, ) elif type(self) == DrivePulse: return DrivePulse( self.start, self.duration, self.amplitude, self.frequency, self.relative_phase, repr(self.shape), # self.shape, self.channel, self.qubit, ) elif type(self) == FluxPulse: return FluxPulse( self.start, self.duration, self.amplitude, self.shape, self.channel, self.qubit, ) else: # return eval(self.serial) return Pulse( self.start, self.duration, self.amplitude, self.frequency, self.relative_phase, repr(self.shape), # self.shape, self.channel, self.type, self.qubit, )
[docs] def shallow_copy(self): # -> Pulse: return Pulse( self.start, self.duration, self.amplitude, self.frequency, self.relative_phase, self.shape, self.channel, self.type, self.qubit, )
[docs] def is_equal_ignoring_start(self, item) -> bool: """Check if two pulses are equal ignoring start time.""" return ( self.duration == item.duration and self.amplitude == item.amplitude and self.frequency == item.frequency and self.relative_phase == item.relative_phase and self.shape == item.shape and self.channel == item.channel and self.type == item.type and self.qubit == item.qubit )
[docs] def plot(self, savefig_filename=None, sampling_rate=SAMPLING_RATE): """Plots the pulse envelope and modulated waveforms. Args: savefig_filename (str): a file path. If provided the plot is save to a file. """ import matplotlib.pyplot as plt from matplotlib import gridspec waveform_i = self.shape.envelope_waveform_i(sampling_rate) waveform_q = self.shape.envelope_waveform_q(sampling_rate) num_samples = len(waveform_i) time = self.start + np.arange(num_samples) / sampling_rate fig = plt.figure(figsize=(14, 5), dpi=200) gs = gridspec.GridSpec(ncols=2, nrows=1, width_ratios=[2, 1]) ax1 = plt.subplot(gs[0]) ax1.plot( time, waveform_i.data, label="envelope i", c="C0", linestyle="dashed", ) ax1.plot( time, waveform_q.data, label="envelope q", c="C1", linestyle="dashed", ) ax1.plot( time, self.shape.modulated_waveform_i(sampling_rate).data, label="modulated i", c="C0", ) ax1.plot( time, self.shape.modulated_waveform_q(sampling_rate).data, label="modulated q", c="C1", ) ax1.plot(time, -waveform_i.data, c="silver", linestyle="dashed") ax1.set_xlabel("Time [ns]") ax1.set_ylabel("Amplitude") ax1.grid( visible=True, which="both", axis="both", color="#888888", linestyle="-" ) ax1.axis([self.start, self.finish, -1, 1]) ax1.legend() modulated_i = self.shape.modulated_waveform_i(sampling_rate).data modulated_q = self.shape.modulated_waveform_q(sampling_rate).data ax2 = plt.subplot(gs[1]) ax2.plot( modulated_i, modulated_q, label="modulated", c="C3", ) ax2.plot( waveform_i.data, waveform_q.data, label="envelope", c="C2", ) ax2.plot( modulated_i[0], modulated_q[0], marker="o", markersize=5, label="start", c="lightcoral", ) ax2.plot( modulated_i[-1], modulated_q[-1], marker="o", markersize=5, label="finish", c="darkred", ) ax2.plot( np.cos(time * 2 * np.pi / self.duration), np.sin(time * 2 * np.pi / self.duration), c="silver", linestyle="dashed", ) ax2.grid( visible=True, which="both", axis="both", color="#888888", linestyle="-" ) ax2.legend() # ax2.axis([ -1, 1, -1, 1]) ax2.axis("equal") plt.suptitle(self.serial) if savefig_filename: plt.savefig(savefig_filename) else: plt.show() plt.close()
[docs]class ReadoutPulse(Pulse): """Describes a readout pulse. See :class: `qibolab.pulses.Pulse` for argument desciption. """ def __init__( self, start, duration, amplitude, frequency, relative_phase, shape, channel=0, qubit=0, ): super().__init__( start, duration, amplitude, frequency, relative_phase, shape, channel, type=PulseType.READOUT, qubit=qubit, ) @property def serial(self): return f"ReadoutPulse({self.start}, {self.duration}, {format(self.amplitude, '.6f').rstrip('0').rstrip('.')}, {format(self.frequency, '_')}, {format(self.relative_phase, '.6f').rstrip('0').rstrip('.')}, {self.shape}, {self.channel}, {self.qubit})" @property def global_phase(self): # readout pulses should have zero global phase so that we can # calculate probabilities in the i-q plane return 0
[docs] def copy(self): # -> Pulse|ReadoutPulse|DrivePulse|FluxPulse: """Returns a new Pulse object with the same attributes.""" return ReadoutPulse( self.start, self.duration, self.amplitude, self.frequency, self.relative_phase, copy.deepcopy(self.shape), # self.shape, self.channel, self.qubit, )
[docs]class DrivePulse(Pulse): """Describes a qubit drive pulse. See :class: `qibolab.pulses.Pulse` for argument desciption. """ def __init__( self, start, duration, amplitude, frequency, relative_phase, shape, channel=0, qubit=0, ): super().__init__( start, duration, amplitude, frequency, relative_phase, shape, channel, type=PulseType.DRIVE, qubit=qubit, ) @property def serial(self): return f"DrivePulse({self.start}, {self.duration}, {format(self.amplitude, '.6f').rstrip('0').rstrip('.')}, {format(self.frequency, '_')}, {format(self.relative_phase, '.6f').rstrip('0').rstrip('.')}, {self.shape}, {self.channel}, {self.qubit})"
[docs]class FluxPulse(Pulse): """Describes a qubit flux pulse. Flux pulses have frequency and relative_phase equal to 0. Their i and q components are equal. See :class: `qibolab.pulses.Pulse` for argument desciption. """ PULSE_TYPE = PulseType.FLUX def __init__(self, start, duration, amplitude, shape, channel=0, qubit=0): super().__init__( start, duration, amplitude, 0, 0, shape, channel, type=self.PULSE_TYPE, qubit=qubit, )
[docs] def envelope_waveform_q(self, sampling_rate=SAMPLING_RATE) -> Waveform: """Flux pulses only have i component.""" return self.shape.envelope_waveform_i(sampling_rate)
[docs] def modulated_waveform_i(self, sampling_rate=SAMPLING_RATE) -> Waveform: return self.shape.envelope_waveform_i(sampling_rate)
[docs] def modulated_waveform_q(self, sampling_rate=SAMPLING_RATE) -> Waveform: return self.shape.envelope_waveform_i(sampling_rate)
@property def serial(self): return f"{self.__class__.__name__}({self.start}, {self.duration}, {format(self.amplitude, '.6f').rstrip('0').rstrip('.')}, {self.shape}, {self.channel}, {self.qubit})"
[docs]class CouplerFluxPulse(FluxPulse): """Describes a coupler flux pulse. See :class: `qibolab.pulses.FluxPulse` for argument desciption. """ PULSE_TYPE = PulseType.COUPLERFLUX
[docs]class PulseConstructor(Enum): """An enumeration to map each ``PulseType`` to the proper pulse constructor.""" READOUT = ReadoutPulse DRIVE = DrivePulse FLUX = FluxPulse
[docs]class PulseSequence: """A collection of scheduled pulses. A quantum circuit can be translated into a set of scheduled pulses that implement the circuit gates. This class contains many supporting fuctions to facilitate the creation and manipulation of these collections of pulses. None of the methods of PulseSequence modify any of the properties of its pulses. """ def __init__(self, *pulses): self.pulses = [] #: list[Pulse] = [] """Pulses (list): a list containing the pulses, ordered by their channel and start times.""" self.add(*pulses) def __len__(self): return len(self.pulses) def __iter__(self): return iter(self.pulses) def __getitem__(self, index): return self.pulses[index] def __setitem__(self, index, value): self.pulses[index] = value def __delitem__(self, index): del self.pulses[index] def __contains__(self, pulse): return pulse in self.pulses def __repr__(self): return self.serial @property def serial(self): """Returns a string representation of the pulse sequence.""" return "PulseSequence\n" + "\n".join(f"{pulse.serial}" for pulse in self.pulses) def __eq__(self, other): if not isinstance(other, PulseSequence): raise TypeError(f"Expected PulseSequence; got {type(other).__name__}") return self.serial == other.serial def __ne__(self, other): if not isinstance(other, PulseSequence): raise TypeError(f"Expected PulseSequence; got {type(other).__name__}") return self.serial != other.serial def __hash__(self): return hash(self.serial) def __add__(self, other): if isinstance(other, PulseSequence): return PulseSequence(*self.pulses, *other.pulses) if isinstance(other, Pulse): return PulseSequence(*self.pulses, other) raise TypeError(f"Expected PulseSequence or Pulse; got {type(other).__name__}") def __radd__(self, other): if isinstance(other, PulseSequence): return PulseSequence(*other.pulses, *self.pulses) if isinstance(other, Pulse): return PulseSequence(other, *self.pulses) raise TypeError(f"Expected PulseSequence or Pulse; got {type(other).__name__}") def __iadd__(self, other): if isinstance(other, PulseSequence): self.add(*other.pulses) elif isinstance(other, Pulse): self.add(other) else: raise TypeError( f"Expected PulseSequence or Pulse; got {type(other).__name__}" ) return self def __mul__(self, n): if not isinstance(n, int): raise TypeError(f"Expected int; got {type(n).__name__}") if n < 0: raise TypeError(f"argument n should be >=0, got {n}") return PulseSequence(*(self.pulses * n)) def __rmul__(self, n): if not isinstance(n, int): raise TypeError(f"Expected int; got {type(n).__name__}") if n < 0: raise TypeError(f"argument n should be >=0, got {n}") return PulseSequence(*(self.pulses * n)) def __imul__(self, n): if not isinstance(n, int): raise TypeError(f"Expected int; got {type(n).__name__}") if n < 1: raise TypeError(f"argument n should be >=1, got {n}") original_set = self.shallow_copy() for x in range(n - 1): self.add(*original_set.pulses) return self @property def count(self): """Returns the number of pulses in the sequence.""" return len(self.pulses)
[docs] def add(self, *items): """Adds pulses to the sequence and sorts them by channel and start time.""" for item in items: if isinstance(item, Pulse): pulse = item self.pulses.append(pulse) elif isinstance(item, PulseSequence): ps = item for pulse in ps.pulses: self.pulses.append(pulse) self.pulses.sort(key=lambda item: (item.start, item.channel))
[docs] def index(self, pulse): """Returns the index of a pulse in the sequence.""" return self.pulses.index(pulse)
[docs] def pop(self, index=-1): """Returns the pulse with the index provided and removes it from the sequence.""" return self.pulses.pop(index)
[docs] def remove(self, pulse): """Removes a pulse from the sequence.""" while pulse in self.pulses: self.pulses.remove(pulse)
[docs] def clear(self): """Removes all pulses from the sequence.""" self.pulses.clear()
[docs] def shallow_copy(self): """Returns a shallow copy of the sequence. It returns a new PulseSequence object with references to the same Pulse objects. """ return PulseSequence(*self.pulses)
[docs] def copy(self): """Returns a deep copy of the sequence. It returns a new PulseSequence with replicates of each of the pulses contained in the original sequence. """ return PulseSequence(*[pulse.copy() for pulse in self.pulses])
@property def ro_pulses(self): """Returns a new PulseSequence containing only its readout pulses.""" new_pc = PulseSequence() for pulse in self.pulses: if pulse.type == PulseType.READOUT: new_pc.add(pulse) return new_pc @property def qd_pulses(self): """Returns a new PulseSequence containing only its qubit drive pulses.""" new_pc = PulseSequence() for pulse in self.pulses: if pulse.type == PulseType.DRIVE: new_pc.add(pulse) return new_pc @property def qf_pulses(self): """Returns a new PulseSequence containing only its qubit flux pulses.""" new_pc = PulseSequence() for pulse in self.pulses: if pulse.type == PulseType.FLUX: new_pc.add(pulse) return new_pc @property def cf_pulses(self): """Returns a new PulseSequence containing only its coupler flux pulses.""" new_pc = PulseSequence() for pulse in self.pulses: if pulse.type is PulseType.COUPLERFLUX: new_pc.add(pulse) return new_pc
[docs] def get_channel_pulses(self, *channels): """Returns a new PulseSequence containing only the pulses on a specific set of channels.""" new_pc = PulseSequence() for pulse in self.pulses: if pulse.channel in channels: new_pc.add(pulse) return new_pc
[docs] def get_qubit_pulses(self, *qubits): """Returns a new PulseSequence containing only the pulses on a specific set of qubits.""" new_pc = PulseSequence() for pulse in self.pulses: if not isinstance(pulse, CouplerFluxPulse): if pulse.qubit in qubits: new_pc.add(pulse) return new_pc
[docs] def coupler_pulses(self, *couplers): """Returns a new PulseSequence containing only the pulses on a specific set of couplers.""" new_pc = PulseSequence() for pulse in self.pulses: if isinstance(pulse, CouplerFluxPulse): if pulse.qubit in couplers: new_pc.add(pulse) return new_pc
@property def is_empty(self): """Returns True if the sequence does not contain any pulses.""" return len(self.pulses) == 0 @property def finish(self) -> int: """Returns the time when the last pulse of the sequence finishes.""" t: int = 0 for pulse in self.pulses: if pulse.finish > t: t = pulse.finish return t @property def start(self) -> int: """Returns the start time of the first pulse of the sequence.""" t = self.finish for pulse in self.pulses: if pulse.start < t: t = pulse.start return t @property def duration(self) -> int: """Returns duration of the sequence calculated as its finish - start times.""" return self.finish - self.start @property def channels(self) -> list: """Returns list containing the channels used by the pulses in the sequence.""" channels = [] for pulse in self.pulses: if not pulse.channel in channels: channels.append(pulse.channel) channels.sort() return channels @property def qubits(self) -> list: """Returns list containing the qubits associated with the pulses in the sequence.""" qubits = [] for pulse in self.pulses: if not pulse.qubit in qubits: qubits.append(pulse.qubit) qubits.sort() return qubits
[docs] def get_pulse_overlaps(self): # -> dict((int,int): PulseSequence): """Returns a dictionary of slices of time (tuples with start and finish times) where pulses overlap.""" times = [] for pulse in self.pulses: if not pulse.start in times: times.append(pulse.start) if not pulse.finish in times: times.append(pulse.finish) times.sort() overlaps = {} for n in range(len(times) - 1): overlaps[(times[n], times[n + 1])] = PulseSequence() for pulse in self.pulses: if (pulse.start <= times[n]) & (pulse.finish >= times[n + 1]): overlaps[(times[n], times[n + 1])] += pulse return overlaps
[docs] def separate_overlapping_pulses(self): # -> dict((int,int): PulseSequence): """Separates a sequence of overlapping pulses into a list of non- overlapping sequences.""" # This routine separates the pulses of a sequence into non-overlapping sets # but it does not check if the frequencies of the pulses within a set have the same frequency separated_pulses = [] for new_pulse in self.pulses: stored = False for ps in separated_pulses: overlaps = False for existing_pulse in ps: if ( new_pulse.start < existing_pulse.finish and new_pulse.finish > existing_pulse.start ): overlaps = True break if not overlaps: ps.add(new_pulse) stored = True break if not stored: separated_pulses.append(PulseSequence(new_pulse)) return separated_pulses
# TODO: Implement separate_different_frequency_pulses() @property def pulses_overlap(self) -> bool: """Returns True if any of the pulses in the sequence overlap.""" overlap = False for pc in self.get_pulse_overlaps().values(): if pc.count > 1: overlap = True return overlap
[docs] def plot(self, savefig_filename=None, sampling_rate=SAMPLING_RATE): """Plots the sequence of pulses. Args: savefig_filename (str): a file path. If provided the plot is save to a file. """ if not self.is_empty: import matplotlib.pyplot as plt from matplotlib import gridspec fig = plt.figure(figsize=(14, 2 * self.count), dpi=200) gs = gridspec.GridSpec(ncols=1, nrows=self.count) vertical_lines = [] for pulse in self.pulses: vertical_lines.append(pulse.start) vertical_lines.append(pulse.finish) n = -1 for qubit in self.qubits: qubit_pulses = self.get_qubit_pulses(qubit) for channel in qubit_pulses.channels: n += 1 channel_pulses = qubit_pulses.get_channel_pulses(channel) ax = plt.subplot(gs[n]) ax.axis([0, self.finish, -1, 1]) for pulse in channel_pulses: num_samples = len( pulse.shape.modulated_waveform_i(sampling_rate) ) time = pulse.start + np.arange(num_samples) / sampling_rate ax.plot( time, pulse.shape.modulated_waveform_q(sampling_rate).data, c="lightgrey", ) ax.plot( time, pulse.shape.modulated_waveform_i(sampling_rate).data, c=f"C{str(n)}", ) ax.plot( time, pulse.shape.envelope_waveform_i(sampling_rate).data, c=f"C{str(n)}", ) ax.plot( time, -pulse.shape.envelope_waveform_i(sampling_rate).data, c=f"C{str(n)}", ) # TODO: if they overlap use different shades ax.axhline(0, c="dimgrey") ax.set_ylabel(f"qubit {qubit} \n channel {channel}") for vl in vertical_lines: ax.axvline(vl, c="slategrey", linestyle="--") ax.axis([0, self.finish, -1, 1]) ax.grid( visible=True, which="both", axis="both", color="#CCCCCC", linestyle="-", ) if savefig_filename: plt.savefig(savefig_filename) else: plt.show() plt.close()