Source code for qiboml.models.encoding

import inspect
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from functools import cached_property
from typing import Optional

import numpy as np
from qibo import Circuit, gates
from qibo.config import raise_error

from qiboml import ndarray


[docs]@dataclass(eq=False) class QuantumEncoding(ABC): """ Abstract Encoder class. Args: nqubits (int): total number of qubits. qubits (tuple[int], optional): set of qubits it acts on, by default ``range(nqubits)``. density_matrix (bool, optional): whether to build the circuit with ``density_matrix=True``, mostly useful for noisy simulations. ``density_matrix=False`` by default. """ nqubits: int qubits: Optional[tuple[int]] = None density_matrix: Optional[bool] = False _circuit: Circuit = None def __post_init__( self, ): """Ancillary post initialization for the dataclass object.""" self.qubits = ( tuple(range(self.nqubits)) if self.qubits is None else tuple(self.qubits) ) self._circuit = Circuit(self.nqubits, density_matrix=self.density_matrix) @cached_property def _data_to_gate(self): """ Mapping between the index of the input and the indices of the gates in the produced encoding circuit queue, where the input is encoded to. For instance, {0: [0,2], 1: [2]}, represents an encoding where the element 0 of the inputs enters the gates with indices 0 and 2 of the queue, whereas the element 1 of the input affects only the the gate in position 2 of the queue. By deafult, the map reproduces a simple encoding where the i-th component of the data is uploaded in the i-th gate of the queue. """ return {f"{i}": [i] for i in range(len(self.qubits))}
[docs] @abstractmethod def __call__(self, x: ndarray) -> Circuit: """Abstract call method.""" pass
@property def circuit( self, ) -> Circuit: """Internal initialized circuit.""" return self._circuit.copy() @property def differentiable(self) -> bool: """Whether the encoder is differentiable. If ``True`` the gradient w.r.t. the inputs is calculated, otherwise it is automatically set to zero. """ return True def __hash__(self) -> int: return hash(self.qubits)
[docs]@dataclass(eq=False) class PhaseEncoding(QuantumEncoding): encoding_gate: type = field(default_factory=lambda: gates.RY) def __post_init__( self, ): """Ancillary post initialization: builds the internal circuit with the rotation gates.""" super().__post_init__() # Retrieving information about the given encoding gate signature = inspect.signature(self.encoding_gate) allowed_params = {"theta", "phi", "lam"} self.gate_encoding_params = { p for p in signature.parameters.keys() } & allowed_params if len(self.gate_encoding_params) != 1: raise NotImplementedError( f"{self} currently support only gates with one parameter." )
[docs] def __call__(self, x: ndarray) -> Circuit: """Construct the circuit encoding the ``x`` data in the chosen encoding gate. Args: x (ndarray): the input real data to encode in rotation angles. Returns: (Circuit): the constructed ``qibo.Circuit``. """ circuit = self.circuit x = x.ravel() for i, q in enumerate(self.qubits): this_gate_params = {"trainable": False} [this_gate_params.update({p: x[i]}) for p in self.gate_encoding_params] circuit.add(self.encoding_gate(q=q, **this_gate_params)) return circuit
[docs]class BinaryEncoding(QuantumEncoding):
[docs] def __call__(self, x: ndarray) -> Circuit: r"""Construct the circuit encoding the ``x`` binary data in some ``RX`` rotation gates with angles either :math:`\pi` (for ones) or 0 (for zeros). Args: x (ndarray): the input binary data. Returns: (Circuit): the constructed ``qibo.Circuit``. """ if x.shape[-1] != len(self.qubits): raise_error( RuntimeError, f"Invalid input dimension {x.shape[-1]}, but the allocated qubits are {self.qubits}.", ) circuit = self.circuit x = x.ravel() for i, q in enumerate(self.qubits): circuit.add(gates.RX(q, theta=x[i] * np.pi, trainable=False)) return circuit
@property def differentiable(self) -> bool: return False