"""
# Measure_Lib support will added in a future version. Particularly essential
# when the Hamiltionian have non-commuting terms.
# from entropica_qaoa.vqe.measurelib import (append_measure_register,
# commuting_decomposition,
# sampling_expectation,
# kron_eigs,
# base_change_fun,
# wavefunction_expectation)
"""
from abc import ABC, abstractmethod, abstractproperty
from typing import Union, List, Dict, Optional, Any, Tuple
from copy import deepcopy
import numpy as np
from .basedevice import DeviceBase
from ..qaoa_components import (
GateMap,
QAOADescriptor,
)
from ..qaoa_components.variational_parameters.variational_baseparams import (
QAOAVariationalBaseParams,
)
from ..utilities import qaoa_probabilities, round_value
from .cost_function import cost_function
class QuantumCircuitBase:
"""
Phantom class to indicate Quantum Circuits constructed using
several acceptable services. For instance, IBMQ, PyQuil
"""
pass
[docs]class VQABaseBackend(ABC):
"""
This is the Abstract Base Class over which other classes will be built.
Since, this is an Abstract Base class, in order to prevent its initialisation
the class methods -- ``__init__`` and ``__cal__`` will be decorated as
`abstractmethods`.
The Child classes MUST implement and override these abstract methods in their
implementation specific to their needs.
NOTE:
In addition one can also implement other methods which are not
necessitated by the ``VQABaseBackend`` Base Class
Parameters
----------
prepend_state: `Union[QuantumCircuitBase, List[complex], np.ndarray]`
The initial state to start the quantum circuit in the backend.
append_state: `Union[QuantumCircuitBase, np.ndarray]`
The final state to append to the quantum circuit in the backend.
"""
@abstractmethod
def __init__(
self,
prepend_state: Optional[Union[QuantumCircuitBase, List[complex], np.ndarray]],
append_state: Optional[Union[QuantumCircuitBase, np.ndarray]],
):
"""The constructor. See class docstring"""
self.prepend_state = prepend_state
self.append_state = append_state
[docs] @abstractmethod
def expectation(self, params: Any) -> float:
"""
Call the execute function on the circuit to compute the
expectation value of the Quantum Circuit w.r.t cost operator
"""
pass
[docs] @abstractmethod
def expectation_w_uncertainty(self, params: Any) -> Tuple[float, float]:
"""
Call the execute function on the circuit to compute the
expectation value of the Quantum Circuit w.r.t cost operator
along with its uncertainty
"""
pass
@abstractproperty
def exact_solution(self):
"""
Use linear algebra to compute the exact solution of the problem
Hamiltonian classically.
"""
pass
[docs]class QAOABaseBackend(VQABaseBackend):
"""
This class inherits from the VQABaseBackend and needs to be backend
agnostic to QAOA implementations on different devices and their
respective SDKs.
Parameters
----------
qaoa_descriptor: `QAOADescriptor`
This object handles the information to design the QAOA circuit ansatz
prepend_state: `Union[QuantumCircuitBase, List[complex]]`
Warm Starting the QAOA problem with some initial state other than the regular
$|+ \\rangle ^{otimes n}$
append_state: `Union[QuantumCircuitBase, List[complex]]`
Appending a user-defined circuit/state to the end of the QAOA routine
init_hadamard: `bool`
Initialises the QAOA circuit with a hadamard when ``True``
cvar_alpha: `float`
"""
def __init__(
self,
qaoa_descriptor: QAOADescriptor,
prepend_state: Optional[Union[QuantumCircuitBase, List[complex], np.ndarray]],
append_state: Optional[Union[QuantumCircuitBase, np.ndarray]],
init_hadamard: bool,
cvar_alpha: float,
):
super().__init__(prepend_state, append_state)
self.qaoa_descriptor = qaoa_descriptor
self.cost_hamiltonian = qaoa_descriptor.cost_hamiltonian
self.n_qubits = self.qaoa_descriptor.n_qubits
self.init_hadamard = init_hadamard
self.cvar_alpha = cvar_alpha
self.problem_qubits = self.qaoa_descriptor.cost_hamiltonian.n_qubits
self.abstract_circuit = deepcopy(self.qaoa_descriptor.abstract_circuit)
# pass the generated mappings if the circuit is routed
if self.qaoa_descriptor.routed == True:
self.initial_qubit_mapping = self.qaoa_descriptor.initial_mapping
if self.qaoa_descriptor.p % 2 != 0:
self.final_mapping = self.qaoa_descriptor.final_mapping
else:
# if even, the initial mapping [0,...,n_qubits-1] is taken as the final mapping
self.final_mapping = list(
range(len(self.qaoa_descriptor.final_mapping))
)
else:
self.initial_qubit_mapping = None
self.final_mapping = None
[docs] def assign_angles(self, params: QAOAVariationalBaseParams) -> None:
"""
Assigns the angle values of the variational parameters to the circuit gates
specified as a list of gates in the ``abstract_circuit``.
Parameters
----------
params: `QAOAVariationalBaseParams`
The variational parameters(angles) to be assigned to the circuit gates
"""
# if circuit is non-parameterised, then assign the angle values to the circuit
abstract_circuit = self.abstract_circuit
for each_gate in abstract_circuit:
gate_label_layer = each_gate.gate_label.layer
gate_label_seq = each_gate.gate_label.sequence
if each_gate.gate_label.n_qubits == 2:
if each_gate.gate_label.type.value == "MIXER":
angle = params.mixer_2q_angles[gate_label_layer, gate_label_seq]
elif each_gate.gate_label.type.value == "COST":
angle = params.cost_2q_angles[gate_label_layer, gate_label_seq]
elif each_gate.gate_label.n_qubits == 1:
if each_gate.gate_label.type.value == "MIXER":
angle = params.mixer_1q_angles[gate_label_layer, gate_label_seq]
elif each_gate.gate_label.type.value == "COST":
angle = params.cost_1q_angles[gate_label_layer, gate_label_seq]
each_gate.angle_value = angle
self.abstract_circuit = abstract_circuit
[docs] def obtain_angles_for_pauli_list(
self, input_gate_list: List[GateMap], params: QAOAVariationalBaseParams
) -> List[float]:
"""
This method uses the pauli gate list information to obtain the pauli angles
from the VariationalBaseParams object. The floats in the list are in the order
of the input GateMaps list.
Parameters
----------
input_gate_list: `List[GateMap]`
The GateMap list including rotation gates
params: `QAOAVariationalBaseParams`
The variational parameters(angles) to be assigned to the circuit gates
Returns
-------
angles_list: `List[float]`
The list of angles in the order of gates in the `GateMap` list
"""
angle_list = []
for each_gate in input_gate_list:
gate_label_layer = each_gate.gate_label.layer
gate_label_seq = each_gate.gate_label.sequence
if each_gate.gate_label.n_qubits == 2:
if each_gate.gate_label.type.value == "MIXER":
angle_list.append(
params.mixer_2q_angles[gate_label_layer, gate_label_seq]
)
elif each_gate.gate_label.type.value == "COST":
angle_list.append(
params.cost_2q_angles[gate_label_layer, gate_label_seq]
)
elif each_gate.gate_label.n_qubits == 1:
if each_gate.gate_label.type.value == "MIXER":
angle_list.append(
params.mixer_1q_angles[gate_label_layer, gate_label_seq]
)
elif each_gate.gate_label.type.value == "COST":
angle_list.append(
params.cost_1q_angles[gate_label_layer, gate_label_seq]
)
return angle_list
[docs] @abstractmethod
def qaoa_circuit(self, params: QAOAVariationalBaseParams) -> QuantumCircuitBase:
"""
Construct the QAOA circuit and append the parameter values to obtain the final
circuit ready for execution on the device.
Parameters
----------
params: `QAOAVariationalBaseParams`
The QAOA parameters as a 1D array (derived from an object of one of
the parameter classes, containing hyperparameters and variable parameters).
Returns
-------
quantum_circuit: `QuantumCircuitBase`
A Quantum Circuit object of type created by the respective
backend service
"""
pass
[docs] @abstractmethod
def get_counts(self, params: QAOAVariationalBaseParams, n_shots=None) -> dict:
"""
This method will be implemented in the child classes according to the type
of backend used.
Parameters
----------
params: `QAOAVariationalBaseParams`
The QAOA parameters - an object of one of the parameter classes, containing
variable parameters.
n_shots: `int`
The number of shots to be used for the measurement. If None, the backend default.
"""
pass
@round_value
def expectation(self, params: QAOAVariationalBaseParams, n_shots=None) -> float:
"""
Compute the expectation value w.r.t the Cost Hamiltonian
Parameters
----------
params: `QAOAVariationalBaseParams`
The QAOA parameters - an object of one of the parameter classes, containing
variable parameters.
n_shots: `int`
The number of shots to be used for the measurement. If None, the backend default.
Returns
-------
float:
Expectation value of cost operator wrt to quantum state produced by QAOA circuit
"""
counts = self.get_counts(params, n_shots)
cost = cost_function(
counts, self.qaoa_descriptor.cost_hamiltonian, self.cvar_alpha
)
return cost
@round_value
def expectation_w_uncertainty(
self, params: QAOAVariationalBaseParams, n_shots=None
) -> Tuple[float, float]:
"""
Compute the expectation value w.r.t the Cost Hamiltonian and its uncertainty
Parameters
----------
params: `QAOAVariationalBaseParams`
The QAOA parameters - an object of one of the parameter classes, containing
variable parameters.
n_shots: `int`
The number of shots to be used for the measurement. If None, the backend default.
Returns
-------
Tuple[float]:
expectation value and its uncertainty of cost operator wrt
to quantum state produced by QAOA circuit.
"""
counts = self.get_counts(params, n_shots)
cost = cost_function(
counts, self.qaoa_descriptor.cost_hamiltonian, self.cvar_alpha
)
cost_sq = cost_function(
counts,
self.qaoa_descriptor.cost_hamiltonian.hamiltonian_squared,
self.cvar_alpha,
)
uncertainty = np.sqrt(cost_sq - cost**2)
return (cost, uncertainty)
[docs] @abstractmethod
def reset_circuit(self):
"""
Reset the circuit attribute
"""
pass
@property
def exact_solution(self):
"""
Computes exactly the minimum energy of the cost function and its
corresponding configuration of variables using standard numpy module.
Returns
-------
(energy, config): `Tuple[float, list]`
- The minimum eigenvalue of the cost Hamiltonian,
- The minimum energy eigenvector as a binary array
configuration: qubit-0 as the first element in the sequence
"""
register = self.qaoa_descriptor.qureg
terms = self.cost_hamiltonian.terms
coeffs = self.cost_hamiltonian.coeffs
constant_energy = self.cost_hamiltonian.constant
diag = np.zeros((2 ** len(register)))
for i, term in enumerate(terms):
out = np.real(coeffs[i])
for qubit in register:
if qubit in term.qubit_indices:
out = np.kron([1, -1], out)
else:
out = np.kron([1, 1], out)
diag += out
# add the constant energy contribution
diag += constant_energy
# index = np.argmin(diag)
energy = np.min(diag)
indices = []
for idx in range(len(diag)):
if diag[idx] == energy:
indices.append(idx)
config_strings = [
np.binary_repr(index, len(register))[::-1] for index in indices
]
configs = [
np.array([int(x) for x in config_str]) for config_str in config_strings
]
return energy, configs
[docs] def bitstring_energy(self, bitstring: Union[List[int], str]) -> float:
"""
Computes the energy of a given bitstring with respect to the cost Hamiltonian.
Parameters
----------
bitstring : `Union[List[int],str]`
A list of integers 0 and 1 of length `n_qubits` representing a configuration.
Returns
-------
float:
The energy of a given bitstring with respect to the cost Hamiltonian.
"""
energy = 0
string_rev = bitstring
terms = self.cost_hamiltonian.terms
coeffs = self.cost_hamiltonian.coeffs
constant_energy = self.cost_hamiltonian.constant
for i, term in enumerate(terms):
variables_product = np.prod([(-1) ** string_rev[k] for k in term])
energy += coeffs[i] * variables_product
energy += constant_energy
return energy
[docs] @abstractmethod
def circuit_to_qasm(self):
"""
Implement a method to construct a QASM string from the current
state of the QuantumCircuit for the backends
"""
pass
[docs]class QAOABaseBackendStatevector(QAOABaseBackend):
"""
Base backend class for a statevector simulator backend
"""
[docs] @abstractmethod
def wavefunction(self, params: QAOAVariationalBaseParams) -> List[complex]:
"""
Get the wavefunction of the state produced by the QAOA circuit.
Parameters
----------
params: `QAOAVariationalBaseParams`
The QAOA parameters - an object of one of the parameter classes, containing
the variational parameters (angles).
Returns
-------
List[complex]:
A list of the wavefunction amplitudes.
"""
pass
[docs] def sample_from_wavefunction(
self, params: QAOAVariationalBaseParams, n_samples: int
) -> np.ndarray:
"""
Get the shot-based measurement results from the statevector. The return object is
a list of shot-results.
Parameters
----------
params: `QAOAVariationalBaseParams`
The QAOA parameters as a 1D array (derived from an object of one of the
parameter classes, containing hyperparameters and variable parameters).
n_samples: `int`
The number of measurement samples required; specified as integer
Returns
-------
np.ndarray:
A list of measurement outcomes sampled from a statevector
"""
wf = self.wavefunction(params)
prob_vec = np.real(np.conjugate(wf) * wf)
samples = np.random.choice(len(prob_vec), p=prob_vec, size=n_samples)
samples = [np.binary_repr(num, self.n_qubits)[::-1] for num in samples]
return samples
[docs] def probability_dict(self, params: QAOAVariationalBaseParams):
"""
Get the counts style probability dictionary with all basis states
and their corresponding probabilities. Constructed using the complete
statevector
Parameters
----------
params: `QAOAVariationalBaseParams`
The QAOA parameters as a 1D array (derived from an object of one of the
parameter classes, containing hyperparameters and variable parameters).
Returns
-------
Dict[str, float]:
A dictionary of all basis states and their corresponding probabilities.
"""
wf = self.wavefunction(params)
return qaoa_probabilities(wf)
[docs] def get_counts(self, params: QAOAVariationalBaseParams, n_shots: int) -> Dict:
"""
Measurement outcome vs frequency information from a circuit execution
represented as a python dictionary
Parameters
----------
params: `VariationalBaseParams`
The QAOA parameters as a 1D array (derived from an object of one of the
parameter classes, containing hyperparameters and variable parameters).
n_shots: `int`
The number of measurement shots required; specified as integer
Returns
-------
Dict[str, float]:
A dictionary of measurement outcomes vs frequency sampled from a statevector
"""
samples = self.sample_from_wavefunction(params, n_shots)
unique_nums, frequency = np.unique(samples, return_counts=True)
counts = dict(zip(unique_nums, frequency))
return counts
[docs]class QAOABaseBackendShotBased(QAOABaseBackend):
"""
Implementation of Backend object specific to shot-based simulators and QPUs
"""
def __init__(
self,
qaoa_descriptor: QAOADescriptor,
n_shots: int,
prepend_state: Optional[QuantumCircuitBase],
append_state: Optional[QuantumCircuitBase],
init_hadamard: bool,
cvar_alpha: float,
):
super().__init__(
qaoa_descriptor, prepend_state, append_state, init_hadamard, cvar_alpha
)
# assert self.n_qubits >= len(prepend_state.qubits), \
# "Cannot attach a bigger circuit to the QAOA routine"
# assert self.n_qubits >= len(append_state.qubits), \
# "Cannot attach a bigger circuit to the QAOA routine"
self.n_shots = n_shots
[docs] @abstractmethod
def get_counts(self, params: QAOAVariationalBaseParams, n_shots=None) -> dict:
"""
Measurement outcome vs frequency information from a circuit execution
represented as a python dictionary
Parameters
----------
params: `QAOAVariationalBaseParams`
The QAOA parameters as a 1D array (derived from an object of one of the
parameter classes, containing hyperparameters and variable parameters).
n_shots: `int`
The number of shots to be used for the measurement. If None, the backend default.
Returns
-------
Dict[str, float]:
A dictionary of measurement outcomes vs frequency sampled from a statevector
"""
pass
[docs]class QAOABaseBackendCloud:
"""
QAOA backend that can be accessed over the cloud offered by the
respective provider through an API based access
"""
def __init__(self, device: DeviceBase):
self.device = device
if (
getattr(self.device, "provider_connected", None) is None
and getattr(self.device, "qpu_connected", None) is None
):
self.device.check_connection()
[docs]class QAOABaseBackendParametric:
"""
Base class to indicate Parametric Circuit Backend
"""
[docs] @abstractmethod
def parametric_qaoa_circuit(self):
pass