Transversal Gates¶
Transversal gates are important in fault-tolerant quantum computing, as they allow for operations that can be performed on multiple qubits simultaneously without introducing errors that propagate through the system. These gates are particularly useful in stabilizer codes and other error-correcting codes. This tutorial will cover the basics of transversal gates and their properties, especially reproduce results in the arxiv paper arxiv-link.
import functools
import numpy as np
try:
import numqi
except ImportError:
%pip install numqi
import numqi
np_rng = np.random.default_rng()
hf_kron = lambda *x: functools.reduce(np.kron, x) #tensor product of matrices
Stabilizer codes¶
Let's begin with two famous stabilizer codes: the 5-qubit code and the Steane code.
5-qubit code ((5,2,3))¶
A quantum error-correcting code (QECC) is a subspace specified by a set of logical basis states
code,info = numqi.qec.get_code_subspace('523')
# logical states are some special state vector in the complete Hilbert space
print('logical 0:', (code[0]*4).astype(np.int64), sep='\n')
print('logical 1:', (code[1]*4).astype(np.int64), sep='\n')
logical 0: [ 1 0 0 -1 0 1 -1 0 0 1 1 0 -1 0 0 -1 0 -1 1 0 1 0 0 -1 -1 0 0 -1 0 -1 -1 0] logical 1: [ 0 1 1 0 1 0 0 1 1 0 0 -1 0 -1 1 0 1 0 0 1 0 -1 -1 0 0 1 -1 0 1 0 0 -1]
Via basis matrix multiplication, one can verify that logical X/Y/Z gate can be implemented by applying the Pauli gates on all physical qubits. Such gates are called transversal gates.
# logical X = X * X * X * X * X
logicalX = code.conj() @ hf_kron(*[-numqi.gate.X]*5) @ code.T
print('logical X:', logicalX, sep='\n')
# logical Y = Y * Y * Y * Y * Y
logicalY = code.conj() @ hf_kron(*[-numqi.gate.Y]*5) @ code.T
print('\nlogical Y:', logicalY, sep='\n')
# logical Z = Z * Z * Z * Z * Z
logicalZ = code.conj() @ hf_kron(*[numqi.gate.Z]*5) @ code.T
print('\nlogical Z:', logicalZ, sep='\n')
logical X: [[0. 1.] [1. 0.]] logical Y: [[0.+0.j 0.-1.j] [0.+1.j 0.+0.j]] logical Z: [[ 1. 0.] [ 0. -1.]]
Besides transversal logical X/Y/Z gates, the 5-qubit code also has transversal logical $F=HS^\dagger$ gate (in Bloch sphere, $F$ gate is a rotation by 120 degrees around (1,1,1) axis). Although 5-qubit code has transversal logical $F=HS^\dagger$, but it does not have transversal logical $H$ and $S$ gates. To support transversal logical $H$ and $S$ gates, we need to use the Steane code which will be introduced in the next section.
op_physical = numqi.gate.Y @ numqi.gate.H @ numqi.gate.S.conj()
logicalF = -np.exp(1j*np.pi/4)*code.conj() @ hf_kron(*[op_physical]*5) @ code.T
print('logical F:', logicalF, sep='\n')
logical F: [[ 0.5-0.5j -0.5-0.5j] [ 0.5-0.5j 0.5+0.5j]]
Besides transversal gates, numqi
also calculate Shor's weight enumerator of the code. Code distance can be read from the weight enumerator which is the first non-equal term in the weight enumerator with its dual. For the 5-qubit code, the distance is 3, which means A3,B3
are different.
print('Shor weight enumerator (A0,A1,A2,A3,A4,A5):', info['qweA'], sep='\n')
print('\ndual of Shor weight enumerator (B0,B1,B2,B3,B4,B5):', info['qweB'], sep='\n')
Shor weight enumerator (A0,A1,A2,A3,A4,A5): [ 1 0 0 0 15 0] dual of Shor weight enumerator (B0,B1,B2,B3,B4,B5): [ 1 0 0 30 15 18]
Steane code ((7,2,3))¶
code,info = numqi.qec.get_code_subspace('steane')
print('code.shape:', code.shape)
print('stabilizer:', info['stab'])
print('Shor weight enumerator:', info['qweA'])
print('dual of Shor weight enumerator:', info['qweB'])
code.shape: (2, 128) stabilizer: ['IIXIXXX', 'IXIXXXI', 'XIIXIXX', 'IIZIZZZ', 'IZIZZZI', 'ZIIZIZZ'] Shor weight enumerator: [ 1 0 0 0 21 0 42 0] dual of Shor weight enumerator: [ 1 0 0 21 21 126 42 45]
Steane code support transversal logical $H$ and $S$ gates, as demonstrated below.
TASK: given these two transversal logical gates, can you find the implementation of transversal logical gate $F=HS^\dagger$?
# logical H = H * H * H * H * H * H * H
logicalH = code.conj() @ hf_kron(*[numqi.gate.H]*7) @ code.T
print('logical H:', logicalH, sep='\n')
# logical S = (S * S * S * S * S * S * S)^\dagger
logicalS = code.conj() @ hf_kron(*[numqi.gate.S.conj()]*7) @ code.T
print('\nlogical S:', logicalS, sep='\n')
logical H: [[ 0.70710678 0.70710678] [ 0.70710678 -0.70710678]] logical S: [[1.+0.j 0.+0.j] [0.+0.j 0.+1.j]]
Actually, transversal logical gates make a group: given two transversal logical gates, then their product is also a transversal logical gate.
Furthermore, Eastin-Knill theorem states that any transversal group of nontrivial (distance >1) QECC is a finite subgroup of SU(K) where K is the dimension of the logical subspace.
Finite subgroup of SU(2) has been classified as follows: cyclic groups $C_{2m}$, binary dihedral groups $\mathrm{BD}_{2m}$, and the three exceptional groups: binary tetrahedral group (2T), binary octahedral group (2O), and binary icosahedral group (2I).
group | notable elements | generators | order |
---|---|---|---|
$C_{2m}$ | $Z(2\pi/m)$ | $Z(2\pi/m)$ | 2m |
$\mathrm{BD}_{2m}$ | $\hat{X},Z(2\pi/m)$ | $\hat{X},Z(2\pi/m)$ | 4m |
2T | $\hat{X},\hat{Z},F$ | $\hat{X},F$ | 24 |
2O (Clifford) | $\hat{X},\hat{Z},\hat{S},\hat{H},F$ | $\hat{S},\hat{H}$ | 48 |
2I | $\hat{X},\hat{Z},F,\Phi$ | $\hat{X},\hat{Z}\Phi$ | 120 |
According to classification, the transversal group of the 5-qubit code is 2T, and the transversal group of the Steane code is 2O which is also isomorphic to 1-qubit Clifford group.
Transversal group of stabilizer code is quite limited, below we demonstrate that transversal group of non-stabilizer code is much richer, which is the main result of our paper arxiv-link.
Non-stabilizer code ((6,2,3))¶
C10¶
code,info = numqi.qec.q623.get_C10(return_info=True)
print('code.shape:', code.shape)
print('Shor weight enumerator:', info['qweA'])
print('dual of Shor weight enumerator:', info['qweB'])
code.shape: (2, 64) Shor weight enumerator: [ 1. 0. 0.84 0. 11.64 15.36 3.16] dual of Shor weight enumerator: [ 1. 0. 0.84 23.36 36.6 39.36 26.84]
info['su2']
stores the SU(2) gates applied to each qubits that implement a Z-rotation logical gate $Z(2\pi/5)$.
print('physical gates for each qubit:')
for i0,op in enumerate(info['su2']):
print(f'qubit {i0}:', op, sep='\n')
op_logical = code.conj() @ hf_kron(*info['su2']) @ code.T
print('\nlogical Z(2pi/5):', op_logical, sep='\n')
physical gates for each qubit: qubit 0: [[0.80901699-0.58778525j 0. +0.j ] [0. +0.j 0.80901699+0.58778525j]] qubit 1: [[0.80901699-0.58778525j 0. +0.j ] [0. +0.j 0.80901699+0.58778525j]] qubit 2: [[0.80901699-0.58778525j 0. +0.j ] [0. +0.j 0.80901699+0.58778525j]] qubit 3: [[0.80901699-0.58778525j 0. +0.j ] [0. +0.j 0.80901699+0.58778525j]] qubit 4: [[0.30901699-0.95105652j 0. +0.j ] [0. +0.j 0.30901699+0.95105652j]] qubit 5: [[-0.30901699-0.95105652j 0. +0.j ] [ 0. +0.j -0.30901699+0.95105652j]] logical Z(2pi/5): [[0.80901699+0.58778525j 0. +0.j ] [0. +0.j 0.80901699-0.58778525j]]
SO(5) code¶
when vece
has at least 4 nonzero entries, then the corresponding code has no transversal logical gates except trivial identity, C2
group.
tmp0 = np_rng.normal(size=4)
vece = tmp0 / np.linalg.norm(tmp0)
code = numqi.qec.q623.get_SO5_code_with_transversal_gate(vece) #no trasversal gate
when 5-dimensional vece
has at most 3 nonzero entries, then transversal group is C4
.
tmp0 = np_rng.normal(size=3)
vece = tmp0 / np.linalg.norm(tmp0)
code,info = numqi.qec.q623.get_SO5_code_with_transversal_gate(vece) #no trasversal gate
print("physical gates' shape:", info['su2'].shape)
logicalZ = code.conj() @ hf_kron(*info['su2']) @ code.T
print('logical Z:', logicalZ, sep='\n')
physical gates' shape: (6, 2, 2) logical Z: [[ 1.00000000e+00+0.00000000e+00j 2.48711257e-18-4.61769199e-20j] [-2.48711257e-18-4.61769199e-20j -1.00000000e+00+0.00000000e+00j]]
when vece
has at most 2 nonzero entries, then transversal group is BD4
.
tmp0 = np_rng.normal(size=2)
vece = tmp0 / np.linalg.norm(tmp0)
code,info = numqi.qec.q623.get_SO5_code_with_transversal_gate(vece) #no trasversal gate
logicalZ = code.conj() @ hf_kron(*info['su2Z']) @ code.T
print('logical Z:', np.around(logicalZ,10), sep='\n')
logicalX = code.conj() @ hf_kron(*info['su2X']) @ code.T
print('logical X:', np.around(logicalX,10), sep='\n')
logical Z: [[ 1.+0.j 0.+0.j] [ 0.+0.j -1.+0.j]] logical X: [[ 0.+0.j 1.-0.j] [ 1.-0.j -0.-0.j]]
((6,2,3)) from ((5,2,3)) stabilizer code¶
see appendix of arxiv-link for details.
Non-stabilizer code ((7,2,3))¶
2T, Cyclic code¶
arxiv-link Characterizing Quantum Codes via the Coefficients in Knill-Laflamme Conditions
Parametrized with the signature norm $\lambda^*\in[0,\sqrt{7}]$, when $\lambda^*=0$, it becomes the Steane code, and when $\lambda^*=\sqrt{7}$, it becomes a permutational-invariant code.
# 0<lambda<sqrt(7), Cyclic code, 2T
code,info = numqi.qec.q723.get_cyclic_code(lambda2=np_rng.uniform(0,7), sign='++', return_info=True)
print('code.shape:', code.shape)
print('Shor weight enumerator:', info['qweA'])
print('dual of Shor weight enumerator:', info['qweB'])
code.shape: (2, 128) Shor weight enumerator: [ 1. 0. 5.05043361 0. 10.89913277 0. 47.05043361 0. ] dual of Shor weight enumerator: [ 1. 0. 5.05043361 36.15130084 10.89913277 95.69739831 47.05043361 60.15130084]
logicalX = code.conj() @ hf_kron(*[numqi.gate.X]*7) @ code.T
print('\nlogical X:', logicalX, sep='\n')
logicalF = code.conj() @ hf_kron(*[numqi.gate.H @ numqi.gate.S.conj()]*7) @ code.T
print('\nlogical F:', logicalF, sep='\n')
logical X: [[0. 1.] [1. 0.]] logical F: [[0.70710678+0.j 0. +0.70710678j] [0.70710678+0.j 0. -0.70710678j]]
When $\lambda^*=0$, extra transversal logical gates are $H$ and $S$, which is the same as Steane code.
code,info = numqi.qec.q723.get_cyclic_code(lambda2=0, sign='++', return_info=True)
logicalH = code.conj() @ hf_kron(*[numqi.gate.H]*7) @ code.T
print('logical H:', logicalH, sep='\n')
logicalS = code.conj() @ hf_kron(*[numqi.gate.S.conj()]*7) @ code.T
print('\nlogical S:', logicalS, sep='\n')
logical H: [[ 0.70710678 0.70710678] [ 0.70710678 -0.70710678]] logical S: [[1.+0.j 0.+0.j] [0.+0.j 0.+1.j]]
When $\lambda^*=\sqrt{7}$, extra transversal logical gate is $\Phi$.
code,info = numqi.qec.q723.get_cyclic_code(lambda2=7, sign='++', return_info=True)
physical_op = numqi.qec.su2_finite_subgroup_gate_dict['Phi']
logicalPhi = code.conj() @ hf_kron(*[physical_op]*7) @ code.T
print('logical Psi:', logicalPhi, sep='\n')
logical Psi: [[-0.30901699+8.09016994e-01j 0.5 +2.09867395e-17j] [-0.5 +3.12223623e-17j -0.30901699-8.09016994e-01j]]
2I, permutation-invariant code¶
coeff = np.zeros((2,8), dtype=np.complex128)
coeff[[0,0,1,1],[0,5,2,7]] = np.array([np.sqrt(3), np.sqrt(7)*1j, np.sqrt(7)*1j, np.sqrt(3)]) / np.sqrt(10)
code = coeff @ (numqi.dicke.get_dicke_basis(7, 2)[::-1])
qweA,qweB = numqi.qec.get_weight_enumerator(code)
print('Shor weight enumerator:', np.around(qweA,3))
print('dual of Shor weight enumerator:', np.around(qweB,3))
Shor weight enumerator: [ 1. 0. 7. 0. 7. 0. 49. 0.] dual of Shor weight enumerator: [ 1. 0. 7. 42. 7. 84. 49. 66.]
logicalX = code.conj() @ hf_kron(*[numqi.gate.X]*7) @ code.T
print('logical X:', logicalX, sep='\n')
logicalZ5 = code.conj() @ hf_kron(*[numqi.gate.rz(6*np.pi/5)]*7) @ code.T
print('\nlogical Z(2pi/5):', logicalZ5, sep='\n')
hfR = lambda a,b,t=1: numqi.gate.I*np.cos(t*np.pi/5) + 1j*np.sin(t*np.pi/5)/np.sqrt(5) * (a*numqi.gate.Y + b*numqi.gate.Z)
physical_op = hfR(-2,-1,3)
logicalR = code.conj() @ hf_kron(*[physical_op]*7) @ code.T #hfR(-2,1)
print('\nlogical R(-2,1):', logicalR, sep='\n')
logical X: [[0.+0.j 1.+0.j] [1.+0.j 0.+0.j]] logical Z(2pi/5): [[0.80901699-0.58778525j 0. +0.j ] [0. +0.j 0.80901699+0.58778525j]] logical R(-2,1): [[ 0.80901699+2.62865556e-01j -0.52573111+1.23259516e-32j] [ 0.52573111+1.23259516e-32j 0.80901699-2.62865556e-01j]]
2I, lambda*=0¶
code,info = numqi.qec.q723.get_2I_lambda0(theta=np_rng.uniform(0,2*np.pi),
phi=np_rng.uniform(0,2*np.pi), sign='+', return_info=True)
print('code.shape:', code.shape)
print('Shor weight enumerator:', info['qweA'])
print('dual of Shor weight enumerator:', info['qweB'])
code.shape: (2, 128) Shor weight enumerator: [ 1. 0. 0. 0. 20.80396554 0.58810338 41.41189662 0.19603446] dual of Shor weight enumerator: [ 1. 0. 0. 20.80396554 21.78413784 124.82379323 42.78413784 44.80396554]
logicalX = code.conj() @ hf_kron(*[numqi.gate.X]*7) @ code.T
print('logical X:', logicalX, sep='\n')
logicalZ5 = code.conj() @ hf_kron(*info['su2']) @ code.T
print('\nlogical Z(2pi/5):', logicalZ5, sep='\n')
# hfR = lambda a,b,t=1: numqi.gate.I*np.cos(t*np.pi/5) + 1j*np.sin(t*np.pi/5)/np.sqrt(5) * (a*numqi.gate.Y + b*numqi.gate.Z)
# physical_op = hfR(-2,-1,3)
logicalR = code.conj() @ hf_kron(*info['su2R']) @ code.T #hfR(-2,1)
print('\nlogical R(-2,1):', logicalR, sep='\n')
logical X: [[0.+0.j 1.+0.j] [1.+0.j 0.+0.j]] logical Z(2pi/5): [[0.80901699-0.58778525j 0. +0.j ] [0. +0.j 0.80901699+0.58778525j]] logical R(-2,1): [[ 0.80901699+2.62865556e-01j -0.52573111+6.93889390e-18j] [ 0.52573111-6.93889390e-18j 0.80901699-2.62865556e-01j]]
2I, lambda*=0.75¶
code,info = numqi.qec.q723.get_2I_lambda075(np_rng.uniform(0,np.sqrt(5/16)),
sign=np.array([1,1,1]), return_info=True)
print('code.shape:', code.shape)
print('Shor weight enumerator:', info['qweA'])
print('dual of Shor weight enumerator:', info['qweB'])
code.shape: (2, 128) Shor weight enumerator: [ 1. 0. 0.75 0. 18.05903865 4.32288405 38.42711595 1.44096135] dual of Shor weight enumerator: [ 1. 0. 0.75 21.80903865 25.2638454 112.8542319 48.5138454 45.80903865]
logicalX = code.conj() @ hf_kron(*[numqi.gate.X]*7) @ code.T
print('logical X:', logicalX, sep='\n')
logicalZ5 = code.conj() @ hf_kron(*info['su2']) @ code.T
print('\nlogical Z(2pi/5):', logicalZ5, sep='\n')
# hfR = lambda a,b,t=1: numqi.gate.I*np.cos(t*np.pi/5) + 1j*np.sin(t*np.pi/5)/np.sqrt(5) * (a*numqi.gate.Y + b*numqi.gate.Z)
# physical_op = hfR(-2,-1,3)
logicalR = code.conj() @ hf_kron(*info['su2R']) @ code.T #hfR(-2,1)
print('\nlogical R(-2,1):', logicalR, sep='\n')
logical X: [[0.+0.j 1.+0.j] [1.+0.j 0.+0.j]] logical Z(2pi/5): [[0.80901699+0.58778525j 0. +0.j ] [0. +0.j 0.80901699-0.58778525j]] logical R(-2,1): [[ 0.80901699+2.62865556e-01j -0.52573111-2.77555756e-17j] [ 0.52573111-2.77555756e-17j 0.80901699-2.62865556e-01j]]
2O lambda*=2¶
code,info = numqi.qec.q723.get_2O_X5(return_info=True)
print('code.shape:', code.shape)
print('Shor weight enumerator:', info['qweA'])
print('dual of Shor weight enumerator:', info['qweB'])
code.shape: (2, 128) Shor weight enumerator: [ 1 0 1 0 19 0 43 0] dual of Shor weight enumerator: [ 1 0 1 24 19 120 43 48]
logicalX = code.conj() @ hf_kron(*info['su2X']) @ code.T
print('logical X:', logicalX, sep='\n')
logicalYSY = code.conj() @ hf_kron(*info['su2YSY']) @ code.T
print('\nlogical Y(pi/4)SY(-pi/4):', logicalYSY, sep='\n')
logical X: [[0. 1.] [1. 0.]] logical Y(pi/4)SY(-pi/4): [[ 7.07106781e-01-0.5j 2.98181678e-18-0.5j] [-3.05576539e-18-0.5j 7.07106781e-01+0.5j]]
BD16, transversal T¶
$$ \begin{align*} 4\left|0_{L}\right\rangle =&e^{i\theta_{1}}\left|0000000\right\rangle +\sqrt{3}\left|0111100\right\rangle +\sqrt{3}\left|1001110\right\rangle \\ &+\sqrt{2}\left|1010101\right\rangle +\sqrt{2}\left|1011001\right\rangle +e^{i\theta_{2}}\left|1110010\right\rangle \\&+2i\left|0100011\right\rangle \\ \left|1_{L}\right\rangle =&X^{\otimes7}\left|0_{L}\right\rangle \end{align*} $$
theta = np_rng.uniform(0,2*np.pi,size=2)
sign = np_rng.integers(2, size=7)*2 - 1
code,info = numqi.qec.q723.get_BD16_veca1222233(*theta, sign=sign, return_info=True)
print('code.shape:', code.shape)
print('Shor weight enumerator:', np.around(info['qweA'],3))
print('dual of Shor weight enumerator:', np.around(info['qweB'],3))
code.shape: (2, 128) Shor weight enumerator: [ 1. 0. 2.625 0.325 12.72 7.139 38.137 2.054] dual of Shor weight enumerator: [ 1. 0. 2.625 27.146 23.967 95.973 55.444 49.845]
logicalX = code.conj() @ hf_kron(*[numqi.gate.X]*7) @ code.T
print('logical X:', logicalX, sep='\n')
logicalT = code.conj() @ hf_kron(*info['su2']) @ code.T
print('\nlogical T:', logicalT, sep='\n')
logical X: [[0.+0.j 1.+0.j] [1.+0.j 0.+0.j]] logical T: [[0.92387953+0.38268343j 0. +0.j ] [0. +0.j 0.92387953-0.38268343j]]
BD32, transversal sqrt(T)¶
sign = np_rng.integers(2, size=9)*2 - 1
code, info = numqi.qec.q723.get_BD32(np_rng.uniform(0,np.sqrt(1/8)), sign=sign, return_info=True)
print('code.shape:', code.shape)
print('Shor weight enumerator:', info['qweA'])
print('dual of Shor weight enumerator:', info['qweB'])
code.shape: (2, 128) Shor weight enumerator: [ 1. 0. 2.08877259 0. 16.82245482 0. 44.08877259 0. ] dual of Shor weight enumerator: [ 1. 0. 2.08877259 27.26631777 16.82245482 113.46736446 44.08877259 51.26631777]
logicalX = code.conj() @ hf_kron(*[numqi.gate.X]*7) @ code.T
print('logical X:', logicalX, sep='\n')
logicalSqrtT = code.conj() @ hf_kron(*info['su2']) @ code.T
print('\nlogical Sqrt(T):', logicalSqrtT, sep='\n')
logical X: [[0.+0.j 1.+0.j] [1.+0.j 0.+0.j]] logical Sqrt(T): [[0.98078528+0.19509032j 0. +0.j ] [0. +0.j 0.98078528-0.19509032j]]
More QECCs¶
Here we provides a list of available QECCs in numqi.qec
:
tmp0 = [x for x in dir(numqi.qec.q723) if x.startswith('get_')]
for x in tmp0:
print(f'numqi.qec.q723.{x}()')
numqi.qec.q723.get_2I_lambda0() numqi.qec.q723.get_2I_lambda075() numqi.qec.q723.get_2O_X5() numqi.qec.q723.get_723_cyclic_code_basis() numqi.qec.q723.get_BD12_veca0122233() numqi.qec.q723.get_BD12_veca1112222() numqi.qec.q723.get_BD14() numqi.qec.q723.get_BD16_5theta() numqi.qec.q723.get_BD16_degenerate() numqi.qec.q723.get_BD16_veca1222233() numqi.qec.q723.get_BD18() numqi.qec.q723.get_BD18_LP() numqi.qec.q723.get_BD20() numqi.qec.q723.get_BD22() numqi.qec.q723.get_BD24() numqi.qec.q723.get_BD26() numqi.qec.q723.get_BD28() numqi.qec.q723.get_BD30() numqi.qec.q723.get_BD32() numqi.qec.q723.get_BD34() numqi.qec.q723.get_BD36() numqi.qec.q723.get_cyclic_code()
tmp0 = [x for x in dir(numqi.qec.q823) if x.startswith('get_')]
for x in tmp0:
print(f'numqi.qec.q823.{x}()')
numqi.qec.q823.get_BD38_LP() numqi.qec.q823.get_BD64() numqi.qec.q823.get_BD72() numqi.qec.q823.get_BD74() numqi.qec.q823.get_BD76() numqi.qec.q823.get_BD78() numqi.qec.q823.get_BD80() numqi.qec.q823.get_BD84()