"""Module with functions that encode classical data into quantum circuits."""
import math
from inspect import signature
from re import finditer
from typing import List, Optional, Union
import numpy as np
from scipy.special import binom
from qibo import gates
from qibo.backends import _check_backend
from qibo.config import raise_error
from qibo.models.circuit import Circuit
[docs]def comp_basis_encoder(
basis_element: Union[int, str, list, tuple], nqubits: Optional[int] = None, **kwargs
):
"""Create circuit that performs encoding of bitstrings into computational basis states.
Args:
basis_element (int or str or list or tuple): bitstring to be encoded.
If ``int``, ``nqubits`` must be specified.
If ``str``, must be composed of only :math:`0`s and :math:`1`s.
If ``list`` or ``tuple``, must be composed of :math:`0`s and
:math:`1`s as ``int`` or ``str``.
nqubits (int, optional): total number of qubits in the circuit.
If ``basis_element`` is ``int``, ``nqubits`` must be specified.
If ``nqubits`` is ``None``, ``nqubits`` defaults to length of ``basis_element``.
Defaults to ``None``.
kwargs (dict, optional): Additional arguments used to initialize a Circuit object.
For details, see the documentation of :class:`qibo.models.circuit.Circuit`.
Returns:
:class:`qibo.models.circuit.Circuit`: Circuit encoding computational basis element.
"""
if not isinstance(basis_element, (int, str, list, tuple)):
raise_error(
TypeError,
"basis_element must be either type int or str or list or tuple, "
+ f"but it is type {type(basis_element)}.",
)
if isinstance(basis_element, (str, list, tuple)):
if any(elem not in ["0", "1", 0, 1] for elem in basis_element):
raise_error(ValueError, "all elements must be 0 or 1.")
if nqubits is not None and not isinstance(nqubits, int):
raise_error(
TypeError, f"``nqubits`` must be type int, but it is type {type(nqubits)}."
)
if nqubits is None:
if isinstance(basis_element, int):
raise_error(
ValueError,
"``nqubits`` must be specified when ``basis_element`` is type int.",
)
else:
nqubits = len(basis_element)
if isinstance(basis_element, int):
basis_element = f"{basis_element:0{nqubits}b}"
if isinstance(basis_element, (str, tuple)):
basis_element = list(basis_element)
basis_element = list(map(int, basis_element))
circuit = Circuit(nqubits, **kwargs)
for qubit, elem in enumerate(basis_element):
if elem == 1:
circuit.add(gates.X(qubit))
return circuit
[docs]def phase_encoder(data, rotation: str = "RY", backend=None, **kwargs):
"""Create circuit that performs the phase encoding of ``data``.
Args:
data (ndarray or list): :math:`1`-dimensional array of phases to be loaded.
rotation (str, optional): If ``"RX"``, uses :class:`qibo.gates.gates.RX` as rotation.
If ``"RY"``, uses :class:`qibo.gates.gates.RY` as rotation.
If ``"RZ"``, uses :class:`qibo.gates.gates.RZ` as rotation.
Defaults to ``"RY"``.
kwargs (dict, optional): Additional arguments used to initialize a Circuit object.
For details, see the documentation of :class:`qibo.models.circuit.Circuit`.
Returns:
:class:`qibo.models.circuit.Circuit`: Circuit that loads ``data`` in phase encoding.
"""
if not isinstance(rotation, str):
raise_error(
TypeError,
f"``rotation`` must be type str, but it is type {type(rotation)}.",
)
backend = _check_backend(backend)
if isinstance(data, list):
# TODO: Fix this mess with qibo native dtypes
try:
type_test = data[0].dtype
except AttributeError: # pragma: no cover
type_test = type(data[0])
data = backend.cast(data, dtype=type_test)
if rotation not in ["RX", "RY", "RZ"]:
raise_error(ValueError, f"``rotation`` {rotation} not found.")
nqubits = len(data)
gate = getattr(gates, rotation.upper())
circuit = Circuit(nqubits, **kwargs)
circuit.add(gate(qubit, 0.0) for qubit in range(nqubits))
circuit.set_parameters(data)
return circuit
[docs]def sparse_encoder(data, nqubits: int = None, backend=None, **kwargs):
"""Create circuit that encodes :math:`1`-dimensional data in a subset of amplitudes
of the computational basis.
Consider a sparse-access model, where for a data vector
:math:`\\mathbf{x} \\in \\mathbb{C}^{d}`, with :math:`d = 2^{n}` and
:math:`s` non-zero amplitudes, one has access to the data vector
:math:`\\mathbf{y}` of the form
.. math::
\\mathbf{y} = \\left\\{ (b_{1}, x_{1}), \\, \\dots, \\, (b_{s}, x_{s}) \\right\\} \\, ,
where :math:`\\{x_{j}\\}_{j\\in[s]}` is the non-zero components of :math:`\\mathbf{x}`
and :math:`\\{b_{j}\\}_{j\\in[s]}` is the set of addresses associated with these values.
Then, this function generates a quantum circuit :math:`s\\text{-}\\mathrm{Load}` that encodes
:math:`\\mathbf{x}` in the amplitudes of an :math:`n`-qubit quantum state as
.. math::
s\\text{-}\\mathrm{Load}(\\mathbf{y}) \\, \\ket{0}^{\\otimes \\, n} = \\sum_{j\\in[s]} \\,
\\frac{x_{j}}{\\|\\mathbf{x}\\|_{F}} \\, \\ket{b_{j}} \\, ,
where :math:`\\|\\cdot\\|_{F}` is the Frobenius norm.
Resulting circuit parametrizes ``data`` in hyperspherical coordinates
in the :math:`(2^{n} - 1)`-unit sphere.
Args:
data (ndarray or list or zip): sequence of tuples of the form :math:`(b_{j}, x_{j})`.
The addresses :math:`b_{j}` can be either integers or in bitstring
format of size :math:`n`.
nqubits (int, optional): total number of qubits in the system.
To be used when :math:`b_j` are integers. If :math:`b_j` are strings and
``nqubits`` is ``None``, defaults to the length of the strings :math:`b_{j}`.
Defaults to ``None``.
Returns:
:class:`qibo.models.circuit.Circuit`: Circuit that loads sparse :math:`\\mathbf{x}`.
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*,
`Phys. Rev. Applied 23, 044014 (2025) <https://doi.org/10.1103/PhysRevApplied.23.044014>`_.
2. `Hyperpherical coordinates <https://en.wikipedia.org/wiki/N-sphere>`_.
"""
from qibo.quantum_info.utils import ( # pylint: disable=import-outside-toplevel
hamming_distance,
hamming_weight,
)
backend = _check_backend(backend)
if isinstance(data, zip):
data = list(data)
# TODO: Fix this mess with qibo native dtypes
try:
type_test = bool("int" in str(data[0][0].dtype))
except AttributeError:
type_test = bool("int" in str(type(data[0][0])))
if type_test and nqubits is None:
raise_error(
ValueError,
"``nqubits`` must be specified when computational basis states are "
+ "indidated by integers.",
)
if isinstance(data[0][0], str) and nqubits is None:
nqubits = len(data[0][0])
_data_test = data[0][1]
_data_test = (
_data_test.dtype if "array" in str(type(_data_test)) else type(_data_test)
)
complex_data = bool("complex" in str(_data_test))
# sort data by HW of the bitstrings
data_sorted, bitstrings_sorted = _sort_data_sparse(data, nqubits, backend)
# calculate phases
_data_sorted = backend.np.abs(data_sorted) if complex_data else data_sorted
thetas = _generate_rbs_angles(
_data_sorted, architecture="diagonal", backend=backend
)
phis = backend.np.zeros(len(thetas) + 1)
if complex_data:
phis[0] = _angle_mod_two_pi(-backend.np.angle(data_sorted[0]))
for k in range(1, len(phis)):
phis[k] = _angle_mod_two_pi(
-backend.np.angle(data_sorted[k]) + backend.np.sum(phis[:k])
)
phis = backend.cast(phis, dtype=phis[0].dtype)
# marking qubits that have suffered the action of a gate
initial_string = [int(bit) for bit in bitstrings_sorted[0]]
circuit = comp_basis_encoder(initial_string, nqubits=nqubits, **kwargs)
touched_qubits = list(np.nonzero(initial_string)[0])
for b_1, b_0, theta, phi in zip(
bitstrings_sorted[1:], bitstrings_sorted[:-1], thetas, phis
):
hw_0, hw_1 = hamming_weight(b_0), hamming_weight(b_1)
distance = hamming_distance(b_1, b_0)
difference = b_1 - b_0
ones, new_ones = (
list(backend.np.argsort(b_0)[-hw_0:]),
list(backend.np.argsort(b_1)[-hw_1:]),
)
ones, new_ones = {int(elem) for elem in ones}, {int(elem) for elem in new_ones}
controls = (set(ones) & set(new_ones)) & set(touched_qubits)
gate = _get_gate_sparse(
distance,
difference,
touched_qubits,
complex_data,
controls,
hw_0,
hw_1,
theta,
phi,
)
circuit.add(gate)
if complex_data:
hw_0 = hamming_weight(bitstrings_sorted[-2])
hw_1 = hamming_weight(bitstrings_sorted[-1])
correction = _get_phase_gate_correction_sparse(
bitstrings_sorted[-1],
bitstrings_sorted[-2],
nqubits,
data_sorted[-1],
data_sorted[-2],
circuit,
phis,
)
if hw_1 == nqubits and hw_0 == nqubits - 1:
circuit.queue = correction
else:
circuit.add(correction)
return circuit
[docs]def binary_encoder(
data, parametrization: str = "hyperspherical", backend=None, **kwargs
):
"""Create circuit that encodes :math:`1`-dimensional data in all amplitudes
of the computational basis.
Given data vector :math:`\\mathbf{x} \\in \\mathbb{C}^{d}`, with :math:`d = 2^{n}`,
this function generates a quantum circuit :math:`\\mathrm{Load}` that encodes
:math:`\\mathbf{x}` in the amplitudes of an :math:`n`-qubit quantum state as
.. math::
\\mathrm{Load}(\\mathbf{x}) \\, \\ket{0}^{\\otimes \\, n} = \\sum_{j=0}^{d-1} \\,
\\frac{x_{j}}{\\|\\mathbf{x}\\|_{F}} \\, \\ket{b_{j}} \\, ,
where :math:`b_{j} \\in \\{0, \\, 1\\}^{\\otimes \\, n}` is the :math:`n`-bit representation
of the integer :math:`j`, :math:`\\|\\cdot\\|_{F}` is the Frobenius norm.
Resulting circuit parametrizes ``data`` in either ``hyperspherical`` or ``Hopf`` coordinates
in the :math:`(2^{n} - 1)`-unit sphere.
Args:
data (ndarray): :math:`1`-dimensional array or length :math:`d = 2^{n}`
to be loaded in the amplitudes of a :math:`n`-qubit quantum state.
Returns:
:class:`qibo.models.circuit.Circuit`: Circuit that loads ``data`` in binary encoding.
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*
`Phys. Rev. Applied 23, 044014 (2025) <https://doi.org/10.1103/PhysRevApplied.23.044014>`_.
2. `Hyperpherical coordinates <https://en.wikipedia.org/wiki/N-sphere>`_.
3. H. S. Cohl, *Fourier, Gegenbauer and Jacobi expansions for a power-law fundamental
solution of the polyharmonic equation and polyspherical addition theorems*, `Symmetry,
Integrability and Geometry: Methods and Applications 10.3842/sigma.2013.042 (2013)
<https://arxiv.org/abs/1209.6047>`_.
"""
backend = _check_backend(backend)
dims = len(data)
nqubits = float(np.log2(dims))
if not nqubits.is_integer():
raise_error(ValueError, "`data` size must be a power of 2.")
nqubits = int(nqubits)
complex_data = bool(
"complex" in str(data.dtype)
) # backend-agnostic way of checking the dtype
if parametrization == "hopf":
return _binary_encoder_hopf(
data, nqubits, complex_data=complex_data, backend=backend, **kwargs
)
return _binary_encoder_hyperspherical(
data, nqubits, complex_data=complex_data, backend=backend, **kwargs
)
[docs]def unary_encoder(data, architecture: str = "tree", backend=None, **kwargs):
"""Create circuit that performs the (deterministic) unary encoding of ``data``.
Args:
data (ndarray): :math:`1`-dimensional array of data to be loaded.
architecture(str, optional): circuit architecture used for the unary loader.
If ``diagonal``, uses a ladder-like structure.
If ``tree``, uses a binary-tree-based structure.
Defaults to ``tree``.
kwargs (dict, optional): Additional arguments used to initialize a Circuit object.
For details, see the documentation of :class:`qibo.models.circuit.Circuit`.
Returns:
:class:`qibo.models.circuit.Circuit`: Circuit that loads ``data`` in unary representation.
"""
backend = _check_backend(backend)
if isinstance(data, list):
data = backend.cast(data, dtype=type(data[0]))
if not isinstance(architecture, str):
raise_error(
TypeError,
f"``architecture`` must be type str, but it is type {type(architecture)}.",
)
if architecture not in ["diagonal", "tree"]:
raise_error(ValueError, f"``architecture`` {architecture} not found.")
if architecture == "tree" and not math.log2(data.shape[0]).is_integer():
raise_error(
ValueError,
"When ``architecture = 'tree'``, len(data) must be a power of 2. "
+ f"However, it is {len(data)}.",
)
nqubits = len(data)
circuit = Circuit(nqubits, **kwargs)
circuit.add(gates.X(nqubits - 1))
circuit_rbs, _ = _generate_rbs_pairs(nqubits, architecture=architecture, **kwargs)
circuit += circuit_rbs
# calculating phases and setting circuit parameters
phases = _generate_rbs_angles(data, architecture, nqubits, backend=backend)
circuit.set_parameters(phases)
return circuit
[docs]def unary_encoder_random_gaussian(
nqubits: int, architecture: str = "tree", seed=None, backend=None, **kwargs
):
"""Create a circuit that performs the unary encoding of a random Gaussian state.
At depth :math:`h` of the tree architecture, the angles :math:`\\theta_{k}
\\in [0, 2\\pi]` of the the gates :math:`RBS(\\theta_{k})` are sampled from
the following probability density function:
.. math::
p_{h}(\\theta) = \\frac{1}{2} \\, \\frac{\\Gamma(2^{h-1})}{\\Gamma^{2}(2^{h-2})} \\,
\\left|\\sin(\\theta) \\, \\cos(\\theta)\\right|^{2^{h-1} - 1} \\, ,
where :math:`\\Gamma(\\cdot)` is the
`Gamma function <https://en.wikipedia.org/wiki/Gamma_function>`_.
Args:
nqubits (int): number of qubits.
architecture(str, optional): circuit architecture used for the unary loader.
If ``tree``, uses a binary-tree-based structure.
Defaults to ``tree``.
seed (int or :class:`numpy.random.Generator`, optional): Either a generator of
random numbers or a fixed seed to initialize a generator. If ``None``,
initializes a generator with a random seed. Defaults to ``None``.
kwargs (dict, optional): Additional arguments used to initialize a Circuit object.
For details, see the documentation of :class:`qibo.models.circuit.Circuit`.
Returns:
:class:`qibo.models.circuit.Circuit`: Circuit that loads a random Gaussian
array in unary representation.
References:
1. A. Bouland, A. Dandapani, and A. Prakash, *A quantum spectral method for simulating
stochastic processes, with applications to Monte Carlo*.
`arXiv:2303.06719v1 [quant-ph] <https://arxiv.org/abs/2303.06719>`_
"""
backend = _check_backend(backend)
if not isinstance(nqubits, int):
raise_error(
TypeError, f"nqubits must be type int, but it is type {type(nqubits)}."
)
if nqubits <= 0.0:
raise_error(
ValueError, f"nqubits must be a positive integer, but it is {nqubits}."
)
if not isinstance(architecture, str):
raise_error(
TypeError,
f"``architecture`` must be type str, but it is type {type(architecture)}.",
)
if architecture != "tree":
raise_error(
NotImplementedError,
"Currently, this function only accepts ``architecture=='tree'``.",
)
if not math.log2(nqubits).is_integer():
raise_error(ValueError, f"nqubits must be a power of 2, but it is {nqubits}.")
if (
seed is not None
and not isinstance(seed, int)
and not isinstance(seed, np.random.Generator)
):
raise_error(
TypeError, "seed must be either type int or numpy.random.Generator."
)
from qibo.quantum_info.random_ensembles import ( # pylint: disable=C0415
_ProbabilityDistributionGaussianLoader,
)
local_state = (
np.random.default_rng(seed) if seed is None or isinstance(seed, int) else seed
)
sampler = _ProbabilityDistributionGaussianLoader(
a=0, b=2 * math.pi, seed=local_state
)
circuit = Circuit(nqubits, **kwargs)
circuit.add(gates.X(nqubits - 1))
circuit_rbs, pairs_rbs = _generate_rbs_pairs(nqubits, architecture, **kwargs)
circuit += circuit_rbs
phases = []
for depth, row in enumerate(pairs_rbs, 1):
phases.extend(sampler.rvs(depth=depth, size=len(row)))
phases = backend.cast(phases, dtype=type(phases[0]))
circuit.set_parameters(phases)
return circuit
[docs]def hamming_weight_encoder(
data,
nqubits: int,
weight: int,
full_hwp: bool = False,
optimize_controls: bool = True,
phase_correction: bool = True,
initial_string=None,
backend=None,
**kwargs,
):
"""Create circuit that encodes ``data`` in the Hamming-weight-:math:`k` basis of ``nqubits``.
Let :math:`\\mathbf{x}` be a :math:`1`-dimensional array of size :math:`d = \\binom{n}{k}` and
:math:`B_{k} \\equiv \\{ \\ket{b_{j}} : b_{j} \\in \\{0, 1\\}^{\\otimes n} \\,\\, \\text{and}
\\,\\, |b_{j}| = k \\}` be a set of :math:`d` computational basis states of :math:`n` qubits
that are represented by bitstrings of Hamming weight :math:`k`. Then, an amplitude encoder
in the basis :math:`B_{k}` is an :math:`n`-qubit parameterized quantum circuit
:math:`\\operatorname{Load}_{B_{k}}` such that
.. math::
\\operatorname{Load}(\\mathbf{x}) \\, \\ket{0}^{\\otimes n} = \\frac{1}{\\|\\mathbf{x}\\|}
\\, \\sum_{j = 1}^{d} \\, x_{j} \\, \\ket{b_{j}}
Args:
data (ndarray): :math:`1`-dimensional array of data to be loaded.
nqubits (int): number of qubits.
weight (int): Hamming weight that defines the subspace in which ``data`` will be encoded.
full_hwp (bool, optional): if ``False``, includes Pauli-:math:`X` gates that prepare the
first bitstring of Hamming weight ``k = weight``. If ``True``, circuit is full
Hamming weight preserving. Defaults to ```False``.
optimize_controls (bool, optional): if ``True``, removes unnecessary controlled operations.
Defaults to ``True``.
kwargs (dict, optional): Additional arguments used to initialize a Circuit object.
For details, see the documentation of :class:`qibo.models.circuit.Circuit`.
Returns:
:class:`qibo.models.circuit.Circuit`: Circuit that loads ``data``in
Hamming-weight-:math:`k` representation.
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*
`Phys. Rev. Applied 23, 044014 (2025) <https://doi.org/10.1103/PhysRevApplied.23.044014>`_.
"""
backend = _check_backend(backend)
complex_data = bool("complex" in str(data.dtype))
if initial_string is None:
initial_string = np.array([1] * weight + [0] * (nqubits - weight))
bitstrings, targets_and_controls = _ehrlich_algorithm(initial_string)
# sort data such that the encoding is performed in lexicographical order
lex_order = [int(string, 2) for string in bitstrings]
lex_order_sorted = np.sort(np.copy(lex_order))
lex_order = [np.where(lex_order_sorted == num)[0][0] for num in lex_order]
data = data[lex_order]
del lex_order, lex_order_sorted
# Calculate all gate phases necessary to encode the amplitudes.
_data = backend.np.abs(data) if complex_data else data
thetas = _generate_rbs_angles(_data, architecture="diagonal", backend=backend)
phis = backend.np.zeros(len(thetas) + 1)
if complex_data:
phis[0] = _angle_mod_two_pi(-backend.np.angle(data[0]))
for k in range(1, len(phis)):
phis[k] = _angle_mod_two_pi(
-backend.np.angle(data[k]) + backend.np.sum(phis[:k])
)
last_qubit = nqubits - 1
circuit = Circuit(nqubits, **kwargs)
if not full_hwp:
circuit.add(
gates.X(qubit) for qubit in range(last_qubit, last_qubit - weight, -1)
)
if optimize_controls:
indices = [
int(binom(nqubits - j, weight - j)) - 1 for j in range(weight - 1, 0, -1)
]
for k, ((targets, controls), theta, phi) in enumerate(
zip(targets_and_controls, thetas, phis)
):
targets = list(last_qubit - np.asarray(targets))
controls = list(last_qubit - np.asarray(controls))
controls.sort()
if optimize_controls:
controls = list(np.asarray(controls)[k >= np.asarray(indices)])
gate = _get_gate(
[targets[0]],
[targets[1]],
controls,
theta,
phi,
complex_data,
)
circuit.add(gate)
if complex_data and phase_correction:
circuit.add(_get_phase_gate_correction(bitstrings[-1], phis[-1]))
return circuit
[docs]def entangling_layer(
nqubits: int,
architecture: str = "diagonal",
entangling_gate: Union[str, gates.Gate] = "CNOT",
closed_boundary: bool = False,
**kwargs,
):
"""Create a layer of two-qubit entangling gates.
If the chosen gate is a parametrized gate, all phases are set to :math:`0.0`.
Args:
nqubits (int): Total number of qubits in the circuit.
architecture (str, optional): Architecture of the entangling layer.
In alphabetical order, options are ``"diagonal"``, ``"even_layer"``,
``"next_nearest"``, ``"odd_layer"``, ``"pyramid"``, ``"shifted"``,
``"v"``, and ``"x"``. The ``"x"`` architecture is only defined for an even number
of qubits. Defaults to ``"diagonal"``.
entangling_gate (str or :class:`qibo.gates.Gate`, optional): Two-qubit gate to be used
in the entangling layer. If ``entangling_gate`` is a parametrized gate,
all phases are initialized as :math:`0.0`. Defaults to ``"CNOT"``.
closed_boundary (bool, optional): If ``True`` and ``architecture not in
["pyramid", "v", "x"]``, adds a closed-boundary condition to the entangling layer.
Defaults to ``False``.
kwargs (dict, optional): Additional arguments used to initialize a Circuit object.
For details, see the documentation of :class:`qibo.models.circuit.Circuit`.
Returns:
:class:`qibo.models.circuit.Circuit`: Circuit containing layer of two-qubit gates.
"""
if not isinstance(nqubits, int):
raise_error(
TypeError, f"nqubits must be type int, but it is type {type(nqubits)}."
)
if nqubits <= 0.0:
raise_error(
ValueError, f"nqubits must be a positive integer, but it is {nqubits}."
)
if not isinstance(architecture, str):
raise_error(
TypeError,
f"``architecture`` must be type str, but it is type {type(architecture)}.",
)
if architecture not in [
"diagonal",
"even_layer",
"next_nearest",
"odd_layer",
"pyramid",
"shifted",
"v",
"x",
]:
raise_error(
NotImplementedError,
f"``architecture`` {architecture} not found.",
)
if architecture == "x" and nqubits % 2 != 0.0:
raise_error(
ValueError, "``x`` architecture only defined for an even number of qubits."
)
if not isinstance(closed_boundary, bool):
raise_error(
TypeError,
f"``closed_boundary`` must be type bool, but it is type {type(closed_boundary)}.",
)
gate = (
getattr(gates, entangling_gate)
if isinstance(entangling_gate, str)
else entangling_gate
)
if gate.__name__ == "GeneralizedfSim":
raise_error(
NotImplementedError,
"This function does not support the ``GeneralizedfSim`` gate.",
)
if architecture in ["next_nearest", "pyramid", "v", "x"]:
circuit = _non_trivial_layers(
nqubits,
architecture=architecture,
entangling_gate=entangling_gate,
closed_boundary=closed_boundary,
**kwargs,
)
else:
# Finds the correct number of parameters to initialize the gate class.
parameters = list(signature(gate).parameters)
if "q2" in parameters:
raise_error(
NotImplementedError, "This function does not accept three-qubit gates."
)
# If gate is parametrized, sets all angles to 0.0
parameters = (0.0,) * (len(parameters) - 3) if len(parameters) > 2 else None
circuit = Circuit(nqubits, **kwargs)
if architecture == "diagonal":
qubits = range(nqubits - 1)
elif architecture == "even_layer":
qubits = range(0, nqubits - 1, 2)
elif architecture == "odd_layer":
qubits = range(1, nqubits - 1, 2)
else:
qubits = tuple(range(0, nqubits - 1, 2)) + tuple(range(1, nqubits - 1, 2))
circuit.add(
_parametrized_two_qubit_gate(gate, qubit, qubit + 1, parameters)
for qubit in qubits
)
if closed_boundary:
circuit.add(_parametrized_two_qubit_gate(gate, nqubits - 1, 0, parameters))
return circuit
[docs]def ghz_state(nqubits: int, **kwargs):
"""Generate an :math:`n`-qubit Greenberger-Horne-Zeilinger (GHZ) state that takes the form
.. math::
\\ket{\\text{GHZ}} = \\frac{\\ket{0}^{\\otimes n} + \\ket{1}^{\\otimes n}}{\\sqrt{2}}
where :math:`n` is the number of qubits.
Args:
nqubits (int): number of qubits :math:`n >= 2`.
kwargs (dict, optional): additional arguments used to initialize a Circuit object.
For details, see the documentation of :class:`qibo.models.circuit.Circuit`.
Returns:
:class:`qibo.models.circuit.Circuit`: Circuit that prepares the GHZ state.
"""
if nqubits < 2:
raise_error(
ValueError,
f"nqubits given as {nqubits}. nqubits needs to be >= 2.",
)
circuit = Circuit(nqubits, **kwargs)
circuit.add(gates.H(0))
circuit.add(gates.CNOT(qubit, qubit + 1) for qubit in range(nqubits - 1))
return circuit
def _generate_rbs_pairs(nqubits: int, architecture: str, **kwargs):
"""Generate list of indexes representing the RBS connections.
Creates circuit with all RBS initialised with 0.0 phase.
Args:
nqubits (int): number of qubits.
architecture(str, optional): circuit architecture used for the unary loader.
If ``diagonal``, uses a ladder-like structure.
If ``tree``, uses a binary-tree-based structure.
Defaults to ``tree``.
kwargs (dict, optional): Additional arguments used to initialize a Circuit object.
For details, see the documentation of :class:`qibo.models.circuit.Circuit`.
Returns:
(:class:`qibo.models.circuit.Circuit`, list): Circuit composed of
:class:`qibo.gates.gates.RBS` and list of indexes of target qubits per depth.
"""
if architecture == "diagonal":
pairs_rbs = np.arange(nqubits)
pairs_rbs = [[pair] for pair in zip(pairs_rbs[:-1], pairs_rbs[1:])]
if architecture == "tree":
pairs_rbs = [[(0, int(nqubits / 2))]]
indexes = list(pairs_rbs[0][0])
for depth in range(2, int(math.log2(nqubits)) + 1):
pairs_rbs_per_depth = [
[(index, index + int(nqubits / 2**depth)) for index in indexes]
]
pairs_rbs += pairs_rbs_per_depth
indexes = list(np.array(pairs_rbs_per_depth).flatten())
pairs_rbs = [
[(nqubits - 1 - a, nqubits - 1 - b) for a, b in row] for row in pairs_rbs
]
circuit = Circuit(nqubits, **kwargs)
for row in pairs_rbs:
for pair in row:
circuit.add(gates.RBS(*pair, 0.0, trainable=True))
return circuit, pairs_rbs
def _generate_rbs_angles(data, architecture: str, nqubits: int = None, backend=None):
"""Generate list of angles for RBS gates based on ``architecture``.
Args:
data (ndarray, optional): :math:`1`-dimensional array of data to be loaded.
architecture(str, optional): circuit architecture used for the unary loader.
If ``diagonal``, uses a ladder-like structure.
If ``tree``, uses a binary-tree-based structure.
Defaults to ``tree``.
nqubits (int): Number of qubits. To be used then ``architecture="tree"``.
Returns:
list: List of phases for RBS gates.
"""
backend = _check_backend(backend)
if architecture == "diagonal":
phases = [
backend.np.arctan2(backend.calculate_vector_norm(data[k + 1 :]), data[k])
for k in range(len(data) - 2)
]
phases.append(backend.np.arctan2(data[-1], data[-2]))
if architecture == "tree":
if nqubits is None: # pragma: no cover
raise_error(
TypeError,
'``nqubits`` must be specified when ``architecture=="tree"``.',
)
j_max = int(nqubits / 2)
r_array = np.zeros(nqubits - 1, dtype=float)
phases = np.zeros(nqubits - 1, dtype=float)
for j in range(1, j_max + 1):
r_array[j_max + j - 2] = math.sqrt(
data[2 * j - 1] ** 2 + data[2 * j - 2] ** 2
)
theta = math.acos(data[2 * j - 2] / r_array[j_max + j - 2])
if data[2 * j - 1] < 0.0:
theta = 2 * math.pi - theta
phases[j_max + j - 2] = theta
for j in range(j_max - 1, 0, -1):
r_array[j - 1] = math.sqrt(r_array[2 * j] ** 2 + r_array[2 * j - 1] ** 2)
phases[j - 1] = math.acos(r_array[2 * j - 1] / r_array[j - 1])
phases = backend.cast(phases, dtype=phases[0].dtype)
return phases
def _parametrized_two_qubit_gate(gate, q0, q1, params=None):
"""Return two-qubit gate initialized with or without phases."""
if params is not None:
return gate(q0, q1, *params)
return gate(q0, q1)
def _next_nearest_layer(
nqubits: int, gate, parameters, closed_boundary: bool, **kwargs
):
"""Create entangling layer with next-nearest-neighbour connectivity."""
circuit = Circuit(nqubits, **kwargs)
circuit.add(
_parametrized_two_qubit_gate(gate, qubit, qubit + 2, parameters)
for qubit in range(nqubits - 2)
)
if closed_boundary:
circuit.add(_parametrized_two_qubit_gate(gate, nqubits - 1, 0, parameters))
return circuit
def _pyramid_layer(nqubits: int, gate, parameters, **kwargs):
"""Create entangling layer in triangular shape."""
_, pairs_gates = _generate_rbs_pairs(nqubits, architecture="diagonal")
pairs_gates = pairs_gates[::-1]
circuit = Circuit(nqubits, **kwargs)
circuit.add(
_parametrized_two_qubit_gate(gate, pair[0][1], pair[0][0], parameters)
for pair in pairs_gates
)
circuit.add(
_parametrized_two_qubit_gate(gate, pair[0][1], pair[0][0], parameters)
for k in range(1, len(pairs_gates))
for pair in pairs_gates[:-k]
)
return circuit
def _v_layer(nqubits: int, gate, parameters, **kwargs):
"""Create entangling layer in V shape."""
_, pairs_gates = _generate_rbs_pairs(nqubits, architecture="diagonal")
pairs_gates = pairs_gates[::-1]
circuit = Circuit(nqubits, **kwargs)
circuit.add(
_parametrized_two_qubit_gate(gate, pair[0][1], pair[0][0], parameters)
for pair in pairs_gates
)
circuit.add(
_parametrized_two_qubit_gate(gate, pair[0][1], pair[0][0], parameters)
for pair in pairs_gates[::-1][1:]
)
return circuit
def _x_layer(nqubits, gate, parameters, **kwargs):
"""Create entangling layer in X shape."""
_, pairs_gates = _generate_rbs_pairs(nqubits, architecture="diagonal")
pairs_gates = pairs_gates[::-1]
middle = int(np.floor(len(pairs_gates) / 2))
pairs_1 = pairs_gates[:middle]
pairs_2 = pairs_gates[-middle:]
circuit = Circuit(nqubits, **kwargs)
for first, second in zip(pairs_1, pairs_2[::-1]):
circuit.add(
_parametrized_two_qubit_gate(gate, first[0][1], first[0][0], parameters)
)
circuit.add(
_parametrized_two_qubit_gate(gate, second[0][1], second[0][0], parameters)
)
circuit.add(
_parametrized_two_qubit_gate(
gate,
pairs_gates[middle][0][1],
pairs_gates[middle][0][0],
parameters,
)
)
for first, second in zip(pairs_1[::-1], pairs_2):
circuit.add(
_parametrized_two_qubit_gate(gate, first[0][1], first[0][0], parameters)
)
circuit.add(
_parametrized_two_qubit_gate(gate, second[0][1], second[0][0], parameters)
)
return circuit
def _non_trivial_layers(
nqubits: int,
architecture: str = "pyramid",
entangling_gate: Union[str, gates.Gate] = "RBS",
closed_boundary: bool = False,
**kwargs,
):
"""Create more intricate entangling layers of different shapes.
Args:
nqubits (int): number of qubits.
architecture (str, optional): Architecture of the entangling layer.
In alphabetical order, options are ``"next_nearest"``, ``"pyramid"``,
``"v"``, and ``"x"``. The ``"x"`` architecture is only defined for
an even number of qubits. Defaults to ``"pyramid"``.
entangling_gate (str or :class:`qibo.gates.Gate`, optional): Two-qubit gate to be used
in the entangling layer. If ``entangling_gate`` is a parametrized gate,
all phases are initialized as :math:`0.0`. Defaults to ``"CNOT"``.
closed_boundary (bool, optional): If ``True`` and ``architecture="next_nearest"``,
adds a closed-boundary condition to the entangling layer. Defaults to ``False``.
kwargs (dict, optional): Additional arguments used to initialize a Circuit object.
For details, see the documentation of :class:`qibo.models.circuit.Circuit`.
Returns:
:class:`qibo.models.circuit.Circuit`: Circuit containing layer of two-qubit gates.
"""
gate = (
getattr(gates, entangling_gate)
if isinstance(entangling_gate, str)
else entangling_gate
)
parameters = list(signature(gate).parameters)
parameters = (0.0,) * (len(parameters) - 3) if len(parameters) > 2 else None
if architecture == "next_nearest":
return _next_nearest_layer(nqubits, gate, parameters, closed_boundary, **kwargs)
if architecture == "v":
return _v_layer(nqubits, gate, parameters, **kwargs)
if architecture == "x":
return _x_layer(nqubits, gate, parameters, **kwargs)
return _pyramid_layer(nqubits, gate, parameters, **kwargs)
def _angle_mod_two_pi(angle):
"""Return angle mod 2pi."""
return angle % (2 * np.pi)
def _get_markers(bitstring, last_run: bool = False):
"""Subroutine of the Ehrlich algorithm."""
nqubits = len(bitstring)
markers = [len(bitstring) - 1]
for ind, value in zip(range(nqubits - 2, -1, -1), bitstring[::-1][1:]):
if value == bitstring[-1]:
markers.append(ind)
else:
break
markers = set(markers)
if not last_run:
markers = set(range(nqubits)) - markers
return markers
def _get_next_bistring(bitstring, markers, hamming_weight):
"""Subroutine of the Ehrlich algorithm."""
if len(markers) == 0: # pragma: no cover
return bitstring
new_bitstring = np.copy(bitstring)
nqubits = len(new_bitstring)
indexes = np.argsort(bitstring)
zeros, ones = np.sort(indexes[:-hamming_weight]), np.sort(indexes[-hamming_weight:])
max_index = max(markers)
nearest_one = ones[ones > max_index]
nearest_one = None if len(nearest_one) == 0 else nearest_one[0]
if new_bitstring[max_index] == 0 and nearest_one is not None:
new_bitstring[max_index] = 1
new_bitstring[nearest_one] = 0
else:
farthest_zero = zeros[zeros > max_index]
if nearest_one is not None:
farthest_zero = farthest_zero[farthest_zero < nearest_one]
farthest_zero = farthest_zero[-1]
new_bitstring[max_index] = 0
new_bitstring[farthest_zero] = 1
markers.remove(max_index)
last_run = _get_markers(new_bitstring, last_run=True)
markers = markers | (set(range(max_index + 1, nqubits)) - last_run)
new_ones = np.argsort(new_bitstring)[-hamming_weight:]
controls = list(set(ones) & set(new_ones))
difference = new_bitstring - bitstring
qubits = [np.where(difference == -1)[0][0], np.where(difference == 1)[0][0]]
return new_bitstring, markers, [qubits, controls]
def _ehrlich_algorithm(initial_string, return_indices: bool = True):
"""Return list of bitstrings with mininal Hamming distance between consecutive strings.
Based on the Gray code called Ehrlich algorithm. For more details, please see Ref. [1].
Args:
initial_string (ndarray): initial bitstring as an :math:`1`-dimensional array
of size :math:`n`. All ones in the bitstring need to be consecutive.
For instance, for :math:`n = 6` and :math:`k = 2`, the bistrings
:math:`000011` and :math:`001100` are examples of acceptable inputs.
In contrast, :math:`001001` is not an acceptable input.
return_indices (bool, optional): if ``True``, returns the list of indices of
qubits that act like controls and targets of the circuit to be created.
Defaults to ``True``.
Returns:
list or tuple(list, list): If ``return_indices=False``, returns list containing
sequence of bistrings in the order generated by the Gray code.
If ``return_indices=True`` returns tuple with the aforementioned list and the list
of control anf target qubits of gates to be implemented based on the sequence of
bitstrings.
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*
`Phys. Rev. Applied 23, 044014 (2025) <https://doi.org/10.1103/PhysRevApplied.23.044014>`_.
"""
k = np.unique(initial_string, return_counts=True)
if len(k[1]) == 1: # pragma: no cover
return ["".join([str(item) for item in initial_string])]
k = k[1][1]
n = len(initial_string)
n_choose_k = int(binom(n, k))
markers = _get_markers(initial_string, last_run=False)
string = initial_string
strings = ["".join(string[::-1].astype(str))]
controls_and_targets = []
for _ in range(n_choose_k - 1):
string, markers, c_and_t = _get_next_bistring(string, markers, k)
strings.append("".join(string[::-1].astype(str)))
controls_and_targets.append(c_and_t)
if return_indices:
return strings, controls_and_targets
return strings
def _get_gate(
qubits_in: List[int],
qubits_out: List[int],
controls: List[int],
theta: float,
phi: float,
complex_data: bool = False,
):
"""Return gate(s) necessary to encode a complex amplitude in a given computational basis state,
given the computational basis state used to encode the previous amplitude.
Information about computational basis states in question are contained
in the indexing of ``qubits_in``, ``qubits_out``, and ``controls``.
Args:
qubits_in (list): list of qubits with ``in`` label.
qubits_out (list): list of qubits with ``out`` label.
controls (list): list of qubits that control the resulting gate.
theta (float): first phase, used to encode the ``abs`` of amplitude.
phi (float): second phase, used to encode the complex phase of amplitude.
complex_data (bool): if ``True``, uses :class:`qibo.gates.U3` to as basis gate.
If ``False``, uses :class:`qibo.gates.RY` as basis gate. Defaults to ``False``.
Returns:
List[:class:`qibo.gates.Gate`]: gate(s) to be added to circuit.
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*
`Phys. Rev. Applied 23, 044014 (2025) <https://doi.org/10.1103/PhysRevApplied.23.044014>`_.
"""
if len(qubits_in) == 0 and len(qubits_out) == 1: # pragma: no cover
gate_list = (
gates.U3(*qubits_out, 2 * theta, 2 * phi, 0.0).controlled_by(*controls)
if complex_data
else gates.RY(*qubits_out, 2 * theta).controlled_by(*controls)
)
gate_list = [gate_list]
elif len(qubits_in) == 1 and len(qubits_out) == 1:
## chooses best combination of complex RBS gate
## given number of controls and if data is real or complex
gate_list = []
gate = gates.RBS(*qubits_in, *qubits_out, theta)
if len(controls) > 0:
gate = gate.controlled_by(*controls)
gate_list.append(gate)
if complex_data:
gate = [gates.RZ(*qubits_in, -phi), gates.RZ(*qubits_out, phi)]
if len(controls) > 0:
gate = [g.controlled_by(*controls) for g in gate]
gate_list.extend(gate)
else: # pragma: no cover
# Important for future sparse encoder
gate_list = [
gates.GeneralizedRBS(
list(qubits_in), list(qubits_out), theta, phi
).controlled_by(*controls)
]
return gate_list
def _get_phase_gate_correction(last_string, phase: float):
"""Return final gate of HW-k circuits that encode complex data."""
# to avoid circular import error
from qibo.quantum_info.utils import ( # pylint: disable=import-outside-toplevel
hamming_weight,
)
if isinstance(last_string, str):
last_string = np.asarray(list(last_string), dtype=int)
last_weight = hamming_weight(last_string)
last_ones = np.argsort(last_string)
last_zero = int(last_ones[0])
last_controls = [int(qubit) for qubit in last_ones[-last_weight:]]
# adding an RZ gate to correct the phase of the last amplitude encoded
return gates.RZ(last_zero, 2 * phase).controlled_by(*last_controls)
def _binary_encoder_hopf(
data, nqubits, complex_data, backend=None, **kwargs
): # pylint: disable=unused-argument
# TODO: generalize to complex-valued data
backend = _check_backend(backend)
dims = 2**nqubits
base_strings = [f"{elem:0{nqubits}b}" for elem in range(dims)]
base_strings = backend.np.reshape(base_strings, (-1, 2))
strings = [base_strings]
for _ in range(nqubits - 1):
base_strings = backend.np.reshape(base_strings[:, 0], (-1, 2))
strings.append(base_strings)
strings = strings[::-1]
targets_and_controls = []
for pairs in strings:
for pair in pairs:
targets, controls, anticontrols = [], [], []
for k, bits in enumerate(zip(pair[0], pair[1])):
if bits == ("0", "0"):
anticontrols.append(k)
elif bits == ("1", "1"):
controls.append(k)
elif bits == ("0", "1"):
targets.append(k)
targets_and_controls.append([targets, controls, anticontrols])
circuit = Circuit(nqubits, **kwargs)
for targets, controls, anticontrols in targets_and_controls:
gate_list = []
if len(anticontrols) > 0:
gate_list.append(gates.X(qubit) for qubit in anticontrols)
gate_list.append(
gates.RY(targets[0], 0.0).controlled_by(*(controls + anticontrols))
)
if len(anticontrols) > 0:
gate_list.append(gates.X(qubit) for qubit in anticontrols)
circuit.add(gate_list)
angles = _generate_rbs_angles(data, "tree", dims, backend=backend)
circuit.set_parameters(2 * angles)
return circuit
def _binary_encoder_hyperspherical(
data, nqubits, complex_data: bool, backend=None, **kwargs
):
backend = _check_backend(backend)
dims = 2**nqubits
last_qubit = nqubits - 1
indexes_to_double, lex_order_global = [0], [0]
circuit = Circuit(nqubits, **kwargs)
if complex_data:
circuit.add(gates.U3(last_qubit, 0.0, 0.0, 0.0))
else:
circuit.add(gates.RY(last_qubit, 0.0))
cummul_n_k = 0
initial_string = np.array([1] + [0] * (nqubits - 1))
for weight in range(1, nqubits):
n_choose_k = int(binom(nqubits, weight))
cummul_n_k += n_choose_k
placeholder = np.random.rand(n_choose_k)
if complex_data:
placeholder = placeholder.astype(complex) + 1j * np.random.rand(n_choose_k)
placeholder = backend.cast(placeholder, dtype=placeholder[0].dtype)
circuit += hamming_weight_encoder(
placeholder,
nqubits,
weight,
full_hwp=True,
optimize_controls=False,
phase_correction=False,
initial_string=initial_string,
backend=backend,
**kwargs,
)
# add gate to be place between blocks of Hamming-weight encoders
gate, lex_order, initial_string, phase_index = _intermediate_gate(
initial_string,
weight,
last_qubit,
cummul_n_k,
complex_data,
)
circuit.add(gate)
lex_order_global.extend(lex_order)
indexes_to_double.append(phase_index)
# sort data such that the encoding is performed in lexicographical order
lex_order_global.append(dims - 1)
lex_order_sorted = np.sort(np.copy(lex_order_global))
lex_order_global = [
np.where(lex_order_sorted == num)[0][0] for num in lex_order_global
]
data = data[lex_order_global]
del lex_order_global, lex_order_sorted
_data = backend.np.abs(data) if complex_data else data
thetas = _generate_rbs_angles(_data, architecture="diagonal", backend=backend)
phis = backend.np.zeros(len(thetas) + 1)
if complex_data:
phis[0] = _angle_mod_two_pi(-backend.np.angle(data[0]))
for k in range(1, len(phis)):
phis[k] = _angle_mod_two_pi(
-backend.np.angle(data[k]) + backend.np.sum(phis[:k])
)
phis = backend.cast(phis, dtype=phis[0].dtype)
zero_casted = backend.cast(0.0, dtype=backend.np.float64) # because of GPU backends
angles = []
for k, (theta, phi) in enumerate(zip(thetas, phis)):
if k in indexes_to_double:
angle = [2 * theta, 2 * phi, zero_casted] if complex_data else [2 * theta]
else:
angle = [theta, -phi, phi] if complex_data else [theta]
angles.extend(angle)
if complex_data:
angles[-2] = 2 * _angle_mod_two_pi(
(backend.np.angle(data[-1]) - backend.np.angle(data[-2])) / 2
)
angles[-1] = 2 * _angle_mod_two_pi(
(-0.5) * (backend.np.angle(data[-2]) + backend.np.angle(data[-1]))
+ backend.np.sum(phis[:-2])
)
circuit.set_parameters(angles)
return circuit
def _intermediate_gate(
initial_string,
weight,
last_qubit,
cummul_n_k,
complex_data,
):
"""Calculate where to place the intermediate gate by finding the last string
of the previous Hamming-weight block that was encoded"""
# sort data such that the encoding is performed in lexicographical order
bitstrings = _ehrlich_algorithm(initial_string, False)
initial_string = bitstrings[-1]
lex_order = [int(string, 2) for string in bitstrings]
controls = [item.start() for item in finditer("1", initial_string)]
index = (
initial_string.find("0")
if weight % 2 == 0
else last_qubit - initial_string[::-1].find("0")
)
initial_string = np.array(list(initial_string), dtype=int)
initial_string[index] = 1
initial_string = initial_string[::-1]
phase_index = cummul_n_k
gate = (
gates.U3(index, 0.0, 0.0, 0.0).controlled_by(*controls)
if complex_data
else gates.RY(index, 0.0).controlled_by(*controls)
)
return gate, lex_order, initial_string, phase_index
def _sort_data_sparse(data, nqubits, backend):
from qibo.quantum_info.utils import ( # pylint: disable=import-outside-toplevel
hamming_weight,
)
# TODO: Fix this mess with qibo native data types
try:
test_dtype = bool("int" in str(data[0][0].dtype))
except AttributeError:
test_dtype = bool("int" in str(type(data[0][0])))
_data = [(f"{row[0]:0{nqubits}b}", row[1]) for row in data] if test_dtype else data
_data = sorted(_data, key=lambda x: hamming_weight(x[0]))
bitstrings_sorted, data_sorted = zip(*_data)
bitstrings_sorted = [
np.array(list(string)).astype(int) for string in bitstrings_sorted
]
bitstrings_sorted = backend.cast(bitstrings_sorted, dtype=backend.np.int8)
data_sorted = backend.cast(data_sorted, dtype=data_sorted[0].dtype)
return data_sorted, bitstrings_sorted
def _get_gate_sparse(
distance,
difference,
touched_qubits,
complex_data,
controls,
hw_0,
hw_1,
theta,
phi,
backend=None,
):
backend = _check_backend(backend)
if distance == 1:
qubit = int(backend.np.where(difference == 1)[0][0])
if qubit not in touched_qubits:
touched_qubits.append(qubit)
gate = (
gates.U3(qubit, 2 * theta, 2 * phi, 0.0).controlled_by(*controls)
if complex_data
else gates.RY(qubit, 2 * theta).controlled_by(*controls)
)
elif distance == 2 and hw_0 == hw_1:
qubits = [
int(np.where(difference == -1)[0][0]),
int(np.where(difference == 1)[0][0]),
]
touched_qubits += list(set(qubits) - set(touched_qubits))
qubits_in = [int(qubits[0])]
qubits_out = [int(qubits[1])]
qubits = [np.where(difference == -1)[0][0], np.where(difference == 1)[0][0]]
gate = _get_gate(
qubits_in,
qubits_out,
controls,
theta,
phi,
complex_data,
)
else:
qubits = [np.where(difference == -1)[0], np.where(difference == 1)[0]]
for row in qubits:
row = [int(elem) for elem in row]
touched_qubits += list(set(row) - set(touched_qubits))
qubits_in = [int(qubit) for qubit in qubits[0]]
qubits_out = [int(qubit) for qubit in qubits[1]]
gate = gates.GeneralizedRBS(qubits_in, qubits_out, theta, -phi).controlled_by(
*controls
)
return gate
def _get_phase_gate_correction_sparse(
last_string,
second_to_last_string,
nqubits,
last_data,
second_to_last_data,
circuit,
phis,
):
from qibo.quantum_info.utils import ( # pylint: disable=import-outside-toplevel
hamming_weight,
)
hw_0 = hamming_weight(second_to_last_string)
hw_1 = hamming_weight(last_string)
if hw_1 == nqubits and hw_0 == nqubits - 1:
phi = _angle_mod_two_pi(np.angle(last_data) - np.angle(second_to_last_data))
lamb = _angle_mod_two_pi(
-(np.angle(second_to_last_data) + np.angle(last_data))
+ 2 * np.sum(phis[:-2])
)
_gate = circuit.queue[-1]
gate = gates.U3(
*_gate.target_qubits, _gate.init_kwargs["theta"], phi, lamb
).controlled_by(*_gate.control_qubits)
new_queue = circuit.queue[:-1] + [gate]
return new_queue
if hw_1 == nqubits: # pragma: no cover
first_one = np.argsort(second_to_last_string)[0]
other_ones = list(set(range(nqubits)) ^ {first_one})
gate = gates.RZ(first_one, 2 * phis[-1]).controlled_by(*other_ones)
else:
gate = _get_phase_gate_correction(last_string, phis[-1])
return gate