# We'll enumerate all error patterns for the 5 noisy CNOTs in the circuit
# Qubits: 0,1,2,3 are data; 4 is ancilla.
# Gates in time order (indices match gate numbers in the text):
# g2: CNOT 1->2
# g3: CNOT 1->0
# g4: CNOT 2->3
# g5: CNOT 3->4
# g6: CNOT 0->4
from itertools import product
n_qubits = 5
# Pauli representation: binary symplectic vector (z|x) length 2*n
# Define helper functions
def zero_pauli(n):
return [0]*(2*n)
# Add (compose) two Paulis modulo 2, ignoring phases
def pauli_add(p, q):
return [(pi ^ qi) for pi, qi in zip(p, q)]
# Conjugate a Pauli by CNOT control c, target t
# Mapping on (z|x): x_t ^= x_c ; z_c ^= z_t
def conj_cnot(p, c, t):
p = p[:] # copy
# x_t ^= x_c
p[n_qubits + t] ^= p[n_qubits + c]
# z_c ^= z_t
p[c] ^= p[t]
return p
# Conjugate by Hadamard on qubit q: swap x<->z on q
def conj_h(p, q):
p = p[:]
p[q], p[n_qubits+q] = p[n_qubits+q], p[q]
return p
# Build 1-qubit Pauli vectors: I,X,Y,Z
single = {
'I': [0,0],
'X': [0,1],
'Z': [1,0],
'Y': [1,1],
}
# Build all 16 two-qubit Paulis on given pair (i,j), including II as index 0.
# We'll map index -> (label, vector)
def two_qubit_paulis(i, j):
labels = []
vecs = []
for a in ['I','X','Y','Z']:
for b in ['I','X','Y','Z']:
labels.append(a+b)
v = [0]*(2*n_qubits)
# set for qubit i
v[i] = single[a][0]
v[n_qubits + i] = single[a][1]
# set for qubit j
v[j] = single[b][0]
v[n_qubits + j] = single[b][1]
vecs.append(v)
return labels, vecs
labels_12, vecs_12 = two_qubit_paulis(1,2)
labels_10, vecs_10 = two_qubit_paulis(1,0)
labels_23, vecs_23 = two_qubit_paulis(2,3)
labels_34, vecs_34 = two_qubit_paulis(3,4)
labels_04, vecs_04 = two_qubit_paulis(0,4)
# Subsequent gates for conjugation after each error location
# After g2 (1->2): subsequent gates are g3 (1->0), g4 (2->3), g5 (3->4), g6 (0->4)
# After g3: subsequent g4,g5,g6; After g4: g5,g6; After g5: g6; After g6: none
# Define a function to propagate an error inserted after a specific gate index
# gate_idx 2..6; input is a Pauli vector (length 2n)
def propagate_to_end(v, gate_idx):
p = v[:]
# Apply subsequent CNOTs
if gate_idx <= 2:
# after g2 -> apply g3,g4,g5,g6
p = conj_cnot(p, 1, 0) # g3
p = conj_cnot(p, 2, 3) # g4
p = conj_cnot(p, 3, 4) # g5
p = conj_cnot(p, 0, 4) # g6
elif gate_idx == 3:
# after g3 -> apply g4,g5,g6
p = conj_cnot(p, 2, 3)
p = conj_cnot(p, 3, 4)
p = conj_cnot(p, 0, 4)
elif gate_idx == 4:
# after g4 -> apply g5,g6
p = conj_cnot(p, 3, 4)
p = conj_cnot(p, 0, 4)
elif gate_idx == 5:
# after g5 -> apply g6
p = conj_cnot(p, 0, 4)
elif gate_idx == 6:
# after g6 -> none
pass
else:
raise ValueError
return p
# Precompute propagated errors for each location and each 16 choices
prop_after = {}
for gate_idx, vecs in zip([2,3,4,5,6], [vecs_12, vecs_10, vecs_23, vecs_34, vecs_04]):
lst = []
for v in vecs:
lst.append(propagate_to_end(v, gate_idx))
prop_after[gate_idx] = lst
# Define acceptance test
# Conditions: ancilla x4 == 0; even parity of x on data 0..3; even parity of z on data 0..3
def accepted(net):
x = net[n_qubits:]
z = net[:n_qubits]
# ancilla acceptance
if x[4] == 1:
return False
# code stabilizers
if (x[0]^x[1]^x[2]^x[3]) == 1:
return False
if (z[0]^z[1]^z[2]^z[3]) == 1:
return False
return True
# Build benign set on data qubits: generated by XXXX, ZZZZ, Z_A=ZZII, Z_B=ZIZI
import itertools
def vec_from_ops(ops):
v = [0]*(2*n_qubits)
for op in ops:
if op == 'XXXX':
for q in range(4):
v[n_qubits+q] ^= 1
elif op == 'ZZZZ':
for q in range(4):
v[q] ^= 1
elif op == 'ZA': # ZZII on (0,1)
v[0] ^= 1; v[1] ^= 1
elif op == 'ZB': # ZIZI on (0,2)
v[0] ^= 1; v[2] ^= 1
else:
raise ValueError
return v
benign_data_set = set()
ops_list = ['XXXX','ZZZZ','ZA','ZB']
for mask in range(16):
ops = [ops_list[i] for i in range(4) if (mask>>i)&1]
v = vec_from_ops(ops)
# only keep data portion (length 8)
benign_data_set.add(tuple(v[:4] + v[5:])) # wait wrong slicing
# Fix the slicing: we want data qubits 0..3, so z[0..3], x[0..3]
benign_data_set = set()
for mask in range(16):
ops = [ops_list[i] for i in range(4) if (mask>>i)&1]
v = vec_from_ops(ops)
data_vec = tuple(v[:4] + v[n_qubits:n_qubits+4])
benign_data_set.add(data_vec)
len(benign_data_set)
# Now enumerate all 16^5 choices across the five gates
# At each gate, index 0 means II; 1..15 map to vecs
A_counts = [0]*6 # A_k: number of accepted patterns with exactly k errors
G_counts = [0]*6 # G_k: number of accepted-good patterns with exactly k errors (no logical flip)
# Also we may want to keep track of L_counts for logical error acceptance
L_counts = [0]*6
# iterate
choices = range(16)
N_total = 0
for idxs in product(choices, repeat=5):
N_total += 1
# build net Pauli vector as sum of propagated contributions
net = [0]*(2*n_qubits)
k = 0
for (gate_idx, idx) in zip([2,3,4,5,6], idxs):
if idx != 0:
k += 1
net = pauli_add(net, prop_after[gate_idx][idx])
# test acceptance
if accepted(net):
A_counts[k] += 1
# determine if benign on data
data_vec = tuple(net[:4] + net[n_qubits:n_qubits+4])
if data_vec in benign_data_set:
G_counts[k] += 1
else:
L_counts[k] += 1
N_total, A_counts, G_counts, L_counts, [A_counts[i]-G_counts[i]-L_counts[i] for i in range(6)]