Challenge

System #0

You are a physics research assistant specializing in solving complex, research-level problems using precise, step-by-step reasoning.

Input Problems will be provided in Markdown format.

Output (Markdown format)

  1. Step-by-Step Derivation - Show every non-trivial step in the solution. Justify steps using relevant physical laws, theorems, mathematical identities or numerical codes.
  2. Mathematical Typesetting - Use LaTeX for all mathematics: $...$ for inline expressions, $$...$$ for display equations.
  3. Conventions and Units - Follow the unit system and conventions specified in the problem.
  4. Final Answer - At the end of the solution, start a new line with “Final Answer:”, and present the final result.

    For final answers involving values, follow the precision requirements specified in the problem. If no precision is specified: - If an exact value is possible, provide it (e.g., \$\sqrt(2)\$, \$\pi/4\$). - If exact form is not feasible, retain at least 12 significant digits in the result.

  5. Formatting Compliance - If the user requests a specific output format (e.g., code, table), provide the final answer accordingly.

User #1

Problem setup:

In quantum error correction, you encode quantum states into logical states made of many qubits in order to improve their resilience to errors. In quantum error detection, you do the same but can only detect the presence of errors and not correct them. In this problem, we will consider a single [[4,2,2]] quantum error detection code, which encodes two logical qubits into four physical qubits, and investigate how robust logical quantum operations in this code are to quantum errors.

Our convention is that the four physical qubits in the [[4,2,2]] code are labelled 0,1,2,3. The two logical qubits are labelled A and B. The stabilizers are \(XXXX\) and \(ZZZZ\), where \(X\) and \(Z\) are Pauli matrices. The logical \(X\) and \(Z\) operators on the two qubits are \(X_A = XIXI\), \(X_B=XXII\), \(Z_A = ZZII\), \(Z_B = ZIZI\), up to multiplication by stabilizers.

We will consider different state preparation circuits consisting of controlled not \(CNOT_{ij}\) gates, where \(CNOT_{ij}\) has control qubit \(i\) and target qubit \(j\). As a simple model of quantum errors in hardware, we will suppose that each \(CNOT_{ij}\) gate in the circuit has a two qubit depolarizing error channel following it that produces one of the 15 non-identity two-qubit Paulis with equal probability \(p/15\). The probability \(p\) indicates the probability of an error in a single two-qubit gate. We will assess the logical infidelity of certain state preparation protocols as a function of the physical infidelity \(p\).

Challenge

Suppose that we prepare a logical two-qubit \(|00\rangle_{AB}\) state in the [[4,2,2]] code. To do so, we introduce an ancilla qubit, qubit 4, and use the following state preparation circuit:

\[ M_4 (CNOT_{04}) (CNOT_{34}) (CNOT_{23}) (CNOT_{10}) (CNOT_{12}) (H_1) \]

Note that this equation is written in matrix multiplication order, while the quantum operations in the circuit occur in the reverse order (from right-to-left in the above equation). \(H\) is a single-qubit Hadamard gate and \(M\) is a single-qubit measurement. The ancilla is used to detect errors in the state preparation circuit and makes the circuit fault-tolerant. If the ancilla measurement is \(|0\rangle\) (\(|1\rangle\)), the state preparation succeeds (fails).

What is the logical state fidelity of the final 2-qubit logical state at the end of the circuit as a function of two-qubit gate error rate \(p\), assuming the state is post-selected on all detectable errors in the code and on the ancilla qubit measuring \(|0\rangle\)?

Assistant #2
Tool Call (code to execute)
import numpy as np
from itertools import product, combinations

# Qubit indexing: 0..4, with 4 the ancilla
nq = 5
N = 2**nq

def kron_n(ops):
    out = ops[0]
    for m in ops[1:]:
        out = np.kron(out, m)
    return out

# Single-qubit Paulis
I = np.array([[1,0],[0,1]], complex)
X = np.array([[0,1],[1,0]], complex)
Y = np.array([[0,-1j],[1j,0]], complex)
Z = np.array([[1,0],[0,-1]], complex)
H = (1/np.sqrt(2))*np.array([[1,1],[1,-1]], complex)

paulis = {'I':I,'X':X,'Y':Y,'Z':Z}

# Build n-qubit op acting on specified qubits for 1q or 2q gate

def one_qubit_op(U, q, nq=5):
    ops = []
    for i in range(nq):
        ops.append(U if i==q else I)
    return kron_n(ops)

def two_qubit_op(U2, q1, q2, nq=5):
    # U2 acts on (q1,q2) in that order in the tensor product
    ops = []
    for i in range(nq):
        if i==q1:
            ops.append(None)  # placeholder
        elif i==q2:
            ops.append(None)
        else:
            ops.append(I)
    # Build kron in order 0..nq-1
    out = None
    for i in range(nq):
        if ops[i] is None:
            # when hitting first of q1 or q2, insert U2 appropriately
            if i==q1:
                block = U2
            elif i==q2:
                # if q2 comes before q1 in ordering; then need to swap order of kron product inputs
                # Our U2 is defined in order (q1,q2), but if q2<q1, we need to account by conjugating with SWAP
                # Simpler: build embedding that respects placement by using identities and then permute? 
                # To avoid complexity, reorder so that q1<q2 always when building U2 embedding
                raise RuntimeError("two_qubit_op called with q2 before q1; provide q1<q2")
            # now append block and skip q2 when we reach it
            if out is None:
                out = block
            else:
                out = np.kron(out, block)
            # skip the partner qubit in the iteration by marking it as used
            # But we can't skip in for loop; We'll mark ops[q2] to identity after adding block
            ops[q2] = 'DONE'
        elif ops[i] == 'DONE':
            continue
        else:
            out = ops[i] if out is None else np.kron(out, ops[i])
    return out

# Let's avoid the above complexity: instead construct general n-qubit op via tensor product building, not using placeholders.
# Simpler approach: Use index mapping to place U2 at positions q1 and q2 by building kron product of 5 factors, where the factor is a 2x2 for other qubits,
# and for q1 use basis e00 T e00 etc is complicated. Alternative: Build U as 32x32 using computational basis mapping by applying U2 to bits of indices.

# Let's implement a general function apply_two_qubit_gate(state, U2, q1, q2) acting on state vector without forming full matrix.

def apply_two_qubit_gate_statevec(state, U2, q1, q2, nq=5):
    # state is vector of length 2**nq
    # U2 acts on qubits q1 (most significant within the pair) and q2 (least significant within the pair) as given
    # We'll loop over basis states and update amplitudes in blocks of size 4 corresponding to q1,q2 bits.
    # For efficiency, precompute bit masks
    if q1==q2:
        raise ValueError
    # Ensure q1<q2 for block enumeration ease
    if q1>q2:
        q1, q2 = q2, q1
        # but then U2 should be swapped: i.e., we need to apply U2' that corresponds to SWAP-conjugated U2
        # For Clifford gates and Paulis it'll be okay if we pass the swapped version.
        # Let's build swap matrix S and compute U2_swapped = S U2 S
        S = np.array([[1,0,0,0],
                      [0,0,1,0],
                      [0,1,0,0],
                      [0,0,0,1]], complex)
        U2 = S @ U2 @ S
    out = np.zeros_like(state)
    bit1 = 1 << (nq-1-q1)
    bit2 = 1 << (nq-1-q2)
    # Iterate over all indices, group them by fixing other bits and the two bits of interest vary over 00,01,10,11
    N = state.shape[0]
    for base in range(0, N, 1):
        # We'll only process when the bits at q1 and q2 are 00; then build the 4-vector with other bits same
        if (base & bit1) or (base & bit2):
            continue
        # collect indices for 00,01,10,11
        idx00 = base
        idx01 = base | bit2
        idx10 = base | bit1
        idx11 = base | bit1 | bit2
        v = np.array([state[idx00], state[idx01], state[idx10], state[idx11]], complex)
        v2 = U2 @ v
        out[idx00] += v2[0]
        out[idx01] += v2[1]
        out[idx10] += v2[2]
        out[idx11] += v2[3]
    return out

# Similarly for one-qubit gate

def apply_one_qubit_gate_statevec(state, U, q, nq=5):
    out = np.zeros_like(state)
    bit = 1 << (nq-1-q)
    N = state.shape[0]
    for base in range(0, N, 1):
        if base & bit:
            continue
        idx0 = base
        idx1 = base | bit
        v = np.array([state[idx0], state[idx1]], complex)
        v2 = U @ v
        out[idx0] += v2[0]
        out[idx1] += v2[1]
    return out

# Build CNOT 2-qubit unitary (4x4) for control->target on those two local qubits
CNOT_2 = np.array([[1,0,0,0],
                   [0,1,0,0],
                   [0,0,0,1],
                   [0,0,1,0]], complex)  # in basis |00>,|01>,|10>,|11> with first = control, second = target

# 2-qubit Pauli errors (on the pair) as 4x4 matrices in order (q1,q2)
pauli_labels = ['I','X','Y','Z']
P2 = {}
for a in pauli_labels:
    for b in pauli_labels:
        lab = a+b
        P2[lab] = np.kron(paulis[a], paulis[b])

# GHZ+ on 4 qubits (data), with ordering qubits 0,1,2,3 as in our labeling
psi_ghz4 = np.zeros(16, complex)
psi_ghz4[0] = 1/np.sqrt(2)  # |0000>
psi_ghz4[-1] = 1/np.sqrt(2) # |1111>

# Stabilizers on 4 qubits
Z4 = np.kron(np.kron(np.kron(Z,Z),Z),Z)
X4 = np.kron(np.kron(np.kron(X,X),X),X)

# Helper: extract data substate conditioned on ancilla = b

def measure_ancilla_and_postselect(psi5, b, nq=5):
    # Return conditional state on data (4-qubit) if measurement outcome b occurs with prob 1, else return None
    # Compute probability of outcome b
    # ancilla is qubit 4 (index 4), as LSB? In our indexing, q index increases left in bit significance; We used bit mask bit = 1 << (nq-1-q)
    # So ancilla bit is bit = 1 << (nq-1-4) = 1 << 0 = 1
    bit = 1
    N = psi5.shape[0]
    tot0 = 0.0
    tot1 = 0.0
    # Gather data subvector for ancilla=b
    data = np.zeros(16, complex)
    for idx in range(N):
        amp = psi5[idx]
        if idx & bit:
            tot1 += (abs(amp)**2)
        else:
            tot0 += (abs(amp)**2)
    # Determine if deterministic
    if b==0:
        if abs(tot0-1.0) < 1e-12:
            # build data state for ancilla=0 branch
            # mapping: index 4 is LSB; So data index corresponds to (idx >> 1)
            for idx in range(N):
                if (idx & bit)==0:
                    data_idx = idx >> 1
                    data[data_idx] = psi5[idx]
            # normalize
            data = data / np.sqrt(tot0)
            return data
        else:
            return None
    else:
        if abs(tot1-1.0) < 1e-12:
            for idx in range(N):
                if (idx & bit):
                    data_idx = (idx ^ bit) >> 1  # remove LSB
                    data[data_idx] = psi5[idx]
            data = data / np.sqrt(tot1)
            return data
        else:
            return None

# Apply stabilizer acceptance check: require +1 for both ZZZZ and XXXX

def stabilizer_accepts(psi4):
    vZ = (psi4.conj().T @ (Z4 @ psi4)).real
    vX = (psi4.conj().T @ (X4 @ psi4)).real
    # round near integers
    return (abs(vZ-1.0) < 1e-9) and (abs(vX-1.0) < 1e-9)

# Fidelity with GHZ+

def fid_ghz(psi4):
    amp = np.vdot(psi_ghz4, psi4)
    return (abs(amp)**2).real

# Build function to run the circuit with two specified errors at gates i<j and labels lab_i, lab_j
# Gates: 1:H1; 2:C12; 3:C10; 4:C23; 5:C34; 6:C04
# We'll label CNOT gates indices 1..5 for errors

# Precompute nothing; We'll apply stepwise

def run_with_two_errors(lab_i, gate_i, lab_j, gate_j):
    # gate indices start 1: C12, 2: C10, 3: C23, 4: C34, 5: C04
    # lab is two-letter string for two-qubit Pauli on that gate's qubits
    # Build initial state
    psi = np.zeros(N, complex)
    psi[0] = 1.0
    # H on qubit 1
    psi = apply_one_qubit_gate_statevec(psi, H, 1, nq)

    # gate 1: CNOT 1->2; then error if gate_i==1 or gate_j==1
    psi = apply_two_qubit_gate_statevec(psi, CNOT_2, 1, 2, nq)
    if gate_i==1:
        E = P2[lab_i]
        psi = apply_two_qubit_gate_statevec(psi, E, 1, 2, nq)
    if gate_j==1:
        E = P2[lab_j]
        psi = apply_two_qubit_gate_statevec(psi, E, 1, 2, nq)

    # gate 2: CNOT 1->0
    psi = apply_two_qubit_gate_statevec(psi, CNOT_2, 1, 0, nq)
    if gate_i==2:
        E = P2[lab_i]
        psi = apply_two_qubit_gate_statevec(psi, E, 1, 0, nq)
    if gate_j==2:
        E = P2[lab_j]
        psi = apply_two_qubit_gate_statevec(psi, E, 1, 0, nq)

    # gate 3: CNOT 2->3
    psi = apply_two_qubit_gate_statevec(psi, CNOT_2, 2, 3, nq)
    if gate_i==3:
        E = P2[lab_i]
        psi = apply_two_qubit_gate_statevec(psi, E, 2, 3, nq)
    if gate_j==3:
        E = P2[lab_j]
        psi = apply_two_qubit_gate_statevec(psi, E, 2, 3, nq)

    # gate 4: CNOT 3->4
    psi = apply_two_qubit_gate_statevec(psi, CNOT_2, 3, 4, nq)
    if gate_i==4:
        E = P2[lab_i]
        psi = apply_two_qubit_gate_statevec(psi, E, 3, 4, nq)
    if gate_j==4:
        E = P2[lab_j]
        psi = apply_two_qubit_gate_statevec(psi, E, 3, 4, nq)

    # gate 5: CNOT 0->4
    psi = apply_two_qubit_gate_statevec(psi, CNOT_2, 0, 4, nq)
    if gate_i==5:
        E = P2[lab_i]
        psi = apply_two_qubit_gate_statevec(psi, E, 0, 4, nq)
    if gate_j==5:
        E = P2[lab_j]
        psi = apply_two_qubit_gate_statevec(psi, E, 0, 4, nq)

    # ancilla measurement postselect on 0 deterministically
    psi4 = measure_ancilla_and_postselect(psi, 0, nq)
    if psi4 is None:
        return False, False  # not accepted by ancilla (or not deterministic 0)

    # stabilizer postselection
    if not stabilizer_accepts(psi4):
        return False, False

    # accepted; now fidelity
    F = fid_ghz(psi4)
    good = abs(F-1.0) < 1e-9
    return True, good

# Now enumerate all pairs of gates and 2q Pauli labels (excluding II)
labels = [a+b for a in pauli_labels for b in pauli_labels if not (a=='I' and b=='I')]

accepted_count = 0
accepted_good = 0
accepted_bad = 0

# We'll accumulate by gate pair
malignant_by_pair = {}
acc_by_pair = {}

for (i,j) in combinations(range(1,6), 2):
    mal = 0
    acc = 0
    for lab_i in labels:
        for lab_j in labels:
            accepted, good = run_with_two_errors(lab_i, i, lab_j, j)
            if accepted:
                acc += 1
                if not good:
                    mal += 1
    malignant_by_pair[(i,j)] = mal
    acc_by_pair[(i,j)] = acc
    accepted_count += acc
    accepted_bad += mal

accepted_good = accepted_count - accepted_bad

accepted_count, accepted_bad, malignant_by_pair
Tool #3
''
Assistant #4
Tool Call (code to execute)
print('Running enumeration...')

# Reuse functions and variables from previous cell by re-defining minimal if needed
import numpy as np
from itertools import product, combinations

nq = 5
N = 2**nq
I = np.array([[1,0],[0,1]], complex)
X = np.array([[0,1],[1,0]], complex)
Y = np.array([[0,-1j],[1j,0]], complex)
Z = np.array([[1,0],[0,-1]], complex)
H = (1/np.sqrt(2))*np.array([[1,1],[1,-1]], complex)

paulis = {'I':I,'X':X,'Y':Y,'Z':Z}

CNOT_2 = np.array([[1,0,0,0],
                   [0,1,0,0],
                   [0,0,0,1],
                   [0,0,1,0]], complex)

pauli_labels = ['I','X','Y','Z']
P2 = {}
for a in pauli_labels:
    for b in pauli_labels:
        lab = a+b
        P2[lab] = np.kron(paulis[a], paulis[b])

psi_ghz4 = np.zeros(16, complex)
psi_ghz4[0] = 1/np.sqrt(2)
psi_ghz4[-1] = 1/np.sqrt(2)

Z4 = np.kron(np.kron(np.kron(Z,Z),Z),Z)
X4 = np.kron(np.kron(np.kron(X,X),X),X)

# Functions

def apply_two_qubit_gate_statevec(state, U2, q1, q2, nq=5):
    if q1==q2:
        raise ValueError
    if q1>q2:
        q1, q2 = q2, q1
        S = np.array([[1,0,0,0],[0,0,1,0],[0,1,0,0],[0,0,0,1]], complex)
        U2 = S @ U2 @ S
    out = np.zeros_like(state)
    bit1 = 1 << (nq-1-q1)
    bit2 = 1 << (nq-1-q2)
    N = state.shape[0]
    for base in range(0, N):
        if (base & bit1) or (base & bit2):
            continue
        idx00 = base
        idx01 = base | bit2
        idx10 = base | bit1
        idx11 = base | bit1 | bit2
        v = np.array([state[idx00], state[idx01], state[idx10], state[idx11]], complex)
        v2 = U2 @ v
        out[idx00] += v2[0]
        out[idx01] += v2[1]
        out[idx10] += v2[2]
        out[idx11] += v2[3]
    return out

def apply_one_qubit_gate_statevec(state, U, q, nq=5):
    out = np.zeros_like(state)
    bit = 1 << (nq-1-q)
    N = state.shape[0]
    for base in range(0, N):
        if base & bit:
            continue
        idx0 = base
        idx1 = base | bit
        v = np.array([state[idx0], state[idx1]], complex)
        v2 = U @ v
        out[idx0] += v2[0]
        out[idx1] += v2[1]
    return out


def measure_ancilla_and_postselect(psi5, b, nq=5):
    bit = 1
    N = psi5.shape[0]
    tot0 = 0.0
    tot1 = 0.0
    data = np.zeros(16, complex)
    for idx in range(N):
        amp = psi5[idx]
        if idx & bit:
            tot1 += (abs(amp)**2)
        else:
            tot0 += (abs(amp)**2)
    if b==0:
        if abs(tot0-1.0) < 1e-12:
            for idx in range(N):
                if (idx & bit)==0:
                    data_idx = idx >> 1
                    data[data_idx] = psi5[idx]
            data = data / np.sqrt(tot0)
            return data
        else:
            return None
    else:
        if abs(tot1-1.0) < 1e-12:
            for idx in range(N):
                if (idx & bit):
                    data_idx = (idx ^ bit) >> 1
                    data[data_idx] = psi5[idx]
            data = data / np.sqrt(tot1)
            return data
        else:
            return None


def stabilizer_accepts(psi4):
    vZ = (psi4.conj().T @ (Z4 @ psi4)).real
    vX = (psi4.conj().T @ (X4 @ psi4)).real
    return (abs(vZ-1.0) < 1e-9) and (abs(vX-1.0) < 1e-9)

def fid_ghz(psi4):
    amp = np.vdot(psi_ghz4, psi4)
    return (abs(amp)**2).real


def run_with_two_errors(lab_i, gate_i, lab_j, gate_j):
    psi = np.zeros(N, complex)
    psi[0] = 1.0
    psi = apply_one_qubit_gate_statevec(psi, H, 1, nq)

    # gate 1: CNOT 1->2
    psi = apply_two_qubit_gate_statevec(psi, CNOT_2, 1, 2, nq)
    if gate_i==1:
        psi = apply_two_qubit_gate_statevec(psi, P2[lab_i], 1, 2, nq)
    if gate_j==1:
        psi = apply_two_qubit_gate_statevec(psi, P2[lab_j], 1, 2, nq)

    # gate 2: CNOT 1->0
    psi = apply_two_qubit_gate_statevec(psi, CNOT_2, 1, 0, nq)
    if gate_i==2:
        psi = apply_two_qubit_gate_statevec(psi, P2[lab_i], 1, 0, nq)
    if gate_j==2:
        psi = apply_two_qubit_gate_statevec(psi, P2[lab_j], 1, 0, nq)

    # gate 3: CNOT 2->3
    psi = apply_two_qubit_gate_statevec(psi, CNOT_2, 2, 3, nq)
    if gate_i==3:
        psi = apply_two_qubit_gate_statevec(psi, P2[lab_i], 2, 3, nq)
    if gate_j==3:
        psi = apply_two_qubit_gate_statevec(psi, P2[lab_j], 2, 3, nq)

    # gate 4: CNOT 3->4
    psi = apply_two_qubit_gate_statevec(psi, CNOT_2, 3, 4, nq)
    if gate_i==4:
        psi = apply_two_qubit_gate_statevec(psi, P2[lab_i], 3, 4, nq)
    if gate_j==4:
        psi = apply_two_qubit_gate_statevec(psi, P2[lab_j], 3, 4, nq)

    # gate 5: CNOT 0->4
    psi = apply_two_qubit_gate_statevec(psi, CNOT_2, 0, 4, nq)
    if gate_i==5:
        psi = apply_two_qubit_gate_statevec(psi, P2[lab_i], 0, 4, nq)
    if gate_j==5:
        psi = apply_two_qubit_gate_statevec(psi, P2[lab_j], 0, 4, nq)

    # ancilla measure 0
    psi4 = measure_ancilla_and_postselect(psi, 0, nq)
    if psi4 is None:
        return False, False
    if not stabilizer_accepts(psi4):
        return False, False
    F = fid_ghz(psi4)
    good = abs(F-1.0) < 1e-9
    return True, good

labels = [a+b for a in pauli_labels for b in pauli_labels if not (a=='I' and b=='I')]

accepted_count = 0
accepted_bad = 0
malignant_by_pair = {}
acc_by_pair = {}

for (i,j) in combinations(range(1,6), 2):
    mal = 0
    acc = 0
    for lab_i in labels:
        for lab_j in labels:
            accepted, good = run_with_two_errors(lab_i, i, lab_j, j)
            if accepted:
                acc += 1
                if not good:
                    mal += 1
    malignant_by_pair[(i,j)] = mal
    acc_by_pair[(i,j)] = acc
    accepted_count += acc
    accepted_bad += mal

print('Total accepted two-fault patterns:', accepted_count)
print('Total malignant (accepted but wrong) two-fault patterns:', accepted_bad)
print('Malignant by pair:', malignant_by_pair)
print('Accepted by pair:', acc_by_pair)
Tool #5
Running enumeration...
Total accepted two-fault patterns: 282
Total malignant (accepted but wrong) two-fault patterns: 144
Malignant by pair: {(1, 2): 0, (1, 3): 0, (1, 4): 16, (1, 5): 16, (2, 3): 16, (2, 4): 24, (2, 5): 16, (3, 4): 16, (3, 5): 24, (4, 5): 16}
Accepted by pair: {(1, 2): 27, (1, 3): 27, (1, 4): 27, (1, 5): 27, (2, 3): 29, (2, 4): 29, (2, 5): 29, (3, 4): 29, (3, 5): 29, (4, 5): 29}
Assistant #6
Tool Call (code to execute)
# Symplectic propagation approach
import numpy as np
from itertools import product

# map of two-qubit Pauli label to (xmask,zmask) on the two qubits
label_list = ['I','X','Y','Z']
label_to_bits = {
    'I': (0,0),
    'X': (1,0),
    'Y': (1,1),
    'Z': (0,1),
}
# gating pairs
pairs = {
    1: (1,2),
    2: (1,0),
    3: (2,3),
    4: (3,4),
    5: (0,4),
}

# Conjugation of (xmask,zmask) by a CNOT(c,t)
# Represent xmask and zmask as 5-bit ints with bit k representing presence on qubit k.

def conj_cnot_masks(xmask, zmask, c, t):
    # x_t' = x_t xor x_c; z_c' = z_c xor z_t
    # We'll compute bits for control and target then update mask ints accordingly.
    # Get bits
    bit_c = 1 << c
    bit_t = 1 << t
    x_c = 1 if (xmask & bit_c) else 0
    x_t = 1 if (xmask & bit_t) else 0
    z_c = 1 if (zmask & bit_c) else 0
    z_t = 1 if (zmask & bit_t) else 0
    # Update bits
    x_t_new = x_t ^ x_c
    z_c_new = z_c ^ z_t
    # Write back
    if x_t != x_t_new:
        xmask ^= bit_t
    if z_c != z_c_new:
        zmask ^= bit_c
    return xmask, zmask

# Precompute, for each gate g and two-qubit Pauli lab ab (16 labels), the end masks (xmask,zmask)
labels16 = [a+b for a in label_list for b in label_list]

# For each gate g need to embed the two single-qubit label bits into the adequate qubits: (control, target)

def embed_two_qubit_label_on_pair(lab, c, t):
    a, b = lab[0], lab[1]
    xa, za = label_to_bits[a]
    xb, zb = label_to_bits[b]
    xmask = 0
    zmask = 0
    if xa: xmask |= (1 << c)
    if za: zmask |= (1 << c)
    if xb: xmask |= (1 << t)
    if zb: zmask |= (1 << t)
    return xmask, zmask

# Precompute tail conjugation for each g, label
Eend = {g: {} for g in range(1,6)}
for g in range(1,6):
    c, t = pairs[g]
    for lab in labels16:
        xmask, zmask = embed_two_qubit_label_on_pair(lab, c, t)
        # propagate through gates h = g+1..5 in order
        for h in range(g+1, 6):
            ch, th = pairs[h]
            xmask, zmask = conj_cnot_masks(xmask, zmask, ch, th)
        Eend[g][lab] = (xmask, zmask)

# Now enumerate patterns across 5 gates; accumulate counts by k=number of non-identity labels
# We'll also verify pair-of-fault counts (k=2) malignant_count same as 144 earlier

acc_counts = [0]*6
good_counts = [0]*6
bad_counts = [0]*6

# Precompute which labels are identity or not
is_I = {lab: (lab=='II') for lab in labels16}

# We'll iterate loops across gates g=1..5; Use nested loops across 16 states each; Should be 1M loops; We'll implement efficient XOR combination

# Precompute arrays for each gate: list of 16 xmask ints and zmask ints; Also store whether non-identity
Xmasks = {}
Zmasks = {}
nonI = {}
for g in range(1,6):
    Xmasks[g] = []
    Zmasks[g] = []
    nonI[g] = []
    for lab in labels16:
        x,z = Eend[g][lab]
        Xmasks[g].append(x)
        Zmasks[g].append(z)
        nonI[g].append(0 if lab=='II' else 1)

# Accept conditions test

def popcount4(x):  # popcount on bits 0..3 only
    x &= 0b1111
    # builtin bit_count available in Python 3.8+ as int.bit_count()
    return x.bit_count()

acc_total = 0
acc_good = 0
acc_bad = 0

# iterate patterns
for a in range(16):
    xa, za = Xmasks[1][a], Zmasks[1][a]
    ka = nonI[1][a]
    for b in range(16):
        xb, zb = Xmasks[2][b], Zmasks[2][b]
        kb = nonI[2][b]
        for c in range(16):
            xc, zc = Xmasks[3][c], Zmasks[3][c]
            kc = nonI[3][c]
            for d in range(16):
                xd, zd = Xmasks[4][d], Zmasks[4][d]
                kd = nonI[4][d]
                for e in range(16):
                    xe, ze = Xmasks[5][e], Zmasks[5][e]
                    ke = nonI[5][e]
                    xmask = xa ^ xb ^ xc ^ xd ^ xe
                    zmask = za ^ zb ^ zc ^ zd ^ ze
                    k = ka + kb + kc + kd + ke
                    # Ancilla acceptance: require x on qubit 4 == 0
                    if (xmask >> 4) & 1:
                        continue
                    # Stabilizer acceptance: parity of x and z on data bits 0..3 even
                    if (popcount4(xmask) % 2) != 0:
                        continue
                    if (popcount4(zmask) % 2) != 0:
                        continue
                    acc_counts[k] += 1
                    acc_total += 1
                    # Good if xmask on data is 0000 or 1111; otherwise (but parity even) it's a weight-2 pattern -> bad
                    xdata = xmask & 0b1111
                    if xdata==0 or xdata==0b1111:
                        good_counts[k] += 1
                        acc_good += 1
                    else:
                        bad_counts[k] += 1
                        acc_bad += 1

print('Accepted counts by number of faults k:', acc_counts)
print('Good counts by k:', good_counts)
print('Bad counts by k:', bad_counts)
print('Total acc, good, bad:', acc_total, acc_good, acc_bad)

# Verify k=2 bad counts equal 144 as found via statevector enumeration restricted to 2 faults
print('k=2 bad count:', bad_counts[2])
Tool #7
Accepted counts by number of faults k: [1, 7, 282, 4222, 31637, 94923]
Good counts by k: [1, 7, 138, 1198, 7397, 24027]
Bad counts by k: [0, 0, 144, 3024, 24240, 70896]
Total acc, good, bad: 131072 32768 98304
k=2 bad count: 144
Assistant #8
Tool Call (code to execute)
from fractions import Fraction
from math import comb

acc_counts = [1, 7, 282, 4222, 31637, 94923]
good_counts = [1, 7, 138, 1198, 7397, 24027]
bad_counts = [0, 0, 144, 3024, 24240, 70896]

# Build polynomials as dict degree->Fraction coeff

def poly_add(A,B):
    C = A.copy()
    for d,c in B.items():
        C[d] = C.get(d, Fraction(0)) + c
        if C[d] == 0:
            del C[d]
    return C

def poly_mul(A,B):
    C = {}
    for da,ca in A.items():
        for db,cb in B.items():
            C[da+db] = C.get(da+db, Fraction(0)) + ca*cb
    return C

# Build polynomial for (1-p)^n

def poly_one_minus_p_pow(n):
    P = {}
    for m in range(n+1):
        coeff = Fraction((-1)**m * comb(n,m), 1)
        P[m] = P.get(m, Fraction(0)) + coeff
    return P

# Build N_good(p) and D_acc(p)

P_good = {}
P_acc = {}
P_bad = {}

for k in range(6):
    a = acc_counts[k]
    g = good_counts[k]
    b = bad_counts[k]
    # term: count * (p/15)^k * (1-p)^(5-k)
    # Represent (p/15)^k as polynomial p^k with coeff 1/15^k
    Pk = {k: Fraction(1, 15**k)}
    Qk = poly_one_minus_p_pow(5-k)
    term_acc = poly_mul(Pk, Qk)
    term_good = {d: c*Fraction(g,1) for d,c in term_acc.items()}
    term_acc = {d: c*Fraction(a,1) for d,c in term_acc.items()}
    term_bad = {d: c*Fraction(b,1) for d,c in term_acc.items()}  # Not directly correct; We'll rebuild separately; Let's redo

# Let's compute separately for good and acc and bad to avoid confusion
P_good = {}
P_acc = {}
P_bad = {}
for k in range(6):
    a = acc_counts[k]
    g = good_counts[k]
    b = bad_counts[k]
    Pk = {k: Fraction(1, 15**k)}
    Qk = poly_one_minus_p_pow(5-k)
    base = poly_mul(Pk, Qk)
    term_acc = {d: c*Fraction(a,1) for d,c in base.items()}
    term_good = {d: c*Fraction(g,1) for d,c in base.items()}
    term_bad = {d: c*Fraction(b,1) for d,c in base.items()}
    P_acc = poly_add(P_acc, term_acc)
    P_good = poly_add(P_good, term_good)
    P_bad = poly_add(P_bad, term_bad)

# Sort terms up to degree 5
print('P_acc coeffs:')
for d in range(6):
    print(d, P_acc.get(d, Fraction(0)))
print('P_good coeffs:')
for d in range(6):
    print(d, P_good.get(d, Fraction(0)))
print('P_bad coeffs:')
for d in range(6):
    print(d, P_bad.get(d, Fraction(0)))

# Leading order expansions for F = P_good/P_acc. We'll compute series expansion up to p^3 as Fraction

# Convert polynomials to lists of Fractions length 6 (degrees 0..5)
A = [P_acc.get(d, Fraction(0)) for d in range(6)]
G = [P_good.get(d, Fraction(0)) for d in range(6)]
B = [P_bad.get(d, Fraction(0)) for d in range(6)]

# Compute series of F(p)=G(p)/A(p) up to order 5 by long division around p=0
# Use formal power series division
F_series = [Fraction(0) for _ in range(6)]
rem = G.copy()
for n in range(6):
    # f_n = rem_n / A0
    if A[0] == 0:
        raise RuntimeError('A0=0')
    f_n = rem[n] / A[0]
    F_series[n] = f_n
    # subtract f_n * p^n * A from rem
    for m in range(n, 6):
        rem[m] -= f_n * A[m-n]

print('F series coeffs up to p^5:')
for d, coeff in enumerate(F_series):
    print(d, coeff, float(coeff))

# Also compute 1 - F for error rate
one_minus_F = [Fraction(0) for _ in range(6)]
one_minus_F[0] = Fraction(1,1) - F_series[0]
for i in range(1,6):
    one_minus_F[i] = -F_series[i]
print('1-F series coeffs up to p^5:')
for d,c in enumerate(one_minus_F):
    print(d, c, float(c))

# Check leading quadratic coefficient equals 16/25
print('Leading coeff (p^2) of 1-F:', one_minus_F[2], 'as float', float(one_minus_F[2]))
Tool #9
P_acc coeffs:
0 1
1 -68/15
2 704/75
3 -32768/3375
4 253952/50625
5 -262144/253125
P_good coeffs:
0 1
1 -68/15
2 656/75
3 -29312/3375
4 223232/50625
5 -229376/253125
P_bad coeffs:
0 0
1 0
2 16/25
3 -128/125
4 2048/3375
5 -32768/253125
F series coeffs up to p^5:
0 1 1.0
1 0 0.0
2 -16/25 -0.64
3 -704/375 -1.8773333333333333
4 -10496/3375 -3.1099259259259258
5 -216064/84375 -2.5607585185185187
1-F series coeffs up to p^5:
0 0 0.0
1 0 0.0
2 16/25 0.64
3 704/375 1.8773333333333333
4 10496/3375 3.1099259259259258
5 216064/84375 2.5607585185185187
Leading coeff (p^2) of 1-F: 16/25 as float 0.64
Assistant #10

Step-by-Step Derivation

1) Ideal target and code data

  • [[4,2,2]] code
  • Stabilizers: Sx = XXXX, Sz = ZZZZ.
  • Logical operators (one convenient choice):
    • X_A = X I X I, X_B = X X I I,
    • Z_A = Z Z I I, Z_B = Z I Z I.
  • The ideal target logical state is the joint +1 eigenstate of Sx, Sz, Z_A, Z_B, i.e. the logical |00⟩_{AB}.

2) What the circuit prepares without noise

The circuit is (right-to-left) H_1 β†’ CNOT_{1β†’2} β†’ CNOT_{1β†’0} β†’ CNOT_{2β†’3} β†’ CNOT_{3β†’4} β†’ CNOT_{0β†’4} β†’ M_4.

  • Starting from |00000⟩ (data qubits 0–3 and ancilla 4),
  • H_1 creates |+⟩ on qubit 1.
  • CNOT_{1β†’2}, CNOT_{1β†’0} create a 3-qubit GHZ state on qubits {0,1,2}.
  • CNOT_{2β†’3} extends this to a 4-qubit GHZ state on qubits 0–3: (|0000⟩ + |1111⟩)/√2.
  • CNOT_{3β†’4}, CNOT_{0β†’4} compute the Z-parity Z_0Z_3 onto the ancilla (target).
  • Since in GHZ the bits 0 and 3 are equal, the ancilla deterministically measures 0.

Therefore, the noiseless data state is |ψ_id⟩ = (|0000⟩ + |1111⟩)/√2, which is stabilized by Sx = XXXX and Sz = ZZZZ, and satisfies Z_A = +1, Z_B = +1. Hence |ψ_id⟩ = |00⟩_L.

3) Noise model and post-selection

  • After each of the 5 CNOTs, a two-qubit depolarizing error is applied: with probability p/15 one of the 15 non-identity two-qubit Paulis on that gate’s two qubits, and with probability 1 βˆ’ p the identity.
  • We post-select on: 1) ancilla outcome M_4 = 0, 2) β€œall detectable errors in the code,” i.e. both stabilizers Sx = +1 and Sz = +1 at the end.

4) How to analyze faults (Clifford/Pauli propagation)

  • Because the circuit is Clifford and the noise is Pauli, each error pattern is equivalent to a single Pauli E_end on the five qubits at the end (Heisenberg picture).
  • Conjugation rules for CNOT_{cβ†’t} on single-qubit Paulis (needed to propagate errors forward):
  • X_c β†’ X_c X_t, Z_c β†’ Z_c,
  • X_t β†’ X_t, Z_t β†’ Z_c Z_t, and similarly for Y via Y = iXZ.
  • Encoding E_end as symplectic bits (x,z) per qubit:
  • Under CNOT_{cβ†’t} the update is x_t ← x_t βŠ• x_c, z_c ← z_c βŠ• z_t, with all other bits unchanged (βŠ• is XOR).
  • Post-selection (acceptance) conditions are purely Pauli commutation conditions:
  • Ancilla acceptance (M_4 = 0): no X or Y on qubit 4 at the end, i.e. x_4 = 0.
  • Code acceptance (both stabilizers +1):
    • Commute with Sz = ZZZZ: parity of X/Y on data must be even: βˆ‘_{i=0}^3 x_i ≑ 0 (mod 2).
    • Commute with Sx = XXXX: parity of Z/Y on data must be even: βˆ‘_{i=0}^3 z_i ≑ 0 (mod 2).

5) Which accepted errors change the logical state?

  • Any accepted Pauli whose restriction to the data is in the subgroup generated by Sx, Sz, Z_A, Z_B leaves |00⟩_L invariant (Z_A and Z_B act trivially on |0⟩ logicals).
  • A logical error that flips |00⟩_L to an orthogonal logical state necessarily contains an X-type component on the data. Modulo stabilizers, the harmful logical errors are those that act like X_A and/or X_B (possibly times Z-logicals).
  • Within the acceptance constraints, this translates to:
  • Accepted β€œgood” patterns: x_data = 0000 or x_data = 1111 (i.e., I or Sx on data, possibly times Z’s).
  • Accepted β€œbad” patterns: x_data has even parity but x_data ∈ {6 weight-2 patterns}, i.e., any pattern with exactly two 1’s among data-qubit X-bits.
  • The ancilla constraint x_4 = 0 is also enforced.

6) First-order fault tolerance

  • Every single CNOT fault (k=1) that passes the ancilla and stabilizer checks necessarily has x_data = 0000 or x_data = 1111 (and even z parity), i.e., it is β€œgood.” Thus there is no accepted logical error at order p: P(logical error | post-selection) = O(p^2).

7) Exact enumeration and closed-form result

Using the symplectic propagation just described, we can exactly enumerate all error patterns across the five noisy CNOTs. Each gate has 16 options (II with prob 1βˆ’p, and 15 non-identity Paulis with prob p/15), so there are 16^5 patterns. Let A_k and G_k denote, respectively, the numbers of accepted patterns (ancilla 0 and both stabilizers +1) and accepted-good patterns with exactly k non-identity faults. One finds:

  • A_k = [1, 7, 282, 4222, 31637, 94923],
  • G_k = [1, 7, 138, 1198, 7397, 24027],
  • B_k = A_k βˆ’ G_k = [0, 0, 144, 3024, 24240, 70896] = accepted-bad counts.

Therefore, with the independent error model, the acceptance probability and the accepted-good probability are polynomials: - P_acc(p) = βˆ‘{k=0}^5 A_k (p/15)^k (1βˆ’p)^{5βˆ’k}, - P_good(p) = βˆ‘{k=0}^5 G_k (p/15)^k (1βˆ’p)^{5βˆ’k}.

Expanding these rationally yields - P_acc(p) = 1 βˆ’ (68/15)p + (704/75)p^2 βˆ’ (32768/3375)p^3 + (253952/50625)p^4 βˆ’ (262144/253125)p^5, - P_good(p) = 1 βˆ’ (68/15)p + (656/75)p^2 βˆ’ (29312/3375)p^3 + (223232/50625)p^4 βˆ’ (229376/253125)p^5.

The accepted-bad probability is P_bad(p) = P_acc(p) βˆ’ P_good(p) = (16/25)p^2 βˆ’ (128/125)p^3 + (2048/3375)p^4 βˆ’ (32768/253125)p^5.

The logical state fidelity conditioned on post-selection is the fraction of accepted runs that are good: F_log(p) = P_good(p) / P_acc(p).

8) Small-p expansion (leading-order behavior)

Since P_bad(p) has no p term, the post-selected logical error rate is quadratic: 1 βˆ’ F_log(p) = P_bad(p) / P_acc(p) = (16/25)p^2 + (704/375)p^3 + (10496/3375)p^4 + (216064/84375)p^5, so to leading order, 1 βˆ’ F_log(p) = (16/25)p^2 + O(p^3), F_log(p) = 1 βˆ’ (16/25)p^2 + O(p^3).

Equivalently, at order p^2 the malignant two-fault coefficient is 144 out of the 10Γ—15^2 = 2250 two-fault placements: 144/2250 Γ— 10? No; conditioning on post-selection yields P(L-error | accepted) = (144/15^2) p^2 + O(p^3) = (16/25)p^2 + O(p^3).

9) Gate-pair breakdown of malignant two-faults (k=2) for transparency

Among the CNOT pairs (in time order: 1=C12, 2=C10, 3=C23, 4=C34, 5=C04), the accepted-bad counts (summing over the 15Γ—15 non-identity choices) are: - (1,2): 0, (1,3): 0, - (1,4): 16, (1,5): 16, - (2,3): 16, (2,4): 24, (2,5): 16, - (3,4): 16, (3,5): 24, - (4,5): 16. Total 144.

Final Answer: The exact post-selected logical state fidelity as a function of the two-qubit gate error rate p is F_log(p) = P_good(p)/P_acc(p), with P_acc(p) = 1 βˆ’ (68/15)p + (704/75)p^2 βˆ’ (32768/3375)p^3 + (253952/50625)p^4 βˆ’ (262144/253125)p^5, P_good(p) = 1 βˆ’ (68/15)p + (656/75)p^2 βˆ’ (29312/3375)p^3 + (223232/50625)p^4 βˆ’ (229376/253125)p^5.

Equivalently, the post-selected logical infidelity is 1 βˆ’ F_log(p) = P_bad(p)/P_acc(p), P_bad(p) = (16/25)p^2 βˆ’ (128/125)p^3 + (2048/3375)p^4 βˆ’ (32768/253125)p^5.

In particular, to leading order for small p, F_log(p) = 1 βˆ’ (16/25)p^2 + O(p^3).

User #11

Populate your final answer into the code template provided below. This step is purely for formatting/display purposes. No additional reasoning or derivation should be performed. Do not import any modules or packages beyond what is provided in the template.

import sympy as sp

p = sp.symbols('p')

def answer(p):
    r"""
    Return the expression of the logical state fidelity of the final 2-qubit logical state
    at the end of the circuit as a function of two-qubit gate error rate $p$.

    Inputs
    ----------
    p: sympy.Symbol, two-qubit gate error rate, $p$

    Outputs
    ----------
    F_logical: sympy.Expr, logical state fidelity of the final 2-qubit logical state
    """

    # ------------------ FILL IN YOUR RESULTS BELOW ------------------
    F_logical = ...  # a SymPy expression of inputs
    # ---------------------------------------------------------------

    return F_logical
Assistant #12
import sympy as sp

p = sp.symbols('p')

def answer(p):
    r"""
    Return the expression of the logical state fidelity of the final 2-qubit logical state
    at the end of the circuit as a function of two-qubit gate error rate $p$.

    Inputs
    ----------
    p: sympy.Symbol, two-qubit gate error rate, $p$

    Outputs
    ----------
    F_logical: sympy.Expr, logical state fidelity of the final 2-qubit logical state
    """

    # ------------------ FILL IN YOUR RESULTS BELOW ------------------
    P_good = (
        1
        - sp.Rational(68, 15) * p
        + sp.Rational(656, 75) * p**2
        - sp.Rational(29312, 3375) * p**3
        + sp.Rational(223232, 50625) * p**4
        - sp.Rational(229376, 253125) * p**5
    )
    P_acc = (
        1
        - sp.Rational(68, 15) * p
        + sp.Rational(704, 75) * p**2
        - sp.Rational(32768, 3375) * p**3
        + sp.Rational(253952, 50625) * p**4
        - sp.Rational(262144, 253125) * p**5
    )
    F_logical = P_good / P_acc  # a SymPy expression of inputs
    # ---------------------------------------------------------------

    return F_logical