Skip to content

Commit

Permalink
Merge pull request #1373 from qiboteam/grbs
Browse files Browse the repository at this point in the history
Add `gates.GeneralizedRBS` gate
  • Loading branch information
scarrazza authored Aug 15, 2024
2 parents 37e1010 + 3e83876 commit 4f57f37
Show file tree
Hide file tree
Showing 7 changed files with 299 additions and 73 deletions.
9 changes: 9 additions & 0 deletions doc/source/api-reference/qibo.rst
Original file line number Diff line number Diff line change
Expand Up @@ -944,6 +944,15 @@ Deutsch
:members:
:member-order: bysource


Generalized Reconfigurable Beam Splitter (RBS)
""""""""""""""""""""""""""""""""""""""""""""""

.. autoclass:: qibo.gates.GeneralizedRBS
:members:
:member-order: bysource


Arbitrary unitary
"""""""""""""""""

Expand Down
157 changes: 86 additions & 71 deletions poetry.lock

Large diffs are not rendered by default.

25 changes: 25 additions & 0 deletions src/qibo/backends/npmatrices.py
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,31 @@ def DEUTSCH(self, theta):
dtype=self.dtype,
)

def GeneralizedRBS(self, qubits_in, qubits_out, theta, phi):
theta = self._cast_parameter(theta)
phi = self._cast_parameter(phi)
bitstring_length = len(qubits_in) + len(qubits_out)
integer_in = "".join(
["1" if k in qubits_in else "0" for k in range(bitstring_length)]
)
integer_in = int(integer_in, 2)
integer_out = "".join(
["1" if k in qubits_out else "0" for k in range(bitstring_length)]
)
integer_out = int(integer_out, 2)

matrix = [
[1 + 0j if l == k else 0j for l in range(2**bitstring_length)]
for k in range(2**bitstring_length)
]
exp, sin, cos = self.np.exp(1j * phi), self.np.sin(theta), self.np.cos(theta)
matrix[integer_in][integer_in] = exp * cos
matrix[integer_in][integer_out] = -exp * sin
matrix[integer_out][integer_in] = self.np.conj(exp) * sin
matrix[integer_out][integer_out] = self.np.conj(exp) * cos

return self._cast(matrix, dtype=self.dtype)

def Unitary(self, u):
return self.np.array(u, dtype=self.dtype, copy=False)

Expand Down
11 changes: 10 additions & 1 deletion src/qibo/backends/numpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,16 @@ def matrix(self, gate):
def matrix_parametrized(self, gate):
"""Convert a parametrized gate to its matrix representation in the computational basis."""
name = gate.__class__.__name__
_matrix = getattr(self.matrices, name)(*gate.parameters)
_matrix = getattr(self.matrices, name)
if name == "GeneralizedRBS":
_matrix = _matrix(
qubits_in=gate.init_args[0],
qubits_out=gate.init_args[1],
theta=gate.init_kwargs["theta"],
phi=gate.init_kwargs["phi"],
)
else:
_matrix = _matrix(*gate.parameters)
return self.cast(_matrix, dtype=_matrix.dtype)

def matrix_fused(self, fgate):
Expand Down
74 changes: 73 additions & 1 deletion src/qibo/gates/gates.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import math
from typing import List
from typing import List, Tuple, Union

import numpy as np

Expand Down Expand Up @@ -2473,6 +2473,78 @@ def __init__(self, q0, q1, q2, theta, trainable=True):
self.init_kwargs = {"theta": theta, "trainable": trainable}


class GeneralizedRBS(ParametrizedGate):
"""The generalized (complex) Reconfigurable Beam Splitter gate (:math:`\\text{gRBS}`).
Given a register called ``qubits_in`` containing :math:`m` qubits and a
register named ``qubits_out`` containing :math:`m'` qubits, the :math:`\\text{gRBS}`
is a :math:`(m + m')`-qubit gate that has the following matrix representation:
.. math::
\\begin{pmatrix}
I & & & & \\\\
& e^{-i\\phi}\\cos\\theta & & e^{-i\\phi}\\sin\\theta & \\\\
& & I' & & \\\\
& -e^{i\\phi}\\sin\\theta & & e^{i\\phi}\\cos\\theta & \\\\
& & & & I\\\\
\\end{pmatrix} \\,\\, ,
where :math:`I` and :math:`I'` are, respectively, identity matrices of size
:math:`2^{m} - 1` and :math:`2^{m}(2^{m'} - 2)`.
This unitary matrix is also known as a
`Givens rotation <https://en.wikipedia.org/wiki/Givens_rotation>`_.
References:
1. R. M. S. Farias, T. O. Maciel, G. Camilo, R. Lin, S. Ramos-Calderer, and L. Aolita,
*Quantum encoder for fixed Hamming-weight subspaces*.
`arXiv:2405.20408 [quant-ph] <https://arxiv.org/abs/2405.20408>`_
Args:
qubits_in (tuple or list): ids of "input" qubits.
qubits_out (tuple or list): ids of "output" qubits.
theta (float): the rotation angle.
phi (float): the phase angle. Defaults to :math:`0.0`.
trainable (bool): whether gate parameter can be updated using
:meth:`qibo.models.circuit.AbstractCircuit.set_parameters`.
Defaults to ``True``.
"""

def __init__(
self,
qubits_in: Union[Tuple[int], List[int]],
qubits_out: Union[Tuple[int], List[int]],
theta: float,
phi: float = 0.0,
trainable: bool = True,
):
super().__init__(trainable)
self.name = "grbs"
self.draw_label = "gRBS"
self.target_qubits = tuple(qubits_in) + tuple(qubits_out)
self.unitary = True

self.parameter_names = "theta"
self.parameters = theta, phi
self.nparams = 2

self.init_args = [qubits_in, qubits_out]
self.init_kwargs = {"theta": theta, "phi": phi, "trainable": trainable}

def decompose(self) -> List[Gate]:
"""Decomposition of :math:`\\text{gRBS}` gate.
Decompose :math:`\\text{gRBS}` gate into :class:`qibo.gates.X`, :class:`qibo.gates.CNOT`,
:class:`qibo.gates.RY`, and :class:`qibo.gates.RZ`.
"""
from qibo.transpiler.decompositions import ( # pylint: disable=C0415
standard_decompositions,
)

return standard_decompositions(self)


class Unitary(ParametrizedGate):
"""Arbitrary unitary gate.
Expand Down
54 changes: 54 additions & 0 deletions src/qibo/transpiler/decompositions.py
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,38 @@ def _u3_to_gpi2(t, p, l):
)


def _decomposition_generalized_RBS(ins, outs, theta, phi, controls):
"""Generalized RBS gate as in Fig. 2 of arXiv:2405.20408"""
rotation_controls = ins[:-1] + outs
if controls is not None:
rotation_controls += controls

list_gates = []
list_gates.append(gates.X(ins[-1]))
list_gates.append(gates.X(outs[0]))
for target in ins[:-1]:
list_gates.append(gates.CNOT(ins[-1], target))
for target in outs[1:][::-1]:
list_gates.append(gates.CNOT(outs[0], target))
list_gates.append(gates.X(ins[-1]))
list_gates.append(gates.X(outs[0]))
list_gates.append(gates.CNOT(ins[-1], outs[0]))
list_gates.append(gates.RY(ins[-1], -2 * theta).controlled_by(*rotation_controls))
if phi != 0.0:
list_gates.append(gates.RZ(ins[-1], 2 * phi).controlled_by(*rotation_controls))
list_gates.append(gates.CNOT(ins[-1], outs[0]))
list_gates.append(gates.X(outs[0]))
list_gates.append(gates.X(ins[-1]))
for target in outs[1:]:
list_gates.append(gates.CNOT(outs[0], target))
for target in ins[:-1][::-1]:
list_gates.append(gates.CNOT(ins[-1], target))
list_gates.append(gates.X(outs[0]))
list_gates.append(gates.X(ins[-1]))

return list_gates


# standard gate decompositions used by :meth:`qibo.gates.gates.Gate.decompose`
standard_decompositions = GateDecompositions()
standard_decompositions.add(gates.SX, [gates.RX(0, np.pi / 2, trainable=False)])
Expand Down Expand Up @@ -502,3 +534,25 @@ def _u3_to_gpi2(t, p, l):
gates.CNOT(0, 1),
],
)
standard_decompositions.add(
gates.GeneralizedRBS,
lambda gate: _decomposition_generalized_RBS(
ins=list(range(len(gate.init_args[0]))),
outs=list(
range(
len(gate.init_args[0]),
len(gate.init_args[0]) + len(gate.init_args[1]),
)
),
theta=gate.init_kwargs["theta"],
phi=gate.init_kwargs["phi"],
controls=list(
range(
len(gate.init_args[0]) + len(gate.init_args[1]),
len(gate.init_args[0])
+ len(gate.init_args[1])
+ len(gate.control_qubits),
)
),
),
)
42 changes: 42 additions & 0 deletions tests/test_gates_gates.py
Original file line number Diff line number Diff line change
Expand Up @@ -1311,6 +1311,48 @@ def test_deutsch(backend):
assert gates.DEUTSCH(0, 1, 2, theta).unitary


def test_generalized_rbs(backend):
theta, phi = 0.1234, 0.4321
qubits_in, qubits_out = [0, 1], [2, 3]
nqubits = len(qubits_in + qubits_out)
integer_in = "".join(["1" if k in qubits_in else "0" for k in range(nqubits)])
integer_out = "".join(["1" if k in qubits_out else "0" for k in range(nqubits)])
integer_in, integer_out = int(integer_in, 2), int(integer_out, 2)

initial_state = random_statevector(2**nqubits, backend=backend)
final_state = apply_gates(
backend,
[gates.GeneralizedRBS(qubits_in, qubits_out, theta, phi)],
nqubits=nqubits,
initial_state=initial_state,
)
# test decomposition
final_state_decompose = apply_gates(
backend,
gates.GeneralizedRBS(qubits_in, qubits_out, theta, phi).decompose(),
nqubits=nqubits,
initial_state=initial_state,
)

matrix = np.eye(2**nqubits, dtype=complex)
exp, sin, cos = np.exp(1j * phi), np.sin(theta), np.cos(theta)
matrix[integer_in, integer_in] = exp * cos
matrix[integer_in, integer_out] = -exp * sin
matrix[integer_out, integer_in] = np.conj(exp) * sin
matrix[integer_out, integer_out] = np.conj(exp) * cos
matrix = backend.cast(matrix, dtype=matrix.dtype)

target_state = matrix @ initial_state
backend.assert_allclose(final_state, target_state)
backend.assert_allclose(final_state_decompose, target_state)

with pytest.raises(NotImplementedError):
gates.GeneralizedRBS(qubits_in, qubits_out, theta, phi).qasm_label

assert not gates.GeneralizedRBS(qubits_in, qubits_out, theta, phi).clifford
assert gates.GeneralizedRBS(qubits_in, qubits_out, theta, phi).unitary


@pytest.mark.parametrize("nqubits", [2, 3])
def test_unitary(backend, nqubits):
initial_state = np.ones(2**nqubits) / np.sqrt(2**nqubits)
Expand Down

0 comments on commit 4f57f37

Please sign in to comment.