# We'll enumerate all Pauli error combinations for the 5 two-qubit gates in the circuit
# Gates (from earliest to latest):
# 1) CNOT_{12} (control 1, target 2)
# 2) CNOT_{10} (control 1, target 0)
# 3) CNOT_{23} (control 2, target 3)
# 4) CNOT_{34} (control 3, target 4)
# 5) CNOT_{04} (control 0, target 4)
# After each gate, a 2-qubit depolarizing error may occur on that gate's qubits.
# We will propagate each inserted two-qubit Pauli forward through the remaining CNOTs,
# accumulate the total Pauli on 5 qubits (0..4), and then check acceptance conditions.
from itertools import product
# Pauli multiplication table ignoring global phase: maps (P,Q) -> (R, phase)
# We'll carry the phase but ultimately ignore for acceptance checks.
paulis = ['I','X','Y','Z']
# multiplication table with phase factors (1, -1, 1j, -1j)
mult = {
('I','I'):('I',1), ('I','X'):('X',1), ('I','Y'):('Y',1), ('I','Z'):('Z',1),
('X','I'):('X',1), ('X','X'):('I',1), ('X','Y'):('Z',1j), ('X','Z'):('Y',-1j),
('Y','I'):('Y',1), ('Y','X'):('Z',-1j), ('Y','Y'):('I',1), ('Y','Z'):('X',1j),
('Z','I'):('Z',1), ('Z','X'):('Y',1j), ('Z','Y'):('X',-1j), ('Z','Z'):('I',1),
}
# Conjugation of single-qubit Pauli by CNOT(c,t)
# We implement conjugation on a full 5-qubit Pauli string P (list of length 5)
def cnot_conj(P, c, t):
P = list(P)
# Copy to avoid in-place mis-order
Pc, Pt = P[c], P[t]
# Apply the standard conjugation rules
# X on control -> X on control and X on target
# Z on target -> Z on control and Z on target
# Y handled by compositions
# We'll update by multiplying appropriate factors
# Initialize to identity and compose corrections
# Do it via explicit cases on (Pc, Pt)
# Map using operator identities:
# Conjugation acts linearly on the Pauli group; we can implement via table mapping of basis elements
# We'll just apply rules per component:
# Start with identity on both positions, then multiply mapped contributions
# But easier: use known mapping on generators X_c, Z_c, X_t, Z_t and reconstruct Y via X*Z up to phase
# We'll decompose Pc and Pt into X/Z bits to apply mapping. Simpler: handle 16 cases directly using rules:
def map_single(Pc, Pt):
# returns new (Pc', Pt') up to a phase. We'll also return the phase.
phase = 1
# Start from identity; later multiply in effect of Pc and Pt one by one
# Conjugation is homomorphism: U (A B) U† = (U A U†)(U B U†)
# So treat Pc at c position and Pt at t position separately, then multiply results on those two qubits
# We'll build arrays for 5-qubit string but only care about c and t positions.
# First: effect of Pc at control
P_c_map = {'I':('I',1), 'X':('X',1), 'Y':('Y',1), 'Z':('Z',1)}
P_t_map = {'I':('I',1), 'X':('X',1), 'Y':('Y',1), 'Z':('Z',1)}
# Apply known rules by replacing c and t operators:
# For control:
if Pc == 'I':
Pc1, phase1 = 'I', 1
Pt1, phase1b = 'I', 1
elif Pc == 'X':
# X_c -> X_c X_t
Pc1, Pt1, phase1, phase1b = 'X', 'X', 1, 1
elif Pc == 'Z':
# Z_c -> Z_c
Pc1, Pt1, phase1, phase1b = 'Z', 'I', 1, 1
elif Pc == 'Y':
# Y_c -> Y_c X_t
Pc1, Pt1, phase1, phase1b = 'Y', 'X', 1, 1
# For target:
if Pt == 'I':
Pc2, phase2 = 'I', 1
Pt2, phase2b = 'I', 1
elif Pt == 'X':
# X_t -> X_t
Pc2, Pt2, phase2, phase2b = 'I', 'X', 1, 1
elif Pt == 'Z':
# Z_t -> Z_c Z_t
Pc2, Pt2, phase2, phase2b = 'Z', 'Z', 1, 1
elif Pt == 'Y':
# Y_t -> Z_c Y_t
Pc2, Pt2, phase2, phase2b = 'Z', 'Y', 1, 1
# Now multiply contributions on c and t qubits: (Pc1 on c) * (Pc2 on c) and (Pt1 on t) * (Pt2 on t)
Pc_new, phc = mult[(Pc1, Pc2)]
Pt_new, pht = mult[(Pt1, Pt2)]
phase = phase1 * phase1b * phase2 * phase2b * phc * pht
return Pc_new, Pt_new, phase
Pc_new, Pt_new, ph = map_single(Pc, Pt)
P[c], P[t] = Pc_new, Pt_new
# Other positions unchanged
return ''.join(P), ph
# Multiply two 5-qubit Pauli strings (ignoring global phase aside from detection). Return product string and phase.
def pauli_mult(P, Q):
phase = 1
out = []
for a,b in zip(P,Q):
r, ph = mult[(a,b)]
out.append(r)
phase *= ph
return ''.join(out), phase
# Generate all 2-qubit non-identity Pauli errors on a given pair (i,j)
# Represent errors as 5-qubit strings with I elsewhere
def two_qubit_paulis(i,j):
out = []
for a in paulis:
for b in paulis:
if a=='I' and b=='I':
continue
s = ['I']*5
s[i] = a
s[j] = b
out.append(''.join(s))
return out
# Precompute error sets for each gate pair in order of insertion (right-to-left mapping):
# But note: we listed them earliest->latest above; we'll use that order for later conjugations
pairs = [(1,2),(1,0),(2,3),(3,4),(0,4)]
errs_per_gate = [two_qubit_paulis(i,j) for (i,j) in pairs]
# Precompute conjugation of any 5-qubit Pauli by each of the subsequent CNOTs
# For speed, we can apply cnot_conj sequentially as needed.
# We need to compute, for a given set of per-gate choices (identity or a specific 2q Pauli), the final total Pauli just before measurement.
# We'll implement enumeration over all combinations but group by k (number of non-identity gates).
from collections import defaultdict
N_acc = [0]*6
N_good = [0]*6
N_total_by_k = [0]*6
# Build the target state's stabilizer group on data qubits (0..3)
# Generators: XXXX, ZZZZ, ZZII, ZIZI
def data_pauli_str(s4):
# s4 is 4-char for data; return 5-char with ancilla I
return s4 + 'I'
def make_full5_from_data(s4):
s = list('I'*5)
for q,ch in enumerate(s4):
s[q] = ch
return ''.join(s)
# Helper to build generators
G_data = []
G_data.append('XXXX')
G_data.append('ZZZZ')
G_data.append('ZZII')
G_data.append('ZIZI')
# Build full 5-qubit versions (with I on ancilla)
G5 = [make_full5_from_data(g) for g in G_data]
# Build the 4-qubit stabilizer group S_target (set of 4-char strings) ignoring phases
S_target = set()
# generate 2^4 combinations
for m0 in [0,1]:
for m1 in [0,1]:
for m2 in [0,1]:
for m3 in [0,1]:
s = 'IIII'
if m0:
s, _ = pauli_mult(s+'I', G5[0]+'I') # We'll just use 5-qubit mult and slice
s = s[:4]
if m1:
s5, _ = pauli_mult(s+'I', G5[1])
s = s5[:4]
if m2:
s5, _ = pauli_mult(s+'I', G5[2])
s = s5[:4]
if m3:
s5, _ = pauli_mult(s+'I', G5[3])
s = s5[:4]
S_target.add(s)
# Alternatively, rebuild more cleanly using 4-qubit multiplication
# We'll create a 4-qubit multiplication that ignores phase.
def pauli_mult4(P,Q):
phase = 1
out = []
for a,b in zip(P,Q):
r, ph = mult[(a,b)]
out.append(r)
phase *= ph
return ''.join(out), phase
# Rebuild S_target cleanly
S_target = set(['IIII'])
for g in ['XXXX','ZZZZ','ZZII','ZIZI']:
# expand by multiplying existing elements by g
S_target = S_target.union({pauli_mult4(s,g)[0] for s in list(S_target)})
# Actually that only adds one generator; we need to generate the whole group by combining; do iterative closure
changed = True
while changed:
changed = False
newS = set(S_target)
for a in S_target:
for b in ['XXXX','ZZZZ','ZZII','ZIZI']:
ab, _ = pauli_mult4(a,b)
if ab not in newS:
newS.add(ab)
changed = True
S_target = newS
# Build the normalizer condition with respect to S_code={XXXX, ZZZZ}: commute iff even X/Y count and even Z/Y count on data
def commutes_with_stabilizers(data4):
# even parity of X/Y across all four: sum x_j mod 2 = 0
# even parity of Z/Y across all four: sum z_j mod 2 = 0
x_par = 0
z_par = 0
for ch in data4:
if ch in ('X','Y'):
x_par ^= 1
if ch in ('Z','Y'):
z_par ^= 1
return (x_par==0) and (z_par==0)
# Ancilla acceptance: ancilla char must be not X or Y (i.e., I or Z)
def ancilla_accept(anc_ch):
return anc_ch in ('I','Z')
# Now enumerate all combinations. For each gate, include 'I' option besides the 15 two-qubit errors.
# We'll prepare a list of per-gate options as [ ['I'], [list of 15 two-qubit strings] ]
errs_with_I = []
for gate_errs in errs_per_gate:
errs_with_I.append(['I'] + gate_errs)
# Precompute subsequent conjugation sequences for performance: For an error inserted after gate g (0-based),
# it must be conjugated by gates g+1..4 (the later ones). We'll implement cnot_conj accordingly.
# define CNOT list for conjugations in order (later gates)
cnot_list = [(1,2),(1,0),(2,3),(3,4),(0,4)]
# Conjugate a 5-qubit Pauli P by the sequence of later CNOTs starting after index g
def conj_after_gate(P, g):
for idx in range(g+1, 5):
c,t = cnot_list[idx]
P, _ = cnot_conj(P, c, t)
return P
# For performance, precompute the image of each possible non-identity error for each gate under later conjugations
pre_images = []
for g, gate_errs in enumerate(errs_per_gate):
imap = {}
for s in gate_errs:
imap[s] = conj_after_gate(s, g)
pre_images.append(imap)
# Function to multiply 5-qubit Paulis (ignore phase)
def mult5(P,Q):
return pauli_mult(P,Q)[0]
# Iterate all combinations; count by k (number of non-identity selections)
from math import comb
N_acc = [0]*6
N_good = [0]*6
N_total_by_k = [0]*6
# We'll iterate more efficiently: for each gate choose index in 0..15 where 0 means identity, 1..15 picks from gate_errs
for idxs in product(*[range(16) for _ in range(5)]):
k = sum(1 for i in idxs if i!=0)
N_total_by_k[k] += 1
# Build total Pauli by multiplying the propagated errors from each non-identity gate
total = 'I'*5
for g,i in enumerate(idxs):
if i==0:
continue
# select the (i-1)th error from errs_per_gate[g]
s = errs_per_gate[g][i-1]
# propagate to end
s_prop = pre_images[g][s]
# multiply into total
total = mult5(total, s_prop)
# Acceptance conditions
data4 = total[:4]
anc = total[4]
if ancilla_accept(anc) and commutes_with_stabilizers(data4):
N_acc[k] += 1
# Good if data4 is in S_target
if data4 in S_target:
N_good[k] += 1
# Report counts
print('N_total_by_k:', N_total_by_k)
print('N_acc:', N_acc)
print('N_good:', N_good)
# Sanity: N_total_by_k should be [1, 5*15, comb(5,2)*15**2, comb(5,3)*15**3, comb(5,4)*15**4, 15**5]
expected = [1, 5*15, comb(5,2)*15**2, comb(5,3)*15**3, comb(5,4)*15**4, 15**5]
print('Expected totals by k:', expected)