---
jupytext:
  formats: ipynb,md:myst
  text_representation:
    extension: .md
    format_name: myst
    format_version: 0.13
    jupytext_version: 1.14.0
kernelspec:
  display_name: Python 3 (phys-555)
  language: python
  name: phys-555
---

```{code-cell} ipython3
:tags: [hide-cell]

import mmf_setup;mmf_setup.nbinit()
import logging;logging.getLogger('matplotlib').setLevel(logging.CRITICAL)
%matplotlib inline
import numpy as np, matplotlib.pyplot as plt
```

# Quantum Gates

As discussed in [section 4.5.2], single qubit operations and ᴄɴᴏᴛ are universal for
quantum computing.  Single qubit operations are easily implemented, for example, using
external magnetic fields for qubits made of spin-½ particles.  The challenge is
generally implementing a ᴄɴᴏᴛ gate:

\begin{gather*}
  \mathsf{\small CNOT} = 
  \begin{pmatrix}
    1 & 0 & 0 & 0\\
    0 & 1 & 0 & 0\\
    0 & 0 & 0 & 1\\
    0 & 0 & 1 & 0
  \end{pmatrix}
\end{gather*}

For example, trying to find a Hamiltonian $\mat{H}$ such that

\begin{gather*}
  \mathsf{\small CNOT} = e^{\mat{H}t/\I\hbar}, \qquad
  \frac{\mat{H} t}{\hbar} = \I\ln(\mathsf{\small CNOT})
  =
  \frac{\pi}{2}
  \begin{pmatrix}
  0\\
  & 0\\
  && -1 & 1\\
  && 1 & -1
  \end{pmatrix}.
\end{gather*}

:::{warning}
The $\ln()$ function is multi-valued.  For example:

\begin{gather*}
  \I\ln(\mathsf{\small CNOT})
  \equiv
  \frac{\pi}{2}
  \begin{pmatrix}
  4\\
  & 4\\
  && -1 & 1\\
  && 1 & -1
  \end{pmatrix}.
\end{gather*}

also exponentiates to give ᴄɴᴏᴛ.
:::

:::{admonition} Do it! Find all 2×2 $\mat{A}$ such that $\exp(\mat{A}) = \mat{1}$.
:class: dropdown

We first note that $e^{2\pi \I n} = 1$, thus:

\begin{gather*}
  \mat{A} =
  2\pi \I \mat{S}^{-1}\begin{pmatrix}
    m\\
    & n\\
  \end{pmatrix}
  \mat{S}
\end{gather*}

where $m$, and $n$ are integers, and $\mat{S}$ is any invertable matrix.

For our purposes, we generally restrict $\mat{S} = \mat{U}$ to be unitary so that
$\I\mat{A}$ is hermitian.  Noting that any unitary matrix can be expressed as (see
e.g. Eq. (B.10) in Appendix B of {cite:p}`Mermin:2007` or Eq. (4.8) of {cite:p}`Nielsen:2010`)

\begin{gather*}
  \mat{U} = u_0\mat{1} + \I\vec{u}\cdot\vec{\mat{\sigma}}, \qquad
  u_0^2 + \abs{\vec{u}}^2 = 1,
\end{gather*}

we can write:

\begin{gather*}
  \mat{A} =
  2\pi \I \Bigl(
    m \mat{1}
    +
    n (u_0\mat{1} - \I\vec{u}\cdot\vec{\mat{\sigma}})
    \mat{\sigma}_z
    (u_0\mat{1} + \I\vec{u}\cdot\vec{\mat{\sigma}})
  \Bigr).
\end{gather*}

:::


This is often a bit tricky to realize, but if we can implement a single-qubit Hadamard
gate, then we can construct ᴄɴᴏᴛ from a controlled-$Z$ gate (exercise 4.20 in
{cite:p}`Nielsen:2010`):

\begin{gather*}
  \mat{C}^Z = 
  \begin{pmatrix}
    1 & 0 & 0 & 0\\
    0 & 1 & 0 & 0\\
    0 & 0 & 1 & 0\\
    0 & 0 & 0 & -1
  \end{pmatrix}, \qquad
  \I\ln(\mat{C}^Z)
  =
  \begin{pmatrix}
  0\\
  & 0\\
  && 0\\
  && & \pi
  \end{pmatrix}.
\end{gather*}

This lays the background for trying to find an implementation of a complete set of
gates.  Now let's look at a specific physical example.

## Physical Implementation with Spins

:::{margin}
Working with spins like this is very feasible for small numbers of qubits, but may not
be very scalable.
:::
Consider two qubits implemented as spin-½ particles.  Assume these can be separated
physically and subject to independent magnetic fields $\vect{B}_{A}$ and
$\vect{B}_{B}$.  This allows us to implement arbitrary single-qubit unitary operations.
Now, suppose that these two particles can be brought together so that they interact via
a symmetric dipole interaction

\begin{gather*}
  \mat{V} = \mu_{D} \vect{\op{S}}_{A} \cdot \vect{\op{S}}_{B}
         = \mu_{D}\begin{pmatrix}
           1\\
           & -1 & 2\\
           & 2 & -1\\
           &&& 1
         \end{pmatrix}
\end{gather*}

Can we use this interaction to build a ᴄɴᴏᴛ gate?  A quick check shows that this matrix
itself is not enough

:::{admonition} Do it!  Compute $\mat{U} = \exp(\mat{V}t/\I\hbar)$.
:class: dropdown

The block structure makes this easy: we really only need to worry about the central
block which we can express
\begin{gather*}
  \mat{A} = 
  \begin{pmatrix}
    -1 & 2\\
    2 & -1
  \end{pmatrix}
  =
  -\mat{1} + 2\mat{\sigma}_x,\\
  e^{\theta\mat{A}/2\I}
  =
  e^{\I\theta/2}
  e^{-\theta\I\mat{\sigma}_x}
  =
  e^{\I\theta/2}
  \left(
    \mat{1}\cos\theta - \I\mat{\sigma}_x\sin\theta
  \right).
\end{gather*}

The key here is to note that $\mat{\sigma}_x$ commutes with the identity, and
$(-\I\mat{\sigma}_x)^2 = -\mat{1}$ acts like a matrix version of $\I$, allowing us to use
the generalization of [Euler's formula] for [quaternion]s.

Putting the pieces together:

\begin{gather*}
  e^{\mat{V} t/\I\hbar} = 
  e^{-\I\theta/2}\begin{pmatrix}
    1\\
    & e^{\I\theta}\cos\theta     & -\I e^{\I\theta}\sin\theta\\
    & -\I e^{\I\theta}\sin\theta & e^{\I\theta}\cos\theta\\
    &&& 1
  \end{pmatrix}, \qquad
  \theta = \frac{2t\mu_D}{\hbar}.
\end{gather*}

This does not have the appropriate structure to implement ᴄɴᴏᴛ.
:::

```{code-cell} ipython3
:tags: [hide-cell]

from scipy.linalg import expm
rng = np.random.default_rng(seed=2)
mu_D, t, hbar = rng.random(size=3)
V = mu_D * np.array([
    [1, 0, 0, 0], 
    [0,-1, 2, 0], 
    [0, 2,-1, 0], 
    [0, 0, 0, 1]])
theta = 2*t*mu_D/hbar
c, s = np.exp(1j*theta)*np.array([np.cos(theta), 1j*np.sin(theta)])
U = np.exp(theta/2j) * np.array([
    [1, 0, 0, 0], 
    [0, c,-s, 0], 
    [0,-s, c, 0], 
    [0, 0, 0, 1]])
assert np.allclose(U, expm(V*t/1j/hbar))
```

However, it does provide the right ingredients.  In particular, a key role of ᴄɴᴏᴛ is to
generate entanglement by producing Bell states:

```{code-cell} ipython3
:tags: [hide-input]

from qiskit.quantum_info import Statevector, Operator
from qiskit import QuantumCircuit
qc = QuantumCircuit(2)
qc.reset([0, 1])
qc.h(1)
qc.cnot(0,1)
display(qc.draw('mpl'))
psi = Statevector.from_label('00')
display(psi.evolve(qc).draw('latex'))
#display(array_to_latex(Operator(qc).data))
```

The effect of the ᴄɴᴏᴛ operation is to produce maximal entanglement from the appropriate
input state.  Note also that all the other Bell states can be obtained by single qubit
operations.  Thus, we can try to use $\mat{V}$ to generate maximum entanglement.  At
first it might seem that we need to explore a rather large parameter space, but noting
that $\vec{\mat{S}}_{A}\cdot\vec{\mat{S}}_{B}$ is rotationally invariant, we can choose
a frame in which the control qubit is $\ket{0}_{A}$ points along the $z$ axis.  We may
use the remaining rotational freedom about the $z$ axis to choose a frame in which the
other qubit points somewhere in the $x-z$ plane.  Physically, the result can only depend
on the angle $\theta$ between the two vectors on the Bloch spheres representing these
input vectors.

Next, we must define a measure for the entanglement, and the von Neumann entropy will
function well:

\begin{gather*}
  S(\mat{\rho}) = -\Tr \mat{\rho}_A\ln\mat{\rho_A}, \qquad
  \mat{\rho}_A = \Tr_{B}\mat{\rho},
\end{gather*}

where $\mat{\rho}_A$ is the reduced density matrix.

\begin{gather*}
  \dot{\mat{\rho}} = \frac{\left[\mat{V}, \mat{\rho}\right]}{\I\hbar},\\
  \ket{\theta} = \mat{R}^{y}_{\theta}\ket{0} = \begin{pmatrix}
    \cos\tfrac{\theta}{2}\\
    \sin\tfrac{\theta}{2}
  \end{pmatrix},\\
  \mat{\rho} = \ket{0}\bra{0}\otimes\ket{\theta}\bra{\theta}.
\end{gather*}

```{code-cell} ipython3
_EPS = np.finfo(float).eps

V = np.array([
  [1, 0, 0, 0],
  [0,-1, 2, 0], 
  [0, 2,-2, 0], 
  [0, 0, 0, 1]]) 

def S(rho):
    assert np.allclose(rho, rho.T.conj())
    lams = np.linalg.eigvalsh(rho) + _EPS
    assert np.all(lams >= 0)
    return -np.sum(lams * np.log(lams))
    
def tr_A(rho):
    return np.einsum('abAb', rho.reshape((2,)*4))
    
def get_dS(theta, V=V):
    """Return the change in von Neumann entropy due to V."""
    ket_0 = np.array([1,0])
    ket_theta = np.array([np.cos(theta/2), np.sin(theta/2)])
    rho = np.einsum(
        'a,A,b,B->abAB',
        ket_0, ket_0.conj(), ket_theta, ket_theta.conj()).reshape((4, 4))
    assert np.allclose(rho, rho.T.conj())
    drho = (V @ rho - rho @ V)/1j
    h = 1e-6
    rho_As = [tr_A(rho + h*drho), tr_A(rho - h*drho))
    dS = (S(rho_As[0]) - S(rho_As[1]))/h
    return dS

get_dS(0.2)
    
      
```

A bit more generally, we can subject each spin to a different magnetic field
$\vec{B}_A$ and $\vec{B}_B$, and add an overall energy shift, in addition to $\mat{V}$,
so a physical Hamiltonian might be:

\begin{align*}
  \mat{H} &= E\mat{1}\otimes\mat{1} - \frac{\hbar\mu_B}{2}\left(
    \vec{B}_A\cdot \vec{\mat{\sigma}}\otimes \mat{1}
    +
    \vec{B}_B\cdot \mat{1}\otimes \vec{\mat{\sigma}}
  \right)
  +
  \mat{V}\\
  &= E\mat{1} 
  + \vec{a}\cdot \vec{\mat{\sigma}}\otimes \mat{1} 
  + \vec{b}\cdot \mat{1}\otimes\vec{\mat{\sigma}}
  + \frac{c}{2}(\vec{\mat{\sigma}}\otimes \mat{1}) \cdot (\mat{1}\otimes\vec{\mat{\sigma}})\\
  &=\begin{pmatrix}
    d_0 & w^* & w^* &\\
    w   & d_1 & c   & w^*\\
    w   & c  & d_2 & w^*\\
        & w   & w   & d_3
  \end{pmatrix}, \qquad
  c = \frac{d_0 - d_1 - d_2 + d_3}{2}.
\end{align*}

where $d_i$ are real, and $w$ is complex.

:::{admonition} Do it!  Show that $\mat{H}$ has this form.
:class: dropdown

\begin{gather*}
  \mat{H} = 
  E\mat{1} + 
    \begin{pmatrix}
    a_z        &            & a_x-\I a_y &\\
               & a_z        &            & a_x-\I a_y\\
    a_x+\I a_y &            & -a_z       &\\
               & a_x+\I a_y &            & -a_z
  \end{pmatrix}
  + \\
  + \begin{pmatrix}
    b_z        & a_x-\I a_y &            &\\
    a_x+\I a_y & -b_z       &            &\\
               &            & b_z        & a_x-\I a_y\\
               &            & a_x+\I a_y & -b_z
  \end{pmatrix}
  + \\
  + \frac{c}{2}\begin{pmatrix}
           1\\
           & -1 & 2\\
           & 2 & -1\\
           &&& 1
      \end{pmatrix},\\
  = 
  \begin{pmatrix}
    E+a_z + b_z + \frac{c}{2}  & a_x-\I a_y & a_x-\I a_y     &\\
    a_x+\I a_y & E+a_z - b_z - \frac{c}{2}  & c             & a_x-\I a_y\\
    a_x+\I a_y & c             & E-a_z + b_z - \frac{c}{2} & a_x-\I a_y\\
               & a_x+\I a_y     & a_x+\I a_y     & E-a_z - b_z + \frac{c}{2}
  \end{pmatrix}.
\end{gather*}

Grouping some terms, we can reduce this to the form

\begin{gather*}
  \mat{H} =
  \begin{pmatrix}
    d_0 & w^* & w^* &\\
    w   & d_1 & c   & w^*\\
    w   & c   & d_2 & w^*\\
        & w   & w   & d_3
  \end{pmatrix}\\
  d_0 = E + a_z + b_z + \frac{c}{2}, \qquad
  d_1 = E + a_z - b_z - \frac{c}{2}, \\
  d_2 = E - a_z + b_z - \frac{c}{2}, \qquad
  d_3 = E - a_z - b_z + \frac{c}{2}, \\
  2c = d_0 - d_1 - d_2 + d_3\\
  w = a_x+\I a_y,
\end{gather*}

where $a_i$ and $b$ are real, and $w$ is complex.
:::

This can be used to implement the controlled-$Z$ gate.

:::{admonition} Do it!
:class: dropdown

Note: you will have to use one of the more general forms of $\I\ln\mat{C}^Z$ and match
the structure.
:::

:::{margin}
After trying to find a solution with brute-force optimization, I think it cannot be done
directly, but as described in {cite:p}`Mermin:2007`, one can implement a ᴄᴢ gate.
:::
How can these ingredients be used to implement a ᴄɴᴏᴛ gate?

## Spin-Spin Interaction

```{code-cell} ipython3
rng = np.random.default_rng(seed=1)

# Pauli matrices
I = np.eye(2)
sigma_x, sigma_y, sigma_z = sigmas = np.array([
  [[0, 1],
   [1, 0]],
  [[0, -1j],
   [1j, 0]],
  [[1, 0],
   [0, -1]],
])

A, B = rng.normal(size=(2, 2, 4)).view(dtype=complex)
assert np.allclose(
    np.kron(A, B),
    np.einsum('ab,cd->acbd', A, B).reshape((4, 4)))

S_A = [np.kron(_s, I) for _s in sigmas]
S_B = [np.kron(I, _s) for _s in sigmas]
V = np.einsum('abc,acd->bd', S_A, S_B)
display(V)
display(np.kron(sigma_x, I) + 2*np.kron(sigma_y, I)+ 3*np.kron(sigma_z, I))
display(np.kron(I, sigma_x) + 2*np.kron(I, sigma_y)+ 3*np.kron(I, sigma_z))
```

```{code-cell} ipython3
from scipy.linalg import logm

CNOT = np.array([
    [1, 0, 0, 0],
    [0, 1, 0, 0],
    [0, 0, 0, 1],
    [0, 0, 1, 0]
])

CZ = np.array([
    [1, 0, 0, 0],
    [0, 1, 0, 0],
    [0, 0, 1, 0],
    [0, 0, 0, -1]
])

display((logm(CNOT)/1j/(np.pi/2)).round(5))
display((logm(CZ)/1j/(np.pi)).round(5))
```

## Symmetric Interactions

Consider a more generic interaction $\op{V}_{AB}$ with the only restriction that
$\op{V}_{AB} = \op{V}_{BA}$.  If we group the indices appropriately, this implies:

\begin{gather*}
  V_{ab;a'b'} = V_{ba;b'a'},\\
  V = \begin{pmatrix}
    V_{00;00} & V_{00;01} & V_{00;10} & V_{00;11}\\
    V_{01;00} & V_{01;01} & V_{01;10} & V_{01;11}\\
    V_{10;00} & V_{10;01} & V_{10;10} & V_{10;11}\\
    V_{11;00} & V_{11;01} & V_{11;10} & V_{11;11}
  \end{pmatrix}\\  
    V = \begin{pmatrix}
    d_0 & b & b & c\\
    d & d_1 & e & f\\
    d & e & d_1 & f\\
    h & g & g & d_2
  \end{pmatrix}
\end{gather*}







[section 4.5.2]: <https://ntserver1.wsulibs.wsu.edu:2171/lib/wsu/reader.action?docID=647366&ppg=225>
[Euler's formula]: <https://en.wikipedia.org/wiki/Euler%27s_formula>
[Quaternion]: <https://en.wikipedia.org/wiki/Quaternion>
