## 1.3.1. Sequencing primitive gates to a Hadamard¶

In this example we want implement a Hadamard gate,

$\begin{split}\text{H}\doteq\frac{1}{\sqrt{2}} \begin{pmatrix} 1 & 1 \\ 1 & -1 \end{pmatrix},\end{split}$

using $$\pi/2$$ rotations about the $$x$$- and $$y$$-axis of the Bloch sphere, $$\pi/2_x$$ and $$\pi/2_y$$, once with optimized gates and once with primitive ones. The Hadamard transform is given by

$\text{H} = \pi/2_x \circ \pi/2_x \circ \pi/2_y.$

The optimized gates we shall use are the exchange-based singlet-triplet qubit gates presented in Cerfontaine et al. (2019). For a single qubit, the control Hamiltonian reads

${H}_c = \Delta B_z \frac{\sigma_x}{2} + J(\epsilon)\frac{\sigma_z}{2}$

with $$\Delta B_z$$ the magnetic field gradient and $$J(\epsilon) = \exp(\epsilon)$$ the exchange coupling a function of the controllable detuning. Note again that, since we use $$\hbar\equiv 1$$, all energies have units of inverse time, $$\text{ns}^{-1}$$ in this case.

:

import sys
from pathlib import Path

import numpy as np

import filter_functions as ff

/home/docs/checkouts/readthedocs.org/user_builds/filter-functions/conda/latest/lib/python3.8/site-packages/traitlets/traitlets.py:3030: FutureWarning: --rc={'figure.dpi': 72} for dict-traits is deprecated in traitlets 5.0. You can pass --rc <key=value> ... multiple times to add items to a dict.
warn(

:

data_path = Path(sys.path).parent.parent.parent / 'examples/data'
gates = ['X2', 'Y2']
# Set up Hamiltonian for X2, Y2 gate
struct = {'X2': loadmat(str(data_path / 'X2ID.mat')),
eps = {key: np.asarray(struct[key]['eps'], order='C') for key in gates}
delta_t = {key: np.asarray(struct[key]['t'].ravel(), order='C')
for key in gates}
B = {key: np.asarray(struct[key]['B'].ravel(), order='C') for key in gates}

J = {key: np.exp(eps[key]) for key in gates}
n_dt = {key: len(delta_t[key]) for key in gates}

# Total duration of the pulses
tau = delta_t['X2'].sum()


Since $$J$$ depends non-trivially on $$\epsilon$$, we include the first derivative as a sensitivity to charge noise on $$\epsilon$$ in the noise Hamiltonian $${H}_n = \sum_\alpha s_\alpha(t) b_\alpha(t) B_\alpha$$.

:

# Set up the Hamiltonian for the PulseSequence
d = 2
H = np.empty((2, d, d), dtype=complex)

Id, Px, Py, Pz = ff.util.paulis
H = 1/2*Px
H = 1/2*Pz

opers = list(H)

# Assume noise only on eps
c_opers = opers.copy()
n_opers = opers[:1]

# The coefficients we are interested in are the first index (the gates we
# loaded are actually two-qubit gates with an identity gate on the second
# qubit).
c_coeffs = {key: [J[key], B[key]*np.ones(n_dt[key])]
for key in gates}
# Noise sensitivity J(eps) for eps.
n_coeffs = {key: [J[key], np.ones(n_dt[key])] for key in gates}

# Identifiers for the operators. We can also use verbose strings
identifiers = [r'$\epsilon$', '$b$']

H_c = {key: {'optimized': list(zip(c_opers, val, identifiers))}
for key, val in c_coeffs.items()}
H_n = {key: {'optimized': list(zip(n_opers, val, identifiers))}
for key, val in n_coeffs.items()}
dt = {key: {'optimized': val} for key, val in delta_t.items()}


For the primitive pulses, we assume to have full control over the $$x$$- and $$y$$-axes of our qubit and simply turn on the control at $$t=0$$ and turn it off once the $$\pi/2$$ rotation is complete. In order to better compare the primitive to the optimized pulses, we assume they are of equal duration tau and adjust the control strength of the primitive pulses accordingly.

:

H_c['X2']['primitive'] = [[1/2*Px, [np.pi/2/tau], '$X$']]
H_n['X2']['primitive'] = [[Pz, , '$Z$']]
dt['X2']['primitive'] = [tau]
H_c['Y2']['primitive'] = [[1/2*Py, [np.pi/2/tau], '$Y$']]
H_n['Y2']['primitive'] = [[Pz, , '$Z$']]
dt['Y2']['primitive'] = [tau]


## 1.3.2. Calculating the pulse correlation filter functions¶

Finally, we can set up the PulseSequences and compute the (fidelity) filter functions. For the Hadamard pulse, we will set an additional flag during concatenation to calculate the ‘pulse correlation filter function’ which captures the cross-correlational effects of individual pulses on the total pulse sequence’s susceptibility to noise.

:

gate_types = ('optimized', 'primitive')
X2 = {key: ff.PulseSequence(H_c['X2'][key], H_n['X2'][key], dt['X2'][key])
for key in gate_types}
Y2 = {key: ff.PulseSequence(H_c['Y2'][key], H_n['Y2'][key], dt['Y2'][key])
for key in gate_types}

omega = {'optimized': np.linspace(0, 1/tau*1e2, 501),
'primitive': np.linspace(0, 1/tau*2e1, 501)}

# %% Get the filter functions
FF_X2 = {key: val.get_filter_function(omega[key]) for key, val in X2.items()}
FF_Y2 = {key: val.get_filter_function(omega[key]) for key, val in Y2.items()}
H = {key: ff.concatenate((Y2, X2, X2), calc_pulse_correlation_FF=True)
for (key, X2), (key, Y2) in zip(X2.items(), Y2.items())}

# Note that we can also slice PulseSequence objects, eg
# X = H['primitive'][1:]
# or
# segments = [segment for segment in H['primitive']]


To verify the pulses, we plot the Bloch vector evolution starting from $$|0\rangle$$. In this case, we expect the pulse to have the action

$\text{H}\colon |0\rangle\to \frac{|0\rangle + |1\rangle}{\sqrt{2}}\equiv |+\rangle.$
:

from filter_functions import plotting

for gate in gate_types:
plotting.plot_bloch_vector_evolution(H[gate], n_samples=501,
figsize=(4, 4))

/home/docs/checkouts/readthedocs.org/user_builds/filter-functions/conda/latest/lib/python3.8/site-packages/filter_functions/plotting.py:227: MatplotlibDeprecationWarning: Axes3D(fig) adding itself to the figure is deprecated since 3.4. Pass the keyword argument auto_add_to_figure=False and use fig.add_axes(ax) to suppress this warning. The default value of auto_add_to_figure will change to False in mpl3.5 and True values will no longer work in 3.6.  This is consistent with other Axes classes.
axes = mplot3d.Axes3D(fig, azim=view, elev=view)
/home/docs/checkouts/readthedocs.org/user_builds/filter-functions/conda/latest/lib/python3.8/site-packages/filter_functions/plotting.py:227: MatplotlibDeprecationWarning: Axes3D(fig) adding itself to the figure is deprecated since 3.4. Pass the keyword argument auto_add_to_figure=False and use fig.add_axes(ax) to suppress this warning. The default value of auto_add_to_figure will change to False in mpl3.5 and True values will no longer work in 3.6.  This is consistent with other Axes classes.
axes = mplot3d.Axes3D(fig, azim=view, elev=view)  We can now inspect the pulse correlation filter functions $$F^{(gg')}(\omega)$$ which for $$g=g'$$ are just the normal filter functions for the $$g$$-th pulse:

:

pulses = ('Y', 'X_1', 'X_2')
fig, ax, leg = plotting.plot_pulse_correlation_filter_function(

# Adjust the titles to something more meaningful
for i in range(3):
for j in range(3):
t = ax[i, j].get_title()[7:]
ax[i, j].set_title('\$F^{(' + pulses[i] + pulses[j] + t)  We can see that the pulse correlation filter functions for different pulses are neither strictly real nor positive in contrast to the ‘usual’ filter functions. In fact, the pulse correlation FF are hermitian, that is,

$\left[F^{(ij)}(\omega)\right]^\dagger = F^{(ij)}(\omega).$

While the imaginary part cancels out when calculating fidelities, $$\mathcal{I}\propto\sum_{ij} \int\mathrm{d}\omega S(\omega)F^{(ij)}(\omega)$$, the real part does not and the offdiagonals therefore lead to corrections in the total fidelity of a composite pulse, $$\mathcal{I}_\text{tot}\neq\sum_g\mathcal{I}^{(g)}$$ with $$\mathcal{I}^{(g)}$$ the infidelities of the individual pulses. These corrections can thus in principle also be negative, leading to improved fidelities for composite pulses.