Skip to content

Commit

Permalink
doc: working on encoder docs
Browse files Browse the repository at this point in the history
  • Loading branch information
BrunoLiegiBastonLiegi committed Feb 4, 2025
1 parent c4c2699 commit 6874848
Show file tree
Hide file tree
Showing 3 changed files with 41 additions and 16 deletions.
10 changes: 5 additions & 5 deletions doc/source/advanced/decoder.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ The `Decoder` is the part of the model in charge of transforming back the quantu

A very simple decoder, for instance, is the :py:class:`qiboml.models.decoding.Probabilities`, which extracts the probabilities from the final state. Similarly, :py:class:`qiboml.models.decoding.Samples` and :py:class:`qiboml.models.decoding.Expectation` respectively reconstruct the measured samples and calculate the expectation value of an observable on the final state.

Hence, the decoder is a function :math:`f_d: C \rightarrow \mathbf{y}\in\mathbb{R}^n`, that expects as input a ``qibo.Circuit``, executes it and finally perform some operation on the obtained final state to recover some classical output data :math:`\mathbb{y}` in the form of a float array.
Hence, the decoder is a function :math:`f_d: C \rightarrow \mathbf{y}\in\mathbb{R}^n`, that expects as input a ``qibo.Circuit``, executes it and finally perform some operation on the obtained final state to recover some classical output data :math:`\mathbf{y}` in the form of a float array.

``qiboml`` provides an abstract :py:class:`qiboml.models.decoding.QuantumDecoding` object which can be subclassed to define custom decoding layers. Let's say, for instance, that we would like to calculate the expectation values of two different observables:

Expand Down Expand Up @@ -51,7 +51,7 @@ To do this we only need to create a decoding layer that constructs the two obser
def output_shape(self) -> tuple(int):
(1, 1)

Note that it is important to also specify what is the expected output shape of the decoder, for example as in this case we are just dealing with expectation values and, thus, scalars, we are going to set it as :math:`(1,1)`.
Note that it is important to also specify what is the expected output shape of the decoder, for example as in this case we are just dealing with expectation values and, thus, scalars, we are going to set it to :math:`(1,1)`.

The ``super().__init__`` and ``super().__call__`` calls here are useful to simplify the implementation of the custom decoder. The ``super().__init__`` sets up the initial features needed, i.e. mainly an empty ``nqubits`` ``qibo.Circuit`` with a measurement appended on each qubit. Whereas, the ``super().__call__`` takes care of executing the ``qibo.Circuit`` passed as input ``x`` and returns a ``qibo.result`` object, hence one in ``(QuantumState, MeasurementOutcomes, CircuitResult)``.

Expand All @@ -70,8 +70,8 @@ In case you needed an even more fine-grained customization, you could always get
def __init__(self, nqubits: int):
self.backend = MyCustomBackend()
# the backends should match!
self.o_even = SymbolicHamiltonian(Z(0)*Z(2), nqubits=nqubits)
self.o_odd = SymbolicHamiltonian(Z(1)*Z(3), nqubits=nqubits)
self.o_even = SymbolicHamiltonian(Z(0)*Z(2), nqubits=nqubits, backend=self.backend)
self.o_odd = SymbolicHamiltonian(Z(1)*Z(3), nqubits=nqubits, backend=self.backend)
def __call__(self, x: Circuit):
final_state = self.backend.execute_circuit(x).state()
Expand All @@ -89,6 +89,6 @@ In case you needed an even more fine-grained customization, you could always get
@property
def analytic(self,) --> bool:
if some_condition:
if is_my_custom_decoder_differentiable:
return True
return False
6 changes: 0 additions & 6 deletions doc/source/advanced/decoder.rst~

This file was deleted.

41 changes: 36 additions & 5 deletions doc/source/advanced/encoder.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,43 @@ For instance, basic examples of this can be found in the :py:class:`qiboml.model

Hence, broadly speaking, the quantum `Encoder` is a function :math:`f_e: \mathbf{x}\in\mathbb{R}^n \rightarrow C` that maps an input array of floats :math:`\mathbf{x}`, be it a ``torch.Tensor`` for the :py:class:`qiboml.interfaces.pytorch.QuantumModel` or a ``tensorflow.Tensor`` for the :py:class:`qiboml.interfaces.keras.QuantumModel`, to an instance of a ``qibo.Circuit`` :math:`C`.

To define a custom encoder, then, one only has to write a custom function like that. Let's take as an example the amplitude encoding, whch aims at embedding the input data in the amplitudes of our quantum state.

In particular, a practical realization of this can be achieved, for instance, through the Mottonen state preparation (`Mottonen et al. (2004) <https://arxiv.org/abs/quant-ph/0407010>`_). The idea is to prepare a set of controlled rotations which, under a suitable choice of angles, reproduce the desired amplitudes. In detail, the angles needed are given by the following expression:
To define a custom encoder, ``qiboml`` handily provides an abstract :py:class:`qiboml.models.encoding.QuantumEncoding` class to inherit from. Let's say for example, that we had some heterogeneous data consisting of both real :math:`x_{real}` and binary :math:`x_{bin}` data stacked on top of each other in a single array

.. math::
\alpha_j^s = 2 \arcsin \frac{ \sqrt{\sum_{l=1}^{2^{s-1}} \mid a_{(2j-1)2^{s-1} + l} \mid^2 } }{ \sqrt{\sum_{l=1}^{2^s} \mid a_{(j-1)2^s + l} \mid^2 } }
\mathbf{x} = x_{real} \lvert x_{bin}\;.
To encode at the same time these two type of data we could define a mixture of the :py:class:`qiboml.models.encoding.BinaryEncoding` and :py:class:`qiboml.models.encoding.PhaseEncoding` encoders

.. testcode::

import numpy as np
from qiboml.models.encoding import QuantumEncoding

class HeterogeneousEncoder(QuantumEncoding):

def __init__(self, nqubits, real_part_len, bin_part_len):
if real_part_len + bin_part_len != nqubits:
raise RuntimeError("``real_part_len`` and ``bin_part_len`` don't sum to ``nqubits``.")

super.__init__(nqubits)

self.real_qubits = self.qubits[:real_part_len]
self.bin_qubits = self.qubits[real_part_len:]

def __call__(self, x) -> Circuit:
# check that the data is binary
if any(x[1] != 1 or x[1] != 0):
raise RuntimeError("Received non binary data")

circuit = self.circuit.copy()

# the first row of x contains the real data
for qubit, value in zip(self.real_qubits, x[0]):
circuit.add(gates.RY(qubit, theta=value, trainable=False))

# the second row contains the binary data
for qubit, bit in zip(self.real_qubits, x[1]):
circuit.add(gates.RX(qubit, theta=bit * np.pi, trainable=False))

TODO...
return circuit

0 comments on commit 6874848

Please sign in to comment.