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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 ]

Expand Down
26 changes: 26 additions & 0 deletions opensquirrel/ir/semantics/canonical_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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})"
9 changes: 7 additions & 2 deletions opensquirrel/ir/two_qubit_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions opensquirrel/passes/decomposer/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -17,6 +18,7 @@
"CNOT2CZDecomposer",
"CNOTDecomposer",
"CZDecomposer",
"Can2CZDecomposer",
"McKayDecomposer",
"SWAP2CNOTDecomposer",
"SWAP2CZDecomposer",
Expand Down
124 changes: 124 additions & 0 deletions opensquirrel/passes/decomposer/can2cz_decomposer.py
Original file line number Diff line number Diff line change
@@ -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,
]
127 changes: 127 additions & 0 deletions tests/passes/decomposer/test_can2cz_decomposer.py
Original file line number Diff line number Diff line change
@@ -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)
10 changes: 5 additions & 5 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading