Gate Decomposition¶
Adapted from qgopt/docs/gate-decomposition
It's known that any two-qubits gate can be decomposed as follows:
u1 u2
cnot
u3 u4
cnot
u5 u6
cnot
u7 u8
where each line represent one layer of either single-qubit gates or CNOT gates. In this tutorial, we will show how to perform gate such decomposition for any two-qubits gate.
In [1]:
Copied!
import numpy as np
import torch
try:
import numqi
except ImportError:
%pip install numqi
import numqi
import numpy as np
import torch
try:
import numqi
except ImportError:
%pip install numqi
import numqi
In [2]:
Copied!
class GateDecompositionModel(torch.nn.Module):
def __init__(self):
super().__init__()
self.manifold = numqi.manifold.SpecialOrthogonal(dim=2, batch_size=8, dtype=torch.complex128)
self.unitary_dag = None #we will set this in .set_unitary()
self.cnot = torch.tensor([[1,0,0,0], [0,1,0,0], [0,0,0,1], [0,0,1,0]], dtype=torch.complex128)
def set_unitary(self, np0):
assert np0.shape==(4,4)
assert np.abs(np0@np0.T.conj() - np.eye(4)).max() < 1e-10, 'unitary matrix required'
self.unitary_dag = torch.tensor(np0.T.conj().copy(), dtype=torch.complex128)
def get_unitary(self, return_numpy=True):
ulist = self.manifold()
matU = torch.kron(ulist[0], ulist[1])
for i in range(3):
matU = self.cnot @ matU
matU = torch.kron(ulist[2*i+2], ulist[2*i+3]) @ matU
ret = ulist, matU
if return_numpy:
ret = ret[0].detach().numpy(), ret[1].detach().numpy()
return ret
def forward(self):
_,matU = self.get_unitary(return_numpy=False)
tmp0 = torch.trace(self.unitary_dag @ matU)
# unitary infidelity https://docs.q-ctrl.com/references/qctrl/Graphs/Graph/unitary_infidelity.html
loss = 1 - (tmp0.real**2 + tmp0.imag**2) / 16
return loss
class GateDecompositionModel(torch.nn.Module):
def __init__(self):
super().__init__()
self.manifold = numqi.manifold.SpecialOrthogonal(dim=2, batch_size=8, dtype=torch.complex128)
self.unitary_dag = None #we will set this in .set_unitary()
self.cnot = torch.tensor([[1,0,0,0], [0,1,0,0], [0,0,0,1], [0,0,1,0]], dtype=torch.complex128)
def set_unitary(self, np0):
assert np0.shape==(4,4)
assert np.abs(np0@np0.T.conj() - np.eye(4)).max() < 1e-10, 'unitary matrix required'
self.unitary_dag = torch.tensor(np0.T.conj().copy(), dtype=torch.complex128)
def get_unitary(self, return_numpy=True):
ulist = self.manifold()
matU = torch.kron(ulist[0], ulist[1])
for i in range(3):
matU = self.cnot @ matU
matU = torch.kron(ulist[2*i+2], ulist[2*i+3]) @ matU
ret = ulist, matU
if return_numpy:
ret = ret[0].detach().numpy(), ret[1].detach().numpy()
return ret
def forward(self):
_,matU = self.get_unitary(return_numpy=False)
tmp0 = torch.trace(self.unitary_dag @ matU)
# unitary infidelity https://docs.q-ctrl.com/references/qctrl/Graphs/Graph/unitary_infidelity.html
loss = 1 - (tmp0.real**2 + tmp0.imag**2) / 16
return loss
Different from QGopt, we choose unitary infidelity as loss function qctrl/unitary_infidelity
$$ \mathcal{L}=1 - \lvert\mathrm{Tr}[V(\theta) U^\dagger]\rvert^2 / d^2 $$
for that we ignore the global phase of the target unitary $V$. Above $d$ denotes the dimension of the Hilbert space $d=4$, $U$ for the target untiary matrix and $V(\theta)$ is the parametrized unitary matrix composed of single-qubit gates and CNOT gates.
In [3]:
Copied!
target_unitary = numqi.random.rand_special_orthogonal_matrix(4, tag_complex=True)
print(f'target unitary:\n{np.around(target_unitary, 3)}')
tmp0 = target_unitary @ target_unitary.T.conj()
print(f'unitary check (U U^dag): \n{np.around(tmp0, 3)}')
target_unitary = numqi.random.rand_special_orthogonal_matrix(4, tag_complex=True)
print(f'target unitary:\n{np.around(target_unitary, 3)}')
tmp0 = target_unitary @ target_unitary.T.conj()
print(f'unitary check (U U^dag): \n{np.around(tmp0, 3)}')
target unitary: [[-0.188+0.864j 0.036+0.082j 0.014+0.455j -0.033-0.027j] [ 0.041-0.012j -0.18 +0.527j 0.071-0.01j 0.745-0.357j] [ 0.216+0.294j -0.516-0.311j -0.45 -0.384j -0.06 -0.388j] [-0.023-0.287j -0.24 -0.512j 0.218+0.624j 0.102-0.387j]] unitary check (U U^dag): [[ 1.+0.j -0.+0.j 0.-0.j -0.+0.j] [-0.-0.j 1.+0.j -0.+0.j 0.+0.j] [ 0.+0.j -0.-0.j 1.+0.j -0.+0.j] [-0.-0.j 0.-0.j -0.-0.j 1.+0.j]]
In [4]:
Copied!
model = GateDecompositionModel()
model.set_unitary(target_unitary)
theta_optim = numqi.optimize.minimize(model, theta0='uniform', num_repeat=1, print_freq=5, tol=1e-10)
model = GateDecompositionModel()
model.set_unitary(target_unitary)
theta_optim = numqi.optimize.minimize(model, theta0='uniform', num_repeat=1, print_freq=5, tol=1e-10)
[step=0][time=0.018 seconds] loss=0.7808158115126216 [step=5][time=0.059 seconds] loss=0.20156773680584394 [step=10][time=0.032 seconds] loss=0.03328672718606285 [step=15][time=0.036 seconds] loss=9.866100433364444e-05 [step=20][time=0.032 seconds] loss=1.6803563362977059e-07
[step=25][time=0.036 seconds] loss=1.5783325757467992e-09
[round=0] min(f)=2.0062840278001204e-12, current(f)=2.0062840278001204e-12
In [5]:
Copied!
ulist, matU = model.get_unitary()
print('first several single-qubit unitary matrices:')
for i in range(3):
print(f'U{i} = \n{np.around(ulist[i], 3)}')
ulist, matU = model.get_unitary()
print('first several single-qubit unitary matrices:')
for i in range(3):
print(f'U{i} = \n{np.around(ulist[i], 3)}')
first several single-qubit unitary matrices: U0 = [[ 0.296-0.667j -0.307+0.611j] [ 0.307+0.611j 0.296+0.667j]] U1 = [[ 0.354+0.622j 0.113+0.689j] [-0.113+0.689j 0.354-0.622j]] U2 = [[ 0.491+0.466j 0.204-0.707j] [-0.204-0.707j 0.491-0.466j]]
In [6]:
Copied!
print(f'final 2-qubit unitary matrix:\n{np.around(matU, 3)}')
tmp0 = matU @ target_unitary.T.conj()
print(f'check unitary (V U^dag):\n{np.around(tmp0,5)}')
print(f'final 2-qubit unitary matrix:\n{np.around(matU, 3)}')
tmp0 = matU @ target_unitary.T.conj()
print(f'check unitary (V U^dag):\n{np.around(tmp0,5)}')
final 2-qubit unitary matrix: [[ 0.478+0.744j 0.084+0.033j 0.332+0.312j -0.042+0.005j] [ 0.021-0.038j 0.245+0.5j 0.043-0.057j 0.275-0.78j ] [ 0.36 +0.055j -0.585+0.145j -0.59 +0.047j -0.317-0.232j] [-0.219-0.187j -0.531-0.192j 0.596+0.287j -0.202-0.346j]] check unitary (V U^dag): [[ 0.70711-0.70711j -0. -0.j 0. +0.j -0. +0.j ] [-0. -0.j 0.70711-0.70711j 0. +0.j 0. -0.j ] [ 0. +0.j 0. +0.j 0.70711-0.70711j 0. +0.j ] [ 0. -0.j -0. +0.j 0. +0.j 0.70711-0.70711j]]