Skip to content

Commit

Permalink
Merge pull request #1141 from qiboteam/unary_architecture
Browse files Browse the repository at this point in the history
Add `diagonal` architectute to deterministic Unary encoder
  • Loading branch information
renatomello authored Jan 12, 2024
2 parents 3f53ddc + 8e96b46 commit f05a722
Show file tree
Hide file tree
Showing 2 changed files with 140 additions and 40 deletions.
152 changes: 119 additions & 33 deletions src/qibo/models/encodings.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from qibo.models.circuit import Circuit


def unary_encoder(data):
def unary_encoder(data, architecture: str = "tree"):
"""Creates circuit that performs the unary encoding of ``data``.
Given a classical ``data`` array :math:`\\mathbf{x} \\in \\mathbb{R}^{d}` such that
Expand All @@ -32,6 +32,10 @@ def unary_encoder(data):
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``.
Returns:
:class:`qibo.models.circuit.Circuit`: circuit that loads ``data`` in unary representation.
Expand All @@ -45,36 +49,38 @@ def unary_encoder(data):
TypeError,
f"``data`` must be a 1-dimensional array, but it has dimensions {data.shape}.",
)
elif not math.log2(data.shape[0]).is_integer():

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, f"len(data) must be a power of 2, but it is {len(data)}."
ValueError,
"When ``architecture = 'tree'``, len(data) must be a power of 2. "
+ f"However, it is {len(data)}.",
)

nqubits = len(data)
j_max = int(nqubits / 2)

circuit, _ = _generate_rbs_pairs(nqubits)
circuit = Circuit(nqubits)
circuit.add(gates.X(nqubits - 1))
circuit_rbs, _ = _generate_rbs_pairs(nqubits, architecture=architecture)
circuit += circuit_rbs

# calculating phases and setting circuit parameters
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 = _generate_rbs_angles(data, nqubits, architecture)
circuit.set_parameters(phases)

return circuit


def unary_encoder_random_gaussian(nqubits: int, seed=None):
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
Expand Down Expand Up @@ -103,6 +109,9 @@ def unary_encoder_random_gaussian(nqubits: int, seed=None):
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``.
Expand All @@ -119,11 +128,25 @@ def unary_encoder_random_gaussian(nqubits: int, seed=None):
raise_error(
TypeError, f"nqubits must be type int, but it is type {type(nqubits)}."
)
elif nqubits <= 0.0:

if nqubits <= 0.0:
raise_error(
ValueError, f"nqubits must be a positive integer, but it is {nqubits}."
)
elif not math.log2(nqubits).is_integer():

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,
f"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 (
Expand All @@ -143,7 +166,10 @@ def unary_encoder_random_gaussian(nqubits: int, seed=None):
a=0, b=2 * math.pi, seed=local_state
)

circuit, pairs_rbs = _generate_rbs_pairs(nqubits)
circuit = Circuit(nqubits)
circuit.add(gates.X(nqubits - 1))
circuit_rbs, pairs_rbs = _generate_rbs_pairs(nqubits, architecture)
circuit += circuit_rbs

phases = []
for depth, row in enumerate(pairs_rbs, 1):
Expand All @@ -154,31 +180,91 @@ def unary_encoder_random_gaussian(nqubits: int, seed=None):
return circuit


def _generate_rbs_pairs(nqubits):
def _generate_rbs_pairs(nqubits: int, architecture: str):
"""Generating list of indexes representing the RBS connections
and creating circuit with all RBS initialised with 0.0 phase."""
pairs_rbs = [[(0, int(nqubits / 2))]]
indexes = list(np.array(pairs_rbs).flatten())
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())
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``.
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)
circuit.add(gates.X(nqubits - 1))
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, nqubits: int, architecture: str):
"""Generating list of angles for RBS gates based on ``architecture``.
Args:
data (ndarray, optional): :math:`1`-dimensional array of data to be loaded.
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``.
Returns:
list: list of phases for RBS gates.
"""
if architecture == "diagonal":
phases = [
math.atan2(np.linalg.norm(data[k + 1 :]), data[k])
for k in range(len(data) - 2)
]
phases.append(math.atan2(data[-1], data[-2]))

if 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])

return phases


class _ProbabilityDistributionGaussianLoader(rv_continuous):
"""Probability density function for sampling phases of
the RBS gates as a function of circuit depth."""
Expand Down
28 changes: 21 additions & 7 deletions tests/test_models_encodings.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,38 @@ def gaussian(x, a, b, c):
return np.exp(a * x**2 + b * x + c)


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

with pytest.raises(TypeError):
data = sampler.random((nqubits, nqubits))
data = backend.cast(data, dtype=data.dtype)
unary_encoder(data)
unary_encoder(data, architecture=architecture)
with pytest.raises(TypeError):
data = sampler.random(nqubits)
data = backend.cast(data, dtype=data.dtype)
unary_encoder(data, architecture=True)
with pytest.raises(ValueError):
data = sampler.random(nqubits + 1)
data = sampler.random(nqubits)
data = backend.cast(data, dtype=data.dtype)
unary_encoder(data)
unary_encoder(data, architecture="semi-diagonal")
if architecture == "tree":
with pytest.raises(ValueError):
data = sampler.random(nqubits + 1)
data = backend.cast(data, dtype=data.dtype)
unary_encoder(data, architecture=architecture)

# sampling random data in interval [-1, 1]
sampler = np.random.default_rng(1)
data = 2 * sampler.random(nqubits) - 1
data = backend.cast(data, dtype=data.dtype)

circuit = unary_encoder(data)
circuit = unary_encoder(data, architecture=architecture)
state = backend.execute_circuit(circuit).state()
indexes = np.flatnonzero(state)
state = state[indexes]
state = np.real(state[indexes])

backend.assert_allclose(state, data / backend.calculate_norm(data, order=2))

Expand All @@ -51,6 +61,10 @@ def test_unary_encoder_random_gaussian(backend, nqubits, seed):
unary_encoder_random_gaussian(-1, seed=seed)
with pytest.raises(ValueError):
unary_encoder_random_gaussian(3, seed=seed)
with pytest.raises(TypeError):
unary_encoder_random_gaussian(nqubits, architecture=True, seed=seed)
with pytest.raises(NotImplementedError):
unary_encoder_random_gaussian(nqubits, architecture="diagonal", seed=seed)
with pytest.raises(TypeError):
unary_encoder_random_gaussian(nqubits, seed="seed")

Expand Down

0 comments on commit f05a722

Please sign in to comment.