Source code for qulearn.qlayer

import math
from enum import Enum
from typing import Dict, Iterable, Optional

import pennylane as qml
import torch
from torch import nn

from .hat_basis import HatBasis
from .mps import HatBasisMPS, MPSQGates
from .mps_kronprod import kron, zkron
from .types import (
    CDevice,
    DType,
    Entropy,
    Expectation,
    Observable,
    Observables,
    Probability,
    QDevice,
    QNode,
    Sample,
    Tensor,
    Wires,
)

DEFAULT_QDEV_CFG = {"name": "default.qubit", "shots": None}


[docs] class MeasurementType(Enum): """Measurement type for a measurement layer.""" """Expectation: return expected value of observable.""" Expectation = "expectation" """Probabilities: return vector of probabilities.""" Probabilities = "probabilities" """Samples: return measurement samples.""" Samples = "samples" """Entropy: calculate the von Neumann entropy of a subsystem.""" Entropy = "entropy"
[docs] class CircuitLayer(nn.Module): """ Base class for a quantum circuit layer. A circuit layer transforms a quantum state but does not perform any measurements. Thus, a circuit layer produces no classical output. It can be combined with a measurement layer that returns classical output. By default, the base class does nothing (zero state). Derived classes need to override :meth:`circuit`. :param wires: Number or list of circuits. :type wires: Wires """ def __init__(self, wires: Wires) -> None: super().__init__() self.set_wires(wires)
[docs] def forward(self, x: Tensor) -> None: """Forward pass. See :meth:`circuit`""" self.circuit(x)
[docs] def circuit(self, _: Tensor) -> None: """Applies input-dependent unitary transformations to a circuit. :param x: Input data samples. :type x: Tensor. :return: None for the base class. :rtype: None. """ return None
[docs] def set_wires(self, wires: Wires) -> None: """Set circuit (qubit) wires.""" if isinstance(wires, int): self.num_wires = wires self.wires = list(range(wires)) else: self.num_wires = len(list(wires)) self.wires = list(wires)
[docs] class IQPEmbeddingLayer(CircuitLayer): """ Layer for IQP (Instantaneous Quantum Polynomial) embedding. :param wires: The wires to be used by the layer :type wires: Wires :param n_repeat: The number of times to repeat the IQP embedding, defaults to 1 :type n_repeat: int, optional :param kwargs: Extra arguments passed to the IQP embedding """ def __init__(self, wires: Wires, n_repeat: int = 1, **kwargs) -> None: super().__init__(wires) self.n_repeat = n_repeat self.kwargs = kwargs self.qfunc = qml.IQPEmbedding
[docs] def circuit(self, x: Tensor) -> None: """ Define the quantum circuit for this layer. :param x: Input tensor that is passed to the quantum circuit. :type x: Tensor """ for _ in range(self.n_repeat): self.qfunc(x, wires=self.wires, **self.kwargs)
[docs] class HatBasisQFE(CircuitLayer): """ Layer for the 1D hat basis quantum feature embedding. :param basis: The hat basis class. :type basis: HatBasis :param wires: The wires to be used by the layer :type wires: Wires :param sqrt: Set flag to take square roots before applying hat basis. :type sqrt: bool :param normalize: Set flag to normalize basis vector before embedding. :type normalize: bool """ def __init__( self, wires: Wires, basis: HatBasis, sqrt: bool = False, normalize: bool = False, ) -> None: super().__init__(wires) self.basis = basis self.sqrt = sqrt self.normalize = normalize self.norm = 1.0 self.hbmps = HatBasisMPS(basis)
[docs] def circuit(self, x: Tensor) -> None: """ Define the quantum circuit for this layer. :param x: Input tensor that is passed to the quantum circuit. :type x: Tensor """ position = int(self.basis.position(x)) a, b = self.basis.nonz_vals(x) if self.sqrt: # sometimes the values are close to 0 and negative a = torch.sqrt(torch.abs(a)) b = torch.sqrt(torch.abs(b)) if position == -1: self.norm = b.item() for q in range(self.num_wires): qml.Identity(wires=self.wires[q]) return None elif position == -2: self.norm = a.item() for q in range(self.num_wires): qml.PauliX(wires=self.wires[q]) return None self.norm = torch.sqrt(a**2 + b**2).item() if self.normalize: a /= self.norm b /= self.norm # for compatibility (TODO: remove) first = a.item() second = b.item() mps = self.hbmps.mps_hatbasis(first, second, position) mpsgates = MPSQGates(mps) s = mpsgates.max_rank_power Us = mpsgates.qgates() N = len(Us) count = 0 for k in range(N - 1, -1, -1): wires_idx = list(range(self.num_wires - count - s - 1, self.num_wires - count)) subwires = [self.wires[idx] for idx in wires_idx] qml.QubitUnitary(Us[k], wires=subwires, unitary_check=False) count += 1
[docs] def compute_norm(self, x: Tensor) -> float: """ Compute the norm of the basis vector for the given input x. :param x: Input tensor that is passed to basis vector. :type x: Tensor :returns: The norm. :rtype: float """ position = int(self.basis.position(x)) a, b = self.basis.nonz_vals(x) if self.sqrt: a = torch.sqrt(a) b = torch.sqrt(b) if position == -1: self.norm = b.item() return self.norm elif position == -2: self.norm = a.item() return self.norm self.norm = torch.sqrt(a**2 + b**2).item() return self.norm
[docs] class Linear2DBasisQFE(CircuitLayer): """ Layer for the 2D hat basis quantum feature embedding. :param basis: The 1D hat basis class. :type basis: HatBasis :param wires: The wires to be used by the layer :type wires: Wires :param sqrt: Set flag to take square roots before applying hat basis. :type sqrt: bool :param normalize: Set flag to normalize basis vector before embedding. :type normalize: bool """ def __init__( self, wires: Wires, basis: HatBasis, sqrt: bool = False, normalize: bool = False, zorder: bool = False, ) -> None: super().__init__(wires) self.basis = basis self.sqrt = sqrt self.normalize = normalize self.norm = 1.0 self.hbmps = HatBasisMPS(basis) self.zorder = zorder self.mps = None self.mps1 = None self.mps2 = None
[docs] def circuit(self, x: Tensor) -> None: """ Define the quantum circuit for this layer. :param x: Input tensor that is passed to the quantum circuit. :type x: Tensor """ self._check_input(x) x1 = x[0] x2 = x[1] position1 = int(self.basis.position(x1)) position2 = int(self.basis.position(x2)) a1, b1 = self.basis.nonz_vals(x1) a2, b2 = self.basis.nonz_vals(x2) if self.sqrt: # sometimes the values are close to 0 and negative a1 = torch.sqrt(torch.abs(a1)) b1 = torch.sqrt(torch.abs(b1)) a2 = torch.sqrt(torch.abs(a2)) b2 = torch.sqrt(torch.abs(b2)) # TODO: cover the case where x or y are outside of bounds val1 = a1 * a2 val2 = a1 * b2 val3 = a2 * b1 val4 = a2 * b2 norm = torch.sqrt(val1**2 + val2**2 + val3**2 + val4**2) if self.normalize: a1 /= torch.sqrt(norm) b1 /= torch.sqrt(norm) a2 /= torch.sqrt(norm) b2 /= torch.sqrt(norm) self.norm = norm.item() # for compatibility (TODO: remove) first1 = a1.item() second1 = b1.item() first2 = a2.item() second2 = b2.item() mps1 = self.hbmps.mps_hatbasis(first1, second1, position1) mps2 = self.hbmps.mps_hatbasis(first2, second2, position2) if self.zorder: mps = zkron(mps2, mps1) else: mps = kron(mps2, mps1) self.mps1 = mps1 self.mps2 = mps2 self.mps = mps mpsgates = MPSQGates(mps) s = mpsgates.max_rank_power Us = mpsgates.qgates() N = len(Us) count = 0 for k in range(N - 1, -1, -1): wires_idx = list(range(self.num_wires - count - s - 1, self.num_wires - count)) subwires = [self.wires[idx] for idx in wires_idx] qml.QubitUnitary(Us[k], wires=subwires, unitary_check=False) count += 1
[docs] def compute_norm(self, x: Tensor) -> float: """ Compute the norm of the basis vector for the given input x. :param x: Input tensor that is passed to basis vector. :type x: Tensor :returns: The norm. :rtype: float """ self._check_input(x) x1 = x[0] x2 = x[1] a1, b1 = self.basis.nonz_vals(x1) a2, b2 = self.basis.nonz_vals(x2) if self.sqrt: # sometimes the values are close to 0 and negative a1 = torch.sqrt(torch.abs(a1)) b1 = torch.sqrt(torch.abs(b1)) a2 = torch.sqrt(torch.abs(a2)) b2 = torch.sqrt(torch.abs(b2)) # TODO: cover the case where x or y are outside of bounds val1 = a1 * a2 val2 = a1 * b2 val3 = a2 * b1 val4 = a2 * b2 self.norm = torch.sqrt(val1**2 + val2**2 + val3**2 + val4**2).item() return self.norm
def _check_input(self, x: Tensor): if x.dim() > 2: raise ValueError("Input tensor must have 2 dimensions") if torch.any(torch.abs(x) >= 1): raise ValueError("Out of bounds case is not implemented")
[docs] class RYCZLayer(CircuitLayer): """ Layer for the RYCZ (Rotation around Y and Controlled-Z) gates. :param wires: The wires to be used by the layer. :type wires: Wires :param n_layers: The number of layers for the simplified two-design architecture, defaults to 1. :type n_layers: int, optional :param cdevice: Classical device to store the initial layer weights and internal layer weights, defaults to None. :type cdevice: CDevice, optional :param dtype: Data type of the weights, defaults to None. :type dtype: DType, optional :param kwargs: Extra arguments passed to the SimplifiedTwoDesign. """ def __init__( self, wires: Wires, n_layers: int = 1, cdevice: Optional[CDevice] = None, dtype: Optional[DType] = None, **kwargs, ) -> None: super().__init__(wires) self.n_layers = n_layers self.qfunc = qml.SimplifiedTwoDesign self.cdevice = cdevice self.dtype = dtype self.kwargs = kwargs self.initial_layer_weights = torch.nn.Parameter( torch.empty(self.num_wires, device=self.cdevice, dtype=self.dtype) ) self.weights = torch.nn.Parameter( torch.empty( (self.n_layers, self.num_wires - 1, 2), device=self.cdevice, dtype=self.dtype, ) ) nn.init.uniform_(self.initial_layer_weights, a=0.0, b=2 * math.pi) nn.init.uniform_(self.weights, a=0.0, b=2 * math.pi)
[docs] def circuit(self, _: Optional[Tensor] = None) -> None: """ Define the quantum circuit for this layer. :param _: Input tensor that is passed to the quantum circuit (ignored). :type x: Optional[Tensor] """ self.qfunc(self.initial_layer_weights, self.weights, self.wires, **self.kwargs)
[docs] class AltRotCXLayer(CircuitLayer): """ Layer for alternating CNOT gates with universal 1-qubit rotation. :param wires: The wires to be used by the layer. :type wires: Wires :param n_layers: The number of layers for the simplified two-design architecture, defaults to 1. :type n_layers: int, optional :param cdevice: Classical device to store the initial layer weights and internal layer weights, defaults to None. :type cdevice: CDevice, optional :param dtype: Data type of the weights, defaults to None. :type dtype: DType, optional :param kwargs: Extra arguments passed to the SimplifiedTwoDesign. """ def __init__( self, wires: Wires, n_layers: int = 1, cdevice: Optional[CDevice] = None, dtype: Optional[DType] = None, **kwargs, ) -> None: super().__init__(wires) self.n_layers = n_layers self.cdevice = cdevice self.dtype = dtype self.kwargs = kwargs # weight parameters self.initial_layer_weights = torch.nn.Parameter( torch.empty((self.num_wires, 3), device=self.cdevice, dtype=self.dtype) ) self.weights = torch.nn.Parameter( torch.empty( (self.n_layers, 2 * (self.num_wires - 1), 3), device=self.cdevice, dtype=self.dtype, ) ) nn.init.uniform_(self.initial_layer_weights, a=0.0, b=2 * math.pi) nn.init.uniform_(self.weights, a=0.0, b=2 * math.pi)
[docs] def circuit(self, _: Optional[Tensor] = None) -> None: """ Define the quantum circuit for this layer. :param _: Input tensor that is passed to the quantum circuit (ignored). :type _: Optional[Tensor] """ for index, q in enumerate(self.wires): qml.Rot( self.initial_layer_weights[index, 0], self.initial_layer_weights[index, 1], self.initial_layer_weights[index, 2], q, ) for layer in range(self.n_layers): for i in range(0, len(self.wires) - 1, 2): qml.CNOT(wires=[self.wires[i], self.wires[i + 1]]) qml.Rot( self.weights[layer, i, 0], self.weights[layer, i, 1], self.weights[layer, i, 2], self.wires[i], ) qml.Rot( self.weights[layer, i + 1, 0], self.weights[layer, i + 1, 1], self.weights[layer, i + 1, 2], self.wires[i + 1], ) offset = int(self.num_wires / 2) * 2 for i in range(1, len(self.wires) - 1, 2): qml.CNOT(wires=[self.wires[i], self.wires[i + 1]]) qml.Rot( self.weights[layer, offset + i - 1, 0], self.weights[layer, offset + i - 1, 1], self.weights[layer, offset + i - 1, 2], self.wires[i], ) qml.Rot( self.weights[layer, offset + i, 0], self.weights[layer, offset + i, 1], self.weights[layer, offset + i, 2], self.wires[i + 1], )
[docs] class AltRXCXLayer(CircuitLayer): """ Layer for alternating CNOT gates with RX rotations. :param wires: The wires to be used by the layer. :type wires: Wires :param n_layers: The number of layers for the simplified two-design architecture, defaults to 1. :type n_layers: int, optional :param cdevice: Classical device to store the initial layer weights and internal layer weights, defaults to None. :type cdevice: CDevice, optional :param dtype: Data type of the weights, defaults to None. :type dtype: DType, optional :param kwargs: Extra arguments passed to the SimplifiedTwoDesign. """ def __init__( self, wires: Wires, n_layers: int = 1, cdevice: Optional[CDevice] = None, dtype: Optional[DType] = None, **kwargs, ) -> None: super().__init__(wires) self.n_layers = n_layers self.cdevice = cdevice self.dtype = dtype self.kwargs = kwargs # weight parameters self.initial_layer_weights = torch.nn.Parameter( torch.empty((self.num_wires), device=self.cdevice, dtype=self.dtype) ) self.weights = torch.nn.Parameter( torch.empty( (self.n_layers, 2 * (self.num_wires - 1)), device=self.cdevice, dtype=self.dtype, ) ) nn.init.uniform_(self.initial_layer_weights, a=0.0, b=2 * math.pi) nn.init.uniform_(self.weights, a=0.0, b=2 * math.pi)
[docs] def circuit(self, _: Optional[Tensor] = None) -> None: """ Define the quantum circuit for this layer. :param _: Input tensor that is passed to the quantum circuit (ignored). :type _: Optional[Tensor] """ for index, q in enumerate(self.wires): qml.RX( self.initial_layer_weights[index], q, ) for layer in range(self.n_layers): for i in range(0, len(self.wires) - 1, 2): qml.CNOT(wires=[self.wires[i], self.wires[i + 1]]) qml.RX( self.weights[layer, i], self.wires[i], ) qml.RX( self.weights[layer, i + 1], self.wires[i + 1], ) offset = int(self.num_wires / 2) * 2 for i in range(1, len(self.wires) - 1, 2): qml.CNOT(wires=[self.wires[i], self.wires[i + 1]]) qml.RX( self.weights[layer, offset + i - 1], self.wires[i], ) qml.RX( self.weights[layer, offset + i], self.wires[i + 1], )
[docs] class IQPERYCZLayer(CircuitLayer): """ Layer combining an IQP embedding layer and an RY-CZ variational layer. :param wires: The wires to be used by the layer. :type wires: Wires :param num_uploads: The number of times to repeat data uploading, defaults to 1. :type num_uploads: int, optional :param num_varlayers: The number of times to repeat the variational layer, defaults to 1. :type num_varlayers: int, optional :param num_repeat: The number of times to repeat the combined layers, defaults to 1. :type num_repeat: int, optional :param base: The base of the exponent by which the inputs are scaled on each repetition, defaults to 1.0 :type base: float, optional :param omega: The exponent for the base of the power by which the inputs are scaled on each repetition, defaults to 0.0 :type omega: float, optional :param cdevice: Classical device to store the observable weights. If None specified, the default device is used. :type cdevice: CDevice, optional :param dtype: Data type of the variational weights. :type dtype: DType, optional :param iqpe_opts: Options for the IQPE class. Defaults to empty. :type iqpe_opts: Dict, optional :param rycz_opts: Options for the RYCZLayer class. Defaults to empty. :type rycz_opts: Dict, optional """ def __init__( self, wires: Wires, num_uploads: int = 1, num_varlayers: int = 1, num_repeat: int = 1, base: Tensor = torch.tensor(1.0), omega: Tensor = torch.tensor(0.0), cdevice: Optional[CDevice] = None, dtype: Optional[DType] = None, iqpe_opts: Dict = {}, rycz_opts: Dict = {}, ) -> None: super().__init__(wires) self.num_uploads = num_uploads self.num_varlayers = num_varlayers self.num_repeat = num_repeat self.base = base self.omega = omega self.cdevice = cdevice self.dtype = dtype self.iqpe_opts = iqpe_opts self.rycz_opts = rycz_opts self.blocks = nn.ModuleList() num_var_repeats = 1 if len(self.wires) == 1: num_var_repeats = self.num_varlayers for _ in range(self.num_repeat): embed_layer = IQPEmbeddingLayer(self.wires, self.num_uploads, **self.iqpe_opts) var_layers = [] for _ in range(num_var_repeats): var_layer = RYCZLayer( self.wires, self.num_varlayers, self.cdevice, self.dtype, **self.rycz_opts, ) var_layers.append(var_layer) all_layers = [embed_layer] + var_layers block = nn.Sequential(*all_layers) self.blocks.append(block)
[docs] def circuit(self, x: Tensor) -> None: """ Define the quantum circuit for this layer. :param x: Input tensor that is passed to the quantum circuit. :type x: Tensor """ for i, block in enumerate(self.blocks): fac = self.base ** (self.omega * i) block(fac * x)
[docs] class IQPEAltRotCXLayer(CircuitLayer): """ Layer combining an IQP embedding layer and an alternating U3-CX variational layer. :param wires: The wires to be used by the layer. :type wires: Wires :param num_uploads: The number of times to repeat data uploading, defaults to 1. :type num_uploads: int, optional :param num_varlayers: The number of times to repeat the variational layer, defaults to 1. :type num_varlayers: int, optional :param num_repeat: The number of times to repeat the combined layers, defaults to 1. :type num_repeat: int, optional :param base: The base of the exponent by which the inputs are scaled on each repetition, defaults to 1.0 :type base: float, optional :param omega: The exponent for the base of the power by which the inputs are scaled on each repetition, defaults to 0.0 :type omega: float, optional :param cdevice: Classical device to store the observable weights. If None specified, the default device is used. :type cdevice: CDevice, optional :param dtype: Data type of the variational weights. :type dtype: DType, optional :param iqpe_opts: Options for the IQPE class. Defaults to empty. :type iqpe_opts: Dict, optional :param altrotcx_opts: Options for the AltRotCXLayer class. Defaults to empty. :type altrotcx_opts: Dict, optional """ def __init__( self, wires: Wires, num_uploads: int = 1, num_varlayers: int = 1, num_repeat: int = 1, base: Tensor = torch.tensor(1.0), omega: Tensor = torch.tensor(0.0), cdevice: Optional[CDevice] = None, dtype: Optional[DType] = None, iqpe_opts: Dict = {}, altrotcx_opts: Dict = {}, ) -> None: super().__init__(wires) self.num_uploads = num_uploads self.num_varlayers = num_varlayers self.num_repeat = num_repeat self.base = base self.omega = omega self.cdevice = cdevice self.dtype = dtype self.iqpe_opts = iqpe_opts self.altrotcx_opts = altrotcx_opts self.blocks = nn.ModuleList() for _ in range(self.num_repeat): embed_layer = IQPEmbeddingLayer(self.wires, self.num_uploads, **self.iqpe_opts) var_layer = AltRotCXLayer( self.wires, self.num_varlayers, self.cdevice, self.dtype, **self.altrotcx_opts, ) block = nn.Sequential(embed_layer, var_layer) self.blocks.append(block)
[docs] def circuit(self, x: Tensor) -> None: """ Define the quantum circuit for this layer. :param x: Input tensor that is passed to the quantum circuit. :type x: Tensor """ for i, block in enumerate(self.blocks): fac = self.base ** (self.omega * i) block(fac * x)
[docs] class HadamardLayer(CircuitLayer): """ A layer that adds Hadamard gates to each wire. :param wires: The wires to be used by the layer. :type wires: Wires """ def __init__(self, wires: Wires): super().__init__(wires) self.qfunc = qml.Hadamard
[docs] def circuit(self, _: Optional[Tensor] = None) -> None: """ Apply the quantum circuit for this layer. :param _: Input tensor that is passed to the quantum circuit (ignored). :type _: Optional[Tensor] """ for wire in self.wires: self.qfunc(wire)
[docs] class ParallelIQPEncoding(CircuitLayer): """ A class that applies the IQPEmbedding to different parts of the input data. :param wires: The wires on which the circuit will be applied :type wires: Wires :param num_features: The number of features in the input :type num_features: int :param n_repeat: The number of times the IQPEmbedding will be repeated, defaults to 1 :type n_repeat: int, optional :param base: The base of the exponent by which the inputs are scaled on each repetition, defaults to 1.0 :type base: float, optional :param omega: The exponent for the base of the power by which the inputs are scaled on each repetition, defaults to 0.0 :type omega: float, optional :raises ValueError: If the number of wires is less than the number of features :raises ValueError: If the number of wires is not a multiple of the number of features """ def __init__( self, wires: Wires, num_features: int, n_repeat: int = 1, base: Tensor = torch.tensor(1.0), omega: Tensor = torch.tensor(0.0), **kwargs, ) -> None: super().__init__(wires) self.num_features = num_features self.n_repeat = n_repeat self.base = base self.omega = omega self.kwargs = kwargs self.qfunc = qml.IQPEmbedding if not self.num_wires >= self.num_features: raise ValueError( f"The number of wires ({self.num_wires}) " f"must be greater than or equal to the number of features ({self.num_features})." ) if not self.num_wires % self.num_features == 0: raise ValueError( f"The number of wires ({self.num_wires}) " f"must be a multiple of the number of features ({self.num_features})." )
[docs] def circuit(self, x: Tensor) -> None: """ Apply the quantum circuit for this layer. :param x: Input tensor that is passed to the quantum circuit. :type x: Tensor """ num_features = x.shape[-1] if num_features != self.num_features: raise ValueError( f"Input tensor last dimension ({num_features}) " f"must be equal to the number of features ({self.num_features})." ) freq = 0 for i in range(0, len(self.wires), num_features): x_ = self.base ** (freq * self.omega) * x self.qfunc( x_, self.wires[i : i + num_features], self.n_repeat, **self.kwargs, ) freq += 1
[docs] class ParallelEntangledIQPEncoding(CircuitLayer): """ A class that applies the IQPEmbedding on the entire constructed large feature vector. :param wires: The wires on which the circuit will be applied :type wires: Wires :param num_features: The number of features in the input :type num_features: int :param n_repeat: The number of times the IQPEmbedding will be repeated, defaults to 1 :type n_repeat: int, optional :param base: The base of the exponent by which the inputs are scaled on each repetition, defaults to 1.0 :type base: float, optional :param omega: The exponent for the base of the power by which the inputs are scaled on each repetition, defaults to 0.0 :type omega: float, optional :raises ValueError: If the number of wires is less than the number of features :raises ValueError: If the number of wires is not a multiple of the number of features """ def __init__( self, wires: Wires, num_features: int, n_repeat: int = 1, base: Tensor = torch.tensor(1.0), omega: Tensor = torch.tensor(0.0), **kwargs, ) -> None: super().__init__(wires) self.num_features = num_features self.n_repeat = n_repeat self.base = base self.omega = omega self.kwargs = kwargs self.qfunc = qml.IQPEmbedding if not self.num_wires >= self.num_features: raise ValueError( f"The number of wires ({self.num_wires}) " f"must be greater than or equal to the number of features ({self.num_features})." ) if not self.num_wires % self.num_features == 0: raise ValueError( f"The number of wires ({self.num_wires}) " f"must be a multiple of the number of features ({self.num_features})." )
[docs] def circuit(self, x: Tensor) -> None: """ Apply the quantum circuit for this layer. :param x: Input tensor that is passed to the quantum circuit. :type x: Tensor """ num_features = x.shape[-1] if num_features != self.num_features: raise ValueError( f"Input tensor last dimension ({num_features}) " f"must be equal to the number of features ({self.num_features})." ) num_repeats = int(self.num_wires / num_features) x_large = [] for j in range(0, num_repeats): x_ = self.base ** (j * self.omega) * x x_large.append(x_) x_final = torch.cat(x_large) self.qfunc(x_final, self.wires, self.n_repeat, **self.kwargs)
[docs] class TwoQubitRotCXMPSLayer(CircuitLayer): """ Layer with 2-qubit MPS sctructure. :param wires: The wires to be used by the layer. :type wires: Wires :param n_layers_mps: The number of layers to repeat the MPS, defaults to 1. :type n_layers: int, optional :param n_layers_block: The number of layers for each block of the MPS, defaults to 1. :type n_layers_block: int, optional :param reverse: Flag to reverse the MPS sequence to bottom to top, defaults to False. :type reverse: bool, optional :param cdevice: Classical device to store the initial layer weights and internal layer weights, defaults to None. :type cdevice: CDevice, optional :param dtype: Data type of the weights, defaults to None. :type dtype: DType, optional """ def __init__( self, wires: Wires, n_layers_mps: int = 1, n_layers_block: int = 1, reverse: bool = False, cdevice: Optional[CDevice] = None, dtype: Optional[DType] = None, ) -> None: super().__init__(wires) self.n_layers_mps = n_layers_mps self.n_layers_block = n_layers_block self.reverse = reverse self.cdevice = cdevice self.dtype = dtype self.n_blocks = self.num_wires - 1 self.weights = torch.nn.Parameter( torch.empty( (self.n_layers_mps, self.n_blocks, 2, self.n_layers_block, 3), device=self.cdevice, dtype=self.dtype, ) ) self.weights_post = torch.nn.Parameter( torch.empty((self.num_wires, 3), device=self.cdevice, dtype=self.dtype) ) nn.init.uniform_(self.weights, a=0.0, b=2 * math.pi) nn.init.uniform_(self.weights_post, a=0.0, b=2 * math.pi)
[docs] def circuit(self, _: Optional[Tensor] = None) -> None: """ Define the quantum circuit for this layer. :param _: Input tensor that is passed to the quantum circuit (ignored). :type x: Optional[Tensor] """ for mps_layer_idx in range(self.n_layers_mps): for block_idx in ( range(self.n_blocks - 1, -1, -1) if self.reverse else range(self.n_blocks) ): self._block(mps_layer_idx, block_idx) for i, q in enumerate(self.wires): qml.Rot( self.weights_post[i, 0], self.weights_post[i, 1], self.weights_post[i, 2], q, )
def _block(self, mps_layer_idx, block_idx): qprev = self.wires[block_idx] qnext = self.wires[block_idx + 1] for block_layer in range(self.n_layers_block): qml.Rot( self.weights[mps_layer_idx, block_idx, 0, block_layer, 0], self.weights[mps_layer_idx, block_idx, 0, block_layer, 1], self.weights[mps_layer_idx, block_idx, 0, block_layer, 2], qprev, ) qml.Rot( self.weights[mps_layer_idx, block_idx, 1, block_layer, 0], self.weights[mps_layer_idx, block_idx, 1, block_layer, 1], self.weights[mps_layer_idx, block_idx, 1, block_layer, 2], qnext, ) qml.CNOT(wires=(qprev, qnext))
[docs] class EmbedU(CircuitLayer): """ Layer that embeds an arbitrary unitary. :param wires: The wires to be used by the layer. :type wires: Wires :param U: The unitary to embed. :type U: Tensor. """ def __init__(self, wires: Wires, U: Tensor) -> None: super().__init__(wires) self.U = U
[docs] def circuit(self, _: Optional[Tensor] = None) -> None: """ Define the quantum circuit for this layer. :param _: Input tensor that is passed to the quantum circuit (ignored). :type x: Optional[Tensor] """ qml.QubitUnitary(self.U, wires=self.wires, unitary_check=False)
[docs] class MeasurementLayer(nn.Module): """ Base class for measurment layers. Measurement layers are appended to quantum circuits and return classical output. :param circuits: Quantum circuits before the measurement. :type circuits: tuple :param qdevice: Quantum device. If None, the default device will be used. :type qdevice: Optional[QDevice], defaults to None :param measurement_type: Type of quantum measurement. :type measurement_type: MeasurementType, defaults to MeasurementType.Probabilities :param observables: Observables to measure. None only works with Probabilities and Samples. :type observables: Optional[Observables], defaults to None :param kwargs: Additional keyword arguments for qml.QNode. """ def __init__( self, *circuits, qdevice: Optional[QDevice] = None, measurement_type: MeasurementType = MeasurementType.Probabilities, observables: Optional[Observables] = None, **kwargs, ) -> None: super().__init__() self.circuits = nn.ModuleList(circuits) if qdevice is None: self.qdevice = qml.device(wires=self.circuits[0].wires, **DEFAULT_QDEV_CFG) else: self.qdevice = qdevice self.wires = self.circuits[0].wires self.measurement_type = measurement_type self.observables = observables if observables is not None and not isinstance(observables, Iterable): self.observables = [observables] self.interface = kwargs.pop("interface", "torch") self.diff_method = kwargs.pop("diff_method", "backprop") self.kwargs = kwargs self.check_measurement_type() self.qnode = self.set_qnode() self.subwires = [0]
[docs] def forward(self, x: Optional[Tensor] = None) -> Tensor: """ Forward pass, depending on measurement type. See :meth:`expectation`, :meth:`probabilities` and :meth:`samples`. :param x: Input data samples. :type circuits: Tensor. :return: Forward evaluation of model on data. :rtype: Tensor """ self.qnode = self.set_qnode() if x is not None: if len(x.shape) == 1: out = self.qnode(x) else: outs = [self.qnode(xk) for xk in torch.unbind(x)] out = torch.stack(outs) if (len(x.shape) == 1 and len(out.shape) == 0) or ( len(x.shape) > 1 and len(out.shape) == 1 ): out = out.unsqueeze(-1) else: out = self.qnode(x) return out
[docs] def expectation(self, x: Optional[Tensor] = None) -> Expectation: """ Calculate the expectation value of the observable for given circuits. :param x: Input tensor that is passed to the quantum circuits, defaults to None. :type x: Optional[Tensor] :return: Expectation value object for the observable. :rtype: Expectation """ for circuit in self.circuits: circuit(x) expec = None if self.observables is not None: expec = [qml.expval(obs) for obs in self.observables] return expec
[docs] def probabilities(self, x: Optional[Tensor] = None) -> Probability: """ Calculate the outcome probabilities for given circuits. :param x: Input tensor that is passed to the quantum circuits, defaults to None. :type x: Optional[Tensor] :return: Probabilities of the outcomes of the circuits. :rtype: Probability """ for circuit in self.circuits: circuit(x) probs = qml.probs(wires=self.wires) return probs
[docs] def samples(self, x: Optional[Tensor] = None) -> Sample: """ Sample the outcomes of the given circuits. :param x: Input tensor that is passed to the quantum circuits, defaults to None. :type x: Optional[Tensor] :return: Samples of the outcomes of the circuits. :rtype: Sample """ for circuit in self.circuits: circuit(x) sample = qml.sample(wires=self.wires) return sample
[docs] def entropy(self, x: Optional[Tensor] = None) -> Entropy: """ Calculate the Von Neumann Entropy of a subsystem. :param x: Input tensor that is passed to the quantum circuits, defaults to None. :type x: Optional[Tensor] :return: Entropy value object for a subsystem. :rtype: Entropy """ for circuit in self.circuits: circuit(x) entropy = qml.vn_entropy(self.subwires) return entropy
[docs] def set_qnode(self) -> QNode: """ Set the quantum node for the layer and measurement type. :return: The set QNode. :rtype: QNode """ if self.measurement_type == MeasurementType.Expectation: circuit = self.expectation elif self.measurement_type == MeasurementType.Probabilities: circuit = self.probabilities elif self.measurement_type == MeasurementType.Samples: circuit = self.samples elif self.measurement_type == MeasurementType.Entropy: circuit = self.entropy qnode = qml.QNode( circuit, self.qdevice, interface=self.interface, diff_method=self.diff_method, **self.kwargs, ) self.qnode = qnode return self.qnode
[docs] def check_measurement_type(self) -> None: """ Check if the measurement type is valid. Raises errors for invalid measurement types. :raises NotImplementedError: If the measurement type is not recognized. :raises ValueError: If the expectation measurement type doesn't have an observable, or if the sample measurement type doesn't have an integer number of shots. """ if not isinstance(self.measurement_type, MeasurementType): raise NotImplementedError(f"Measurement type ({self.measurement_type}) not recognized") if self.measurement_type == MeasurementType.Expectation: if self.observables is None: raise ValueError( f"Measurement type ({self.measurement_type}) " "requires an observable" ) if self.measurement_type == MeasurementType.Samples and self.qdevice.shots is None: raise ValueError( f"Measurement type ({self.measurement_type}) " "requires integer number of shots" )
[docs] class HamiltonianLayer(MeasurementLayer): """ A layer that computes the expectation of a Hamiltonian. The Hamiltonian is defined by a list of observables and their associated weights. The weights are trainable parameters. :param circuits: Quantum circuits that make up the circuit layer before measurement. :type circuits: tuple :param observables: Observables defining the Hamiltonian. :type observables: list of Observable :param qdevice: Quantum device. If None specified, the default device is used. :type qdevice: Optional[QDevice] :param cdevice: Classical device to store the observable weights. If None specified, the default device is used. :type cdevice: CDevice, optional :param dtype: Data type of the observable weights. :type dtype: DType, optional :param kwargs: Additional keyword arguments passed to the superclass. """ def __init__( self, *circuits, observables: Iterable[Observable], qdevice: Optional[QDevice] = None, cdevice=None, dtype=None, **kwargs, ) -> None: super().__init__( *circuits, qdevice=qdevice, measurement_type=MeasurementType.Expectation, observables=observables, **kwargs, ) self.cdevice = cdevice self.dtype = dtype # set observable weights self.num_weights = len(list(observables)) self.observable_weights = torch.nn.Parameter( torch.empty(self.num_weights, device=self.cdevice, dtype=self.dtype) ) nn.init.normal_(self.observable_weights) self.observable = qml.Hamiltonian(self.observable_weights, observables)
[docs] def expectation(self, x: Optional[Tensor] = None) -> Expectation: """ Compute the expectation of the Hamiltonian. :param x: Input tensor, defaults to None. :type x: Optional[Tensor] :return: Expectation value of the Hamiltonian. :rtype: Expectation """ for circuit in self.circuits: circuit(x) self.observable = qml.Hamiltonian(self.observable_weights, self.observables) expec = qml.expval(self.observable) return expec