diff --git a/doc/source/apidoc/qutip_qip.algorithms.rst b/doc/source/apidoc/qutip_qip.algorithms.rst new file mode 100644 index 000000000..7c41a39f5 --- /dev/null +++ b/doc/source/apidoc/qutip_qip.algorithms.rst @@ -0,0 +1,24 @@ +:orphan: + +qutip\_qip.algorithms +====================== + +.. automodule:: qutip_qip.algorithms + :members: + :show-inheritance: + :imported-members: + + .. rubric:: Classes + + .. autosummary:: + + BitFlipCode + PhaseFlipCode + ShorCode + + .. rubric:: Functions + + .. autosummary:: + + qft + qpe diff --git a/src/qutip_qip/algorithms/__init__.py b/src/qutip_qip/algorithms/__init__.py index 8bdea8f7f..45954f653 100644 --- a/src/qutip_qip/algorithms/__init__.py +++ b/src/qutip_qip/algorithms/__init__.py @@ -1,2 +1,5 @@ from .qft import * from .qpe import * +from .bit_flip import * +from .phase_flip import * +from .shor_code import * diff --git a/src/qutip_qip/algorithms/bit_flip.py b/src/qutip_qip/algorithms/bit_flip.py new file mode 100644 index 000000000..14317df5b --- /dev/null +++ b/src/qutip_qip/algorithms/bit_flip.py @@ -0,0 +1,152 @@ +from qutip_qip.circuit import QubitCircuit + +__all__ = ["BitFlipCode"] + + +class BitFlipCode: + """ + Implementation of the 3-qubit bit-flip quantum error correction code using + projective measurements and classically controlled correction gates. + + This code detects and corrects a single bit-flip (X) error on any of the three qubits + using two syndrome (ancilla) qubits. + """ + + def __init__(self): + """ + Initializes the bit-flip code with 3 data qubits and 2 syndrome qubits. + """ + self._n_data = 3 + self._n_syndrome = 2 + + @property + def n_data(self): + """ + Returns: + int: Number of data qubits (always 3 for bit-flip code). + """ + return self._n_data + + @property + def n_syndrome(self): + """ + Returns: + int: Number of syndrome qubits used for error detection (2 for this code). + """ + return self._n_syndrome + + def encode_circuit(self, data_qubits): + """ + Constructs the encoding circuit for the bit-flip code. The first qubit is the control, + and CNOT gates are applied from it to the other data qubits to encode logical states + :math:`|0\\rangle` or :math:`|1\\rangle`. + + Args: + data_qubits (list[int]): List of 3 integers representing data qubit indices. + + Returns: + QubitCircuit: The encoding quantum circuit. + + Raises: + ValueError: If the number of data qubits is not 3. + """ + + if len(data_qubits) != self.n_data: + raise ValueError( + f"Expected {self.n_data} data qubits, got {len(data_qubits)}." + ) + qc = QubitCircuit(max(data_qubits) + 1) + control = data_qubits[0] + for target in data_qubits[1:]: + qc.add_gate("CNOT", controls=control, targets=target) + return qc + + def syndrome_and_correction_circuit(self, data_qubits, syndrome_qubits): + """ + Constructs the circuit for syndrome extraction and classical error correction. + The circuit measures parity between qubit pairs and applies X gates conditionally. + + Args: + data_qubits (list[int]): List of 3 integers representing data qubit indices. + syndrome_qubits (list[int]): List of 2 integers representing syndrome qubit indices. + + Returns: + QubitCircuit: The quantum circuit for syndrome measurement and correction. + + Raises: + ValueError: If the number of data or syndrome qubits is incorrect. + """ + if len(data_qubits) != self.n_data: + raise ValueError( + f"Expected {self.n_data} data qubits, got {len(data_qubits)}." + ) + if len(syndrome_qubits) != self.n_syndrome: + raise ValueError( + f"Expected {self.n_syndrome} syndrome qubits, got {len(syndrome_qubits)}." + ) + + total_qubits = max(data_qubits + syndrome_qubits) + 1 + classical_bits = len(syndrome_qubits) + qc = QubitCircuit(N=total_qubits, num_cbits=classical_bits) + + dq = data_qubits + sq = syndrome_qubits + + # Syndrome extraction: 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]) + + # Measurements into classical registers + qc.add_measurement(sq[0], sq[0], classical_store=0) + qc.add_measurement(sq[1], sq[1], classical_store=1) + + # Classical-controlled corrections based on measurement outcomes + # 2 (10): X on qubit 0, 3 (11): X on qubit 1, 1 (01): X on qubit 2 + qc.add_gate( + "X", + targets=dq[0], + classical_controls=[0, 1], + classical_control_value=2, + ) + qc.add_gate( + "X", + targets=dq[1], + classical_controls=[0, 1], + classical_control_value=3, + ) + qc.add_gate( + "X", + targets=dq[2], + classical_controls=[0, 1], + classical_control_value=1, + ) + + return qc + + def decode_circuit(self, data_qubits): + """ + Constructs the decoding circuit which is the inverse of the encoding operation, + used to recover the original logical qubit. TOFFOLI gate verifies parity. + + Args: + data_qubits (list[int]): List of 3 integers representing data qubit indices. + + Returns: + QubitCircuit: The decoding quantum circuit. + + Raises: + ValueError: If the number of data qubits is not 3. + """ + if len(data_qubits) != self.n_data: + raise ValueError( + f"Expected {self.n_data} data qubits, got {len(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) + + qc.add_gate("TOFFOLI", controls=data_qubits[1:], targets=control) + return qc diff --git a/src/qutip_qip/algorithms/phase_flip.py b/src/qutip_qip/algorithms/phase_flip.py new file mode 100644 index 000000000..2ffd170b5 --- /dev/null +++ b/src/qutip_qip/algorithms/phase_flip.py @@ -0,0 +1,158 @@ +from qutip_qip.circuit import QubitCircuit + +__all__ = ["PhaseFlipCode"] + + +class PhaseFlipCode: + """ + Implementation of the 3-qubit phase-flip quantum error correction code. + + This code protects against a single Z (phase) error by encoding the logical + qubit into a 3-qubit entangled state using Hadamard transformations and + CNOT gates, then measuring parity between pairs of qubits using ancilla qubits. + Conditional Z corrections are applied based on classical measurement outcomes. + """ + + def __init__(self): + """ + Initializes the PhaseFlipCode with 3 data qubits and 2 syndrome (ancilla) qubits. + """ + self._n_data = 3 + self._n_syndrome = 2 + + @property + def n_data(self): + """ + Returns: + int: Number of data qubits (3). + """ + return self._n_data + + @property + def n_syndrome(self): + """ + Returns: + int: Number of syndrome qubits (2). + """ + return self._n_syndrome + + 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. + + Args: + data_qubits (list[int]): Indices of 3 data qubits. + + Returns: + QubitCircuit: The encoding quantum circuit. + + Raises: + ValueError: If the number of data qubits is not 3. + """ + if len(data_qubits) != 3: + 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) + + 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. + Measurements are stored in classical bits, and Z corrections are applied + conditionally based on the measured syndrome. + + Args: + data_qubits (list[int]): Indices of 3 data qubits. + syndrome_qubits (list[int]): Indices of 2 syndrome qubits. + + Returns: + QubitCircuit: Circuit for syndrome measurement and Z correction. + + Raises: + ValueError: If the number of qubits is incorrect. + """ + if len(data_qubits) != 3 or len(syndrome_qubits) != 2: + raise ValueError("Expected 3 data qubits and 2 syndrome qubits.") + + total_qubits = max(data_qubits + syndrome_qubits) + 1 + qc = QubitCircuit(N=total_qubits, num_cbits=2) + + dq = data_qubits + sq = syndrome_qubits + + # 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]) + + # Measure syndrome qubits + qc.add_measurement(sq[0], sq[0], classical_store=0) + qc.add_measurement(sq[1], sq[1], classical_store=1) + + # Classically controlled Z corrections + qc.add_gate( + "Z", + targets=dq[0], + classical_controls=[0, 1], + classical_control_value=2, + ) + qc.add_gate( + "Z", + targets=dq[1], + classical_controls=[0, 1], + classical_control_value=3, + ) + qc.add_gate( + "Z", + targets=dq[2], + classical_controls=[0, 1], + classical_control_value=1, + ) + + return qc + + 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. + + Args: + data_qubits (list[int]): Indices of 3 data qubits. + + Returns: + QubitCircuit: The decoding circuit. + + Raises: + ValueError: If the number of data qubits is not 3. + """ + if len(data_qubits) != 3: + 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]) + + return qc diff --git a/src/qutip_qip/algorithms/shor_code.py b/src/qutip_qip/algorithms/shor_code.py new file mode 100644 index 000000000..e239f342d --- /dev/null +++ b/src/qutip_qip/algorithms/shor_code.py @@ -0,0 +1,40 @@ +from qutip_qip.circuit import QubitCircuit +from qutip_qip.algorithms import BitFlipCode, PhaseFlipCode + +__all__ = ["ShorCode"] + + +class ShorCode: + """ + Constructs the 9-qubit Shor code encoding circuit using BitFlipCode and PhaseFlipCode. + + The Shor code protects against arbitrary single-qubit errors by combining + bit-flip and phase-flip redundancy encoding. + """ + + def __init__(self): + self.n_qubits = 9 # Total qubits in the Shor code + + def encode_circuit(self): + """ + Construct the 9-qubit Shor code encoding circuit. + + Returns: + QubitCircuit: Circuit that encodes one logical qubit into the Shor code. + """ + qc = QubitCircuit(N=self.n_qubits) + + # Step 1: Bit-flip encode qubit 0 → [0, 1, 2] + bit_code = BitFlipCode() + bit_encode = bit_code.encode_circuit([0, 1, 2]) + qc.gates.extend(bit_encode.gates) + + # Step 2: Phase-flip encode each of [0,1,2] across 3 qubits each: + phase_blocks = [[0, 3, 6], [1, 4, 7], [2, 5, 8]] + + for block in phase_blocks: + phase_code = PhaseFlipCode() + phase_encode = phase_code.encode_circuit(block) + qc.gates.extend(phase_encode.gates) + + return qc diff --git a/tests/test_bit_flip.py b/tests/test_bit_flip.py new file mode 100644 index 000000000..58d0fb881 --- /dev/null +++ b/tests/test_bit_flip.py @@ -0,0 +1,63 @@ +import pytest +import qutip +from qutip_qip.algorithms import BitFlipCode +from qutip_qip.circuit import QubitCircuit + + +@pytest.fixture +def code(): + return BitFlipCode() + + +@pytest.fixture +def data_qubits(): + return [0, 1, 2] + + +@pytest.fixture +def syndrome_qubits(): + return [3, 4] + + +def test_encode_circuit_structure(code, data_qubits): + qc = code.encode_circuit(data_qubits) + assert len(qc.gates) == 2 + assert qc.gates[0].name == "CNOT" + 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_bitflip_correction(code, data_qubits, syndrome_qubits): + # Initial random state |ψ⟩ on qubit 0 + psi = qutip.rand_ket(2) + + # Full state: |ψ⟩ ⊗ |0000⟩ (qubits 1,2,3,4) + state = qutip.tensor([psi] + [qutip.basis(2, 0)] * 4) + + # Step 1: Encode |ψ⟩ over qubits 0,1,2 + qc_encode = code.encode_circuit(data_qubits) + state = qc_encode.run(state) + + # Step 2: Apply bit-flip error to qubit 0 + qc_error = QubitCircuit(N=5) + qc_error.add_gate("X", targets=[0]) + state = qc_error.run(state) + + # Step 3: Syndrome + correction + qc_correct = code.syndrome_and_correction_circuit( + data_qubits, syndrome_qubits + ) + state = qc_correct.run(state) + + # Step 4: Decode + qc_decode = code.decode_circuit(data_qubits) + state = qc_decode.run(state) + + # Step 5: Trace out ancillas/qubits 1-4, keep logical qubit 0 + final_qubit = state.ptrace(0) + + # Fidelity between original |ψ⟩ and final decoded state + fidelity = qutip.fidelity(final_qubit, psi) + assert fidelity > 0.99, f"Fidelity too low: {fidelity:.4f}" diff --git a/tests/test_phase_flip.py b/tests/test_phase_flip.py new file mode 100644 index 000000000..048df27ab --- /dev/null +++ b/tests/test_phase_flip.py @@ -0,0 +1,78 @@ +import pytest +import qutip +from qutip_qip.circuit import QubitCircuit +from qutip_qip.algorithms import PhaseFlipCode + + +@pytest.fixture +def code(): + return PhaseFlipCode() + + +@pytest.fixture +def data_qubits(): + return [0, 1, 2] + + +@pytest.fixture +def syndrome_qubits(): + return [3, 4] + + +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("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] + + +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] + + +def test_phaseflip_correction_simulation(code, data_qubits, syndrome_qubits): + """ + Simulate the full encoding, Z-error, correction, and decoding process + for a random qubit state |ψ⟩. Fidelity after correction should be ~1. + """ + # Random initial qubit state + psi = qutip.rand_ket(2) + + # Full system: qubit + 2 redundant qubits + 2 ancillas + state = qutip.tensor([psi] + [qutip.basis(2, 0)] * 4) + + # Encode in X-basis + qc_encode = code.encode_circuit(data_qubits) + state = qc_encode.run(state) + + # Apply Z (phase-flip) error to qubit 1 + qc_error = QubitCircuit(N=5) + qc_error.add_gate("Z", targets=[1]) + state = qc_error.run(state) + + # Syndrome measurement and Z correction + qc_correct = code.syndrome_and_correction_circuit( + data_qubits, syndrome_qubits + ) + state = qc_correct.run(state) + + # Decode to return to original basis + qc_decode = code.decode_circuit(data_qubits) + state = qc_decode.run(state) + + # Extract logical qubit (0th qubit) + final = state.ptrace(0) + fidelity = qutip.fidelity(psi, final) + + assert fidelity > 0.99, f"Fidelity too low: {fidelity:.4f}" diff --git a/tests/test_shor_code.py b/tests/test_shor_code.py new file mode 100644 index 000000000..bc130e869 --- /dev/null +++ b/tests/test_shor_code.py @@ -0,0 +1,65 @@ +import pytest +import qutip +import numpy as np +from qutip_qip.circuit import QubitCircuit +from qutip_qip.algorithms import ShorCode + + +@pytest.fixture +def code(): + return ShorCode() + + +def test_shor_circuit_structure(code): + qc = code.encode_circuit() + assert qc.N == 9 + assert len(qc.gates) > 0 + + +def test_shor_encodes_zero(code): + qc = code.encode_circuit() + zero_state = qutip.tensor([qutip.basis(2, 0)] * 9) + encoded = qc.run(zero_state) + assert abs(encoded.norm() - 1.0) < 1e-10 + + +def test_shor_encodes_one(code): + qc = code.encode_circuit() + one_input = qutip.tensor([qutip.basis(2, 1)] + [qutip.basis(2, 0)] * 8) + encoded = qc.run(one_input) + assert abs(encoded.norm() - 1.0) < 1e-10 + + +def test_shor_linearity(code): + qc = code.encode_circuit() + + # Encode |0⟩ and |1⟩ + zero_input = qutip.tensor([qutip.basis(2, 0)] + [qutip.basis(2, 0)] * 8) + one_input = qutip.tensor([qutip.basis(2, 1)] + [qutip.basis(2, 0)] * 8) + encoded_zero = qc.run(zero_input) + encoded_one = qc.run(one_input) + + # Encode superposition + alpha, beta = 0.6, 0.8 + superpos = alpha * qutip.basis(2, 0) + beta * qutip.basis(2, 1) + superpos_input = qutip.tensor([superpos] + [qutip.basis(2, 0)] * 8) + encoded_superpos = qc.run(superpos_input) + + # Check linearity + expected = alpha * encoded_zero + beta * encoded_one + fidelity = qutip.fidelity(encoded_superpos, expected) + assert fidelity > 0.99 + + +def test_shor_orthogonality(code): + qc = code.encode_circuit() + + zero_input = qutip.tensor([qutip.basis(2, 0)] + [qutip.basis(2, 0)] * 8) + one_input = qutip.tensor([qutip.basis(2, 1)] + [qutip.basis(2, 0)] * 8) + + encoded_zero = qc.run(zero_input) + encoded_one = qc.run(one_input) + + # Should be orthogonal + overlap = abs(encoded_zero.overlap(encoded_one)) + assert overlap < 0.1