import itertools
from collections import defaultdict
# Define single-qubit Pauli multiplication ignoring overall phase
mult = {
('I','I'):'I', ('I','X'):'X', ('I','Y'):'Y', ('I','Z'):'Z',
('X','I'):'X', ('X','X'):'I', ('X','Y'):'Z', ('X','Z'):'Y',
('Y','I'):'Y', ('Y','X'):'Z', ('Y','Y'):'I', ('Y','Z'):'X',
('Z','I'):'Z', ('Z','X'):'Y', ('Z','Y'):'X', ('Z','Z'):'I'
}
# Pauli letters
paulis = ['I','X','Y','Z']
# Multiplication on strings (ignoring global phase)
def mult_strings(a,b):
return [mult[(x,y)] for x,y in zip(a,b)]
# Conjugation of a 5-qubit Pauli string through a CNOT (control c, target t)
# Returns new string after P' = CNOT P CNOT^
def conj_cnot(P, c, t):
P = P.copy()
Lc, Lt = P[c], P[t]
# If control has X or Y, multiply X onto target
if Lc in ('X','Y'):
# Lt := Lt * X
Lt = mult[(Lt,'X')]
# If target has Z or Y, multiply Z onto control
if Lt in ('Z','Y'):
# BUT careful: above we updated Lt; in conjugation rules, the effect of target->control should use original Lt
pass
# To implement correctly, we should use original letters before any update
# Let's implement properly: use originals
def conj_cnot(P, c, t):
P = P.copy()
Lc0, Lt0 = P[c], P[t]
# Apply target->control update based on original target
if Lt0 in ('Z','Y'):
P[c] = mult[(P[c],'Z')]
# Apply control->target update based on original control
if Lc0 in ('X','Y'):
P[t] = mult[(P[t],'X')]
# The letters on c and t themselves remain the same otherwise (we already updated them via multiplications)
return P
# Build list of 2-qubit Paulis for a given pair (i,j)
# We'll include identity as well
pair_paulis = [(a,b) for a in paulis for b in paulis]
# Locations after CNOTs: order of noisy locations with the pair of qubits for the CNOT at that step
# Gates (right to left sequence): H1, CNOT12, CNOT10, CNOT23, CNOT34, CNOT04
# Noises after each CNOT in that order: after CNOT12 (loc0), after CNOT10 (loc1), after CNOT23 (loc2), after CNOT34 (loc3), after CNOT04 (loc4)
noisy_pairs = [(1,2),(1,0),(2,3),(3,4),(0,4)]
# Subsequent CNOTs after each location
subseqs = {
0: [(1,0),(2,3),(3,4),(0,4)], # after first CNOT12
1: [(2,3),(3,4),(0,4)],
2: [(3,4),(0,4)],
3: [(0,4)],
4: []
}
# Precompute mapping from each location and each 2-qubit Pauli on its pair to the 5-qubit Pauli at the end of circuit (conjugated forward)
# Represent 5-qubit Pauli string as list of 5 letters
end_maps = []
for loc, (i,j) in enumerate(noisy_pairs):
m = []
# Iterate over 2-qubit (including identity)
for a,b in pair_paulis:
P = ['I']*5
P[i] = a
P[j] = b
# Conjugate through all subsequent CNOTs
for (c,t) in subseqs[loc]:
P = conj_cnot(P, c, t)
m.append(tuple(P))
end_maps.append(m)
# Now set up codewords on qubits 0-3
import numpy as np
# Basis states map index->bitstring of length4
basis_states = [tuple(((i>>k)&1) for k in (3,2,1,0)) for i in range(16)]
# Build vectors for codewords over C^16
vecs = {}
# |overline{00}> = (|0000>+|1111>)/sqrt2
v00 = np.zeros(16, dtype=complex)
# index for |0000> is 0; for |1111> is 15
v00[0] = 1/np.sqrt(2)
v00[15] = 1/np.sqrt(2)
vecs['00'] = v00
# 01: (|0011> + |1100>)/sqrt2 -> indices for |0011>= 3, |1100>=12
v01 = np.zeros(16, dtype=complex)
v01[3] = 1/np.sqrt(2)
v01[12] = 1/np.sqrt(2)
vecs['01'] = v01
# 10: (|0101> + |1010>)/sqrt2 -> indices 5 and 10
v10 = np.zeros(16, dtype=complex)
v10[5] = 1/np.sqrt(2)
v10[10] = 1/np.sqrt(2)
vecs['10'] = v10
# 11: (|0110> + |1001>)/sqrt2 -> indices 6 and 9
v11 = np.zeros(16, dtype=complex)
v11[6] = 1/np.sqrt(2)
v11[9] = 1/np.sqrt(2)
vecs['11'] = v11
# Function: apply 4-qubit Pauli string (on qubits 0..3) to |0000> and |1111> basis to get resulting vector s'
def apply_pauli_to_basis(bits, letters):
# bits: tuple of 4 bits
phase = 1+0j
new_bits = list(bits)
for q in range(4):
b = new_bits[q]
L = letters[q]
if L=='I':
pass
elif L=='X':
new_bits[q] = 1-b
elif L=='Z':
if b==1:
phase *= -1
elif L=='Y':
# Y|0> = i|1>, Y|1> = -i|0>
if b==0:
phase *= 1j
new_bits[q] = 1
else:
phase *= -1j
new_bits[q] = 0
else:
raise ValueError
return tuple(new_bits), phase
# Build a cache mapping 4-letter Pauli to resulting vector when applied to v00 (GHZ) to avoid recomputation
pauli4_letters = list(itertools.product(paulis, repeat=4))
pauli4_to_state = {}
for letters in pauli4_letters:
# Apply to |0000> and |1111>
b0, p0 = apply_pauli_to_basis((0,0,0,0), letters)
b1, p1 = apply_pauli_to_basis((1,1,1,1), letters)
v = np.zeros(16, dtype=complex)
idx0 = (b0[0]<<3)|(b0[1]<<2)|(b0[2]<<1)|b0[3]
idx1 = (b1[0]<<3)|(b1[1]<<2)|(b1[2]<<1)|b1[3]
v[idx0] += p0/np.sqrt(2)
v[idx1] += p1/np.sqrt(2)
pauli4_to_state[letters] = v
# Function to check which codeword state equals a given state vector (up to global phase). Returns key '00','01','10','11' or None
def which_codeword(v):
# Compute overlaps squared
overlaps = {k: abs(np.vdot(vecs[k], v))**2 for k in vecs}
# Find max
kmax = max(overlaps, key=lambda k: overlaps[k])
if abs(overlaps[kmax]-1.0) < 1e-12:
return kmax
else:
return None
# Check acceptance conditions helper
def accept_conditions(P_end):
# P_end is 5-letter tuple
# Ancilla acceptance: letter 4 in {I,Z}
if P_end[4] not in ('I','Z'):
return False
# Code stabilizer acceptance: commute with XXXX and ZZZZ
letters = P_end[:4]
# Parity of Z or Y must be even (commute with XXXX)
count_ZY = sum(1 for L in letters if L in ('Z','Y'))
if count_ZY % 2 != 0:
return False
# Parity of X or Y must be even (commute with ZZZZ)
count_XY = sum(1 for L in letters if L in ('X','Y'))
if count_XY % 2 != 0:
return False
return True
# Enumerate all patterns (16^5). For each, compute total end Pauli and classify
counts_accept_by_k = defaultdict(int)
counts_correct_by_k = defaultdict(int)
# Precompute end_maps again to ensure correct indexing order per location from 0..4
# end_maps[loc][pidx] gives 5-letter tuple
# We'll iterate pidx per location over 0..15 correspond to pair_paulis order
# Precompute total number patterns for sanity check
n_total = 16**5
# Let's loop
for p0 in range(16):
P0 = end_maps[0][p0]
for p1 in range(16):
# Multiply P0 and P1
P01 = tuple(mult[(x,y)] for x,y in zip(P0, end_maps[1][p1]))
for p2 in range(16):
P012 = tuple(mult[(x,y)] for x,y in zip(P01, end_maps[2][p2]))
for p3 in range(16):
P0123 = tuple(mult[(x,y)] for x,y in zip(P012, end_maps[3][p3]))
for p4 in range(16):
Pend = tuple(mult[(x,y)] for x,y in zip(P0123, end_maps[4][p4]))
# Count number of non-identity applied across the 5 locations
k = (p0!=0) + (p1!=0) + (p2!=0) + (p3!=0) + (p4!=0)
if accept_conditions(Pend):
counts_accept_by_k[k] += 1
# Determine whether logical state is |00>
letters4 = Pend[:4]
v = pauli4_to_state[letters4]
which = which_codeword(v)
if which == '00':
counts_correct_by_k[k] += 1
# Produce counts
counts_accept = [counts_accept_by_k[k] for k in range(6)]
counts_correct = [counts_correct_by_k[k] for k in range(6)]
print('Total patterns:', n_total)
print('Accepted counts by k=0..5:', counts_accept)
print('Correct counts by k=0..5:', counts_correct)
# Sanity: all counts should be integers and <= 16^5
# For small-p expansion, compute coefficients of P_acc and P_corr polynomials: sum_k counts * (1-p)^{5-k} * (p/15)^k
from fractions import Fraction
# We can produce expanded polynomials with rational coefficients
import sympy as sp
p = sp.symbols('p')
Pacc = 0
Pcorr = 0
for k in range(6):
cA = counts_accept_by_k.get(k,0)
cC = counts_correct_by_k.get(k,0)
term = (sp.Integer(cA)) * (1-p)**(5-k) * (p/15)**k
Pacc += term
termC = (sp.Integer(cC)) * (1-p)**(5-k) * (p/15)**k
Pcorr += termC
# Simplify and express as rational polynomials
Pacc_simpl = sp.simplify(sp.together(Pacc))
Pcorr_simpl = sp.simplify(sp.together(Pcorr))
print('P_acc(p) =', sp.simplify(Pacc_simpl))
print('P_corr(p) =', sp.simplify(Pcorr_simpl))
F = sp.simplify(sp.together(Pcorr_simpl / Pacc_simpl))
print('F_logical(p) =', F)
# Series expansion around p=0 up to p^3
print('Series F_logical:', sp.series(F, p, 0, 4))