Skip to content
Merged
Show file tree
Hide file tree
Changes from 59 commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
1d88449
Refactor gate decomposition to support controlled gates
GiacomoFrn May 29, 2025
30008d9
fix typo
GiacomoFrn May 29, 2025
36e732a
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 29, 2025
edf33b3
Improve controlled gate decomposition and control mask logic
GiacomoFrn May 29, 2025
762509b
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 29, 2025
48106cb
change imports
GiacomoFrn May 30, 2025
ff20fb4
Update test for angles summing to 2pi
GiacomoFrn May 30, 2025
97763de
Update src/qibo/gates/abstract.py
GiacomoFrn May 30, 2025
d4c7174
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 30, 2025
667d242
Ensure all _base_decompose methods accept *free and use_toffolis for …
GiacomoFrn May 30, 2025
aa0ccd3
Merge remote-tracking branch 'origin/decomposition' into decomposition
GiacomoFrn May 30, 2025
f4e2acb
Fix multi-control X decomposition error check for free qubits
GiacomoFrn May 30, 2025
aba148b
fix so now all tests pass
GiacomoFrn May 30, 2025
3e1e2c8
Update src/qibo/gates/abstract.py
GiacomoFrn Jun 9, 2025
dd83065
add "use_toffolis" description to decompose and _base_decompose_ func…
GiacomoFrn Jun 9, 2025
b5e8751
Update src/qibo/gates/abstract.py
GiacomoFrn Jun 9, 2025
7f5ba81
Update src/qibo/gates/abstract.py
GiacomoFrn Jun 9, 2025
a050f90
Update src/qibo/gates/abstract.py
GiacomoFrn Jun 9, 2025
584f825
Update src/qibo/gates/abstract.py
GiacomoFrn Jun 9, 2025
48b15ae
Update src/qibo/gates/abstract.py
GiacomoFrn Jun 9, 2025
dd1e2aa
Update src/qibo/gates/abstract.py
GiacomoFrn Jun 9, 2025
36e36d1
Update src/qibo/gates/abstract.py
GiacomoFrn Jun 9, 2025
b070413
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 9, 2025
c3a5f5c
Update src/qibo/gates/abstract.py
GiacomoFrn Jun 9, 2025
7f7687f
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 9, 2025
193a7f1
Update src/qibo/gates/abstract.py
GiacomoFrn Jun 9, 2025
78a88e3
fix small errors
GiacomoFrn Jun 11, 2025
c174204
add tests for controlled decomposition
GiacomoFrn Jun 11, 2025
f1f7e72
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 11, 2025
dcc4637
Update src/qibo/gates/abstract.py
GiacomoFrn Jun 11, 2025
18cf16a
fix return in _gates_cancel
GiacomoFrn Jun 11, 2025
93d7647
Update src/qibo/gates/abstract.py
GiacomoFrn Jun 11, 2025
a6cd78f
Update src/qibo/gates/abstract.py
GiacomoFrn Jun 12, 2025
740fd78
Update tests/test_gates_abstract.py
GiacomoFrn Jun 12, 2025
772016a
Update src/qibo/gates/abstract.py
GiacomoFrn Jun 12, 2025
340daef
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 12, 2025
6bdd3cc
add circuit execution to decomposition test
GiacomoFrn Jun 12, 2025
2805919
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 12, 2025
5c3f558
fix gates_cancels and add test
GiacomoFrn Jun 12, 2025
1cdcebe
Merge remote-tracking branch 'origin/decomposition' into decomposition
GiacomoFrn Jun 12, 2025
a7476bb
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 12, 2025
0fe9c47
update decompose since control_qubits are empty tuple by default
GiacomoFrn Jun 12, 2025
2706ef6
Update tests/test_gates_abstract.py
GiacomoFrn Jun 13, 2025
4b4ac68
add description to gates_cancel
GiacomoFrn Jun 13, 2025
b57a1cf
add cases for gates_cancel test
GiacomoFrn Jun 13, 2025
4a585cf
Update tests/test_gates_abstract.py
renatomello Jun 13, 2025
0de4c70
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 13, 2025
34bcb2b
Merged main into decomposition with conflict resolution
GiacomoFrn Jun 14, 2025
ad69291
Update tests/test_gates_abstract.py
renatomello Jun 16, 2025
1d07ce8
Update tests/test_gates_abstract.py
renatomello Jun 16, 2025
313dddb
Update tests/test_gates_abstract.py
renatomello Jun 16, 2025
096e572
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 16, 2025
70e6c3d
Update src/qibo/gates/abstract.py
renatomello Jun 16, 2025
67f6dd0
Update tests/test_gates_abstract.py
renatomello Jun 16, 2025
f8fc6ba
Update tests/test_gates_abstract.py
renatomello Jun 16, 2025
a5fc90d
Update src/qibo/gates/abstract.py
renatomello Jun 16, 2025
420a6f3
Update src/qibo/gates/abstract.py
renatomello Jun 16, 2025
48496e6
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 16, 2025
8706a25
Update src/qibo/gates/abstract.py
renatomello Jun 16, 2025
ee436ef
Update src/qibo/gates/gates.py
renatomello Jun 16, 2025
9b01855
Update src/qibo/gates/gates.py
renatomello Jun 16, 2025
fddeb46
Update tests/test_gates_abstract.py
renatomello Jun 16, 2025
bf26719
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 16, 2025
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
101 changes: 99 additions & 2 deletions src/qibo/gates/abstract.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import collections
import json
from math import pi
from typing import List, Sequence, Tuple

import sympy
Expand Down Expand Up @@ -359,18 +360,114 @@ def controlled_by(self, *qubits: int) -> "Gate":
self.control_qubits = qubits
return self

def decompose(self, *free) -> List["Gate"]:
def _base_decompose(self, *free, use_toffolis=True) -> List["Gate"]:
"""Base decomposition for gates.

Returns a list containing the gate itself. Should be overridden by
subclasses that support decomposition to simpler gates.

Args:
free: Ids of free qubits to use for the gate decomposition.
use_toffolis: If ``True`` the decomposition contains only ``TOFFOLI`` gates.
If ``False`` a congruent representation is used for ``TOFFOLI`` gates.
See :class:`qibo.gates.TOFFOLI` for more details on this representation.

Returns:
list: Synthesis of the original gate in another gate set.
"""
return [self.__class__(*self.init_args, **self.init_kwargs)]

@staticmethod
def _gates_cancel(g1, g2):
"""Determines if two gates cancel each other.

Two gates are considered to cancel if:
- They are of the same type (class).
- They act on the same target and control qubits.
- For fixed gates (like H, CX, X, Y, Z, SWAP), they always cancel in pairs.
- For parametrized rotation gates (subclasses of _Rn_), their parameters sum to a multiple of 2π.

Note:
Multi-parameter gates are not currently supported by this check.

Args:
g1, g2: Gate instances to compare.

Returns:
bool: True if the gates cancel each other, False otherwise.
"""
if g1.__class__ != g2.__class__:
return False

if g1.target_qubits != g2.target_qubits:
return False

if g1.control_qubits != g2.control_qubits:
return False

# Identity conditions for fixed gates
name = g1.name
if name in ("h", "cx", "x", "y", "z", "swap", "ecr", "ccx", "ccz"):
return True

# Check for parametrized rotation gates
if "_Rn_" in [base.__name__ for base in g1.__class__.__bases__]:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better thanks, however, this is still not supporting multi parameters gates, which might be fine for the moment, maybe just point this out in the docstring.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is true, I thought that in general I could assume that multi parameters gates wouldn't appear at this stage of the decomposition, but I am probably wrong. I will update the docstring.

theta1 = g1.parameters[0]
theta2 = g2.parameters[0]
# Check if theta1 + theta2 is a multiple of 2π
return bool((theta1 + theta2) % (2 * pi) < 1e-8)

return False

def _control_mask_after_stripping(self, gates: List["Gate"]) -> List[bool]:
"""Returns a mask indicating which gates should be controlled."""
left = 0
right = len(gates) - 1
mask = [True] * len(gates)
while left < right:
g1, g2 = gates[left], gates[right]
if self._gates_cancel(g1, g2):
mask[left] = False
mask[right] = False
left += 1
right -= 1
return mask

def decompose(self, *free, use_toffolis: bool = True) -> List["Gate"]:
"""Decomposes multi-control gates to gates supported by OpenQASM.

Decompositions are based on `arXiv:9503016 <https://arxiv.org/abs/quant-ph/9503016>`_.
If the gate is already controlled, it recursively decomposes the base gate and updates
the control qubits accordingly.

Args:
free: Ids of free qubits to use for the gate decomposition.
use_toffolis(bool, optional): If ``True``, the decomposition contains only
:class:`qibo.gates.TOFFOLI` gates. If ``False``, a congruent
representation is used for :class:`qibo.gates.TOFFOLI` gates.
See :class:`qibo.gates.TOFFOLI` for more details on this representation.

Returns:
list: gates that have the same effect as applying the original gate.
"""
return [self.__class__(*self.init_args, **self.init_kwargs)]
if self.is_controlled_by:
# Step 1: Error check with all controls/targets
if set(free) & set(self.qubits):
raise_error(
ValueError,
"Cannot decompose multi-controlled ``X`` gate if free "
"qubits coincide with target or controls.",
)
# Step 2: Decompose base gate without controls
base_gate = self.__class__(*self.init_args, **self.init_kwargs)
decomposed = base_gate._base_decompose(*free, use_toffolis=use_toffolis)
mask = self._control_mask_after_stripping(decomposed)
for bool_value, gate in zip(mask, decomposed):
if bool_value:
gate.is_controlled_by = True
gate.control_qubits += self.control_qubits
return decomposed
return self._base_decompose(*free, use_toffolis=use_toffolis)

def matrix(self, backend=None):
"""Returns the matrix representation of the gate.
Expand Down
48 changes: 24 additions & 24 deletions src/qibo/gates/gates.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ def controlled_by(self, *q):
gate = super().controlled_by(*q)
return gate

def decompose(self, *free, use_toffolis=True):
def _base_decompose(self, *free, use_toffolis=True):
"""Decomposes multi-control ``X`` gate to one-qubit, ``CNOT`` and ``TOFFOLI`` gates.

Args:
Expand Down Expand Up @@ -133,12 +133,12 @@ def decompose(self, *free, use_toffolis=True):
m1 = n // 2
free1 = controls[m1:] + (target,) + tuple(free[1:])
x1 = self.__class__(free[0]).controlled_by(*controls[:m1])
part1 = x1.decompose(*free1, use_toffolis=use_toffolis)
part1 = x1._base_decompose(*free1, use_toffolis=use_toffolis)

free2 = controls[:m1] + tuple(free[1:])
controls2 = controls[m1:] + (free[0],)
x2 = self.__class__(target).controlled_by(*controls2)
part2 = x2.decompose(*free2, use_toffolis=use_toffolis)
part2 = x2._base_decompose(*free2, use_toffolis=use_toffolis)

decomp_gates = [*part1, *part2]

Expand Down Expand Up @@ -286,7 +286,7 @@ def clifford(self):
def qasm_label(self):
return "sx"

def decompose(self):
def _base_decompose(self, *free, use_toffolis=True):
"""Decomposition of :math:`\\sqrt{X}` up to global phase.

A global phase difference exists between the definitions of
Expand Down Expand Up @@ -336,7 +336,7 @@ def clifford(self):
def qasm_label(self):
return "sxdg"

def decompose(self):
def _base_decompose(self, *free, use_toffolis=True):
"""Decomposition of :math:`(\\sqrt{X})^{\\dagger}` up to global phase.

A global phase difference exists between the definitions of
Expand Down Expand Up @@ -789,7 +789,7 @@ def _dagger(self) -> "Gate":
self.target_qubits[0], theta, phi
) # pylint: disable=E1130

def decompose(self):
def _base_decompose(self, *free, use_toffolis=True):
"""Decomposition of Phase-:math:`RX` gate."""
from qibo.transpiler.decompositions import ( # pylint: disable=C0415
standard_decompositions,
Expand Down Expand Up @@ -1052,7 +1052,7 @@ def _dagger(self) -> "Gate":
theta, lam, phi = tuple(-x for x in self.parameters) # pylint: disable=E1130
return self.__class__(self.target_qubits[0], theta, phi, lam)

def decompose(self) -> List[Gate]:
def _base_decompose(self, *free, use_toffolis=True) -> List[Gate]:
"""Decomposition of :math:`U_{3}` up to global phase.

A global phase difference exists between the definitions of
Expand Down Expand Up @@ -1154,7 +1154,7 @@ def clifford(self):
def qasm_label(self):
return "cx"

def decompose(self, *free, use_toffolis: bool = True) -> List[Gate]:
def _base_decompose(self, *free, use_toffolis: bool = True) -> List[Gate]:
q0, q1 = self.control_qubits[0], self.target_qubits[0]
return [self.__class__(q0, q1)]

Expand Down Expand Up @@ -1194,7 +1194,7 @@ def clifford(self):
def qasm_label(self):
return "cy"

def decompose(self) -> List[Gate]:
def _base_decompose(self, *free, use_toffolis=True) -> List[Gate]:
"""Decomposition of :math:`\\text{CY}` gate.

Decompose :math:`\\text{CY}` gate into :class:`qibo.gates.SDG` in
Expand Down Expand Up @@ -1247,7 +1247,7 @@ def hamming_weight(self):
def qasm_label(self):
return "cz"

def decompose(self) -> List[Gate]:
def _base_decompose(self, *free, use_toffolis=True) -> List[Gate]:
"""Decomposition of :math:`\\text{CZ}` gate.

Decompose :math:`\\text{CZ}` gate into :class:`qibo.gates.H` in
Expand Down Expand Up @@ -1292,7 +1292,7 @@ def __init__(self, q0, q1):
def qasm_label(self):
return "csx"

def decompose(self, *free, use_toffolis: bool = True) -> List[Gate]:
def _base_decompose(self, *free, use_toffolis: bool = True) -> List[Gate]:
""""""
from qibo.transpiler.decompositions import ( # pylint: disable=C0415
standard_decompositions,
Expand Down Expand Up @@ -1336,7 +1336,7 @@ def __init__(self, q0, q1):
def qasm_label(self):
return "csxdg"

def decompose(self, *free, use_toffolis: bool = True) -> List[Gate]:
def _base_decompose(self, *free, use_toffolis: bool = True) -> List[Gate]:
""""""
from qibo.transpiler.decompositions import ( # pylint: disable=C0415
standard_decompositions,
Expand Down Expand Up @@ -1457,7 +1457,7 @@ def hamming_weight(self):
def qasm_label(self):
return "cry"

def decompose(self) -> List[Gate]:
def _base_decompose(self, *free, use_toffolis=True) -> List[Gate]:
"""Decomposition of :math:`\\text{CRY}` gate."""
from qibo.transpiler.decompositions import ( # pylint: disable=C0415
standard_decompositions,
Expand Down Expand Up @@ -1856,10 +1856,10 @@ def hamming_weight(self):
def qasm_label(self):
return "fswap"

def decompose(self, *free, use_toffolis: bool = True) -> List[Gate]:
def _base_decompose(self, *free, use_toffolis: bool = True) -> List[Gate]:
""""""
q0, q1 = self.target_qubits
return [X(q1)] + GIVENS(q0, q1, np.pi / 2).decompose() + [X(q0)]
return [X(q1)] + GIVENS(q0, q1, np.pi / 2)._base_decompose() + [X(q0)]


class fSim(ParametrizedGate):
Expand Down Expand Up @@ -2185,7 +2185,7 @@ def __init__(self, q0, q1, theta, trainable=True):
def hamming_weight(self):
return _is_hamming_weight_given_angle(self.parameters[0])

def decompose(self, *free, use_toffolis: bool = True) -> List[Gate]:
def _base_decompose(self, *free, use_toffolis: bool = True) -> List[Gate]:
""""""
from qibo.transpiler.decompositions import ( # pylint: disable=C0415
standard_decompositions,
Expand Down Expand Up @@ -2227,7 +2227,7 @@ def __init__(self, q0, q1, theta, trainable=True):
def hamming_weight(self):
return True

def decompose(self, *free, use_toffolis: bool = True) -> List[Gate]:
def _base_decompose(self, *free, use_toffolis: bool = True) -> List[Gate]:
"""Decomposition of :math:`\\text{R_{XX-YY}}` up to global phase.

This decomposition has a global phase difference with respect to
Expand Down Expand Up @@ -2354,7 +2354,7 @@ def _dagger(self) -> "Gate":
""""""
return self.__class__(*self.target_qubits, -self.parameters[0])

def decompose(self, *free, use_toffolis: bool = True) -> List[Gate]:
def _base_decompose(self, *free, use_toffolis: bool = True) -> List[Gate]:
"""Decomposition of RBS gate according to `ArXiv:2109.09685
<https://arxiv.org/abs/2109.09685>`_."""
from qibo.transpiler.decompositions import ( # pylint: disable=C0415
Expand Down Expand Up @@ -2412,7 +2412,7 @@ def _dagger(self) -> "Gate":
""""""
return self.__class__(*self.target_qubits, -self.parameters[0])

def decompose(self, *free, use_toffolis: bool = True) -> List[Gate]:
def _base_decompose(self, *free, use_toffolis: bool = True) -> List[Gate]:
"""Decomposition of RBS gate according to `ArXiv:2109.09685
<https://arxiv.org/abs/2109.09685>`_."""
from qibo.transpiler.decompositions import ( # pylint: disable=C0415
Expand Down Expand Up @@ -2453,7 +2453,7 @@ def __init__(self, q0, q1):
def clifford(self):
return True

def decompose(self, *free, use_toffolis: bool = True) -> List[Gate]:
def _base_decompose(self, *free, use_toffolis: bool = True) -> List[Gate]:
"""Decomposition of :math:`\\textup{ECR}` gate up to global phase.

A global phase difference exists between the definitions of
Expand Down Expand Up @@ -2506,7 +2506,7 @@ def __init__(self, q0, q1, q2):
def qasm_label(self):
return "ccx"

def decompose(self, *free, use_toffolis: bool = True) -> List[Gate]:
def _base_decompose(self, *free, use_toffolis: bool = True) -> List[Gate]:
"""Decomposition of :math:`\\text{TOFFOLI}` gate.

Decompose :math:`\\text{TOFFOLI}` gate into :class:`qibo.gates.CNOT` gates,
Expand Down Expand Up @@ -2537,7 +2537,7 @@ def congruent(self, use_toffolis: bool = True) -> List[Gate]:
applying the original ``TOFFOLI`` gate.
"""
if use_toffolis:
return self.decompose()
return self._base_decompose()

control0, control1 = self.control_qubits
target = self.target_qubits[0]
Expand Down Expand Up @@ -2592,7 +2592,7 @@ def hamming_weight(self):
def qasm_label(self):
return "ccz"

def decompose(self) -> List[Gate]:
def _base_decompose(self, *free, use_toffolis=True) -> List[Gate]:
"""Decomposition of :math:`\\text{CCZ}` gate.

Decompose :math:`\\text{CCZ}` gate into :class:`qibo.gates.H` in
Expand Down Expand Up @@ -2712,7 +2712,7 @@ def __init__(
def hamming_weight(self):
return len(self.init_args[0]) == len(self.init_args[1])

def decompose(self) -> List[Gate]:
def _base_decompose(self, *free, use_toffolis=True) -> List[Gate]:
"""Decomposition of :math:`\\text{gRBS}` gate.

Decompose :math:`\\text{gRBS}` gate into :class:`qibo.gates.X`, :class:`qibo.gates.CNOT`,
Expand Down
Loading