Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 24 additions & 15 deletions src/qutip_qip/algorithms/phase_flip.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,8 @@ def encode_circuit(self, data_qubits):
"""
Constructs the encoding circuit for the phase-flip code.

The logical qubit is encoded into an entangled state in the X-basis using Hadamard
(SNOT) gates followed by two CNOT gates. This creates redundancy to detect and correct
a single phase error.
The logical qubit is first encoded by two CNOT gates and then converted to the X-basis
using Hadamard (H). This creates redundancy to detect and correct a single phase error.

Args:
data_qubits (list[int]): Indices of 3 data qubits.
Expand All @@ -57,22 +56,23 @@ def encode_circuit(self, data_qubits):
raise ValueError("Expected 3 data qubits.")
qc = QubitCircuit(max(data_qubits) + 1)

# Convert to X-basis
for q in data_qubits:
qc.add_gate("SNOT", targets=[q])

# Bit-flip-style encoding
control = data_qubits[0]
for target in data_qubits[1:]:
qc.add_gate("CNOT", controls=control, targets=target)

# Convert to X-basis
for q in data_qubits:
qc.add_gate("H", targets=[q])

return qc

def syndrome_and_correction_circuit(self, data_qubits, syndrome_qubits):
"""
Builds the circuit for syndrome extraction and correction.

Parity is measured between data qubit pairs using ancillas and CNOT gates.
The data qubits are temporarily converted back to the Z-basis so parity
can be measured between pairs using ancillas and CNOT gates.
Measurements are stored in classical bits, and Z corrections are applied
conditionally based on the measured syndrome.

Expand All @@ -95,12 +95,20 @@ def syndrome_and_correction_circuit(self, data_qubits, syndrome_qubits):
dq = data_qubits
sq = syndrome_qubits

# Convert back from X-basis
for q in data_qubits:
qc.add_gate("H", targets=[q])

# Parity checks
qc.add_gate("CNOT", controls=dq[0], targets=sq[0])
qc.add_gate("CNOT", controls=dq[1], targets=sq[0])
qc.add_gate("CNOT", controls=dq[1], targets=sq[1])
qc.add_gate("CNOT", controls=dq[2], targets=sq[1])

# Convert to X-basis
for q in data_qubits:
qc.add_gate("H", targets=[q])

# Measure syndrome qubits
qc.add_measurement(sq[0], sq[0], classical_store=0)
qc.add_measurement(sq[1], sq[1], classical_store=1)
Expand Down Expand Up @@ -131,8 +139,9 @@ def decode_circuit(self, data_qubits):
"""
Constructs the decoding circuit that reverses the encoding operation.

It first applies the inverse of the CNOT encoding, then converts the qubits
back from the X-basis to the Z-basis using Hadamard (SNOT) gates.
It first applies the Hadamard (H) gates to convert the qubits back from
the X-basis to the Z-basis, then applies the inverse of the CNOT encoding to
decode the qubits.

Args:
data_qubits (list[int]): Indices of 3 data qubits.
Expand All @@ -147,12 +156,12 @@ def decode_circuit(self, data_qubits):
raise ValueError("Expected 3 data qubits.")
qc = QubitCircuit(max(data_qubits) + 1)

control = data_qubits[0]
for target in reversed(data_qubits[1:]):
qc.add_gate("CNOT", controls=control, targets=target)

# Convert back from X-basis
for q in data_qubits:
qc.add_gate("SNOT", targets=[q])
qc.add_gate("H", targets=[q])

control = data_qubits[0]
for target in data_qubits[:0:-1]:
qc.add_gate("CNOT", controls=control, targets=target)

return qc
50 changes: 30 additions & 20 deletions tests/test_phase_flip.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import pytest
import qutip
import numpy as np
from qutip_qip.circuit import QubitCircuit
from qutip_qip.algorithms import PhaseFlipCode

Expand All @@ -22,33 +23,38 @@ def syndrome_qubits():
def test_encode_circuit_structure(code, data_qubits):
qc = code.encode_circuit(data_qubits)
gate_names = [g.name for g in qc.gates]
assert gate_names.count("SNOT") == 3
assert gate_names.count("H") == 3
assert gate_names.count("CNOT") == 2
assert qc.gates[3].controls == [0]
assert qc.gates[3].targets == [1]
assert qc.gates[4].controls == [0]
assert qc.gates[4].targets == [2]
assert qc.gates[0].controls == [0]
assert qc.gates[0].targets == [1]
assert qc.gates[1].controls == [0]
assert qc.gates[1].targets == [2]


def test_decode_circuit_structure(code, data_qubits):
qc = code.decode_circuit(data_qubits)
gate_names = [g.name for g in qc.gates]
assert gate_names.count("CNOT") == 2
assert gate_names.count("SNOT") == 3
assert qc.gates[0].controls == [0]
assert qc.gates[0].targets == [2]
assert qc.gates[1].controls == [0]
assert qc.gates[1].targets == [1]
assert gate_names.count("H") == 3
assert qc.gates[3].controls == [0]
assert qc.gates[3].targets == [2]
assert qc.gates[4].controls == [0]
assert qc.gates[4].targets == [1]


@pytest.mark.xfail(reason="Known error in phase flip code. See Issue #283")
def test_phaseflip_correction_simulation(code, data_qubits, syndrome_qubits):
@pytest.mark.parametrize("seed", [42, 123, 777, 1337, 9999])
@pytest.mark.parametrize("error_qubit", [None, 0, 1, 2])
def test_phaseflip_correction_simulation(
code, data_qubits, syndrome_qubits, error_qubit, seed
):
"""
Simulate the full encoding, Z-error, correction, and decoding process
for a random qubit state |ψ⟩. Fidelity after correction should be ~1.
Simulate the full encoding, Z-error, correction, and decoding process.
Test across all possible single-qubit errors and multiple random initial states.
"""
rng = np.random.default_rng(seed)

# Random initial qubit state
psi = qutip.rand_ket(2)
psi = qutip.rand_ket(2, seed=rng)

# Full system: qubit + 2 redundant qubits + 2 ancillas
state = qutip.tensor([psi] + [qutip.basis(2, 0)] * 4)
Expand All @@ -57,10 +63,11 @@ def test_phaseflip_correction_simulation(code, data_qubits, syndrome_qubits):
qc_encode = code.encode_circuit(data_qubits)
state = qc_encode.run(state)

# Apply Z (phase-flip) error to qubit 1
qc_error = QubitCircuit(num_qubits=5)
qc_error.add_gate("Z", targets=[1])
state = qc_error.run(state)
# Inject error
if error_qubit is not None:
qc_error = QubitCircuit(num_qubits=5)
qc_error.add_gate("Z", targets=[error_qubit])
state = qc_error.run(state)

# Syndrome measurement and Z correction
qc_correct = code.syndrome_and_correction_circuit(
Expand All @@ -76,4 +83,7 @@ def test_phaseflip_correction_simulation(code, data_qubits, syndrome_qubits):
final = state.ptrace(0)
fidelity = qutip.fidelity(psi, final)

assert fidelity > 0.99, f"Fidelity too low: {fidelity:.4f}"
assert fidelity > 0.99, (
f"Failed on random seed {seed} on error qubit {error_qubit}. "
f"Fidelity: {fidelity:.4f}"
)