import numpy as np
import pennylane as qml
import torch
[docs]
class QuantumFCLayer:
"""
Quantum fully-connected layer for hybrid quantum-classical neural networks.
This class defines a quantum layer that applies various quantum encodings
and ansatz circuits to process input data using a quantum neural network.
Attributes:
input_size (int): Dimension of the input data.
n_layers (int): Number of layers in the quantum ansatz circuit.
encoding (str): Name of the data encoding method.
Supported options: 'qubit', 'amplitude', 'ZZFeatureMap', 'QAOA'.
ansatz (int): Identifier for the ansatz quantum circuit (1-6).
observables (str or list of str): Observables measured at the end of the circuit.
If `None`, defaults to 'Z' * nqbits.
backend (str): Quantum backend used for execution.
Defaults to 'default.qubit' (Pennylane simulator).
nqbits (int): Number of qubits required for encoding.
dev (qml.Device): Quantum device used for simulation.
"""
def __init__(self, input_size, n_layers=2, encoding='qubit', ansatz=1,
observables=None, backend="default.qubit"):
"""
Initializes the quantum fully-connected layer.
Args:
input_size (int): Dimension of the input data.
n_layers (int, optional): Number of layers in the quantum ansatz circuit.
Defaults to 2.
encoding (str, optional): Encoding method for quantum data.
Supported options: 'qubit', 'amplitude', 'ZZFeatureMap', 'QAOA'. Defaults to 'qubit'.
ansatz (int, optional): Identifier for the quantum ansatz circuit (1-6). Defaults to 1.
observables (str or list, optional): Observables measured at the end of the circuit.
If `None`, defaults to 'Z' * nqbits.
backend (str, optional): Quantum backend for execution.
Defaults to 'default.qubit' (Pennylane simulator).
Raises:
NotImplementedError: If an invalid ansatz number is provided.
"""
self.input_size = input_size
self.n_layers = n_layers
self.encoding = encoding
# Calculate the number of qubits needed for the data encoding
if self.encoding == 'amplitude':
self.nqbits = int(np.ceil(np.log2(self.input_size)))
else:
self.nqbits = self.input_size
ansatz_names = [
"circuit_10", "circuit_9", "circuit_15", "circuit_14",
"circuit_13", "circuit_6"
]
if type(ansatz) != int or ansatz < 1 or ansatz > 6:
raise NotImplementedError(
'Choose a quantum ansatz between 1 and 6')
self.ansatz = ansatz_names[ansatz-1]
self.get_shapes()
self.observables = \
"Z"*self.nqbits if observables is None else observables
if isinstance(self.observables, list): # Convert to operators
self.observables = [qml.pauli.string_to_pauli_word(o) for o in self.observables]
self.backend = backend
self.dev = qml.device(self.backend, wires=self.nqbits)
###########################################################################
# Quantum encodings
###########################################################################
def _qubit_encoding(self, nqbits, feature_vector):
"""
Generates the circuit that performs qubit encoding.
Args:
nqbits (int): Number of qubits
feature_vector (array): input data
"""
qml.AngleEmbedding(features=feature_vector, wires=range(nqbits), rotation='X')
def _ZZFeatureMap_encoding(self, nqbits, feature_vector, n_layers=2):
"""
Generates the circuit that performs ZZFeatureMap encoding.
Args:
nqbits (int): Number of qubits
feature_vector (array): Input data
n_layers (int): Number of layers
"""
for _ in range(n_layers):
for i in range(nqbits):
qml.Hadamard(wires = i)
qml.U1(2*feature_vector[i], wires =i)
# CNOT + hadamard
for i in range(1,nqbits):
if i-2>=0:
qml.CNOT(wires=[i-2, i-1])
qml.CNOT(wires=[i-1, i])
qml.U1(2*(np.pi - feature_vector[i-1])*(np.pi - feature_vector[i]), wires = i)
qml.CNOT(wires=[nqbits - 2, nqbits-1])
def _amplitude_encoding(self, feature_vector):
"""
Generates the circuit that performs amplitude encoding.
Args:
feature_vector (array): Input data
"""
qml.AmplitudeEmbedding(features=feature_vector, wires=range(self.nqbits),normalize=True)
def _QAOA_encoding(self, feature_vector, input_weights):
"""
Generates the circuit that performs QAOA encoding. Notice that this encoding has trainable parameters
Args:
feature_vector (array): Input data
input_weights (array): shape (L,1) for 1 qubit, (L,3) for two qubits and (L, 2*nqbits) otherwise
"""
qml.QAOAEmbedding(features=feature_vector, weights=input_weights, wires=range(len(feature_vector)))
###########################################################################
# Quantum ansatz
###########################################################################
def _circuit_10(self, qubits, weights):
"""
Generates the quantum circuit corresponding to the circuit_10 ansatz
(option 1)
Args:
qbits (int): Qubits to apply this circuit
weights (array): shape (n_layers, nqbits, 3).
"""
# Choose the number of layers of the PQC
qml.StronglyEntanglingLayers(weights=weights, wires=qubits)
def _circuit_6(self, qubits, weights):
"""
Generates the quantum circuit corresponding to the circuit_6 ansatz
(option 6)
Args:
qbits (int): Qubits to apply this circuit
weights (array): shape (nqbits, [ 1 (Rx1) + 1 (Rz1) + 1 x nqbits -1 (cRx) + 1 (Rx2) + 1 (Rz2)] x n_layers)
"""
size = weights.shape[1]
nqbits = len(qubits)
n_layers = int(size/(4 + (nqbits - 1)))
for layer in range(n_layers):
# First Rx layer
for i in range(nqbits):
qml.RX(weights[i,0 + layer-1], wires = qubits[i])
# First RZ layer
for i in range(nqbits):
qml.RZ(weights[i,1 + layer-1], wires = qubits[i])
# Controlled Rx
l_idx = 0
for i in range(nqbits-1, -1, -1): # i = controlled qubits
for q in range(nqbits-1, -1, -1):
if i == q:
l_idx += 1
# Can't link to itself
continue
ly = l_idx//nqbits
lx = l_idx%nqbits
qml.CRX(weights[lx, 1 + ly + layer-1], wires = [qubits[i], qubits[q]])
l_idx += 1
# Second Rx layer
for i in range(nqbits):
qml.RX(weights[i,4 + nqbits - 3 + layer-1], wires = qubits[i])
# Second Ry layer
for i in range(nqbits):
qml.RZ(weights[i,4 + nqbits - 2 + layer-1], wires = qubits[i])
def _circuit_9(self, qubits, weights):
"""
Generates the quantum circuit corresponding to the circuit_9 ansatz
(option 2)
Args:
qbits (int): Qubits to apply this circuit
weights (array): shape (nqbits, n_layers)
"""
n_layers = weights.shape[1]
nqbits = len(qubits)
qbit_list = [(i, i-1) for i in range(nqbits-1, 0, -1)]
for layer in range(n_layers):
# 1. Hadamard
for q in range(nqbits):
qml.Hadamard(wires = qubits[q])
# 2. Phase shifts
for (qbit1, qbit2) in qbit_list:
qml.CZ(wires = [qubits[qbit1], qubits[qbit2]])
# 3. Ry gates
for q in range(nqbits):
qml.RX(weights[q, layer], wires = qubits[q])
def _circuit_15(self, qubits, weights):
"""
Generates the quantum circuit corresponding to the circuit_15 ansatz
(option 3)
Args:
qbits (int): Qubits to apply this circuit
weights (array): shape (n_layers, nqbits)
"""
qml.BasicEntanglerLayers(weights=weights, wires=qubits)
def _circuit_14(self, qubits, weights):
"""
Generates the quantum circuit corresponding to the circuit_14 ansatz
(option 4)
Args:
qbits (int): Qubits to apply this circuit
weights (array): shape (nqbits , [1 (Ry1) + 1 (CRx1) + 1 (Ry2) + 1 (CRx2)]*n_layers)
"""
n_layers = int(weights.shape[1]/4)
nqbits = len(qubits)
qbit_list = [(0, nqbits-1)] + [(i,i-1) for i in range(nqbits-1,0,-1)]
qbit_list2 = [(nqbits-2, nqbits-1), (nqbits-1,0)] + [(i,i+1) for i in range(0, nqbits-2)]
for layer in range(n_layers):
# 1. Ry gates
for i in range(nqbits):
qml.RY(weights[i,0 + layer -1], wires = qubits[i])
# 2. Controlled Rx
for (qbit1, qbit2) in qbit_list:
qml.CRX(weights[i,1 + layer -1], wires = [qubits[qbit2], qubits[qbit1]])
# 3. Ry gates
for i in range(nqbits):
qml.RY(weights[i,2 + layer -1], wires = qubits[i])
# 4. Controlled Rx
for (qbit1, qbit2) in qbit_list2:
qml.CRX(weights[i,1 + layer -1], wires = [qubits[qbit2], qubits[qbit1]])
def _circuit_13(self, qubits, weights):
"""
Generates the quantum circuit corresponding to the circuit_9 ansatz
(option 5)
Args:
qbits (int): Qubits to apply this circuit
weights (array): shape (nqbits x [1 (Ry1) + 1 (CRx1) + 1 (Ry2) + 1 (CRx2)]*n_layers)
"""
n_layers = int(weights.shape[1]/4)
nqbits = len(qubits)
qbit_list = [(0, nqbits-1)] + [(i,i-1) for i in range(nqbits-1,0,-1)]
qbit_list2 = [(nqbits-2, nqbits-1), (nqbits-1,0)] + [(i,i+1) for i in range(0, nqbits-2)]
for layer in range(n_layers):
# 1. Ry gates
for i in range(nqbits):
qml.RY(weights[i,0 + layer -1], wires = qubits[i])
# 2. Controlled Rx
for (qbit1, qbit2) in qbit_list:
qml.CRZ(weights[i,1 + layer -1], wires = [qubits[qbit2], qubits[qbit1]])
# 3. Ry gates
for i in range(nqbits):
qml.RY(weights[i,2 + layer -1], wires = qubits[i])
# 4. Controlled Rx
for (qbit1, qbit2) in qbit_list2:
qml.CRZ(weights[i,1 + layer -1], wires = [qubits[qbit2], qubits[qbit1]])
[docs]
def apply_ansatz(self, weights_layer, qubits = None):
"""
Applies the quantum ansatz to the quantum neural network.
Args:
weight_layers (array): Weights of the ansatz layer
qubits: qubits to apply the ansatz to
Returns:
(QuantumNN): quantum neural network
"""
# Ansatz
name_to_func = {
"circuit_10": self._circuit_10,
"circuit_6": self._circuit_6,
"circuit_9": self._circuit_9,
"circuit_15": self._circuit_15,
"circuit_14": self._circuit_14,
"circuit_13": self._circuit_13,
}
if self.ansatz not in name_to_func:
raise NotImplementedError(
f"Quantum Circuit model '{self.ansatz}' not implemented. "
"Implemented models: " +
", ".join(list(name_to_func.keys()))
)
if qubits is None:
qubits = range(self.nqbits)
# Apply anstatz
name_to_func[self.ansatz](qubits, weights_layer)
if self.observables!=False:
# Return measurements
if self.observables=='probs': # Case 1: Measuring probabilities
return qml.probs(wires=list(range(self.nqbits)))
elif self.observables=='state': # Case 2: Measuring states
return qml.state()
else: # Case 3: Measuring observables
return [qml.expval(obs) for obs in self.observables]
[docs]
def qnn_layer(self,inputs, weights_layers):
"""
Creates the quantum neural network composed of the quantum encoding with qubit, amplitude or ZZ encoding,
and a quantum ansatz.
Args:
inputs (array): Input data
weight_layers (array): Weights of the ansatz layer
Returns:
(QuantumNN): quantum neural network
"""
# Data encoding
if self.encoding == 'angle' or self.encoding=='qubit':
self._qubit_encoding(self.nqbits, inputs)
elif self.encoding == 'amplitude':
self._amplitude_encoding(inputs)
elif self.encoding == 'ZZFeatureMap':
self._ZZFeatureMap_encoding(self.nqbits,inputs)
else:
raise NotImplementedError('Data encoding method not implemented')
return self.apply_ansatz(weights_layers)
[docs]
def qnn_layer_QAOA(self,inputs, weights_layers, weights_input):
"""
Creates the quantum neural network composed of the quantum encoding with QAOA encoding,
and a quantum ansatz.
Args
inputs (array): Input data
weight_layers (array): Weights of the ansatz layer
weight_input (array): Weights of the QAOA quantum encoding
Returns:
(QuantumNN): quantum neural network
"""
# Data encoding
self._QAOA_encoding(inputs, weights_input)
return self.apply_ansatz(weights_layers)
[docs]
def get_shapes(self):
# Define weight shapes
if self.ansatz=='circuit_10':
self.weights_shape = (self.n_layers,self.nqbits, 3)
elif self.ansatz=='circuit_6':
self.weights_shape = (self.nqbits,(3 + self.nqbits)*self.n_layers)
elif self.ansatz=='circuit_9':
self.weights_shape = (self.nqbits, self.n_layers)
elif self.ansatz=='circuit_15':
self.weights_shape = ( self.n_layers, self.nqbits)
elif self.ansatz=='circuit_14':
self.weights_shape = (self.nqbits,4*self.n_layers)
elif self.ansatz=='circuit_13':
self.weights_shape = (self.nqbits,4*self.n_layers)
[docs]
def create_layer(self, type_layer='keras'):
"""
Creates a Quantum fully connected layer and initializes it.
Args:
type_layer (str): Type of quantum layer. It can either be 'keras' or 'torch'
Returns:
Quantum layer
"""
# Define weight input shapes
if self.encoding=='QAOA':
if self.nqbits==1:
self.weights_input_shape = (2,1) # 2 layers
elif self.nqbits==2:
self.weights_input_shape = (2,3)
else:
self.weights_input_shape = (2,2*self.nqbits)
shapes = {"weights_input":self.weights_input_shape, "weights_layers": self.weights_shape} # Define shapes
self.qnode = qml.QNode(self.qnn_layer_QAOA, self.dev) # Create quantum node
else:
shapes = {"weights_layers": self.weights_shape} # Define shapes
self.qnode = qml.QNode(self.qnn_layer, self.dev)# Create quantum node
# Create quantum layer
if type_layer=='keras':
if isinstance(self.observables, list):
output_dim = len(self.observables)
else:
output_dim = 2**self.nqbits
qlayer = qml.qnn.KerasLayer(self.qnode, shapes, output_dim=output_dim) # Create keras layer
elif type_layer=='torch':
if self.encoding=='QAOA':
init_method = {"weights_layers": torch.nn.init.normal_, "weights_input": torch.nn.init.normal_}
else:
init_method = {"weights_layers": torch.nn.init.normal_}
qlayer = qml.qnn.TorchLayer(self.qnode, shapes, init_method=init_method) # Create Torch layer
else:
raise NotImplementedError(
'type_layer should either be keras or torch.')
return qlayer