diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c2583bc..d156067d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,9 @@ arbitrarily applied mapper pass. - The following 2-qubit gates: `CV`, `CY`, `DCNOT`, `ECR`, `ISWAP`, `InvSqrtSWAP`, `M`, `MS`, `SqrtISWAP`, and `SqrtSWAP` - Add `add_instruction` method to the `CircuitBuilder` - libQASM parser accepts measure instruction aliases: `measureX`, `measureY`, and `measureZ` +- The following 3-qubit gates: `CCX` (alias `CCNOT`) and `CSWAP` +- `ThreeQubitGateDecomposer` to decompose the 3-qubit gates into CZ gates and single-qubit gates +- The `CqasmV1Exporter` exports the Toffoli gate ## [ 0.9.0 ] - [ 2025-12-19 ] diff --git a/opensquirrel/__init__.py b/opensquirrel/__init__.py index 332d2456..1dd81f8a 100644 --- a/opensquirrel/__init__.py +++ b/opensquirrel/__init__.py @@ -8,8 +8,10 @@ Wait, ) from opensquirrel.ir.default_gates import ( + CCX, CNOT, CR, + CSWAP, CV, CY, CZ, @@ -47,8 +49,10 @@ from opensquirrel.register_manager import BitRegister, QubitRegister __all__ = [ + "CCX", "CNOT", "CR", + "CSWAP", "CV", "CY", "CZ", diff --git a/opensquirrel/default_instructions.py b/opensquirrel/default_instructions.py index 0af9687a..abfbba01 100644 --- a/opensquirrel/default_instructions.py +++ b/opensquirrel/default_instructions.py @@ -10,8 +10,10 @@ Wait, ) from opensquirrel.ir.default_gates import ( + CCX, CNOT, CR, + CSWAP, CV, CY, CZ, @@ -50,6 +52,7 @@ if TYPE_CHECKING: from opensquirrel.ir import ControlInstruction, Gate, Instruction, NonUnitary from opensquirrel.ir.single_qubit_gate import SingleQubitGate + from opensquirrel.ir.three_qubit_gate import ThreeQubitGate from opensquirrel.ir.two_qubit_gate import TwoQubitGate default_bsr_without_params_set: dict[str, type[SingleQubitGate]] = { @@ -101,13 +104,22 @@ "SWAP": SWAP, } +default_three_qubit_gate_set: dict[str, type[ThreeQubitGate]] = { + "CCX": CCX, + "CSWAP": CSWAP, +} + default_gate_alias_set = { + "CCNOT": CCX, + "Fredkin": CSWAP, "Hadamard": H, "Identity": I, + "Toffoli": CCX, } default_gate_set: dict[str, type[Gate]] = { **default_single_qubit_gate_set, **default_two_qubit_gate_set, + **default_three_qubit_gate_set, **default_gate_alias_set, } diff --git a/opensquirrel/ir/default_gates/__init__.py b/opensquirrel/ir/default_gates/__init__.py index c0fffb42..3d8032e6 100644 --- a/opensquirrel/ir/default_gates/__init__.py +++ b/opensquirrel/ir/default_gates/__init__.py @@ -20,6 +20,7 @@ Y, Z, ) +from opensquirrel.ir.default_gates.three_qubit_gates import CCX, CSWAP from opensquirrel.ir.default_gates.two_qubit_gates import ( CNOT, CR, @@ -39,8 +40,10 @@ ) __all__ = [ + "CCX", "CNOT", "CR", + "CSWAP", "CV", "CY", "CZ", diff --git a/opensquirrel/ir/default_gates/three_qubit_gates.py b/opensquirrel/ir/default_gates/three_qubit_gates.py new file mode 100644 index 00000000..c8bbb35d --- /dev/null +++ b/opensquirrel/ir/default_gates/three_qubit_gates.py @@ -0,0 +1,59 @@ +import numpy as np + +from opensquirrel.ir.expression import Qubit, QubitLike +from opensquirrel.ir.semantics import MatrixGateSemantic +from opensquirrel.ir.three_qubit_gate import ThreeQubitGate + + +class CCX(ThreeQubitGate): + def __init__(self, control_qubit_0: QubitLike, control_qubit_1: QubitLike, target_qubit: QubitLike) -> None: + super().__init__( + qubit0=control_qubit_0, + qubit1=control_qubit_1, + qubit2=target_qubit, + gate_semantic=MatrixGateSemantic( + matrix=np.array( + [ + [1, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 1], + [0, 0, 0, 0, 0, 0, 1, 0], + ], + ), + ), + name="CCX", + ) + self.control_qubit_0 = Qubit(control_qubit_0) + self.control_qubit_1 = Qubit(control_qubit_1) + self.target_qubit = Qubit(target_qubit) + + +class CSWAP(ThreeQubitGate): + def __init__(self, control_qubit: QubitLike, qubit_0: QubitLike, qubit_1: QubitLike) -> None: + super().__init__( + qubit0=control_qubit, + qubit1=qubit_0, + qubit2=qubit_1, + gate_semantic=MatrixGateSemantic( + matrix=np.array( + [ + [1, 0, 0, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 1], + ], + ), + ), + name="CSWAP", + ) + self.control_qubit = Qubit(control_qubit) + self.qubit_0 = Qubit(qubit_0) + self.qubit_1 = Qubit(qubit_1) diff --git a/opensquirrel/ir/ir.py b/opensquirrel/ir/ir.py index 1140846e..7ebae5f2 100644 --- a/opensquirrel/ir/ir.py +++ b/opensquirrel/ir/ir.py @@ -35,6 +35,7 @@ from opensquirrel.ir.semantics.canonical_gate import CanonicalAxis from opensquirrel.ir.single_qubit_gate import SingleQubitGate from opensquirrel.ir.statement import Instruction, Statement + from opensquirrel.ir.three_qubit_gate import ThreeQubitGate from opensquirrel.ir.two_qubit_gate import TwoQubitGate @@ -67,6 +68,8 @@ def visit_single_qubit_gate(self, gate: SingleQubitGate) -> Any: ... def visit_two_qubit_gate(self, gate: TwoQubitGate) -> Any: ... + def visit_three_qubit_gate(self, gate: ThreeQubitGate) -> Any: ... + def visit_bloch_sphere_rotation(self, bloch_sphere_rotation: BlochSphereRotation) -> Any: ... def visit_bsr_no_params(self, gate: BsrNoParams) -> Any: ... diff --git a/opensquirrel/ir/three_qubit_gate.py b/opensquirrel/ir/three_qubit_gate.py new file mode 100644 index 00000000..38a2f40d --- /dev/null +++ b/opensquirrel/ir/three_qubit_gate.py @@ -0,0 +1,60 @@ +from functools import cached_property +from typing import Any + +from opensquirrel.ir import Gate, IRVisitor, Qubit, QubitLike +from opensquirrel.ir.semantics import MatrixGateSemantic +from opensquirrel.ir.semantics.gate_semantic import GateSemantic + + +class ThreeQubitGate(Gate): + def __init__( + self, + qubit0: QubitLike, + qubit1: QubitLike, + qubit2: QubitLike, + gate_semantic: GateSemantic, + name: str = "ThreeQubitGate", + ) -> None: + Gate.__init__(self, name) + self.qubit0 = Qubit(qubit0) + self.qubit1 = Qubit(qubit1) + self.qubit2 = Qubit(qubit2) + + # A three-qubit gate can only be described by a matrix. ControlledGateSemantic describes a + # single control qubit acting on a Bloch sphere rotation, and CanonicalGateSemantic + # describes the canonical decomposition of a two-qubit gate. + self._matrix = gate_semantic if isinstance(gate_semantic, MatrixGateSemantic) else None + self.gate_semantic = gate_semantic + + if self._check_repeated_qubit_operands(self.qubit_operands): + msg = "qubit operands cannot be the same qubit" + raise ValueError(msg) + + @cached_property + def matrix(self) -> MatrixGateSemantic: + if self._matrix: + return self._matrix + + msg = f"invalid gate semantic: {self.gate_semantic}" + raise ValueError(msg) + + @property + def qubit_operands(self) -> tuple[Qubit, ...]: + return (self.qubit0, self.qubit1, self.qubit2) + + def accept(self, visitor: IRVisitor) -> Any: + """Accepts visitor and processes this IR node.""" + visit_parent = super().accept(visitor) + return visit_parent if visit_parent is not None else visitor.visit_three_qubit_gate(self) + + def is_identity(self) -> bool: + """Checks if the three-qubit gate is an identity gate. + + Returns: + True if the three-qubit gate is an identity gate, False otherwise. + + """ + return self.matrix.is_identity() + + def __repr__(self) -> str: + return f"ThreeQubitGate(qubits=[{self.qubit0, self.qubit1, self.qubit2}], gate_semantic={self.gate_semantic})" diff --git a/opensquirrel/passes/decomposer/__init__.py b/opensquirrel/passes/decomposer/__init__.py index 776a5a9f..c1fd1031 100644 --- a/opensquirrel/passes/decomposer/__init__.py +++ b/opensquirrel/passes/decomposer/__init__.py @@ -13,6 +13,7 @@ from opensquirrel.passes.decomposer.mckay_decomposer import McKayDecomposer from opensquirrel.passes.decomposer.swap2cnot_decomposer import SWAP2CNOTDecomposer from opensquirrel.passes.decomposer.swap2cz_decomposer import SWAP2CZDecomposer +from opensquirrel.passes.decomposer.three_qubit_gate_decomposer import ThreeQubitGateDecomposer __all__ = [ "CNOT2CZDecomposer", @@ -22,6 +23,7 @@ "McKayDecomposer", "SWAP2CNOTDecomposer", "SWAP2CZDecomposer", + "ThreeQubitGateDecomposer", "XYXDecomposer", "XZXDecomposer", "YXYDecomposer", diff --git a/opensquirrel/passes/decomposer/three_qubit_gate_decomposer.py b/opensquirrel/passes/decomposer/three_qubit_gate_decomposer.py new file mode 100644 index 00000000..541c9722 --- /dev/null +++ b/opensquirrel/passes/decomposer/three_qubit_gate_decomposer.py @@ -0,0 +1,81 @@ +from __future__ import annotations + +from math import pi +from typing import TYPE_CHECKING + +from opensquirrel import CZ, Ry, T, TDagger +from opensquirrel.ir.three_qubit_gate import ThreeQubitGate +from opensquirrel.passes.decomposer.general_decomposer import Decomposer + +if TYPE_CHECKING: + from opensquirrel.ir import Gate, Qubit + + +class ThreeQubitGateDecomposer(Decomposer): + def decompose(self, instruction: Gate) -> list[Gate]: + """Predefined decomposition of the three-qubit gates into CZ gates and single-qubit gates. + + The Toffoli gate (CCX) is decomposed into 6 CZ gates and single-qubit rotations, which is + the minimum possible according to + [Shende and Markov (2008)](https://arxiv.org/abs/0803.2316). The Fredkin gate (CSWAP) is + decomposed as a Toffoli gate conjugated by two CNOT gates, giving 8 CZ gates. + + Note: + This decomposition preserves the global phase of the three-qubit gate. + Gates other than CCX and CSWAP are returned unchanged. + + Args: + instruction: three-qubit gate to decompose. + + Returns: + A sequence of CZ gates and single-qubit gates that decompose the three-qubit gate. + + """ + if not isinstance(instruction, ThreeQubitGate) or instruction.name not in ("CCX", "CSWAP"): + return [instruction] + + gate = instruction + + if gate.name == "CCX": + control_qubit_0, control_qubit_1, target_qubit = gate.qubit_operands + return self._get_toffoli_gates(control_qubit_0, control_qubit_1, target_qubit) + + control_qubit, qubit_0, qubit_1 = gate.qubit_operands + return [ + *self._get_cnot_gates(qubit_1, qubit_0), + *self._get_toffoli_gates(control_qubit, qubit_0, qubit_1), + *self._get_cnot_gates(qubit_1, qubit_0), + ] + + def _get_cnot_gates(self, control_qubit: Qubit, target_qubit: Qubit) -> list[Gate]: + """CNOT gate expressed as a CZ gate conjugated by Ry rotations, as in the CNOT2CZDecomposer.""" + return [ + Ry(target_qubit, -pi / 2), + CZ(control_qubit, target_qubit), + Ry(target_qubit, pi / 2), + ] + + def _get_toffoli_gates(self, control_qubit_0: Qubit, control_qubit_1: Qubit, target_qubit: Qubit) -> list[Gate]: + """Toffoli gate as 6 CNOT gates and T rotations, with every CNOT gate rewritten in terms of CZ. + + The Hadamard gates that conjugate the target qubit in the textbook circuit are replaced by + the same Ry rotations used above, which likewise map Z onto X under conjugation. + """ + a, b, c = control_qubit_0, control_qubit_1, target_qubit + return [ + Ry(c, -pi / 2), + *self._get_cnot_gates(b, c), + TDagger(c), + *self._get_cnot_gates(a, c), + T(c), + *self._get_cnot_gates(b, c), + TDagger(c), + *self._get_cnot_gates(a, c), + T(b), + T(c), + Ry(c, pi / 2), + *self._get_cnot_gates(a, b), + T(a), + TDagger(b), + *self._get_cnot_gates(a, b), + ] diff --git a/opensquirrel/passes/exporter/cqasmv1_exporter.py b/opensquirrel/passes/exporter/cqasmv1_exporter.py index 22011322..0295ebf5 100644 --- a/opensquirrel/passes/exporter/cqasmv1_exporter.py +++ b/opensquirrel/passes/exporter/cqasmv1_exporter.py @@ -31,6 +31,7 @@ from opensquirrel.circuit import Circuit from opensquirrel.ir.expression import Axis from opensquirrel.ir.single_qubit_gate import SingleQubitGate + from opensquirrel.ir.three_qubit_gate import ThreeQubitGate from opensquirrel.ir.two_qubit_gate import TwoQubitGate from opensquirrel.register_manager import RegisterManager @@ -57,6 +58,11 @@ def export(self, circuit: Circuit) -> str: return _post_process(cqasmv1_creator.output).rstrip() + "\n" +# cQASM v1 names for the three-qubit gates it supports. The Fredkin gate (CSWAP) is not part of the +# cQASM v1 default instruction set, and is therefore absent. +CQASM_V1_THREE_QUBIT_GATE_NAMES = {"CCX": "toffoli"} + + class CqasmV1ExporterParseError(Exception): pass @@ -128,6 +134,13 @@ def visit_two_qubit_gate(self, gate: TwoQubitGate) -> Any: else: self.output += f"{gate.name.lower()} {qubit_operand_0}, {qubit_operand_1}\n" + def visit_three_qubit_gate(self, gate: ThreeQubitGate) -> Any: + if gate.name not in CQASM_V1_THREE_QUBIT_GATE_NAMES: + raise UnsupportedGateError(gate) + + qubit_operands = ", ".join(qubit.accept(self) for qubit in gate.qubit_operands) + self.output += f"{CQASM_V1_THREE_QUBIT_GATE_NAMES[gate.name]} {qubit_operands}\n" + def visit_measure(self, measure: Measure) -> None: qubit_argument = measure.qubit_operands[0].accept(self) axis = measure.axis diff --git a/opensquirrel/reindexer/qubit_reindexer.py b/opensquirrel/reindexer/qubit_reindexer.py index 765fa660..b97b655d 100644 --- a/opensquirrel/reindexer/qubit_reindexer.py +++ b/opensquirrel/reindexer/qubit_reindexer.py @@ -15,6 +15,7 @@ Wait, ) from opensquirrel.ir.single_qubit_gate import SingleQubitGate +from opensquirrel.ir.three_qubit_gate import ThreeQubitGate from opensquirrel.ir.two_qubit_gate import TwoQubitGate from opensquirrel.register_manager import ( DEFAULT_BIT_REGISTER_NAME, @@ -68,7 +69,15 @@ def visit_single_qubit_gate(self, gate: SingleQubitGate) -> SingleQubitGate: def visit_two_qubit_gate(self, gate: TwoQubitGate) -> TwoQubitGate: qubit0 = self.qubit_indices.index(gate.qubit0.index) qubit1 = self.qubit_indices.index(gate.qubit1.index) - return TwoQubitGate(qubit0=qubit0, qubit1=qubit1, gate_semantic=gate.gate_semantic) + return TwoQubitGate(qubit0=qubit0, qubit1=qubit1, gate_semantic=gate.gate_semantic, name=gate.name) + + def visit_three_qubit_gate(self, gate: ThreeQubitGate) -> ThreeQubitGate: + qubit0 = self.qubit_indices.index(gate.qubit0.index) + qubit1 = self.qubit_indices.index(gate.qubit1.index) + qubit2 = self.qubit_indices.index(gate.qubit2.index) + return ThreeQubitGate( + qubit0=qubit0, qubit1=qubit1, qubit2=qubit2, gate_semantic=gate.gate_semantic, name=gate.name + ) def get_reindexed_circuit( diff --git a/opensquirrel/utils/matrix_expander.py b/opensquirrel/utils/matrix_expander.py index 9fdabff2..2004f028 100644 --- a/opensquirrel/utils/matrix_expander.py +++ b/opensquirrel/utils/matrix_expander.py @@ -23,6 +23,7 @@ if TYPE_CHECKING: from opensquirrel.ir import Gate from opensquirrel.ir.single_qubit_gate import SingleQubitGate + from opensquirrel.ir.three_qubit_gate import ThreeQubitGate from opensquirrel.ir.two_qubit_gate import TwoQubitGate @@ -137,6 +138,9 @@ def visit_two_qubit_gate(self, gate: TwoQubitGate) -> NDArray[np.complex128]: return self._matrix_gate(gate) return self._controlled_gate(gate) + def visit_three_qubit_gate(self, gate: ThreeQubitGate) -> NDArray[np.complex128]: + return self._matrix_gate(gate) + def _controlled_gate(self, gate: TwoQubitGate) -> NDArray[np.complex128]: if not gate.controlled: msg = f"gate {gate!r} does not have a controlled gate semantic" @@ -172,7 +176,7 @@ def _controlled_gate(self, gate: TwoQubitGate) -> NDArray[np.complex128]: col[col_index] = 1 return np.asarray(expanded_matrix, dtype=np.complex128) - def _matrix_gate(self, gate: TwoQubitGate) -> NDArray[np.complex128]: + def _matrix_gate(self, gate: TwoQubitGate | ThreeQubitGate) -> NDArray[np.complex128]: # The convention is to write gate matrices with operands reversed. # For instance, the first operand of CNOT is the control qubit, and this is written as # 1, 0, 0, 0 diff --git a/opensquirrel/writer/writer.py b/opensquirrel/writer/writer.py index 8b675630..23e69938 100644 --- a/opensquirrel/writer/writer.py +++ b/opensquirrel/writer/writer.py @@ -4,7 +4,7 @@ from opensquirrel.circuit import Circuit from opensquirrel.common import ATOL -from opensquirrel.default_instructions import default_two_qubit_gate_set +from opensquirrel.default_instructions import default_three_qubit_gate_set, default_two_qubit_gate_set from opensquirrel.ir import ( AsmDeclaration, Barrier, @@ -28,6 +28,7 @@ BsrUnitaryParams, ) from opensquirrel.ir.single_qubit_gate import SingleQubitGate, try_match_replace_with_default_gate +from opensquirrel.ir.three_qubit_gate import ThreeQubitGate from opensquirrel.ir.two_qubit_gate import TwoQubitGate from opensquirrel.register_manager import RegisterManager @@ -98,6 +99,14 @@ def visit_two_qubit_gate(self, gate: TwoQubitGate) -> Any: else: self.output += f"{gate.name} {qubit_operand_0}, {qubit_operand_1}\n" + def visit_three_qubit_gate(self, gate: ThreeQubitGate) -> Any: + qubit_operands = ", ".join(qubit.accept(self) for qubit in gate.qubit_operands) + + if gate.name not in default_three_qubit_gate_set: + self.output += f"{gate}\n" + else: + self.output += f"{gate.name} {qubit_operands}\n" + def visit_bsr_no_params(self, gate: BsrNoParams) -> str: return "" diff --git a/tests/ir/test_three_qubit_gate.py b/tests/ir/test_three_qubit_gate.py new file mode 100644 index 00000000..3d917865 --- /dev/null +++ b/tests/ir/test_three_qubit_gate.py @@ -0,0 +1,91 @@ +import numpy as np +import pytest + +from opensquirrel import CCX, CSWAP +from opensquirrel.ir import Qubit +from opensquirrel.ir.semantics import CanonicalGateSemantic, MatrixGateSemantic +from opensquirrel.ir.three_qubit_gate import ThreeQubitGate +from opensquirrel.utils.matrix_expander import get_matrix + + +class TestThreeQubitGate: + @pytest.fixture + def gate(self) -> ThreeQubitGate: + # The specific matrix is irrelevant here, as long as it is not the identity. + ccz_matrix = np.diag([1, 1, 1, 1, 1, 1, 1, -1]) + return ThreeQubitGate(42, 100, 7, gate_semantic=MatrixGateSemantic(ccz_matrix)) + + def test_qubit_operands(self, gate: ThreeQubitGate) -> None: + assert gate.qubit_operands == (Qubit(42), Qubit(100), Qubit(7)) + + def test_same_qubits(self) -> None: + with pytest.raises(ValueError, match="qubit operands cannot be the same qubit"): + ThreeQubitGate(0, 1, 0, gate_semantic=MatrixGateSemantic(np.eye(8, dtype=np.complex128))) + + def test_matrix_gate_semantic(self, gate: ThreeQubitGate) -> None: + assert isinstance(gate.matrix, MatrixGateSemantic) + + def test_invalid_gate_semantic(self) -> None: + gate = ThreeQubitGate(0, 1, 2, gate_semantic=CanonicalGateSemantic((0, 0, 0))) + with pytest.raises(ValueError, match="invalid gate semantic"): + _ = gate.matrix + + def test_is_identity(self) -> None: + gate = ThreeQubitGate(0, 1, 2, gate_semantic=MatrixGateSemantic(np.eye(8, dtype=np.complex128))) + assert gate.is_identity() + + def test_is_not_identity(self, gate: ThreeQubitGate) -> None: + assert not gate.is_identity() + + +class TestDefaultThreeQubitGates: + @pytest.mark.parametrize( + ("gate", "expected_name"), + [(CCX(0, 1, 2), "CCX"), (CSWAP(0, 1, 2), "CSWAP")], + ids=["CCX", "CSWAP"], + ) + def test_name(self, gate: ThreeQubitGate, expected_name: str) -> None: + assert gate.name == expected_name + + def test_ccx_qubit_operands(self) -> None: + gate = CCX(0, 1, 2) + assert (gate.control_qubit_0, gate.control_qubit_1, gate.target_qubit) == (Qubit(0), Qubit(1), Qubit(2)) + + def test_cswap_qubit_operands(self) -> None: + gate = CSWAP(0, 1, 2) + assert (gate.control_qubit, gate.qubit_0, gate.qubit_1) == (Qubit(0), Qubit(1), Qubit(2)) + + def test_ccx_flips_target_when_both_controls_are_set(self) -> None: + matrix = get_matrix(CCX(0, 1, 2), 3) + # Basis states are indexed such that qubit i corresponds to bit i. + for state in range(8): + expected = state ^ 0b100 if state & 0b001 and state & 0b010 else state + assert np.argmax(matrix[:, state]) == expected + + def test_cswap_swaps_targets_when_control_is_set(self) -> None: + matrix = get_matrix(CSWAP(0, 1, 2), 3) + for state in range(8): + expected = state + if state & 0b001: + qubit_1, qubit_2 = (state >> 1) & 1, (state >> 2) & 1 + expected = (state & 0b001) | (qubit_2 << 1) | (qubit_1 << 2) + assert np.argmax(matrix[:, state]) == expected + + @pytest.mark.parametrize( + "gate", + [CCX(0, 1, 2), CSWAP(0, 1, 2)], + ids=["CCX", "CSWAP"], + ) + def test_gate_is_unitary_and_self_inverse(self, gate: ThreeQubitGate) -> None: + matrix = get_matrix(gate, 3) + np.testing.assert_almost_equal(matrix @ matrix.conj().T, np.eye(8)) + np.testing.assert_almost_equal(matrix @ matrix, np.eye(8)) + + def test_ccx_controls_are_interchangeable(self) -> None: + assert CCX(0, 1, 2) == CCX(1, 0, 2) + + def test_ccx_target_is_not_interchangeable_with_a_control(self) -> None: + assert CCX(0, 1, 2) != CCX(0, 2, 1) + + def test_cswap_targets_are_interchangeable(self) -> None: + assert CSWAP(0, 1, 2) == CSWAP(0, 2, 1) diff --git a/tests/passes/decomposer/test_three_qubit_gate_decomposer.py b/tests/passes/decomposer/test_three_qubit_gate_decomposer.py new file mode 100644 index 00000000..83e7519f --- /dev/null +++ b/tests/passes/decomposer/test_three_qubit_gate_decomposer.py @@ -0,0 +1,102 @@ +from __future__ import annotations + +from math import pi +from typing import TYPE_CHECKING + +import numpy as np +import pytest + +from opensquirrel import CCX, CNOT, CSWAP, CZ, SWAP, H, Ry, TDagger +from opensquirrel.circuit_matrix_calculator import get_circuit_matrix +from opensquirrel.passes.decomposer import ThreeQubitGateDecomposer +from opensquirrel.passes.decomposer.general_decomposer import check_gate_decomposition +from opensquirrel.reindexer import get_reindexed_circuit + +if TYPE_CHECKING: + from opensquirrel.ir import Gate + + +@pytest.fixture +def decomposer() -> ThreeQubitGateDecomposer: + return ThreeQubitGateDecomposer() + + +@pytest.mark.parametrize( + ("gate", "expected_result"), + [ + (H(0), [H(0)]), + (Ry(0, 2.345), [Ry(0, 2.345)]), + ], + ids=["Hadamard", "rotation_gate"], +) +def test_ignores_1q_gates(decomposer: ThreeQubitGateDecomposer, gate: Gate, expected_result: list[Gate]) -> None: + check_gate_decomposition(gate, expected_result) + assert decomposer.decompose(gate) == expected_result + + +@pytest.mark.parametrize( + ("gate", "expected_result"), + [ + (CNOT(0, 1), [CNOT(0, 1)]), + (CZ(0, 1), [CZ(0, 1)]), + (SWAP(0, 1), [SWAP(0, 1)]), + ], + ids=["CNOT_gate", "CZ_gate", "SWAP_gate"], +) +def test_ignores_2q_gates(decomposer: ThreeQubitGateDecomposer, gate: Gate, expected_result: list[Gate]) -> None: + check_gate_decomposition(gate, expected_result) + assert decomposer.decompose(gate) == expected_result + + +@pytest.mark.parametrize( + "gate", + [CCX(0, 1, 2), CCX(2, 0, 1), CCX(1, 2, 0), CSWAP(0, 1, 2), CSWAP(2, 0, 1), CSWAP(1, 2, 0)], + ids=["CCX_0_1_2", "CCX_2_0_1", "CCX_1_2_0", "CSWAP_0_1_2", "CSWAP_2_0_1", "CSWAP_1_2_0"], +) +def test_decomposition_is_valid(decomposer: ThreeQubitGateDecomposer, gate: Gate) -> None: + decomposed_gate = decomposer.decompose(gate) + check_gate_decomposition(gate, decomposed_gate) + + +@pytest.mark.parametrize( + "gate", + [CCX(0, 1, 2), CSWAP(0, 1, 2)], + ids=["CCX", "CSWAP"], +) +def test_decomposition_preserves_global_phase(decomposer: ThreeQubitGateDecomposer, gate: Gate) -> None: + qubit_indices = gate.qubit_indices + original_matrix = get_circuit_matrix(get_reindexed_circuit([gate], qubit_indices)) + decomposed_matrix = get_circuit_matrix(get_reindexed_circuit(decomposer.decompose(gate), qubit_indices)) + np.testing.assert_allclose(original_matrix, decomposed_matrix, atol=1e-12) + + +@pytest.mark.parametrize( + ("gate", "expected_cz_count"), + [(CCX(0, 1, 2), 6), (CSWAP(0, 1, 2), 8)], + ids=["CCX", "CSWAP"], +) +def test_decomposition_uses_expected_number_of_cz_gates( + decomposer: ThreeQubitGateDecomposer, gate: Gate, expected_cz_count: int +) -> None: + decomposed_gate = decomposer.decompose(gate) + assert sum(1 for g in decomposed_gate if g.name == "CZ") == expected_cz_count + + +@pytest.mark.parametrize( + "gate", + [CCX(0, 1, 2), CSWAP(0, 1, 2)], + ids=["CCX", "CSWAP"], +) +def test_decomposition_yields_only_cz_and_single_qubit_gates(decomposer: ThreeQubitGateDecomposer, gate: Gate) -> None: + for decomposed_gate in decomposer.decompose(gate): + assert len(decomposed_gate.qubit_operands) == 1 or decomposed_gate.name == "CZ" + + +def test_decomposes_CCX(decomposer: ThreeQubitGateDecomposer) -> None: # noqa: N802 + assert decomposer.decompose(CCX(0, 1, 2))[:5] == [ + Ry(2, -pi / 2), + Ry(2, -pi / 2), + CZ(1, 2), + Ry(2, pi / 2), + TDagger(2), + ] diff --git a/tests/passes/exporter/test_cqasmv1_exporter.py b/tests/passes/exporter/test_cqasmv1_exporter.py index ad13a73f..349fbb59 100644 --- a/tests/passes/exporter/test_cqasmv1_exporter.py +++ b/tests/passes/exporter/test_cqasmv1_exporter.py @@ -357,3 +357,17 @@ def test_barrier_groups(exporter: CqasmV1Exporter, program: str, expected_output circuit = Circuit.from_string(program) output = circuit.export(exporter=exporter) assert output == expected_output + + +@pytest.mark.parametrize("gate_name", ["CCX", "CCNOT"], ids=["CCX", "CCNOT"]) +def test_toffoli(exporter: CqasmV1Exporter, gate_name: str) -> None: + builder = CircuitBuilder(3) + getattr(builder, gate_name)(0, 1, 2) + assert builder.to_circuit().export(exporter=exporter) == ("version 1.0\n\nqubits 3\n\ntoffoli q[0], q[1], q[2]\n") + + +def test_cswap_is_unsupported(exporter: CqasmV1Exporter) -> None: + builder = CircuitBuilder(3) + builder.CSWAP(0, 1, 2) + with pytest.raises(UnsupportedGateError, match="not supported"): + builder.to_circuit().export(exporter=exporter) diff --git a/tests/writer/test_writer.py b/tests/writer/test_writer.py index 4af23849..39775e3d 100644 --- a/tests/writer/test_writer.py +++ b/tests/writer/test_writer.py @@ -231,3 +231,18 @@ def test_anonymous_gate() -> None: CR(1.234) q[0], q[1] """ # noqa: E501 ) + + +def test_three_qubit_gates() -> None: + builder = CircuitBuilder(3) + builder.CCX(0, 1, 2).CSWAP(2, 0, 1) + assert ( + str(builder.to_circuit()) + == """version 3.0 + +qubit[3] q + +CCX q[0], q[1], q[2] +CSWAP q[2], q[0], q[1] +""" + )