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

This cell adds /home/docs/checkouts/readthedocs.org/user_builds/physics-555-quantum-technologies/checkouts/latest/src to your path, and contains some definitions for equations and some CSS for styling the notebook. If things look a bit strange, please try the following:

  • Choose "Trust Notebook" from the "File" menu.
  • Re-execute this cell.
  • Reload the notebook.

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 ᴄɴᴏᴛ.

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 [Nielsen and Chuang, 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#

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

Hide code cell content
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:

Hide code cell source
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))
../_images/51442fe41385034eb1f6911db1cad9131492688d85068920c7a0a05038fa492e.png
\[\frac{\sqrt{2}}{2} |00\rangle+\frac{\sqrt{2}}{2} |10\rangle\]

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*}\]
_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)
    
      
  Cell In[4], line 28
    rho_As = [tr_A(rho + h*drho), tr_A(rho - h*drho))
                                                    ^
SyntaxError: closing parenthesis ')' does not match opening parenthesis '['

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.

This can be used to implement the controlled-\(Z\) gate.

How can these ingredients be used to implement a ᴄɴᴏᴛ gate?

Spin-Spin Interaction#

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))
array([[ 1.+0.j,  0.+0.j,  0.+0.j,  0.+0.j],
       [ 0.+0.j, -1.+0.j,  2.+0.j,  0.+0.j],
       [ 0.+0.j,  2.+0.j, -1.+0.j,  0.+0.j],
       [ 0.+0.j,  0.+0.j,  0.+0.j,  1.+0.j]])
array([[ 3.+0.j,  0.+0.j,  1.-2.j,  0.+0.j],
       [ 0.+0.j,  3.+0.j,  0.+0.j,  1.-2.j],
       [ 1.+2.j,  0.+0.j, -3.+0.j,  0.+0.j],
       [ 0.+0.j,  1.+2.j,  0.+0.j, -3.+0.j]])
array([[ 3.+0.j,  1.-2.j,  0.+0.j,  0.+0.j],
       [ 1.+2.j, -3.+0.j,  0.+0.j,  0.+0.j],
       [ 0.+0.j,  0.+0.j,  3.+0.j,  1.-2.j],
       [ 0.+0.j,  0.+0.j,  1.+2.j, -3.+0.j]])
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))
array([[ 0.+0.j,  0.+0.j,  0.+0.j,  0.+0.j],
       [ 0.+0.j,  0.+0.j,  0.+0.j,  0.+0.j],
       [ 0.+0.j,  0.+0.j,  1.+0.j, -1.-0.j],
       [ 0.+0.j,  0.+0.j, -1.-0.j,  1.+0.j]])
array([[0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],
       [0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],
       [0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j],
       [0.+0.j, 0.+0.j, 0.+0.j, 1.+0.j]])

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*}\]