From 74e85a5a07c00925d1e2c43e7de08cc5bed35aef Mon Sep 17 00:00:00 2001 From: Chris Elenbaas Date: Thu, 30 Apr 2026 10:25:39 +0200 Subject: [PATCH 1/8] Start on can2cz_decomposer. --- .../passes/decomposer/can2cz_decomposer.py | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 opensquirrel/passes/decomposer/can2cz_decomposer.py diff --git a/opensquirrel/passes/decomposer/can2cz_decomposer.py b/opensquirrel/passes/decomposer/can2cz_decomposer.py new file mode 100644 index 00000000..917a1045 --- /dev/null +++ b/opensquirrel/passes/decomposer/can2cz_decomposer.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +from math import pi + +from opensquirrel import CZ, Ry +from opensquirrel.ir import Gate +from opensquirrel.passes.decomposer.general_decomposer import Decomposer + + +class CNOT2CZDecomposer(Decomposer): + def decompose(self, instruction: Gate) -> list[Gate]: + """General decomposition of a 2-qubit gate into (at most 3) CZ gate(s) with single-qubit rotations. + + Note: + This decomposition does not, in general, preserve the global phase of the original gate. + + Args: + instruction (Gate): 2-qubit gate to decompose. + + Returns: + Decomposition of the original gate into a sequence of gates. + + """ + if not isinstance(instruction, Gate): + return [instruction] + + gate = instruction + q0, q1 = gate.qubit_operands + return [ + Ry(q1, -pi / 2), + CZ(q0, q1), + Ry(q1, pi / 2), + ] From 13a6a175eda536e500c386043e580bc7f1855b26 Mon Sep 17 00:00:00 2001 From: Chris Elenbaas Date: Thu, 30 Apr 2026 14:24:54 +0200 Subject: [PATCH 2/8] Implemented Can2CZDecomposer. --- .../passes/decomposer/can2cz_decomposer.py | 71 ++++++++++++++++--- 1 file changed, 63 insertions(+), 8 deletions(-) diff --git a/opensquirrel/passes/decomposer/can2cz_decomposer.py b/opensquirrel/passes/decomposer/can2cz_decomposer.py index 917a1045..fc9faa8e 100644 --- a/opensquirrel/passes/decomposer/can2cz_decomposer.py +++ b/opensquirrel/passes/decomposer/can2cz_decomposer.py @@ -2,17 +2,25 @@ from math import pi -from opensquirrel import CZ, Ry +import numpy as np + +from opensquirrel import CZ, Ry, H, S, Z, X90, MinusX90, SDagger from opensquirrel.ir import Gate +from opensquirrel.ir.semantics.bsr import BlochSphereRotation +from opensquirrel.ir.single_qubit_gate import SingleQubitGate from opensquirrel.passes.decomposer.general_decomposer import Decomposer -class CNOT2CZDecomposer(Decomposer): +class Can2CZDecomposer(Decomposer): + def decompose(self, instruction: Gate) -> list[Gate]: - """General decomposition of a 2-qubit gate into (at most 3) CZ gate(s) with single-qubit rotations. + """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 decomposition pass. Args: instruction (Gate): 2-qubit gate to decompose. @@ -26,8 +34,55 @@ def decompose(self, instruction: Gate) -> list[Gate]: gate = instruction q0, q1 = gate.qubit_operands - return [ - Ry(q1, -pi / 2), - CZ(q0, q1), - Ry(q1, pi / 2), - ] + + if gate.semantic.axis == np.array([0.5, 0, 0]): + return [ + H(q0), S(q0), + H(q1), S(q1), H(q1), + Ry(q1, -pi / 2), + CZ(q0, q1), + Ry(q1, pi / 2), + H(q0), + ] + elif np.isclose(gate.semantic.axis[2], 0): + tx, ty, _ = gate.semantic.axis + Xtx = SingleQubitGate(q0, BlochSphereRotation(axis=(1, 0, 0), angle=pi * tx, phase=pi/2 * tx)) + Zty = SingleQubitGate(q1, BlochSphereRotation(axis=(0, 0, 1), angle=pi * ty, phase=pi/2 * ty)) + return [ + 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), + ] + else: + tx, ty, tz = gate.semantic.axis + 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)) + Ytx = SingleQubitGate(q1, BlochSphereRotation(axis=(0, 1, 0), angle=pi * ytx, phase=pi/2 * ytx)) + Yty = SingleQubitGate(q1, BlochSphereRotation(axis=(0, 1, 0), angle=pi * yty, phase=pi/2 * yty)) + return [ + 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), + ] From 1f98f6002adc7b2d6c8d9acd47401af172ca20aa Mon Sep 17 00:00:00 2001 From: Chris Elenbaas Date: Thu, 30 Apr 2026 16:22:52 +0200 Subject: [PATCH 3/8] Add simple tests (CNOT test does not pass yet). --- opensquirrel/ir/semantics/canonical_gate.py | 26 ++++++++ opensquirrel/passes/decomposer/__init__.py | 2 + .../passes/decomposer/can2cz_decomposer.py | 23 ++++++-- .../decomposer/test_can2cz_decomposer.py | 59 +++++++++++++++++++ 4 files changed, 105 insertions(+), 5 deletions(-) create mode 100644 tests/passes/decomposer/test_can2cz_decomposer.py diff --git a/opensquirrel/ir/semantics/canonical_gate.py b/opensquirrel/ir/semantics/canonical_gate.py index 0da54a85..2cb6ad8f 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) + ) + + 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/passes/decomposer/__init__.py b/opensquirrel/passes/decomposer/__init__.py index e44be7c1..2eb29e81 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 @@ -14,6 +15,7 @@ from opensquirrel.passes.decomposer.swap2cz_decomposer import SWAP2CZDecomposer __all__ = [ + "Can2CZDecomposer", "CNOT2CZDecomposer", "CNOTDecomposer", "CZDecomposer", diff --git a/opensquirrel/passes/decomposer/can2cz_decomposer.py b/opensquirrel/passes/decomposer/can2cz_decomposer.py index fc9faa8e..67f027b0 100644 --- a/opensquirrel/passes/decomposer/can2cz_decomposer.py +++ b/opensquirrel/passes/decomposer/can2cz_decomposer.py @@ -6,8 +6,10 @@ from opensquirrel import CZ, Ry, H, S, Z, X90, MinusX90, SDagger from opensquirrel.ir import Gate +from opensquirrel.ir.default_gates.two_qubit_gates import CNOT 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 @@ -29,13 +31,24 @@ def decompose(self, instruction: Gate) -> list[Gate]: Decomposition of the original gate into a sequence of gates. """ - if not isinstance(instruction, Gate): + if not isinstance(instruction, TwoQubitGate): return [instruction] gate = instruction q0, q1 = gate.qubit_operands - if gate.semantic.axis == np.array([0.5, 0, 0]): + if gate == CZ(q0, q1): + return [gate] + + # if gate == CNOT(q0, q1): + # return [ + # Ry(q1, -pi / 2), + # CZ(q0, q1), + # Ry(q1, pi / 2), + # ] + + if np.allclose(gate.canonical.axis.value, np.array([0.5, 0, 0])): + print() return [ H(q0), S(q0), H(q1), S(q1), H(q1), @@ -44,8 +57,8 @@ def decompose(self, instruction: Gate) -> list[Gate]: Ry(q1, pi / 2), H(q0), ] - elif np.isclose(gate.semantic.axis[2], 0): - tx, ty, _ = gate.semantic.axis + elif np.isclose(gate.canonical.axis.value[2], 0): + tx, ty, _ = gate.canonical.axis.value Xtx = SingleQubitGate(q0, BlochSphereRotation(axis=(1, 0, 0), angle=pi * tx, phase=pi/2 * tx)) Zty = SingleQubitGate(q1, BlochSphereRotation(axis=(0, 0, 1), angle=pi * ty, phase=pi/2 * ty)) return [ @@ -63,7 +76,7 @@ def decompose(self, instruction: Gate) -> list[Gate]: X90(q1), Z(q1), ] else: - tx, ty, tz = gate.semantic.axis + tx, ty, tz = gate.canonical.axis.value ztz = tz - 0.5 ytx = tx - 0.5 yty = 0.5 - ty diff --git a/tests/passes/decomposer/test_can2cz_decomposer.py b/tests/passes/decomposer/test_can2cz_decomposer.py new file mode 100644 index 00000000..73e502ec --- /dev/null +++ b/tests/passes/decomposer/test_can2cz_decomposer.py @@ -0,0 +1,59 @@ +from __future__ import annotations + +import math +from typing import TYPE_CHECKING + +import pytest + +from opensquirrel import CNOT, CR, CZ, SWAP, CRk, H, Ry +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, -math.pi / 2), CZ(0, 1), Ry(1, math.pi / 2)]), + (CNOT(1, 0), [Ry(0, -math.pi / 2), CZ(1, 0), Ry(0, math.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 From 35da78779ef1a16c7695a1efb2e35e5d364f6f9c Mon Sep 17 00:00:00 2001 From: Chris Elenbaas Date: Fri, 1 May 2026 09:58:14 +0200 Subject: [PATCH 4/8] Add K-rotations to decomposition. --- .../passes/decomposer/can2cz_decomposer.py | 29 +++++++++++-------- .../decomposer/test_can2cz_decomposer.py | 2 +- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/opensquirrel/passes/decomposer/can2cz_decomposer.py b/opensquirrel/passes/decomposer/can2cz_decomposer.py index 67f027b0..58addf78 100644 --- a/opensquirrel/passes/decomposer/can2cz_decomposer.py +++ b/opensquirrel/passes/decomposer/can2cz_decomposer.py @@ -39,29 +39,31 @@ def decompose(self, instruction: Gate) -> list[Gate]: if gate == CZ(q0, q1): return [gate] - - # if gate == CNOT(q0, q1): - # return [ - # Ry(q1, -pi / 2), - # CZ(q0, q1), - # Ry(q1, pi / 2), - # ] - if np.allclose(gate.canonical.axis.value, np.array([0.5, 0, 0])): - print() + gate_axis = gate.canonical.axis + gate_rotations = gate.canonical.rotations + 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, ] - elif np.isclose(gate.canonical.axis.value[2], 0): - tx, ty, _ = gate.canonical.axis.value + elif 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)) Zty = SingleQubitGate(q1, BlochSphereRotation(axis=(0, 0, 1), angle=pi * ty, phase=pi/2 * ty)) return [ + K1, K2, Z(q0), MinusX90(q0), Z(q1), MinusX90(q1), Ry(q1, -pi / 2), @@ -74,9 +76,10 @@ def decompose(self, instruction: Gate) -> list[Gate]: Ry(q1, pi / 2), X90(q0), Z(q0), X90(q1), Z(q1), + K3, K4, ] else: - tx, ty, tz = gate.canonical.axis.value + tx, ty, tz = gate_axis.value ztz = tz - 0.5 ytx = tx - 0.5 yty = 0.5 - ty @@ -84,6 +87,7 @@ def decompose(self, instruction: Gate) -> list[Gate]: Ytx = SingleQubitGate(q1, BlochSphereRotation(axis=(0, 1, 0), angle=pi * ytx, phase=pi/2 * ytx)) Yty = SingleQubitGate(q1, BlochSphereRotation(axis=(0, 1, 0), angle=pi * yty, phase=pi/2 * yty)) return [ + K1, K2, S(q1), Ry(q0, -pi / 2), CZ(q1, q0), @@ -98,4 +102,5 @@ def decompose(self, instruction: Gate) -> list[Gate]: 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 index 73e502ec..e1eece07 100644 --- a/tests/passes/decomposer/test_can2cz_decomposer.py +++ b/tests/passes/decomposer/test_can2cz_decomposer.py @@ -56,4 +56,4 @@ def test_decomposes_CZ(decomposer: Can2CZDecomposer, gate: Gate, expected_result 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 + # assert decomposed_gate == expected_result From 7e9106f6750852b3b436f0b33d672e9c74088d71 Mon Sep 17 00:00:00 2001 From: Chris Elenbaas Date: Fri, 1 May 2026 11:53:24 +0200 Subject: [PATCH 5/8] Add tests of different gate semantics. There remains a bug in the qubit ordering and that of K1...K4. --- .../passes/decomposer/can2cz_decomposer.py | 9 ++- .../decomposer/test_can2cz_decomposer.py | 77 ++++++++++++++++++- 2 files changed, 80 insertions(+), 6 deletions(-) diff --git a/opensquirrel/passes/decomposer/can2cz_decomposer.py b/opensquirrel/passes/decomposer/can2cz_decomposer.py index 58addf78..83890b94 100644 --- a/opensquirrel/passes/decomposer/can2cz_decomposer.py +++ b/opensquirrel/passes/decomposer/can2cz_decomposer.py @@ -4,9 +4,8 @@ import numpy as np -from opensquirrel import CZ, Ry, H, S, Z, X90, MinusX90, SDagger +from opensquirrel import CZ, CNOT, Ry, H, S, Z, X90, MinusX90, SDagger from opensquirrel.ir import Gate -from opensquirrel.ir.default_gates.two_qubit_gates import CNOT from opensquirrel.ir.semantics.bsr import BlochSphereRotation from opensquirrel.ir.single_qubit_gate import SingleQubitGate from opensquirrel.ir.two_qubit_gate import TwoQubitGate @@ -40,6 +39,12 @@ def decompose(self, instruction: Gate) -> list[Gate]: if gate == CZ(q0, q1): return [gate] + if gate == CNOT(q0, q1): + return [Ry(q1, -pi / 2), CZ(q0, q1), Ry(q1, pi / 2)] + + # Canonical 4x4 factors follow tensor-product order (higher index first). + q0, q1 = (q0, q1) if q0.index > q1.index else (q1, q0) + gate_axis = gate.canonical.axis gate_rotations = gate.canonical.rotations K1 = SingleQubitGate(q0, gate_rotations[0]) diff --git a/tests/passes/decomposer/test_can2cz_decomposer.py b/tests/passes/decomposer/test_can2cz_decomposer.py index e1eece07..fa20b971 100644 --- a/tests/passes/decomposer/test_can2cz_decomposer.py +++ b/tests/passes/decomposer/test_can2cz_decomposer.py @@ -1,11 +1,17 @@ from __future__ import annotations -import math +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 @@ -48,12 +54,75 @@ def test_decomposes_CZ(decomposer: Can2CZDecomposer, gate: Gate, expected_result @pytest.mark.parametrize( ("gate", "expected_result"), [ - (CNOT(0, 1), [Ry(1, -math.pi / 2), CZ(0, 1), Ry(1, math.pi / 2)]), - (CNOT(1, 0), [Ry(0, -math.pi / 2), CZ(1, 0), Ry(0, math.pi / 2)]), + (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 + 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=0, + qubit1=1, + 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) From b283753c1964adba33a22fff27fb0f1650f12036 Mon Sep 17 00:00:00 2001 From: Chris Elenbaas Date: Fri, 1 May 2026 12:00:19 +0200 Subject: [PATCH 6/8] Fix linting and typing. --- opensquirrel/ir/semantics/canonical_gate.py | 2 +- opensquirrel/passes/decomposer/__init__.py | 2 +- .../passes/decomposer/can2cz_decomposer.py | 108 ++++++++++-------- .../decomposer/test_can2cz_decomposer.py | 23 ++-- uv.lock | 10 +- 5 files changed, 80 insertions(+), 65 deletions(-) diff --git a/opensquirrel/ir/semantics/canonical_gate.py b/opensquirrel/ir/semantics/canonical_gate.py index 2cb6ad8f..0963ea10 100644 --- a/opensquirrel/ir/semantics/canonical_gate.py +++ b/opensquirrel/ir/semantics/canonical_gate.py @@ -149,7 +149,7 @@ def __eq__(self, other: Any) -> bool: 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) + r1 == r2 for r1, r2 in zip(self.rotations, other.rotations, strict=False) ) def __hash__(self) -> int: diff --git a/opensquirrel/passes/decomposer/__init__.py b/opensquirrel/passes/decomposer/__init__.py index 2eb29e81..776a5a9f 100644 --- a/opensquirrel/passes/decomposer/__init__.py +++ b/opensquirrel/passes/decomposer/__init__.py @@ -15,10 +15,10 @@ from opensquirrel.passes.decomposer.swap2cz_decomposer import SWAP2CZDecomposer __all__ = [ - "Can2CZDecomposer", "CNOT2CZDecomposer", "CNOTDecomposer", "CZDecomposer", + "Can2CZDecomposer", "McKayDecomposer", "SWAP2CNOTDecomposer", "SWAP2CZDecomposer", diff --git a/opensquirrel/passes/decomposer/can2cz_decomposer.py b/opensquirrel/passes/decomposer/can2cz_decomposer.py index 83890b94..c49e2410 100644 --- a/opensquirrel/passes/decomposer/can2cz_decomposer.py +++ b/opensquirrel/passes/decomposer/can2cz_decomposer.py @@ -1,19 +1,21 @@ from __future__ import annotations from math import pi +from typing import TYPE_CHECKING import numpy as np -from opensquirrel import CZ, CNOT, Ry, H, S, Z, X90, MinusX90, SDagger -from opensquirrel.ir import Gate +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): +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. @@ -47,30 +49,40 @@ def decompose(self, instruction: Gate) -> list[Gate]: gate_axis = gate.canonical.axis gate_rotations = gate.canonical.rotations - 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 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), + 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, + k3, + k4, ] - elif np.isclose(gate_axis.value[2], 0): + 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)) - Zty = SingleQubitGate(q1, BlochSphereRotation(axis=(0, 0, 1), angle=pi * ty, phase=pi/2 * ty)) + 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), + k1, + k2, + Z(q0), + MinusX90(q0), + Z(q1), + MinusX90(q1), Ry(q1, -pi / 2), CZ(q0, q1), Ry(q1, pi / 2), @@ -79,33 +91,37 @@ def decompose(self, instruction: Gate) -> list[Gate]: Ry(q1, -pi / 2), CZ(q0, q1), Ry(q1, pi / 2), - X90(q0), Z(q0), - X90(q1), Z(q1), - K3, K4, - ] - else: - 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)) - Ytx = SingleQubitGate(q1, BlochSphereRotation(axis=(0, 1, 0), angle=pi * ytx, phase=pi/2 * ytx)) - Yty = SingleQubitGate(q1, BlochSphereRotation(axis=(0, 1, 0), angle=pi * yty, phase=pi/2 * yty)) - 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, + 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 index fa20b971..c769f4c3 100644 --- a/tests/passes/decomposer/test_can2cz_decomposer.py +++ b/tests/passes/decomposer/test_can2cz_decomposer.py @@ -95,31 +95,30 @@ def test_decomposes_known_two_qubit_gates(decomposer: Can2CZDecomposer, gate: Ga 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) - ) + 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( + matrix=(1 / 2) + * np.array( [ - [1, 1, 1, 1], - [1, -1, 1, -1], - [1, 1, -1, -1], - [1, -1, -1, 1], + [1, 1, 1, 1], + [1, -1, 1, -1], + [1, 1, -1, -1], + [1, -1, -1, 1], ], dtype=np.complex128, ) - ) - ), + ), + ), ], ids=["canonical", "controlled", "matrix"], ) 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]] From 334fe070d99e41bca06eb56601b3b35e40e3e795 Mon Sep 17 00:00:00 2001 From: Chris Elenbaas Date: Thu, 21 May 2026 10:02:38 +0200 Subject: [PATCH 7/8] [WIP] Trying to fix semantics qubit ordering. --- CHANGELOG.md | 1 + opensquirrel/passes/decomposer/can2cz_decomposer.py | 5 +---- opensquirrel/utils/matrix_expander.py | 4 ++-- tests/passes/decomposer/test_can2cz_decomposer.py | 4 ++-- tests/utils/test_matrix_expander.py | 4 ++-- 5 files changed, 8 insertions(+), 10 deletions(-) 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/passes/decomposer/can2cz_decomposer.py b/opensquirrel/passes/decomposer/can2cz_decomposer.py index c49e2410..3a223f84 100644 --- a/opensquirrel/passes/decomposer/can2cz_decomposer.py +++ b/opensquirrel/passes/decomposer/can2cz_decomposer.py @@ -23,7 +23,7 @@ def decompose(self, instruction: Gate) -> list[Gate]: 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 decomposition pass. + 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. @@ -44,9 +44,6 @@ def decompose(self, instruction: Gate) -> list[Gate]: if gate == CNOT(q0, q1): return [Ry(q1, -pi / 2), CZ(q0, q1), Ry(q1, pi / 2)] - # Canonical 4x4 factors follow tensor-product order (higher index first). - q0, q1 = (q0, q1) if q0.index > q1.index else (q1, q0) - gate_axis = gate.canonical.axis gate_rotations = gate.canonical.rotations if gate_rotations is None: diff --git a/opensquirrel/utils/matrix_expander.py b/opensquirrel/utils/matrix_expander.py index 9fdabff2..de8eb644 100644 --- a/opensquirrel/utils/matrix_expander.py +++ b/opensquirrel/utils/matrix_expander.py @@ -437,8 +437,8 @@ def canonical_decomposition( q1 = m @ o1 @ m_dag q2 = m @ o2 @ m_dag - k1, k2 = nearest_kronecker_product(q2) - k3, k4 = nearest_kronecker_product(q1) + k1, k2 = nearest_kronecker_product(q1) + k3, k4 = nearest_kronecker_product(q2) return k1, k2, k3, k4, CanonicalAxis(tx, ty, tz) diff --git a/tests/passes/decomposer/test_can2cz_decomposer.py b/tests/passes/decomposer/test_can2cz_decomposer.py index c769f4c3..19915994 100644 --- a/tests/passes/decomposer/test_can2cz_decomposer.py +++ b/tests/passes/decomposer/test_can2cz_decomposer.py @@ -86,8 +86,8 @@ def test_decomposes_known_two_qubit_gates(decomposer: Can2CZDecomposer, gate: Ga "gate", [ TwoQubitGate( - qubit0=0, - qubit1=1, + qubit0=1, + qubit1=0, gate_semantic=CanonicalGateSemantic( axis=(0.3, 0.2, 0.1), rotations=[ diff --git a/tests/utils/test_matrix_expander.py b/tests/utils/test_matrix_expander.py index 4af26566..353885e9 100644 --- a/tests/utils/test_matrix_expander.py +++ b/tests/utils/test_matrix_expander.py @@ -163,7 +163,7 @@ def test_canonical_decomposition(axis: tuple[float]) -> None: k1, k2, k3, k4, axis_recov = canonical_decomposition(x) - y = np.kron(k3, k4) @ can2(axis_recov) @ np.kron(k1, k2) + y = np.kron(k1, k2) @ can2(axis_recov) @ np.kron(k3, k4) assert are_matrices_equivalent_up_to_global_phase(x, y) @@ -176,5 +176,5 @@ def test_canonical_decomposition_nontrivial_local_operators() -> None: k1, k2, k3, k4, axis_recov = canonical_decomposition(x) - y = np.kron(k3, k4) @ can2(axis_recov) @ np.kron(k1, k2) + y = np.kron(k1, k2) @ can2(axis_recov) @ np.kron(k3, k4) assert are_matrices_equivalent_up_to_global_phase(x, y) From 9b066180ea6cee2435680846e6b705e931ab54ee Mon Sep 17 00:00:00 2001 From: Chris Elenbaas Date: Thu, 21 May 2026 12:01:58 +0200 Subject: [PATCH 8/8] Build controlled matrix according to operand order convention. --- opensquirrel/ir/two_qubit_gate.py | 9 +++++++-- opensquirrel/utils/matrix_expander.py | 4 ++-- tests/utils/test_matrix_expander.py | 4 ++-- 3 files changed, 11 insertions(+), 6 deletions(-) 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/utils/matrix_expander.py b/opensquirrel/utils/matrix_expander.py index de8eb644..9fdabff2 100644 --- a/opensquirrel/utils/matrix_expander.py +++ b/opensquirrel/utils/matrix_expander.py @@ -437,8 +437,8 @@ def canonical_decomposition( q1 = m @ o1 @ m_dag q2 = m @ o2 @ m_dag - k1, k2 = nearest_kronecker_product(q1) - k3, k4 = nearest_kronecker_product(q2) + k1, k2 = nearest_kronecker_product(q2) + k3, k4 = nearest_kronecker_product(q1) return k1, k2, k3, k4, CanonicalAxis(tx, ty, tz) diff --git a/tests/utils/test_matrix_expander.py b/tests/utils/test_matrix_expander.py index 353885e9..4af26566 100644 --- a/tests/utils/test_matrix_expander.py +++ b/tests/utils/test_matrix_expander.py @@ -163,7 +163,7 @@ def test_canonical_decomposition(axis: tuple[float]) -> None: k1, k2, k3, k4, axis_recov = canonical_decomposition(x) - y = np.kron(k1, k2) @ can2(axis_recov) @ np.kron(k3, k4) + y = np.kron(k3, k4) @ can2(axis_recov) @ np.kron(k1, k2) assert are_matrices_equivalent_up_to_global_phase(x, y) @@ -176,5 +176,5 @@ def test_canonical_decomposition_nontrivial_local_operators() -> None: k1, k2, k3, k4, axis_recov = canonical_decomposition(x) - y = np.kron(k1, k2) @ can2(axis_recov) @ np.kron(k3, k4) + y = np.kron(k3, k4) @ can2(axis_recov) @ np.kron(k1, k2) assert are_matrices_equivalent_up_to_global_phase(x, y)