From 687484897853a33c27db2922b33c16de874eaf4c Mon Sep 17 00:00:00 2001 From: BrunoLiegiBastonLiegi Date: Tue, 4 Feb 2025 18:33:40 +0100 Subject: [PATCH] doc: working on encoder docs --- doc/source/advanced/decoder.rst | 10 ++++---- doc/source/advanced/decoder.rst~ | 6 ----- doc/source/advanced/encoder.rst | 41 ++++++++++++++++++++++++++++---- 3 files changed, 41 insertions(+), 16 deletions(-) delete mode 100644 doc/source/advanced/decoder.rst~ diff --git a/doc/source/advanced/decoder.rst b/doc/source/advanced/decoder.rst index 2e1bcbd..89180a8 100644 --- a/doc/source/advanced/decoder.rst +++ b/doc/source/advanced/decoder.rst @@ -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: @@ -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)``. @@ -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() @@ -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 diff --git a/doc/source/advanced/decoder.rst~ b/doc/source/advanced/decoder.rst~ deleted file mode 100644 index 2352974..0000000 --- a/doc/source/advanced/decoder.rst~ +++ /dev/null @@ -1,6 +0,0 @@ -Defining a custom Decoder -------------------------- - -The `Decoder` is the part of the model in charge of transforming back the quantum information contained in a quantum state, to some classical representation consumable by classical calculators. This, in practice, translates to: obtaining the final quantum state, first, and, second, performing any suitable postprocessing onto it. - -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. diff --git a/doc/source/advanced/encoder.rst b/doc/source/advanced/encoder.rst index 3c56d19..32ba901 100644 --- a/doc/source/advanced/encoder.rst +++ b/doc/source/advanced/encoder.rst @@ -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) `_). 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