diff --git a/CHANGELOG.md b/CHANGELOG.md index c14f980b..68a8b2e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ parameter arbitrarily applied mapper pass. - The measure instruction accepts an axis parameter - `MeasureDecomposer` to decompose arbitrary measurements to a decomposition of single-qubit gates and a +Z measurement. +- `Can2CZDecomposer` to decompose arbitrary two-qubit gates. ## [ 0.9.0 ] - [ 2025-12-19 ] diff --git a/opensquirrel/ir/semantics/canonical_gate.py b/opensquirrel/ir/semantics/canonical_gate.py index 0da54a85..0963ea10 100644 --- a/opensquirrel/ir/semantics/canonical_gate.py +++ b/opensquirrel/ir/semantics/canonical_gate.py @@ -133,6 +133,32 @@ def is_identity(self) -> bool: return self.axis == CanonicalAxis((0, 0, 0)) return self.axis == CanonicalAxis((0, 0, 0)) and all(rotation.is_identity() for rotation in self.rotations) + def __eq__(self, other: Any) -> bool: + """Checks if two CanonicalGateSemantic instances are equal. + + Returns: + True if both have the same axis and rotations, False otherwise. + + """ + if not isinstance(other, CanonicalGateSemantic): + return False + if not np.allclose(self.axis, other.axis, atol=1e-9): + return False + if self.rotations is None and other.rotations is None: + return True + if self.rotations is None or other.rotations is None: + return False + return len(self.rotations) == len(other.rotations) and all( + r1 == r2 for r1, r2 in zip(self.rotations, other.rotations, strict=False) + ) + + def __hash__(self) -> int: + """Returns hash for use in sets and dicts. + + Note: Returns a fixed hash since CanonicalGateSemantic contains mutable rotations list. + """ + return hash(CanonicalGateSemantic) + def __repr__(self) -> str: rotation_str = f", rotations={self.rotations}" if self.rotations else "" return f"CanonicalGateSemantic(axis={self.axis}{rotation_str})" diff --git a/opensquirrel/ir/two_qubit_gate.py b/opensquirrel/ir/two_qubit_gate.py index 9227b1f6..156d7402 100644 --- a/opensquirrel/ir/two_qubit_gate.py +++ b/opensquirrel/ir/two_qubit_gate.py @@ -7,7 +7,6 @@ from opensquirrel.ir.semantics import CanonicalGateSemantic, ControlledGateSemantic, MatrixGateSemantic from opensquirrel.ir.semantics.bsr import bsr_from_matrix from opensquirrel.ir.semantics.gate_semantic import GateSemantic -from opensquirrel.utils import get_matrix class TwoQubitGate(Gate): @@ -33,7 +32,13 @@ def matrix(self) -> MatrixGateSemantic: return self._matrix if self._controlled: - self._matrix = MatrixGateSemantic(get_matrix(self, 2)) + from opensquirrel.utils.matrix_expander import can1 + + target_bsr = self._controlled.target_bsr + u_target = can1(target_bsr.axis, target_bsr.angle, target_bsr.phase) + m = np.eye(4, dtype=np.complex128) + m[2:, 2:] = u_target + self._matrix = MatrixGateSemantic(m) return self._matrix if self._canonical: diff --git a/opensquirrel/passes/decomposer/__init__.py b/opensquirrel/passes/decomposer/__init__.py index e44be7c1..776a5a9f 100644 --- a/opensquirrel/passes/decomposer/__init__.py +++ b/opensquirrel/passes/decomposer/__init__.py @@ -6,6 +6,7 @@ ZXZDecomposer, ZYZDecomposer, ) +from opensquirrel.passes.decomposer.can2cz_decomposer import Can2CZDecomposer from opensquirrel.passes.decomposer.cnot2cz_decomposer import CNOT2CZDecomposer from opensquirrel.passes.decomposer.cnot_decomposer import CNOTDecomposer from opensquirrel.passes.decomposer.cz_decomposer import CZDecomposer @@ -17,6 +18,7 @@ "CNOT2CZDecomposer", "CNOTDecomposer", "CZDecomposer", + "Can2CZDecomposer", "McKayDecomposer", "SWAP2CNOTDecomposer", "SWAP2CZDecomposer", diff --git a/opensquirrel/passes/decomposer/can2cz_decomposer.py b/opensquirrel/passes/decomposer/can2cz_decomposer.py new file mode 100644 index 00000000..3a223f84 --- /dev/null +++ b/opensquirrel/passes/decomposer/can2cz_decomposer.py @@ -0,0 +1,124 @@ +from __future__ import annotations + +from math import pi +from typing import TYPE_CHECKING + +import numpy as np + +from opensquirrel import CNOT, CZ, X90, H, MinusX90, Ry, S, SDagger, Z +from opensquirrel.ir.semantics.bsr import BlochSphereRotation +from opensquirrel.ir.single_qubit_gate import SingleQubitGate +from opensquirrel.ir.two_qubit_gate import TwoQubitGate +from opensquirrel.passes.decomposer.general_decomposer import Decomposer + +if TYPE_CHECKING: + from opensquirrel.ir import Gate + + +class Can2CZDecomposer(Decomposer): + def decompose(self, instruction: Gate) -> list[Gate]: + """General decomposition of an arbitrary 2-qubit gate into (at most 3) CZ gate(s) with single-qubit rotations. + + Adapted from [Quantum Gates by G.E. Crooks (2024), Section 7.3](https://threeplusone.com/pubs/on_gates.pdf). + + Note: + This decomposition does not, in general, preserve the global phase of the original gate. + It is advised to run the single-qubit gates merger pass after this two-qubit gate decomposition pass. + + Args: + instruction (Gate): 2-qubit gate to decompose. + + Returns: + Decomposition of the original gate into a sequence of gates. + + """ + if not isinstance(instruction, TwoQubitGate): + return [instruction] + + gate = instruction + q0, q1 = gate.qubit_operands + + if gate == CZ(q0, q1): + return [gate] + + if gate == CNOT(q0, q1): + return [Ry(q1, -pi / 2), CZ(q0, q1), Ry(q1, pi / 2)] + + gate_axis = gate.canonical.axis + gate_rotations = gate.canonical.rotations + if gate_rotations is None: + gate_rotations = [BlochSphereRotation(axis=(1, 0, 0), angle=0, phase=0)] * 4 + k1 = SingleQubitGate(q0, gate_rotations[0]) + k2 = SingleQubitGate(q1, gate_rotations[1]) + k3 = SingleQubitGate(q0, gate_rotations[2]) + k4 = SingleQubitGate(q1, gate_rotations[3]) + + if np.allclose(gate_axis.value, np.array([0.5, 0, 0])): + return [ + k1, + k2, + H(q0), + S(q0), + H(q1), + S(q1), + H(q1), + Ry(q1, -pi / 2), + CZ(q0, q1), + Ry(q1, pi / 2), + H(q0), + k3, + k4, + ] + if np.isclose(gate_axis.value[2], 0): + tx, ty, _ = gate_axis.value + Xtx = SingleQubitGate(q0, BlochSphereRotation(axis=(1, 0, 0), angle=pi * tx, phase=pi / 2 * tx)) # noqa: N806 + Zty = SingleQubitGate(q1, BlochSphereRotation(axis=(0, 0, 1), angle=pi * ty, phase=pi / 2 * ty)) # noqa: N806 + return [ + k1, + k2, + Z(q0), + MinusX90(q0), + Z(q1), + MinusX90(q1), + Ry(q1, -pi / 2), + CZ(q0, q1), + Ry(q1, pi / 2), + Xtx, + Zty, + Ry(q1, -pi / 2), + CZ(q0, q1), + Ry(q1, pi / 2), + X90(q0), + Z(q0), + X90(q1), + Z(q1), + k3, + k4, + ] + tx, ty, tz = gate_axis.value + ztz = tz - 0.5 + ytx = tx - 0.5 + yty = 0.5 - ty + Ztz = SingleQubitGate(q0, BlochSphereRotation(axis=(0, 0, 1), angle=pi * ztz, phase=pi / 2 * ztz)) # noqa: N806 + Ytx = SingleQubitGate(q1, BlochSphereRotation(axis=(0, 1, 0), angle=pi * ytx, phase=pi / 2 * ytx)) # noqa: N806 + Yty = SingleQubitGate(q1, BlochSphereRotation(axis=(0, 1, 0), angle=pi * yty, phase=pi / 2 * yty)) # noqa: N806 + return [ + k1, + k2, + S(q1), + Ry(q0, -pi / 2), + CZ(q1, q0), + Ry(q0, pi / 2), + Ztz, + Ytx, + Ry(q1, -pi / 2), + CZ(q0, q1), + Ry(q1, pi / 2), + Yty, + Ry(q0, -pi / 2), + CZ(q1, q0), + Ry(q0, pi / 2), + SDagger(q0), + k3, + k4, + ] diff --git a/tests/passes/decomposer/test_can2cz_decomposer.py b/tests/passes/decomposer/test_can2cz_decomposer.py new file mode 100644 index 00000000..19915994 --- /dev/null +++ b/tests/passes/decomposer/test_can2cz_decomposer.py @@ -0,0 +1,127 @@ +from __future__ import annotations + +from math import pi +from typing import TYPE_CHECKING + +import numpy as np +import pytest + +from opensquirrel import CNOT, CR, CZ, SWAP, CRk, H, Ry +from opensquirrel.ir.semantics.bsr import BlochSphereRotation +from opensquirrel.ir.semantics.canonical_gate import CanonicalGateSemantic +from opensquirrel.ir.semantics.controlled_gate import ControlledGateSemantic +from opensquirrel.ir.semantics.matrix_gate import MatrixGateSemantic +from opensquirrel.ir.two_qubit_gate import TwoQubitGate +from opensquirrel.passes.decomposer import Can2CZDecomposer +from opensquirrel.passes.decomposer.general_decomposer import check_gate_decomposition + +if TYPE_CHECKING: + from opensquirrel.ir import Gate + + +@pytest.fixture +def decomposer() -> Can2CZDecomposer: + return Can2CZDecomposer() + + +@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: Can2CZDecomposer, 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"), + [ + (CZ(0, 1), [CZ(0, 1)]), + (CZ(1, 0), [CZ(1, 0)]), + ], + ids=["CZ_0_1", "CZ_1_0"], +) +def test_decomposes_CZ(decomposer: Can2CZDecomposer, gate: Gate, expected_result: list[Gate]) -> None: # noqa: N802 + decomposed_gate = decomposer.decompose(gate) + check_gate_decomposition(gate, decomposed_gate) + assert decomposed_gate == expected_result + + +@pytest.mark.parametrize( + ("gate", "expected_result"), + [ + (CNOT(0, 1), [Ry(1, -pi / 2), CZ(0, 1), Ry(1, pi / 2)]), + (CNOT(1, 0), [Ry(0, -pi / 2), CZ(1, 0), Ry(0, pi / 2)]), + ], + ids=["CNOT_0_1", "CNOT_1_0"], +) +def test_decomposes_CNOT(decomposer: Can2CZDecomposer, gate: Gate, expected_result: list[Gate]) -> None: # noqa: N802 + decomposed_gate = decomposer.decompose(gate) + check_gate_decomposition(gate, decomposed_gate) + assert decomposed_gate == expected_result + + +@pytest.mark.parametrize( + "gate", + [ + CRk(0, 1, 2), + CRk(1, 0, 2), + CR(0, 1, pi / 3), + CR(1, 0, pi / 3), + SWAP(0, 1), + SWAP(1, 0), + ], + ids=["CRk_0_1_2", "CRk_1_0_2", "CR_0_1_pi_3", "CR_1_0_pi_3", "SWAP_0_1", "SWAP_1_0"], +) +def test_decomposes_known_two_qubit_gates(decomposer: Can2CZDecomposer, gate: Gate) -> None: + decomposed_gate = decomposer.decompose(gate) + check_gate_decomposition(gate, decomposed_gate) + + +@pytest.mark.parametrize( + "gate", + [ + TwoQubitGate( + qubit0=1, + qubit1=0, + gate_semantic=CanonicalGateSemantic( + axis=(0.3, 0.2, 0.1), + rotations=[ + BlochSphereRotation((0, 1, 0), 0.4 * pi, 0.2 * pi), + BlochSphereRotation((1, 0, 0), 0.5 * pi, 0.25 * pi), + BlochSphereRotation((1, 0, 0), 0.2 * pi, 0.1 * pi), + BlochSphereRotation((0, 0, 1), 0.3 * pi, 0.15 * pi), + ], + ), + ), + TwoQubitGate( + qubit0=0, + qubit1=1, + gate_semantic=ControlledGateSemantic(target_bsr=BlochSphereRotation((0, 1, 0), pi / 5, pi / 10)), + ), + TwoQubitGate( + qubit0=0, + qubit1=1, + gate_semantic=MatrixGateSemantic( + matrix=(1 / 2) + * np.array( + [ + [1, 1, 1, 1], + [1, -1, 1, -1], + [1, 1, -1, -1], + [1, -1, -1, 1], + ], + dtype=np.complex128, + ) + ), + ), + ], + ids=["canonical", "controlled", "matrix"], +) +def test_decomposes_other_two_qubit_gate_semantics(decomposer: Can2CZDecomposer, gate: Gate) -> None: + decomposed_gate = decomposer.decompose(gate) + check_gate_decomposition(gate, decomposed_gate) diff --git a/uv.lock b/uv.lock index 0c501775..909d5ed1 100644 --- a/uv.lock +++ b/uv.lock @@ -3328,11 +3328,11 @@ export = [ { name = "pyqt5-qt5", marker = "sys_platform != 'darwin'", specifier = "==5.15.2" }, { name = "qcodes", marker = "sys_platform != 'darwin'", specifier = "<0.57.0" }, { name = "quantify-scheduler", marker = "sys_platform != 'darwin'", specifier = "==0.28.0" }, - { name = "spirack", marker = "sys_platform != 'darwin'", specifier = "<=0.2.12" }, + { name = "spirack", marker = "sys_platform != 'darwin'", specifier = "<=0.2.17" }, ] qgym-mapper = [ { name = "qgym", specifier = "==0.3.1" }, - { name = "sb3-contrib", specifier = "==2.7.1" }, + { name = "sb3-contrib", specifier = "==2.8.0" }, { name = "stable-baselines3", specifier = "==2.8.0" }, ] @@ -4947,14 +4947,14 @@ wheels = [ [[package]] name = "sb3-contrib" -version = "2.7.1" +version = "2.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "stable-baselines3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1d/ee/0d0a0a964e4dc290784b8dc7d540e6a2a4e688efdbda5e4c397d27e9d085/sb3_contrib-2.7.1.tar.gz", hash = "sha256:491070fb14c6a59757cbcc1aea5c62c894c2312f3f8a1ccf8f363a48eb3126ad", size = 89982, upload-time = "2025-12-05T11:31:18.904Z" } +sdist = { url = "https://files.pythonhosted.org/packages/13/c1/d50b8f886c586864fbda8855c03b0da235dd345ff66e05500ef887711efe/sb3_contrib-2.8.0.tar.gz", hash = "sha256:25656d093169239db1081b7665091f168f6113776878387387fdf4e3e894c7f1", size = 90527, upload-time = "2026-04-01T10:59:58.367Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/40/4d/6737dc879ba484419fc6db2bdfbd13aa6b1a3b516368ad0370546693ae82/sb3_contrib-2.7.1-py3-none-any.whl", hash = "sha256:27dd9db11e4f0b5e172bc73eef32c6342e7de8d6b4b833a8a28933e1679e907f", size = 93189, upload-time = "2025-12-05T11:31:17.587Z" }, + { url = "https://files.pythonhosted.org/packages/8c/17/3a00211e7453d382b98783995174e1f587c461719ac916078bac26dbe597/sb3_contrib-2.8.0-py3-none-any.whl", hash = "sha256:14c81ce4e15c747a13e2b1a8945e7658da6e320b1341b9e971358c35837598b2", size = 93043, upload-time = "2026-04-01T10:59:56.704Z" }, ] [[package]]