Skip to content

Commit

Permalink
Merge branch 'master' into quantum_networks
Browse files Browse the repository at this point in the history
  • Loading branch information
renatomello committed Feb 7, 2024
2 parents ee58ad8 + 5eb7367 commit cda5633
Show file tree
Hide file tree
Showing 7 changed files with 201 additions and 37 deletions.
Binary file added doc/source/_static/comp_basis_encoder.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc/source/_static/phase_encoder.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc/source/_static/unary_encoder_ladder.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc/source/_static/unary_encoder_tree.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
101 changes: 101 additions & 0 deletions doc/source/api-reference/qibo.rst
Original file line number Diff line number Diff line change
Expand Up @@ -270,18 +270,119 @@ For instance, the following two circuit generations are equivalent:
circuit_2.add(gates.X(2))


.. image:: ../_static/comp_basis_encoder.png
:width: 3400px
:height: 2000px
:scale: 25 %
:align: center


.. autofunction:: qibo.models.encodings.comp_basis_encoder


Phase Encoder
"""""""""""""

Encodes data of length :math:`n` into the phases of :math:`n` qubits.


For instance, the following two circuit generations are equivalent:

.. testsetup::

import numpy as np

from qibo import Circuit, gates
from qibo.models.encodings import phase_encoder

.. testcode::

nqubits = 3
phases = np.random.rand(nqubits)

circuit_1 = phase_encoder(phases, rotation="RX")

circuit_2 = Circuit(3)
circuit_2.add(gates.RX(qubit, phases[qubit]) for qubit in range(nqubits))


.. image:: ../_static/phase_encoder.png
:width: 1333px
:height: 1552px
:scale: 30 %
:align: center


.. autofunction:: qibo.models.encodings.phase_encoder


Unary Encoder
"""""""""""""

Given a classical ``data`` array :math:`\mathbf{x} \in \mathbb{R}^{d}` such that

.. math::
\mathbf{x} = (x_{1}, x_{2}, \dots, x_{d}) \, ,
this function generate the circuit that prepares the following quantum state
:math:`\ket{\psi} \in \mathcal{H}`:

.. math::
\ket{\psi} = \frac{1}{\|\mathbf{x}\|_{\textup{HS}}} \,
\sum_{k=1}^{d} \, x_{k} \, \ket{k} \, ,
with :math:`\mathcal{H} \cong \mathbb{C}^{d}` being a :math:`d`-qubit Hilbert space,
and :math:`\|\cdot\|_{\textup{HS}}` being the Hilbert-Schmidt norm.

Here, :math:`\ket{k}` is a unary representation of the number :math:`k`.
For instance, for :math:`d = 3`, the final state would be

.. math::
\ket{\psi} = \frac{1}{\|\mathbf{x}\|_{\textup{HS}}} \,
\left( x_{1} \ket{001} + x_{2} \ket{010} + x_{3} \ket{100} \right) \, .
There are multiple circuit architechtures that lead to unary encoding of classical data.
For example, to encode a :math:`8`-dimensional data, one could use the so-called
*tree* architechture below:

.. image:: ../_static/unary_encoder_tree.png
:width: 1333px
:height: 1552px
:scale: 30 %
:align: center

where the first gate is the :class:`qibo.gates.X`
and the parametrized gates are the :class:`qibo.gates.RBS`.
To know how the angles :math:`\{\theta_{k}\}_{[k]}` are calculated for this architecture,
please refer to S. Johri *et al.*, *Nearest Centroid Classification on a Trapped Ion Quantum Computer*,
`arXiv:2012.04145v2 [quant-ph] <https://arxiv.org/abs/2012.04145>`_.

On the other hand, the same encoding could be performed using the so-called
*diagonal* (also known as *ladder*) architecture below:

.. image:: ../_static/unary_encoder_ladder.png
:width: 1867px
:height: 1552px
:scale: 30 %
:align: center

This architecture leads to a choice of angles based on
`spherical coordinates in a d-dimensional hypersphere
<https://en.wikipedia.org/wiki/N-sphere#Spherical_coordinates>`_.


.. autofunction:: qibo.models.encodings.unary_encoder


Unary Encoder for Random Gaussian States
""""""""""""""""""""""""""""""""""""""""

Performs the same unary encoder as :class:`qibo.models.encodings.unary_encoder`
using the *tree* architecture , with the difference being that now each entry
of the :math:`d`-dimensional array is sampled from a Gaussian distribution
:math:`\mathcal{N}(0, 1)`.


.. autofunction:: qibo.models.encodings.unary_encoder_random_gaussian


Expand Down
81 changes: 45 additions & 36 deletions src/qibo/models/encodings.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,40 +70,63 @@ def comp_basis_encoder(
return circuit


def unary_encoder(data, architecture: str = "tree"):
"""Creates circuit that performs the unary encoding of ``data``.
def phase_encoder(data, rotation: str = "RY"):
"""Creates circuit that performs the phase encoding of ``data``.
Given a classical ``data`` array :math:`\\mathbf{x} \\in \\mathbb{R}^{d}` such that
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"``.
.. math::
\\mathbf{x} = (x_{1}, x_{2}, \\dots, x_{d}) \\, ,
Returns:
:class:`qibo.models.circuit.Circuit`: circuit that loads ``data`` in phase encoding.
"""
if isinstance(data, list):
data = np.array(data)

if len(data.shape) != 1:
raise_error(
TypeError,
f"``data`` must be a 1-dimensional array, but it has dimensions {data.shape}.",
)

if not isinstance(rotation, str):
raise_error(
TypeError,
f"``rotation`` must be type str, but it is type {type(rotation)}.",
)

this function generate the circuit that prepares the following quantum state
:math:`\\ket{\\psi} \\in \\mathcal{H}`:
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)
circuit.add(gate(qubit, 0.0) for qubit in range(nqubits))
circuit.set_parameters(data)

return circuit

.. math::
\\ket{\\psi} = \\frac{1}{\\|\\mathbf{x}\\|_{\\textup{HS}}} \\,
\\sum_{k=1}^{d} \\, x_{k} \\, \\ket{k} \\, ,

with :math:`\\mathcal{H} \\cong \\mathbb{C}^{d}` being a :math:`d`-qubit Hilbert space,
and :math:`\\|\\cdot\\|_{\\textup{HS}}` being the Hilbert-Schmidt norm.
Here, :math:`\\ket{k}` is a unary representation of the number :math:`1` through
:math:`d`.
def unary_encoder(data, architecture: str = "tree"):
"""Creates circuit that performs the (deterministic) unary encoding of ``data``.
Args:
data (ndarray, optional): :math:`1`-dimensional array of data to be loaded.
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``.
Returns:
:class:`qibo.models.circuit.Circuit`: circuit that loads ``data`` in unary representation.
References:
1. S. Johri *et al.*, *Nearest Centroid Classification on a Trapped Ion Quantum Computer*.
`arXiv:2012.04145v2 [quant-ph] <https://arxiv.org/abs/2012.04145>`_.
"""
if isinstance(data, list):
data = np.array(data)

if len(data.shape) != 1:
raise_error(
TypeError,
Expand Down Expand Up @@ -143,26 +166,12 @@ def unary_encoder(data, architecture: str = "tree"):
def unary_encoder_random_gaussian(nqubits: int, architecture: str = "tree", seed=None):
"""Creates a circuit that performs the unary encoding of a random Gaussian state.
Given :math:`d` qubits, encodes the quantum state
:math:`\\ket{\\psi} \\in \\mathcal{H}` such that
.. math::
\\ket{\\psi} = \\frac{1}{\\|\\mathbf{x}\\|_{\\textup{HS}}} \\,
\\sum_{k=1}^{d} \\, x_{k} \\, \\ket{k}
where :math:`x_{k}` are independent Gaussian random variables,
:math:`\\mathcal{H} \\cong \\mathbb{C}^{d}` is a :math:`d`-qubit Hilbert space,
and :math:`\\|\\cdot\\|_{\\textup{HS}}` being the Hilbert-Schmidt norm.
Here, :math:`\\ket{k}` is a unary representation of the number :math:`1` through
:math:`d`.
At depth :math:`h`, the angles :math:`\\theta_{k} \\in [0, 2\\pi]` of the the
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})}
\\abs{\\sin(\\theta) \\, \\cos(\\theta)}^{2^{h-1} - 1} \\, ,
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>`_.
Expand Down
56 changes: 55 additions & 1 deletion tests/test_models_encodings.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
"""Tests for qibo.models.encodings"""

import math
from itertools import product

import numpy as np
import pytest
from scipy.optimize import curve_fit

from qibo.models.encodings import (
comp_basis_encoder,
phase_encoder,
unary_encoder,
unary_encoder_random_gaussian,
)
Expand Down Expand Up @@ -48,9 +50,58 @@ def test_comp_basis_encoder(backend, basis_element):
backend.assert_allclose(state, target)


@pytest.mark.parametrize("kind", [None, list])
@pytest.mark.parametrize("rotation", ["RX", "RY", "RZ"])
def test_phase_encoder(backend, rotation, kind):
sampler = np.random.default_rng(1)

nqubits = 3
dims = 2**nqubits

with pytest.raises(TypeError):
data = sampler.random((nqubits, nqubits))
data = backend.cast(data, dtype=data.dtype)
phase_encoder(data, rotation=rotation)
with pytest.raises(TypeError):
data = sampler.random(nqubits)
data = backend.cast(data, dtype=data.dtype)
phase_encoder(data, rotation=True)
with pytest.raises(ValueError):
data = sampler.random(nqubits)
data = backend.cast(data, dtype=data.dtype)
phase_encoder(data, rotation="rzz")

phases = np.random.rand(nqubits)

if rotation in ["RX", "RY"]:
functions = list(product([np.cos, np.sin], repeat=nqubits))
target = []
for row in functions:
elem = 1.0
for phase, func in zip(phases, row):
elem *= func(phase / 2)
if rotation == "RX" and func.__name__ == "sin":
elem *= -1.0j
target.append(elem)
else:
target = [np.exp(-0.5j * sum(phases))] + [0.0] * (dims - 1)

target = np.array(target, dtype=complex)
target = backend.cast(target, dtype=target.dtype)

if kind is not None:
phases = kind(phases)

state = phase_encoder(phases, rotation=rotation)
state = backend.execute_circuit(state).state()

backend.assert_allclose(state, target)


@pytest.mark.parametrize("kind", [None, list])
@pytest.mark.parametrize("architecture", ["tree", "diagonal"])
@pytest.mark.parametrize("nqubits", [8])
def test_unary_encoder(backend, nqubits, architecture):
def test_unary_encoder(backend, nqubits, architecture, kind):
sampler = np.random.default_rng(1)

with pytest.raises(TypeError):
Expand All @@ -76,6 +127,9 @@ def test_unary_encoder(backend, nqubits, architecture):
data = 2 * sampler.random(nqubits) - 1
data = backend.cast(data, dtype=data.dtype)

if kind is not None:
data = kind(data)

circuit = unary_encoder(data, architecture=architecture)
state = backend.execute_circuit(circuit).state()
indexes = np.flatnonzero(state)
Expand Down

0 comments on commit cda5633

Please sign in to comment.