import numpy as np
from typing import Union, List, Tuple, Optional
# IBM Qiskit imports
from qiskit import QuantumCircuit, QuantumRegister
from qiskit.providers.aer import AerSimulator
from qiskit.providers.aer.noise import NoiseModel
from qiskit.quantum_info import Statevector, SparsePauliOp
from qiskit.circuit import Parameter
from .gates_qiskit import QiskitGateApplicator
from openqaoa.backends.basebackend import (
QAOABaseBackendParametric,
QAOABaseBackendShotBased,
QAOABaseBackendStatevector,
)
from openqaoa.qaoa_components import QAOADescriptor
from openqaoa.qaoa_components.variational_parameters.variational_baseparams import (
QAOAVariationalBaseParams,
)
from openqaoa.utilities import (
flip_counts,
generate_uuid,
round_value,
)
from openqaoa.backends.cost_function import cost_function
from openqaoa.qaoa_components.ansatz_constructor import (
RXGateMap,
RYGateMap,
RZGateMap,
RXXGateMap,
RYYGateMap,
RZZGateMap,
RZXGateMap,
)
[docs]class QAOAQiskitBackendShotBasedSimulator(
QAOABaseBackendShotBased, QAOABaseBackendParametric
):
"""
Local Shot-based simulators offered by Qiskit
Parameters
----------
qaoa_descriptor: `QAOADescriptor`
An object of the class ``QAOADescriptor`` which contains information on
circuit construction and depth of the circuit.
n_shots: `int`
The number of shots to be taken for each circuit.
seed_simulator: `int`
Pseudorandom list of numbers of a seed
prepend_state: `QuantumCircuit`
The state prepended to the circuit.
append_state: `QuantumCircuit`
The state appended to the circuit.
init_hadamard: `bool`
Whether to apply a Hadamard gate to the beginning of the
QAOA part of the circuit.
cvar_alpha: `float`
The value of alpha for the CVaR cost function.
qiskit_simulation_method: `str`
The method to be used for the simulation.
noise_model: `NoiseModel`
The Qiskit noise model to be used for the simulation.
"""
QISKIT_GATEMAP_LIBRARY = [
RXGateMap,
RYGateMap,
RZGateMap,
RXXGateMap,
RYYGateMap,
RZZGateMap,
RZXGateMap,
]
def __init__(
self,
qaoa_descriptor: QAOADescriptor,
n_shots: int,
prepend_state: Optional[QuantumCircuit],
append_state: Optional[QuantumCircuit],
init_hadamard: bool,
cvar_alpha: float,
qiskit_simulation_method: str = "automatic",
seed_simulator: Optional[int] = None,
noise_model: Optional[NoiseModel] = None,
):
QAOABaseBackendShotBased.__init__(
self,
qaoa_descriptor,
n_shots,
prepend_state,
append_state,
init_hadamard,
cvar_alpha,
)
self.gate_applicator = QiskitGateApplicator()
self.qureg = QuantumRegister(self.n_qubits)
if self.prepend_state:
assert self.n_qubits >= len(prepend_state.qubits), (
"Cannot attach a bigger circuit " "to the QAOA routine"
)
# options = {"seed_simulator":1}
self.backend_simulator = AerSimulator(
method=qiskit_simulation_method.lower(),
noise_model=noise_model,
seed_simulator=seed_simulator,
)
# For parametric circuits
self.parametric_circuit = self.parametric_qaoa_circuit
[docs] def qaoa_circuit(self, params: QAOAVariationalBaseParams) -> QuantumCircuit:
"""
The final QAOA circuit to be executed on the QPU.
Parameters
----------
params: `QAOAVariationalBaseParams`
Returns
-------
qaoa_circuit: `QuantumCircuit`
The final QAOA circuit after binding angles from variational parameters.
"""
angles_list = self.obtain_angles_for_pauli_list(self.abstract_circuit, params)
memory_map = dict(zip(self.qiskit_parameter_list, angles_list))
circuit_with_angles = self.parametric_circuit.assign_parameters(memory_map)
if self.append_state:
circuit_with_angles = circuit_with_angles.compose(self.append_state)
circuit_with_angles.measure_all()
return circuit_with_angles
@property
def parametric_qaoa_circuit(self) -> QuantumCircuit:
"""
Creates a parametric QAOA circuit, given the qubit pairs, single qubits with biases,
and a set of circuit angles. Note that this function does not actually run
the circuit.
"""
# self.reset_circuit()
parametric_circuit = QuantumCircuit(
self.qureg
) # consider changing this too with my new function
if self.prepend_state:
parametric_circuit = parametric_circuit.compose(self.prepend_state)
# Initial state is all |+>
if self.init_hadamard:
parametric_circuit.h(self.qureg)
self.qiskit_parameter_list = []
for each_gate in self.abstract_circuit:
# if gate is of type mixer or cost gate, assign parameter to it
if each_gate.gate_label.type.value in ["MIXER", "COST"]:
angle_param = Parameter(each_gate.gate_label.__repr__())
self.qiskit_parameter_list.append(angle_param)
each_gate.angle_value = angle_param
if (
type(each_gate)
in QAOAQiskitBackendShotBasedSimulator.QISKIT_GATEMAP_LIBRARY
):
decomposition = each_gate.decomposition("trivial")
else:
decomposition = each_gate.decomposition("standard")
# Create Circuit
for each_tuple in decomposition:
gate = each_tuple[0](self.gate_applicator, *each_tuple[1])
gate.apply_gate(parametric_circuit)
return parametric_circuit
[docs] def get_counts(self, params: QAOAVariationalBaseParams, n_shots=None) -> dict:
"""
Returns the counts of the final QAOA circuit after binding angles from variational parameters.
Parameters
----------
params: `QAOAVariationalBaseParams`
The QAOA parameters - an object of one of the parameter classes, containing variable parameters.
n_shots: `int`
The number of times to run the circuit. If None, n_shots is set to the default: self.n_shots
Returns
-------
counts: `dict`
The counts of the final QAOA circuit after binding angles from variational parameters.
"""
# generate a job id for the wavefunction evaluation
self.job_id = generate_uuid()
# set the number of shots, if not specified take the default
n_shots = self.n_shots if n_shots == None else n_shots
qaoa_circuit = self.qaoa_circuit(params)
counts = (
self.backend_simulator.run(qaoa_circuit, shots=n_shots)
.result()
.get_counts()
)
final_counts = flip_counts(counts)
self.measurement_outcomes = final_counts
return final_counts
[docs] def circuit_to_qasm(self):
"""
A method to convert the QAOA circuit to QASM.
"""
raise NotImplementedError()
# qasm_circuit = self.parametric_circuit.qasm()
# return qasm_circuit
[docs] def reset_circuit(self):
raise NotImplementedError()
[docs]class QAOAQiskitBackendStatevecSimulator(
QAOABaseBackendStatevector, QAOABaseBackendParametric
):
"""
Local Statevector-based simulators offered by Qiskit
Parameters
----------
qaoa_descriptor: `QAOADescriptor`
An object of the class ``QAOADescriptor`` which contains information on
circuit construction and depth of the circuit.
n_shots: `int`
The number of shots to be taken for each circuit.
prepend_state: `np.ndarray` or `QuantumCircuit`
The state prepended to the circuit.
append_state: `QuantumCircuit or np.ndarray`
The state appended to the circuit.
init_hadamard: `bool`
Whether to apply a Hadamard gate to the beginning of the
QAOA part of the circuit.
cvar_alpha: `float`
The value of alpha for the CVaR cost function.
"""
QISKIT_GATEMAP_LIBRARY = [
RXGateMap,
RYGateMap,
RZGateMap,
RXXGateMap,
RYYGateMap,
RZZGateMap,
RZXGateMap,
]
def __init__(
self,
qaoa_descriptor: QAOADescriptor,
prepend_state: Optional[Union[np.ndarray, QuantumCircuit]],
append_state: Optional[Union[np.ndarray, QuantumCircuit]],
init_hadamard: bool,
cvar_alpha: float = 1,
):
QAOABaseBackendStatevector.__init__(
self,
qaoa_descriptor,
prepend_state,
append_state,
init_hadamard,
cvar_alpha,
)
assert (
cvar_alpha == 1
), "Please use the shot-based simulator for simulations with cvar_alpha < 1"
self.qureg = QuantumRegister(self.n_qubits)
self.gate_applicator = QiskitGateApplicator()
if self.prepend_state:
assert self.n_qubits >= len(prepend_state.qubits), (
"Cannot attach a bigger circuit " "to the QAOA routine"
)
# For parametric circuits
self.parametric_circuit = self.parametric_qaoa_circuit
self.qiskit_cost_hamil = self.qiskit_cost_hamiltonian
self.qiskit_cost_hamil_sq = self.qiskit_cost_hamil**2
@property
def qiskit_cost_hamiltonian(self):
"""
The qiskit cost hamiltonian for the QAOA circuit represented
as a `SparsePauliOp` object.
"""
cost_hamil = self.cost_hamiltonian
n_qubits = cost_hamil.n_qubits
pauli_strings_list = ["I" * n_qubits] * len(cost_hamil.terms)
for i, pauli_op in enumerate(cost_hamil.terms):
pauli_term = list(pauli_strings_list[i])
for pauli, qubit in zip(pauli_op.pauli_str, pauli_op.qubit_indices):
pauli_term[qubit] = pauli
# reverse pauli_strings because qiskit supports little endian notation
pauli_strings_list[i] = "".join(str(term) for term in pauli_term)[::-1]
pauli_strings_list.append("I" * n_qubits)
pauli_coeffs = cost_hamil.coeffs
qiskit_pauli_op = [
[pauli_strings, coeff]
for pauli_strings, coeff in zip(pauli_strings_list, pauli_coeffs)
]
qiskit_pauli_op.append(["I" * n_qubits, cost_hamil.constant])
qiskit_cost_hamil = SparsePauliOp.from_list(qiskit_pauli_op)
return qiskit_cost_hamil
[docs] def qaoa_circuit(self, params: QAOAVariationalBaseParams) -> QuantumCircuit:
"""
The final QAOA circuit to be executed on the QPU.
Parameters
----------
params: `QAOAVariationalBaseParams`
Returns
-------
qaoa_circuit: `QuantumCircuit`
The final QAOA circuit after binding angles from variational parameters.
"""
# generate a job id for the wavefunction evaluation
self.job_id = generate_uuid()
angles_list = self.obtain_angles_for_pauli_list(self.abstract_circuit, params)
memory_map = dict(zip(self.qiskit_parameter_list, angles_list))
circuit_with_angles = self.parametric_circuit.assign_parameters(memory_map)
return circuit_with_angles
@property
def parametric_qaoa_circuit(self) -> QuantumCircuit:
"""
Creates a parametric QAOA circuit, given the qubit pairs, single qubits with biases,
and a set of circuit angles. Note that this function does not actually run
the circuit. To do this, you will need to subsequently execute the command self.eng.flush().
Parameters
----------
params:
Object of type QAOAVariationalBaseParams
"""
# self.reset_circuit()
parametric_circuit = QuantumCircuit(self.qureg)
if self.prepend_state:
parametric_circuit = parametric_circuit.compose(self.prepend_state)
# Initial state is all |+>
if self.init_hadamard:
parametric_circuit.h(self.qureg)
self.qiskit_parameter_list = []
for each_gate in self.abstract_circuit:
# if gate is of type mixer or cost gate, assign parameter to it
if each_gate.gate_label.type.value in ["MIXER", "COST"]:
angle_param = Parameter(each_gate.gate_label.__repr__())
self.qiskit_parameter_list.append(angle_param)
each_gate.angle_value = angle_param
if (
type(each_gate)
in QAOAQiskitBackendStatevecSimulator.QISKIT_GATEMAP_LIBRARY
):
decomposition = each_gate.decomposition("trivial")
else:
decomposition = each_gate.decomposition("standard")
# Create Circuit
for each_tuple in decomposition:
gate = each_tuple[0](self.gate_applicator, *each_tuple[1])
gate.apply_gate(parametric_circuit)
if self.append_state:
parametric_circuit = parametric_circuit.compose(self.append_state)
return parametric_circuit
[docs] def wavefunction(
self, params: QAOAVariationalBaseParams
) -> Union[List[complex], np.ndarray]:
"""
Get the wavefunction of the state produced by the parametric circuit.
Parameters
----------
params: `QAOAVariationalBaseParams`
Returns
-------
wf: `List[complex]` or `np.ndarray[complex]`
A list of the wavefunction amplitudes.
"""
ckt = self.qaoa_circuit(params)
wf = Statevector(ckt).data
self.measurement_outcomes = wf
return wf
@round_value
def expectation(self, params: QAOAVariationalBaseParams) -> 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.
Returns
-------
`float`
expectation value of cost operator wrt to quantum state produced by QAOA circuit
"""
ckt = self.qaoa_circuit(params)
output_wf = Statevector(ckt)
self.measurement_outcomes = output_wf.data
cost = np.real(output_wf.expectation_value(self.qiskit_cost_hamil))
return cost
@round_value
def expectation_w_uncertainty(
self, params: QAOAVariationalBaseParams
) -> 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.
Returns
-------
`Tuple[float]`
expectation value and its uncertainty of cost operator wrt
to quantum state produced by QAOA circuit.
"""
ckt = self.qaoa_circuit(params)
output_wf = Statevector(ckt)
self.measurement_outcomes = output_wf.data
cost = np.real(Statevector(ckt).expectation_value(self.qiskit_cost_hamil))
cost_sq = np.real(Statevector(ckt).expectation_value(self.qiskit_cost_hamil_sq))
uncertainty = np.sqrt(cost_sq - cost**2)
return (cost, uncertainty)
[docs] def reset_circuit(self):
"""
Reset self.circuit after performing a computation
TODO: Check if only the first instruction is needed. Two might
be redundant
"""
raise NotImplementedError()
[docs] def circuit_to_qasm(self):
""" """
raise NotImplementedError()