Source code for quaccatoo.pulsed_sim.predef_dd_seqs

"""
This module contains dynamical decoupling pulse sequences, used in quantum sensing and for extending
coherence of quantum systems.
"""

import warnings
from collections.abc import Callable

import numpy as np
from qutip import Qobj

from ..qsys.qsys import QSys
from .pulse_shapes import square_pulse
from .pulsed_sim import PulsedSim

__all__ = ["CPMG", "XY", "XY8"]

####################################################################################################


[docs] class CPMG(PulsedSim): """ This class contains a Carr-Purcell-Meiboom-Gill sequence used in quantum sensing experiments. The CPMG sequence consists of a series of pi pulses and free evolution times, such that these periodicals inversions will cancel out oscillating noises except for frequencies corresponding to the pulse separation. Methods ------- CPMG_sequence : defines the Carr-Purcell-Meiboom-Gill sequence for a given free evolution time tau and the set of attributes defined in the constructor, returning the final state. The sequence is to be called by the parallel_map method of QuTip. _get_pulse_profiles : Generates the pulse profiles for the CPMG sequence for a given tau. The pulse profiles are stored in the pulse_profiles attribute of the object. plot_pulses : Overwrites the plot_pulses method of the parent class in order to first generate the pulse profiles for the CPMG sequence for a given tau and then plot them. Notes ----- The CPMG sequence inherits the methods and attributes from the PulsedSim class. """ # the sequence opens and closes with tau/2 around the pi pulses, therefore # tau must be at least twice the pi_pulse_duration to avoid negative free evolutions _min_tau_factor = 2 def __init__( self, free_duration: np.ndarray | list[float], *, system: QSys, M: int, pi_pulse_duration: float, h1: Qobj | list[Qobj] | None = None, Rx: Qobj | None = None, Ry: Qobj | None = None, H2: tuple[Qobj, Callable] | None = None, projection_pulse: bool = True, pulse_shape: Callable = square_pulse, pulse_params: dict[str, float] | None = None, time_steps: int = 100, options: dict | None = None, ) -> None: """ Class constructor for the Carr-Purcell-Meiboom-Gill sequence Parameters ---------- free_duration : numpy array Time array for the simulation representing the free evolution time to be used as the variable attribute for the simulation system : QSys Quantum system object containing the initial state, internal time independent Hamiltonian and collapse operators M : int Order of the CPMG sequence pi_pulse_duration : float, int or 0 Duration of the pi pulse. If set to 0, the pulses are perfect delta pulses and the time-evolution is calculated with the rotation operator. h1 : Qobj or list of Qobj Control Hamiltonian of the system. Rx : Qobj or None Rotation operator around the x-axis, used only if the pi_pulse_duration is set to 0. Ry : Qobj or None Rotation operator around the y-axis, used only if the pi_pulse_duration is set to 0. H2 : list(Qobj or function) Time dependent sensing Hamiltonian of the system projection_pulse : bool Boolean to determine if a final pi/2 pulse is to be included in order to project the measurement into the Sz basis pulse_shape : FunctionType or list(FunctionType) Pulse shape function or list of pulse shape functions representing the time modulation of h1 pulse_params : dict Dictionary of parameters for the pulse_shape functions time_steps : int Number of time steps in the pulses for the simulation """ super().__init__(system, H2=H2) self.sequence = self.CPMG_sequence self._check_attr_predef_seqs( h1, Rx, Ry, pulse_shape, pulse_params, options, time_steps, free_duration, pi_pulse_duration, M, projection_pulse, ) base_pulse = self.pulse_params.copy() self.pulse_params = [{**base_pulse, "phi_t": 0}, {**base_pulse, "phi_t": -np.pi / 2}]
[docs] def CPMG_sequence(self, tau: float) -> Qobj: """ Defines the CPMG sequence for a given free evolution time tau and the set of attributes defined in the constructor. The sequence consists of an initial pi/2 pulse, and M pi-pulses separated by free evolution time tau. If projection_pulse is True, the sequence will include a final pi/2 pulse on Y axis to project the measurement into the Sz basis. If the pi_pulse_duration is set to 0, the pulses are perfect delta pulses and the time-evolution is calculated with the rotation operator. The sequence is to be called by the parallel_map method of QuTip. Parameters ---------- tau : float Free evolution time Returns ------- rho : Qobj Final state """ self._reset_sequence() if self.pi_pulse_duration == 0: # initial pi/2 pulse on Y self._delta_pulse(self.Ry_half) self._free_evolution(tau / 2, self.options) # repeat M times the pi pulse and free evolution of tau for idx_M in range(self.M): self._delta_pulse(self.Rx) if idx_M != self.M - 1: self._free_evolution(tau, self.options) self._free_evolution(tau / 2, self.options) if self.projection_pulse: # final pi/2 pulse on Y self._delta_pulse(self.Ry_half) else: ps = tau - self.pi_pulse_duration # initial pi/2 pulse on Y self._pulse(self.Ht, self.pi_pulse_duration / 2, self.options, self.pulse_params[1]) self._free_evolution(ps / 2 - self.pi_pulse_duration / 2, self.options) # repeat M times the pi pulse and free evolution of ps for idx_M in range(self.M): self._pulse(self.Ht, self.pi_pulse_duration, self.options, self.pulse_params[0]) if idx_M != self.M - 1: self._free_evolution(ps, self.options) if self.projection_pulse: self._free_evolution(ps / 2 - self.pi_pulse_duration / 2, self.options) # final pi/2 pulse on Y self._pulse(self.Ht, self.pi_pulse_duration / 2, self.options, self.pulse_params[1]) else: self._free_evolution(ps / 2, self.options) return self.rho
def _get_pulse_profiles(self, tau: float | None = None) -> None: """ Generates the pulse profiles for the CPMG sequence for a given tau. Parameters ---------- tau : float Free evolution variable or pulse spacing for the sequence. """ tau = self._check_tau(tau) self.pulse_profiles = [] ps = tau - self.pi_pulse_duration self._append_pulse_to_profile(0, self.pi_pulse_duration / 2, self.pulse_params[1]) t0 = self.pi_pulse_duration / 2 self.pulse_profiles.append( ["free_evo", [t0, t0 + ps / 2 - self.pi_pulse_duration / 2], None, None] ) t0 += ps / 2 - self.pi_pulse_duration / 2 for idx_M in range(self.M): self._append_pulse_to_profile(t0, self.pi_pulse_duration, self.pulse_params[0]) t0 += self.pi_pulse_duration if idx_M != self.M - 1: self.pulse_profiles.append(["free_evo", [t0, t0 + ps], None, None]) t0 += ps if self.projection_pulse: self.pulse_profiles.append( ["free_evo", [t0, t0 + ps / 2 - self.pi_pulse_duration / 2], None, None] ) t0 += ps / 2 - self.pi_pulse_duration / 2 self._append_pulse_to_profile(t0, self.pi_pulse_duration / 2, self.pulse_params[1]) t0 += self.pi_pulse_duration / 2 else: self.pulse_profiles.append(["free_evo", [t0, t0 + ps / 2], None, None]) t0 += ps / 2 self.total_time = t0
[docs] def plot_pulses( self, figsize: tuple[int, int] = (6, 4), xlabel: str = "Time", ylabel: str = "Pulse Intensity", title: str = "Pulse Profiles of XY8 Sequence", tau: float | None = None, ) -> None: """ Overwrites the plot_pulses method of the parent class in order to first generate the pulse profiles for the CPMG sequence for a given tau and then plot them. Parameters ---------- tau : float Free evolution time for the sequence. Contrary to the run method, tau must be a single number in order to plot the pulse profiles. """ self._get_pulse_profiles(tau) # call the plot_pulses method of the parent class super().plot_pulses(figsize, xlabel, ylabel, title)
####################################################################################################
[docs] class XY(PulsedSim): """ This class contains the XY-M pulse sequence. The sequence is composed of intercalated X and Y pi pulses and free evolutions repeated M times. It acts similar to the CPMG sequence, but the alternation of the pulse improves noise suppression on different axis. The initial and final pi/2 pulses are played on the Y axis, while the refocusing pi pulses alternate between X and Y. Notes ----- The XY sequence inherits the methods and attributes from the PulsedSim class. On the phase convention of the pi/2 pulses: contrary to the XY8 sequence implemented in this module, which prepares and projects along X, the XY-M sequence here prepares and projects along Y. This is deliberate and must not be "harmonized" with XY8. Differently from XY8, whose eight pulse phase pattern is reproduced consistently across the literature, the plain XY-M sequence is not standardized in the field, with the axis of the initial pi/2 pulse varying between references. The convention used here is the one of the experimental setup the sequence was written for, and published results were obtained with it. The choice is not merely cosmetic, as the phase of the preparation pulse relative to the refocusing pulses determines whether the prepared state is locked along a refocusing axis, which changes how sensitive the sequence is to pulse errors. Methods ------- XY_sequence : Defines the XY sequence for a given free evolution time tau and the set of attributes defined in the constructor, returning the final state. The sequence is to be called by the parallel_map method of QuTip. _get_pulse_profiles : Generates the pulse profiles for the XY-M sequence for a given tau. The pulse profiles are stored in the pulse_profiles attribute of the object. plot_pulses : Overwrites the plot_pulses method of the parent class in order to first generate the pulse profiles for the XY-M sequence for a given tau and then plot them. """ # the sequence opens and closes with tau/2 around the pi pulses, therefore # tau must be at least twice the pi_pulse_duration to avoid negative free evolutions _min_tau_factor = 2 def __init__( self, free_duration: np.ndarray | list[float], *, system: QSys, M: int, pi_pulse_duration: float, h1: Qobj | list[Qobj] | None = None, Rx: Qobj | None = None, Ry: Qobj | None = None, H2: tuple[Qobj, Callable] | None = None, projection_pulse: bool = True, pulse_shape: Callable = square_pulse, pulse_params: dict[str, float] | None = None, time_steps: int = 100, options: dict | None = None, ) -> None: """ Class constructor for the XY sequence Parameters ---------- free_duration : numpy array Time array for the simulation representing the free evolution time to be used as the variable attribute for the simulation system : QSys Quantum system object containing the initial state, internal time independent Hamiltonian and collapse operators M : int Order of the XY sequence pi_pulse_duration : float, int or 0 Duration of the pi pulse. If set to 0, the pulses are perfect delta pulses and the time-evolution is calculated with the rotation operator. h1 : Qobj or list of Qobj Control Hamiltonian of the system. Rx : Qobj or None Rotation operator around the x-axis, used only if the pi_pulse_duration is set to 0. Ry : Qobj or None Rotation operator around the y-axis, used only if the pi_pulse_duration is set to 0. H2 : Qobj, list(Qobj), optional Time dependent sensing Hamiltonian of the system pulse_shape : FunctionType, list(FunctionType), optional Pulse shape function or list of pulse shape functions representing the time modulation of h1 pulse_params : dict, optional Dictionary of parameters for the pulse_shape functions time_steps : int, optional Number of time steps in the pulses for the simulation options : dict, optional Dictionary of solver options """ super().__init__(system, H2=H2) self.sequence = self.XY_sequence self._check_attr_predef_seqs( h1, Rx, Ry, pulse_shape, pulse_params, options, time_steps, free_duration, pi_pulse_duration, M, projection_pulse, ) base_pulse = self.pulse_params.copy() self.pulse_params = [{**base_pulse, "phi_t": 0}, {**base_pulse, "phi_t": -np.pi / 2}]
[docs] def XY_sequence(self, tau: float) -> Qobj: """ Defines the XY-M composed of intercalated pi pulses on X and Y axis with free evolutions of time tau repeated M times. The sequence starts with a pi/2 pulse on the Y axis, see the note on the phase convention in the class docstring. If projection_pulse is True, the sequence will include a final pi/2 pulse on Y axis to project the measurement into the Sz basis. If the pi_pulse_duration is set to 0, the pulses are perfect delta pulses and the time-evolution is calculated with the rotation operator. The sequence is to be called by the parallel_map method of QuTip. Parameters ---------- tau : float Free evolution time Returns ------- rho : Qobj Final state """ self._reset_sequence() if self.pi_pulse_duration == 0: # initial pi/2 pulse on Y, matching the finite pulse branch below self._delta_pulse(self.Ry_half) self._free_evolution(tau / 2, self.options) # repeat M times the pi X pulse, free evolution of ps, pi Y pulse and free evolution of # ps for idx_M in range(self.M): self._delta_pulse(self.Rx) self._free_evolution(tau, self.options) self._delta_pulse(self.Ry) if idx_M != self.M - 1: self._free_evolution(tau, self.options) # the sequence is symmetric, therefore it closes with tau/2 as it started, # giving a total sequence time of 2*M*tau self._free_evolution(tau / 2, self.options) if self.projection_pulse: self._delta_pulse(self.Ry_half) else: ps = tau - self.pi_pulse_duration # initial pi/2 pulse on Y, see the note on the phase convention in the class docstring self._pulse(self.Ht, self.pi_pulse_duration / 2, self.options, self.pulse_params[1]) self._free_evolution(ps / 2 - self.pi_pulse_duration / 2, self.options) # repeat M times the pi X pulse, free evolution of ps, pi Y pulse and free evolution of # ps for idx_M in range(self.M): self._pulse(self.Ht, self.pi_pulse_duration, self.options, self.pulse_params[0]) self._free_evolution(ps, self.options) self._pulse(self.Ht, self.pi_pulse_duration, self.options, self.pulse_params[1]) if idx_M != self.M - 1: self._free_evolution(ps, self.options) if self.projection_pulse: self._free_evolution(ps / 2 - self.pi_pulse_duration / 2, self.options) # final pi/2 pulse on Y self._pulse(self.Ht, self.pi_pulse_duration / 2, self.options, self.pulse_params[1]) else: self._free_evolution(ps / 2, self.options) return self.rho
def _get_pulse_profiles(self, tau: float | None = None) -> None: """ Generates the pulse profiles for the XY-M sequence for a given tau. The pulse profiles are stored in the pulse_profiles attribute of the object. Parameters ---------- tau : float free evolution variable or pulse spacing for the sequence """ tau = self._check_tau(tau) self.pulse_profiles = [] ps = tau - self.pi_pulse_duration # the first and last pi/2 pulses are played on Y by XY_sequence, thus pulse_params[1] self._append_pulse_to_profile(0, self.pi_pulse_duration / 2, self.pulse_params[1]) t0 = self.pi_pulse_duration / 2 self.pulse_profiles.append( ["free_evo", [t0, t0 + ps / 2 - self.pi_pulse_duration / 2], None, None] ) t0 += ps / 2 - self.pi_pulse_duration / 2 for idx_M in range(2 * self.M): self._append_pulse_to_profile(t0, self.pi_pulse_duration, self.pulse_params[idx_M % 2]) t0 += self.pi_pulse_duration if idx_M != 2 * self.M - 1: self.pulse_profiles.append(["free_evo", [t0, t0 + ps], None, None]) t0 += ps if self.projection_pulse: self.pulse_profiles.append( ["free_evo", [t0, t0 + ps / 2 - self.pi_pulse_duration / 2], None, None] ) t0 += ps / 2 - self.pi_pulse_duration / 2 self._append_pulse_to_profile(t0, self.pi_pulse_duration / 2, self.pulse_params[1]) t0 += self.pi_pulse_duration / 2 else: self.pulse_profiles.append(["free_evo", [t0, t0 + ps / 2], None, None]) t0 += ps / 2 self.total_time = t0
[docs] def plot_pulses( self, figsize: tuple[int, int] = (6, 6), xlabel: str = "Time", ylabel: str = "Pulse Intensity", title: str = "Pulse Profiles of XY8 Sequence", tau: float | None = None, ) -> None: """ Overwrites the plot_pulses method of the parent class in order to first generate the pulse profiles for the XY-M sequence for a given tau and then plot them. Parameters ---------- tau : float Free evolution time for the sequence. Contrary to the run method, tau must be a single number in order to plot the pulse profiles. """ # generate the pulse profiles for the given tau self._get_pulse_profiles(tau) # call the plot_pulses method of the parent class super().plot_pulses(figsize, xlabel, ylabel, title)
####################################################################################################
[docs] class XY8(PulsedSim): """ This contains the XY8-M sequence. The XY8-M is a further improvement from the XY-M sequence, where the X and Y pulses are group antisymmetrically in pairs of 4 as X-Y-X-Y-Y-X-Y-X, in order to improve noise suppression and pulse errors. Methods ------- XY_sequence : Defines the XY8-M sequence for a given free evolution time tau and the set of attributes defined in the constructor, returning the final state. The sequence is to be called by the parallel_map method of QuTip. _get_pulse_profiles : Generates the pulse profiles for the XY8-M sequence for a given tau. The pulse profiles are stored in the pulse_profiles attribute of the object. plot_pulses : Overwrites the plot_pulses method of the parent class in order to first generate the pulse profiles for the XY8-M sequence for a given tau and then plot them. Notes ----- The XY8 sequence inherits the methods and attributes from the PulsedSim class. """ # the sequence opens and closes with tau/2 around the pi pulses, therefore # tau must be at least twice the pi_pulse_duration to avoid negative free evolutions _min_tau_factor = 2 def __init__( self, free_duration: np.ndarray | list[float], *, system: QSys, M: int, pi_pulse_duration: float, h1: Qobj | list[Qobj] | None = None, Rx: Qobj | None = None, Ry: Qobj | None = None, H2: tuple[Qobj, Callable] | None = None, projection_pulse: bool = True, pulse_shape: Callable = square_pulse, pulse_params: dict[str, float] | None = None, time_steps: int = 100, options: dict | None = None, RXY8: bool = False, seed: int | None = None, ) -> None: """ Class constructor for the XY8 sequence Parameters ---------- free_duration : numpy array Time array for the simulation representing the free evolution time to be used as the variable attribute for the simulation system : QSys Quantum system object containing the initial state, internal time independent Hamiltonian and collapse operators M : int Order of the XY sequence pi_pulse_duration : float, int or 0 Duration of the pi pulse. If set to 0, the pulses are perfect delta pulses and the time-evolution is calculated with the rotation operator. h1 : Qobj or list of Qobj Control Hamiltonian of the system. Rx : Qobj or None Rotation operator around the x-axis, used only if the pi_pulse_duration is set to 0. Ry : Qobj or None Rotation operator around the y-axis, used only if the pi_pulse_duration is set to 0. H2 : Qobj, list(Qobj), optional Time dependent sensing Hamiltonian of the system pulse_shape : FunctionType, list(FunctionType), optional Pulse shape function or list of pulse shape functions representing the time modulation of h1 pulse_params : dict, optional Dictionary of parameters for the pulse_shape functions time_steps : int, optional Number of time steps in the pulses for the simulation options : dict, optional Dictionary of solver options from Qutip projection_pulse : bool Boolean to determine if a final pi/2 pulse is to be included in order to project the measurement into the Sz basis RXY8 : bool Boolen to determine if a random phase is to be added to each XY8 block seed : int or None Seed for the random phases of the RXY8 sequence, so that the simulation is reproducible. Only used if RXY8 is True. """ if not isinstance(RXY8, bool): raise ValueError( # noqa: TRY004 f"RXY8 must be a boolean value indicating whether to add a random phase to each XY8 block or not, got {RXY8}." ) super().__init__(system, H2=H2) self.sequence = self.XY8_sequence self._check_attr_predef_seqs( h1, Rx, Ry, pulse_shape, pulse_params, options, time_steps, free_duration, pi_pulse_duration, M, projection_pulse, ) if self.pi_pulse_duration == 0 and RXY8: warnings.warn( "RXY8 with delta pulses is not implemented, as it has not experimental relevance.", stacklevel=2, ) random_phases = np.zeros(M) elif RXY8: # the phases are drawn once for the whole simulation, so that every point of the # variable is calculated with the same sequence random_phases = np.random.default_rng(seed).random(M) * 2 * np.pi else: random_phases = np.zeros(M) # generate the pulse parameters for the XY8 sequence with the correct order of the x and y # pulses base_pulse = self.pulse_params.copy() self.pulse_params = [] for idx_M in range(M): phi_x = 0 + random_phases[idx_M] phi_y = -np.pi / 2 + random_phases[idx_M] px = base_pulse.copy() py = base_pulse.copy() px["phi_t"] = phi_x py["phi_t"] = phi_y self.pulse_params.extend([px, py, px, py, py, px, py, px])
[docs] def XY8_sequence(self, tau: float) -> Qobj: """ Defines the XY8-M composed of 8 intercalated pi pulses on X and Y axis with free evolutions of time tau repeated M times. If random_phase is set to True, a random phase is added in each XY8 block. If projection_pulse is True, the sequence will include a final pi/2 pulse on X axis to project the measurement into the Sz basis. If the pi_pulse_duration is set to 0, the pulses are perfect delta pulses and the time-evolution is calculated with the rotation operator. The sequence is to be called by the parallel_map method of QuTip. Parameters ---------- tau : float Free evolution time Returns ------- rho : Qobj Final state """ self._reset_sequence() if self.pi_pulse_duration == 0: # initial pi/2 pulse on X self._delta_pulse(self.Rx_half) self._free_evolution(tau / 2, self.options) # repeat 8 times the block for idx_M in range(self.M): self._delta_pulse(self.Rx) self._free_evolution(tau, self.options) self._delta_pulse(self.Ry) self._free_evolution(tau, self.options) self._delta_pulse(self.Rx) self._free_evolution(tau, self.options) self._delta_pulse(self.Ry) self._free_evolution(tau, self.options) self._delta_pulse(self.Ry) self._free_evolution(tau, self.options) self._delta_pulse(self.Rx) self._free_evolution(tau, self.options) self._delta_pulse(self.Ry) self._free_evolution(tau, self.options) self._delta_pulse(self.Rx) if idx_M != self.M - 1: self._free_evolution(tau, self.options) self._free_evolution(tau / 2, self.options) if self.projection_pulse: self._delta_pulse(self.Rx_half) else: ps = tau - self.pi_pulse_duration # initial pi/2 pulse on X self._pulse(self.Ht, self.pi_pulse_duration / 2, self.options, self.pulse_params[0]) self._free_evolution(ps / 2 - self.pi_pulse_duration / 2, self.options) # repeat 8*M-1 times the pi pulse and free evolution of ps for idx_M in range(8 * self.M): self._pulse(self.Ht, self.pi_pulse_duration, self.options, self.pulse_params[idx_M]) if idx_M != 8 * self.M - 1: self._free_evolution(ps, self.options) if self.projection_pulse: self._free_evolution(ps / 2 - self.pi_pulse_duration / 2, self.options) self._pulse(self.Ht, self.pi_pulse_duration / 2, self.options, self.pulse_params[0]) else: self._free_evolution(ps / 2, self.options) return self.rho
def _get_pulse_profiles(self, tau: float | None = None) -> None: """ Generates the pulse profiles for the XY8-M sequence for a given tau. The pulse profiles are stored in the pulse_profiles attribute of the object. Parameters ---------- tau : float free evolution variable or pulse spacing for the sequence """ tau = self._check_tau(tau) self.pulse_profiles = [] ps = tau - self.pi_pulse_duration self._append_pulse_to_profile(0, self.pi_pulse_duration / 2, self.pulse_params[0]) t0 = self.pi_pulse_duration / 2 self.pulse_profiles.append( ["free_evo", [t0, t0 + ps / 2 - self.pi_pulse_duration / 2], None, None] ) t0 += ps / 2 - self.pi_pulse_duration / 2 for idx_M in range(8 * self.M): self._append_pulse_to_profile(t0, self.pi_pulse_duration, self.pulse_params[idx_M]) t0 += self.pi_pulse_duration if idx_M != 8 * self.M - 1: self.pulse_profiles.append(["free_evo", [t0, t0 + ps], None, None]) t0 += ps if self.projection_pulse: self.pulse_profiles.append( ["free_evo", [t0, t0 + ps / 2 - self.pi_pulse_duration / 2], None, None] ) t0 += ps / 2 - self.pi_pulse_duration / 2 self._append_pulse_to_profile(t0, self.pi_pulse_duration / 2, self.pulse_params[0]) t0 += self.pi_pulse_duration / 2 else: self.pulse_profiles.append(["free_evo", [t0, t0 + ps / 2], None, None]) t0 += ps / 2 self.total_time = t0
[docs] def plot_pulses( self, figsize: tuple[int, int] = (6, 4), xlabel: str = "Time", ylabel: str = "Pulse Intensity", title: str = "Pulse Profiles of XY8 Sequence", tau: float | None = None, ) -> None: """ Overwrites the plot_pulses method of the parent class in order to first generate the pulse profiles for the XY8-M sequence for a given tau and then plot them. Parameters ---------- tau : float Free evolution time for the sequence. Contrary to the run method, tau must be a single number in order to plot the pulse profiles. """ self._get_pulse_profiles(tau) # call the plot_pulses method of the parent class super().plot_pulses(figsize, xlabel, ylabel, title)