"""
This module contains dynamical decoupling pulse sequences, used in quantum sensing and for extending coherence of quantum systems.
"""
import numpy as np
from qutip import Qobj
from .pulse_shapes import square_pulse
from .pulsed_sim import PulsedSim
####################################################################################################
[docs]
class CPMG(PulsedSim):
"""
This class contains a Carr-Purcell-Meiboom-Gill sequence used in quantum sensing experiments, inheriting from the PulsedSim class.
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.
Attributes
----------
M : int
number of pulses (order) in the CPMG sequence
free_duration : numpy array
time array for the simulation representing the free evolution time to be used as the variable attribute for the simulation
pi_pulse_duration : float or int
duration of the pi pulse
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
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.
CPMG_sequence_proj :
CPMG sequence with a final pi/2 pulse.
_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 :
plots the pulse profiles for the CPMG sequence for a given tau. The pulse profiles are generated by the _get_pulse_profiles method.
The plot is generated using matplotlib.
"""
def __init__(self, M, free_duration, pi_pulse_duration, system, H1, H2=None, projection_pulse=True, pulse_shape=square_pulse, pulse_params=None, options=None, time_steps=100):
"""
Class constructor for the Carr-Purcell-Meiboom-Gill sequence
Parameters
----------
M : int
order of the CPMG sequence
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
H1 : Qobj or list(Qobj)
control Hamiltonian of the system
pi_pulse_duration : float or int
duration of the pi pulse
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)
self._check_attr_predef_seqs(H1, pulse_shape, pulse_params, options, time_steps, free_duration, pi_pulse_duration, M)
# initialize the pulse_params attribute as a list of dictionaries for X and Y pulses
self.pulse_params = np.empty(2, dtype=dict)
# X pulse parameters are the first element in the list and Y pulses are the second element
self.pulse_params[0] = {**pulse_params, **{"phi_t": 0}}
self.pulse_params[1] = {**pulse_params, **{"phi_t": -np.pi / 2}}
# If projection_pulse is True, the sequence is set to the CPMG_sequence_proj method with the initial and final projection pulses into the Sz basis
# otherwise it is set to the CPMG_sequence method without the projection pulses
if projection_pulse:
self.sequence = self.CPMG_sequence_proj
elif not projection_pulse:
self.sequence = self.CPMG_sequence
else:
raise ValueError("projection_pulse must be a boolean")
self.projection_pulse = projection_pulse
[docs]
def CPMG_sequence(self, tau):
"""
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.
The sequence is to be called by the parallel_map method of QuTip.
Parameters
----------
tau : float
free evolution time
Returns
-------
rho : Qobj
final state
"""
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-1 times the pi pulse and free evolution of ps
for itr_M in range(self.M - 1):
self._pulse(self.Ht, self.pi_pulse_duration, self.options, self.pulse_params[0])
self._free_evolution(ps, self.options)
# perform the last pi pulse on X
self._pulse(self.Ht, self.pi_pulse_duration, self.options, self.pulse_params[0])
self._free_evolution(ps/2, self.options)
return self.rho
[docs]
def CPMG_sequence_proj(self, tau):
"""
Defines the CPMG sequence, but with a final pi/2 pulse in order to project the result into the Sz basis.
The sequence is to be called by the parallel_map method of QuTip.
Parameters
----------
tau : float
free evolution time
Returns
-------
rho : Qobj
final state
"""
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-1 times the pi pulse and free evolution of ps
for itr_M in range(self.M - 1):
self._pulse(self.Ht, self.pi_pulse_duration, self.options, self.pulse_params[0])
self._free_evolution(ps, self.options)
# perform the last pi pulse on X
self._pulse(self.Ht, self.pi_pulse_duration, self.options, self.pulse_params[0])
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])
return self.rho
def _get_pulse_profiles(self, tau=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.
"""
# check if tau is correctly defined
if tau is None:
tau = self.variable[-1]
elif not isinstance(tau, (int, float)) or tau < self.pi_pulse_duration:
raise ValueError("tau must be a positive real number larger than pi_pulse_duration")
self.pulse_profiles = []
# add the first pi/2 pulse on Y
if isinstance(self.H1, Qobj):
self.pulse_profiles.append([self.H1, np.linspace(0, self.pi_pulse_duration / 2, self.time_steps), self.pulse_shape, self.pulse_params[1]])
elif isinstance(self.H1, list):
self.pulse_profiles.append([[self.H1[i], np.linspace(0, self.pi_pulse_duration / 2, self.time_steps), self.pulse_shape[i], self.pulse_params[1]] for i in range(len(self.H1))])
t0 = self.pi_pulse_duration / 2
ps = tau - self.pi_pulse_duration
# add the first free evolution
self.pulse_profiles.append([None, [t0, t0 + ps/2 - self.pi_pulse_duration/2], None, None])
t0 += ps/2 - self.pi_pulse_duration/2
# add pulses and free evolution 8*M-1 times
for itr_M in range(8*self.M - 1):
# add a pi pulse on X
if isinstance(self.H1, Qobj):
self.pulse_profiles.append([self.H1, np.linspace(t0, t0 + self.pi_pulse_duration, self.time_steps), self.pulse_shape, self.pulse_params[0]])
elif isinstance(self.H1, list):
self.pulse_profiles.append([[self.H1[i], np.linspace(t0, t0 + self.pi_pulse_duration, self.time_steps), self.pulse_shape[i], self.pulse_params[0]] for i in range(len(self.H1))])
t0 += self.pi_pulse_duration
# add a free evolution
self.pulse_profiles.append([None, [t0, t0 + ps], None, None])
t0 += ps
# add another pi pulse on X
if isinstance(self.H1, Qobj):
self.pulse_profiles.append([self.H1, np.linspace(t0, t0 + self.pi_pulse_duration, self.time_steps), self.pulse_shape, self.pulse_params[0]])
elif isinstance(self.H1, list):
self.pulse_profiles.append([[self.H1[i], np.linspace(t0, t0 + self.pi_pulse_duration, self.time_steps), self.pulse_shape[i], self.pulse_params[0]] for i in range(len(self.H1))])
t0 += self.pi_pulse_duration
if self.projection_pulse:
# add the last free evolution
self.pulse_profiles.append([None, [t0, t0 + ps/2 - self.pi_pulse_duration/2], None, None])
t0 += ps/2 - self.pi_pulse_duration/2
if isinstance(self.H1, Qobj):
# add the last pi/2 pulse
self.pulse_profiles.append([self.H1, np.linspace(t0, t0 + self.pi_pulse_duration / 2, self.time_steps), self.pulse_shape, self.pulse_params[1]])
elif isinstance(self.H1, list):
# add the first pi/2 pulse
self.pulse_profiles.append([[self.H1[i], np.linspace(t0, t0 + self.pi_pulse_duration / 2, self.time_steps), self.pulse_shape[i], self.pulse_params[1]] for i in range(len(self.H1))])
t0 += self.pi_pulse_duration / 2
else:
# add the last free evolution
self.pulse_profiles.append([None, [t0, t0 + ps/2], None, None])
t0 += ps/2
self.total_time = t0
[docs]
def plot_pulses(self, figsize=(6, 6), xlabel=None, ylabel="Expectation Value", title="Pulse Profiles", tau=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, the free evolution must be a single number in order to plot the pulse profiles.
figsize : tuple
Size of the figure to be passed to matplotlib.pyplot
xlabel : str
Label of the x-axis
ylabel : str
Label of the y-axis
title : str
Title of the plot
"""
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, inheriting from PulsedSim class.
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.
Attributes
----------
M : int
Order of the XY sequence
free_duration : numpy array
Time array for the simulation representing the free evolution time to be used as the variable attribute for the simulation
pi_pulse_duration : float, int
Duration of the pi pulse
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
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.
XY_sequence_proj :
XY sequence with projection pulse.
_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.
"""
def __init__(self, M, free_duration, pi_pulse_duration, system, H1, H2=None, projection_pulse=True, pulse_shape=square_pulse, pulse_params=None, options=None, time_steps=100):
"""
Class constructor for the XY sequence
Parameters
----------
M : int
Order of the XY sequence
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
H1 : Qobj, list(Qobj)
Control Hamiltonian of the system
pi_pulse_duration : float, int
Duration of the pi pulse
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)
self._check_attr_predef_seqs(H1, pulse_shape, pulse_params, options, time_steps, free_duration, pi_pulse_duration, M)
# initialize the pulse_params attribute as a list of dictionaries for X and Y pulses
self.pulse_params = np.empty(2, dtype=dict)
# X pulse parameters are the first element in the list and Y pulses are the second element
self.pulse_params[0] = {**pulse_params, **{"phi_t": 0}}
self.pulse_params[1] = {**pulse_params, **{"phi_t": -np.pi / 2}}
# If projection_pulse is True, the sequence is set to the XY_sequence_proj method with the final projection pulse into the Sz basis, otherwise it is set to the XY_sequence method without the projection pulse
if projection_pulse:
self.sequence = self.XY_sequence_proj
elif not projection_pulse:
self.sequence = self.XY_sequence
else:
raise ValueError("projection_pulse must be a boolean")
self.projection_pulse = projection_pulse
[docs]
def XY_sequence(self, tau):
"""
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 is to be called by the parallel_map method of QuTip.
Parameters
----------
tau : float
Free evolution time
Returns
-------
rho : Qobj
Final state
"""
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 M-1 times the pi X pulse, free evolution of ps, pi Y pulse and free evolution of ps
for itr_M in range(2 * self.M - 1):
self._pulse(self.Ht, self.pi_pulse_duration, self.options, self.pulse_params[itr_M % 2])
self._free_evolution(ps, self.options)
self._pulse(self.Ht, self.pi_pulse_duration, self.options, self.pulse_params[1])
self._free_evolution(ps/2, self.options)
return self.rho
[docs]
def XY_sequence_proj(self, tau):
"""
Defines the XY-M sequence with an initial pi/2 pulse and a final pi/2 pulse in order to project the measurement in the Sz basis.
The sequence is to be called by the parallel_map method of QuTip.
Parameters
----------
tau : float
Free evolution time
Returns
-------
rho : Qobj
Final state
"""
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 M-1 times the pi X pulse, free evolution of ps, pi Y pulse and free evolution of ps
for itr_M in range(2 * self.M - 1):
self._pulse(self.Ht, self.pi_pulse_duration, self.options, self.pulse_params[itr_M % 2])
self._free_evolution(ps, self.options)
self._pulse(self.Ht, self.pi_pulse_duration, self.options, self.pulse_params[1])
self._free_evolution(ps/2 - self.pi_pulse_duration/2, self.options)
# final pi/2 pulse on X
self._pulse(self.Ht, self.pi_pulse_duration / 2, self.options, self.pulse_params[0])
return self.rho
def _get_pulse_profiles(self, tau=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
"""
# checl if tau is correctly define
if tau is None:
tau = self.variable[-1]
elif not isinstance(tau, (int, float)) or tau < self.pi_pulse_duration:
raise ValueError("tau must be a positive real number larger than pi_pulse_duration")
self.pulse_profiles = []
# add the first pi/2 pulse on X axis
if isinstance(self.H1, Qobj):
self.pulse_profiles.append([self.H1, np.linspace(0, self.pi_pulse_duration / 2, self.time_steps), self.pulse_shape, self.pulse_params[0]])
elif isinstance(self.H1, list):
self.pulse_profiles.append([[self.H1[i], np.linspace(0, self.pi_pulse_duration / 2, self.time_steps), self.pulse_shape[i], self.pulse_params[0]] for i in range(len(self.H1))])
t0 = self.pi_pulse_duration / 2
ps = tau - self.pi_pulse_duration
# add the first free evolution
self.pulse_profiles.append([None, [t0, t0 + ps/2 - self.pi_pulse_duration/2], None, None])
t0 += ps/2 - self.pi_pulse_duration/2
# add pulses and free evolution M-1 times
for itr_M in range(2 * self.M - 1):
# add a pi pulse
if isinstance(self.H1, Qobj):
self.pulse_profiles.append([self.H1, np.linspace(t0, t0 + self.pi_pulse_duration, self.time_steps), self.pulse_shape, self.pulse_params[itr_M % 2]])
elif isinstance(self.H1, list):
self.pulse_profiles.append(
[[self.H1[i], np.linspace(t0, t0 + self.pi_pulse_duration, self.time_steps), self.pulse_shape[i], self.pulse_params[itr_M % 2]] for i in range(len(self.H1))]
)
t0 += self.pi_pulse_duration
# add a free evolution
self.pulse_profiles.append([None, [t0, t0 + ps], None, None])
t0 += ps
# add another pi pulse
if isinstance(self.H1, Qobj):
self.pulse_profiles.append([self.H1, np.linspace(t0, t0 + self.pi_pulse_duration, self.time_steps), self.pulse_shape, self.pulse_params[1]])
elif isinstance(self.H1, list):
self.pulse_profiles.append([[self.H1[i], np.linspace(t0, t0 + self.pi_pulse_duration, self.time_steps), self.pulse_shape[i], self.pulse_params[1]] for i in range(len(self.H1))])
t0 += self.pi_pulse_duration
if self.projection_pulse:
# add the last free evolution
self.pulse_profiles.append([None, [t0, t0 + ps/2 - self.pi_pulse_duration/2], None, None])
t0 += ps/2 - self.pi_pulse_duration/2
if isinstance(self.H1, Qobj):
# add the last pi/2 pulse
self.pulse_profiles.append([self.H1, np.linspace(t0, t0 + self.pi_pulse_duration / 2, self.time_steps), self.pulse_shape, self.pulse_params[0]])
elif isinstance(self.H1, list):
# add the first pi/2 pulse
self.pulse_profiles.append([[self.H1[i], np.linspace(t0, t0 + self.pi_pulse_duration / 2, self.time_steps), self.pulse_shape[i], self.pulse_params[0]] for i in range(len(self.H1))])
t0 += self.pi_pulse_duration / 2
else:
# add the last free evolution
self.pulse_profiles.append([None, [t0, t0 + ps/2], None, None])
t0 += ps/2
self.total_time = t0
[docs]
def plot_pulses(self, figsize=(6, 6), xlabel=None, ylabel="Expectation Value", title="Pulse Profiles", tau=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, the free evolution must be a single number in order to plot the pulse profiles.
figsize : tuple
Size of the figure to be passed to matplotlib.pyplot
xlabel : str
Label of the x-axis
ylabel : str
Label of the y-axis
title : str
Title of the plot
"""
# 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, inheriting from Pulsed Simulation.
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.
Attributes
----------
M : int
Order of the XY sequence
free_duration : numpy array
Time array for the simulation representing the free evolution time to be used as the variable attribute for the simulation
pi_pulse_duration : float, int
Duration of the pi pulse
projection_pulse : bool
Boolean to determine if a final pi/2 pulse is to be included in order to project the measurement in the Sz basis
Methods
-------
XY8_sequence(tau)
Defines the XY8 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.
XY8_sequence_proj(tau)
XY8 sequence with projection pulse.
XY8_sequence_H2(tau)
XY8 sequence with time dependent H2 or collapse operators.
XY8_sequence_proj_H2(tau)
XY8 sequence with time dependent H2 or collapse operators and a final pi/2 pulse.
_get_pulse_profiles(tau)
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.
"""
def __init__(self, M, free_duration, pi_pulse_duration, system, H1, H2=None, projection_pulse=True, pulse_shape=square_pulse, pulse_params=None, options=None, time_steps=100):
"""
Class constructor for the XY8 sequence
Parameters
----------
M : int
Order of the XY sequence
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
H1 : Qobj, list(Qobj)
Control Hamiltonian of the system
pi_pulse_duration : float, int
Duration of the pi pulse
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
"""
super().__init__(system, H2)
self._check_attr_predef_seqs(H1, pulse_shape, pulse_params, options, time_steps, free_duration, pi_pulse_duration, M)
# initialize the pulse_params attribute as a list of dictionaries for X and Y pulses
self.pulse_params = np.empty(8, dtype=dict)
# define XY8 pulse parameters for the sequence: X-Y-X-Y-Y-X-Y-X
self.pulse_params[0] = {**pulse_params, **{"phi_t": 0}}
self.pulse_params[1] = {**pulse_params, **{"phi_t": -np.pi / 2}}
self.pulse_params[2] = self.pulse_params[0]
self.pulse_params[3] = self.pulse_params[1]
self.pulse_params[4] = self.pulse_params[1]
self.pulse_params[5] = self.pulse_params[0]
self.pulse_params[6] = self.pulse_params[1]
self.pulse_params[7] = self.pulse_params[0]
# If projection_pulse is True, the sequence is set to the XY8_sequence_proj method with the final projection pulse into the Sz basis, otherwise it is set to the XY8_sequence method without the projection pulse
if projection_pulse:
self.sequence = self.XY8_sequence_proj
elif not projection_pulse:
self.sequence = self.XY8_sequence
else:
raise ValueError("projection_pulse must be a boolean")
self.projection_pulse = projection_pulse
[docs]
def XY8_sequence(self, tau):
"""
Defines the XY8-M composed of 8 intercalated pi pulses on X and Y axis with free evolutions of time tau repeated M times.
The sequence is to be called by the parallel_map method of QuTip.
Parameters
----------
tau : float
Free evolution time
Returns
-------
rho : Qobj
Final state
"""
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 itr_M in range(8*self.M - 1):
self._pulse(self.Ht, self.pi_pulse_duration, self.options, self.pulse_params[itr_M % 8])
self._free_evolution(ps, self.options)
# perform the last pi pulse on X
self._pulse(self.Ht, self.pi_pulse_duration, self.options, self.pulse_params[0])
self._free_evolution(ps/2, self.options)
return self.rho
[docs]
def XY8_sequence_proj(self, tau):
"""
Defines the XY8-M composed of 8 intercalated pi pulses on X and Y axis with free evolutions of time tau repeated M times.
The sequence is to be called by the parallel_map method of QuTip.
Parameters
----------
tau : float
Free evolution time
Returns
-------
rho : Qobj
Final state
"""
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 itr_M in range(8*self.M - 1):
self._pulse(self.Ht, self.pi_pulse_duration, self.options, self.pulse_params[itr_M % 8])
self._free_evolution(ps, self.options)
# perform the last pi pulse on X
self._pulse(self.Ht, self.pi_pulse_duration, self.options, self.pulse_params[0])
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])
return self.rho
def _get_pulse_profiles(self, tau=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
"""
# check if tau is correctly defined
if tau is None:
tau = self.variable[-1]
elif not isinstance(tau, (int, float)) or tau < self.pi_pulse_duration:
raise ValueError("tau must be a positive real number larger than pi_pulse_duration")
self.pulse_profiles = []
# add the first pi/2 pulse on X axis
if isinstance(self.H1, Qobj):
self.pulse_profiles.append([self.H1, np.linspace(0, self.pi_pulse_duration / 2, self.time_steps), self.pulse_shape, self.pulse_params[0]])
elif isinstance(self.H1, list):
self.pulse_profiles.append([[self.H1[i], np.linspace(0, self.pi_pulse_duration / 2, self.time_steps), self.pulse_shape[i], self.pulse_params[0]] for i in range(len(self.H1))])
t0 = self.pi_pulse_duration / 2
ps = tau - self.pi_pulse_duration
# add the first free evolution
self.pulse_profiles.append([None, [t0, t0 + ps/2 - self.pi_pulse_duration/2], None, None])
t0 += ps/2 - self.pi_pulse_duration/2
# add pulses and free evolution M-1 times
for itr_M in range(8 * self.M - 1):
# add a pi pulse
if isinstance(self.H1, Qobj):
self.pulse_profiles.append([self.H1, np.linspace(t0, t0 + self.pi_pulse_duration, self.time_steps), self.pulse_shape, self.pulse_params[itr_M % 8]])
elif isinstance(self.H1, list):
self.pulse_profiles.append(
[[self.H1[i], np.linspace(t0, t0 + self.pi_pulse_duration, self.time_steps), self.pulse_shape[i], self.pulse_params[itr_M % 8]] for i in range(len(self.H1))]
)
t0 += self.pi_pulse_duration
# add a free evolution
self.pulse_profiles.append([None, [t0, t0 + ps], None, None])
t0 += ps
# add another pi pulse
if isinstance(self.H1, Qobj):
self.pulse_profiles.append([self.H1, np.linspace(t0, t0 + self.pi_pulse_duration, self.time_steps), self.pulse_shape, self.pulse_params[0]])
elif isinstance(self.H1, list):
self.pulse_profiles.append([[self.H1[i], np.linspace(t0, t0 + self.pi_pulse_duration, self.time_steps), self.pulse_shape[i], self.pulse_params[0]] for i in range(len(self.H1))])
t0 += self.pi_pulse_duration
if self.projection_pulse:
# add the last free evolution
self.pulse_profiles.append([None, [t0, t0 + ps/2 - self.pi_pulse_duration/2], None, None])
t0 += ps/2 - self.pi_pulse_duration/2
if isinstance(self.H1, Qobj):
# add the last pi/2 pulse
self.pulse_profiles.append([self.H1, np.linspace(t0, t0 + self.pi_pulse_duration / 2, self.time_steps), self.pulse_shape, self.pulse_params[0]])
elif isinstance(self.H1, list):
# add the first pi/2 pulse
self.pulse_profiles.append([[self.H1[i], np.linspace(t0, t0 + self.pi_pulse_duration / 2, self.time_steps), self.pulse_shape[i], self.pulse_params[0]] for i in range(len(self.H1))])
t0 += self.pi_pulse_duration / 2
else:
# add the last free evolution
self.pulse_profiles.append([None, [t0, t0 + ps/2], None, None])
t0 += ps/2
# set the total_time attribute to the total time of the pulse sequence
self.total_time = t0
[docs]
def plot_pulses(self, figsize=(6, 6), xlabel=None, ylabel="Expectation Value", title="Pulse Profiles", tau=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, the free evolution must be a single number in order to plot the pulse profiles.
figsize : tuple
Size of the figure to be passed to matplotlib.pyplot
xlabel : str
Label of the x-axis
ylabel : str
Label of the y-axis
title : str
Title of the plot
"""
self._get_pulse_profiles(tau)
# call the plot_pulses method of the parent class
super().plot_pulses(figsize, xlabel, ylabel, title)
####################################################################################################
[docs]
class RXY8(XY8):
"""
This contains the RXY8-M sequence, inheriting from XY8. The RXY8-M is a further improvement from the XY8-M sequence,
where a random phase is added in each XY8 block in order to improve noise suppression and pulse errors.
Attributes
----------
same as XY8
Methods
-------
RXY8_sequence :
Defines the RXY8 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.
RXY8_sequence_proj :
RXY8 sequence with projection pulse.
_get_pulse_profiles :
Generates the pulse profiles for the RXY8-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 RXY8-M sequence for a given tau and then plot them.
"""
def __init__(self, M, free_duration, pi_pulse_duration, system, H1, H2=None, projection_pulse=True, pulse_shape=square_pulse, pulse_params=None, options=None, time_steps=100):
"""
Class constructor for the RXY8 sequence
Parameters
----------
same as XY8
"""
super().__init__(M, free_duration, pi_pulse_duration, system, H1, H2, projection_pulse, pulse_shape, pulse_params, options, time_steps)
if projection_pulse:
self.sequence = self.RXY8_sequence_proj
elif not projection_pulse:
self.sequence = self.RXY8_sequence
else:
raise ValueError("projection_pulse must be a boolean")
# initialize the pulse_params attribute as a list of dictionaries for X and Y pulses
self.pulse_params = np.empty(M*8, dtype=dict)
# define RXY8 pulse parameters for the sequence: X-Y-X-Y-Y-X-Y-X with a random phase in each block
for itr_M in range(M):
random_phase = np.random.rand()*2*np.pi
self.pulse_params[0+8*itr_M] = {**pulse_params, **{"phi_t": 0 + random_phase}}
self.pulse_params[1+8*itr_M] = {**pulse_params, **{"phi_t": -np.pi / 2 + random_phase}}
self.pulse_params[2+8*itr_M] = self.pulse_params[0+8*itr_M]
self.pulse_params[3+8*itr_M] = self.pulse_params[1+8*itr_M]
self.pulse_params[4+8*itr_M] = self.pulse_params[1+8*itr_M]
self.pulse_params[5+8*itr_M] = self.pulse_params[0+8*itr_M]
self.pulse_params[6+8*itr_M] = self.pulse_params[1+8*itr_M]
self.pulse_params[7+8*itr_M] = self.pulse_params[0+8*itr_M]
[docs]
def RXY8_sequence(self, tau):
"""
Defines the RXY8-M composed of 8 intercalated pi pulses on X and Y axis with free evolutions of time tau repeated M times.
A random phase is added in each XY8 block.
The sequence is to be called by the parallel_map method of QuTip.
Parameters
----------
tau : float
Free evolution time
Returns
-------
rho : Qobj
Final state
"""
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 itr_M in range(8*self.M - 1):
self._pulse(self.Ht, self.pi_pulse_duration, self.options, self.pulse_params[itr_M])
self._free_evolution(ps, self.options)
# perform the last pi pulse on X
self._pulse(self.Ht, self.pi_pulse_duration, self.options, self.pulse_params[-1])
self._free_evolution(ps/2, self.options)
return self.rho
[docs]
def RXY8_sequence_proj(self, tau):
"""
Defines the RXY8-M composed of 8 intercalated pi pulses on X and Y axis with free evolutions of time tau repeated M times,
plus the projection pulse.
A random phase is added in each XY8 block.
The sequence is to be called by the parallel_map method of QuTip.
Parameters
----------
tau : float
Free evolution time
Returns
-------
rho : Qobj
Final state
"""
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 itr_M in range(8*self.M - 1):
self._pulse(self.Ht, self.pi_pulse_duration, self.options, self.pulse_params[itr_M])
self._free_evolution(ps, self.options)
# perform the last pi pulse on X
self._pulse(self.Ht, self.pi_pulse_duration, self.options, self.pulse_params[-1])
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])
return self.rho
def _get_pulse_profiles(self, tau=None):
"""
Generates the pulse profiles for the RXY8-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
"""
# check if tau is correctly defined
if tau is None:
tau = self.variable[-1]
elif not isinstance(tau, (int, float)) or tau < self.pi_pulse_duration:
raise ValueError("tau must be a positive real number larger than pi_pulse_duration")
self.pulse_profiles = []
# add the first pi/2 pulse on X axis
if isinstance(self.H1, Qobj):
self.pulse_profiles.append([self.H1, np.linspace(0, self.pi_pulse_duration / 2, self.time_steps), self.pulse_shape, self.pulse_params[0]])
elif isinstance(self.H1, list):
self.pulse_profiles.append([[self.H1[i], np.linspace(0, self.pi_pulse_duration / 2, self.time_steps), self.pulse_shape[i], self.pulse_params[0]] for i in range(len(self.H1))])
t0 = self.pi_pulse_duration / 2
ps = tau - self.pi_pulse_duration
# add the first free evolution
self.pulse_profiles.append([None, [t0, t0 + ps/2 - self.pi_pulse_duration / 2], None, None])
t0 += ps/2 - self.pi_pulse_duration / 2
# add pulses and free evolution M-1 times
for itr_M in range(8 * self.M - 1):
# add a pi pulse
if isinstance(self.H1, Qobj):
self.pulse_profiles.append([self.H1, np.linspace(t0, t0 + self.pi_pulse_duration, self.time_steps), self.pulse_shape, self.pulse_params[itr_M]])
elif isinstance(self.H1, list):
self.pulse_profiles.append(
[[self.H1[i], np.linspace(t0, t0 + self.pi_pulse_duration, self.time_steps), self.pulse_shape[i], self.pulse_params[itr_M % 8]] for i in range(len(self.H1))]
)
t0 += self.pi_pulse_duration
# add a free evolution
self.pulse_profiles.append([None, [t0, t0 + ps], None, None])
t0 += ps
# add another pi pulse
if isinstance(self.H1, Qobj):
self.pulse_profiles.append([self.H1, np.linspace(t0, t0 + self.pi_pulse_duration, self.time_steps), self.pulse_shape, self.pulse_params[-1]])
elif isinstance(self.H1, list):
self.pulse_profiles.append([[self.H1[i], np.linspace(t0, t0 + self.pi_pulse_duration, self.time_steps), self.pulse_shape[i], self.pulse_params[-1]] for i in range(len(self.H1))])
t0 += self.pi_pulse_duration
if self.projection_pulse:
# add the last free evolution
self.pulse_profiles.append([None, [t0, t0 + ps/2 - self.pi_pulse_duration/2], None, None])
t0 += ps/2 - self.pi_pulse_duration/2
if isinstance(self.H1, Qobj):
# add the last pi/2 pulse
self.pulse_profiles.append([self.H1, np.linspace(t0, t0 + self.pi_pulse_duration / 2, self.time_steps), self.pulse_shape, self.pulse_params[0]])
elif isinstance(self.H1, list):
# add the first pi/2 pulse
self.pulse_profiles.append([[self.H1[i], np.linspace(t0, t0 + self.pi_pulse_duration / 2, self.time_steps), self.pulse_shape[i], self.pulse_params[0]] for i in range(len(self.H1))])
t0 += self.pi_pulse_duration / 2
else:
# add the last free evolution
self.pulse_profiles.append([None, [t0, t0 + ps/2], None, None])
t0 += ps/2
# set the total_time attribute to the total time of the pulse sequence
self.total_time = t0