from __future__ import annotations
from abc import ABC, abstractproperty
from typing import List, Union, Tuple, Any, Callable, Iterable, Optional, TYPE_CHECKING
if TYPE_CHECKING:
from ...backends.basedevice import DeviceBase
import numpy as np
from enum import Enum
import copy
from .operators import Hamiltonian
from .hamiltonianmapper import HamiltonianMapper
from .gatemap import RotationGateMap, SWAPGateMap, GateMap
from .gatemaplabel import GateMapType
def _is_iterable_empty(in_iterable):
if isinstance(in_iterable, Iterable): # Is Iterable
return all(map(_is_iterable_empty, in_iterable))
return False # Not an Iterable
class shapedArray:
"""Decorator-Descriptor for arrays that have a fixed shape.
This is used to facilitate automatic input checking for all the different
internal parameters. Each instance of this class can be removed without
replacement and the code should still work, provided the user provides
only correct angles to below parameter classes
Parameters
----------
shape: `Callable[[Any], Tuple]`
Returns the shape for self.values
Example
-------
With this descriptor, the two following are equivalent:
.. code-block:: python
class foo():
def __init__(self):
self.shape = (n, m)
self._my_attribute = None
@property
def my_attribute(self):
return _my_attribute
@my_attribute.setter
def my_attribute(self):
try:
self._my_attribute = np.reshape(values, self.shape)
except ValueError:
raise ValueError("my_attribute must have shape "
f"{self.shape}")
can be simplified to
.. code-block:: python
class foo():
def __init__(self):
self.shape = (n, m)
@shapedArray
def my_attribute(self):
return self.shape
"""
def __init__(self, shape: Callable[[Any], Tuple]):
"""The constructor. See class documentation"""
self.name = shape.__name__
self.shape = shape
def __set__(self, obj, values):
"""The setter with input checking."""
try:
# also round to 12 decimal places to avoid floating point errors
setattr(
obj, f"__{self.name}", np.round(np.reshape(values, self.shape(obj)), 12)
)
except ValueError:
raise ValueError(f"{self.name} must have shape {self.shape(obj)}")
def __get__(self, obj, objtype):
"""The getter."""
return getattr(obj, f"__{self.name}")
[docs]class AnsatzDescriptor(ABC):
"""
Parameters class to construct a specific quantum ansatz to attack
a problem
Parameters
----------
algorithm: `str`
The algorithm corresponding to the ansatz
Attributes
----------
algorithm: `str`
"""
def __init__(self, algorithm: str):
self.algorithm = algorithm
@abstractproperty
def n_qubits(self) -> int:
pass
[docs]class QAOADescriptor(AnsatzDescriptor):
"""
Create the problem attributes consisting of the Hamiltonian, QAOA 'p'
value and other specific parameters.
Attributes
----------
cost_hamiltonian: `Hamiltonian`
qureg: `List[int]`
cost_block_coeffs: `List[float]`
cost_single_qubit_coeffs: `List[float]`
cost_qubits_singles: `List[str]`
cost_pair_qubit_coeffs: `List[float]`
cost_qubits_pairs: `List[str]`
mixer_block_coeffs: `List[float]`
cost_blocks: `List[RotationGateMap]`
mixer_blocks: `List[RotationGateMap]`
Properties
----------
n_qubits: `int`
abstract_circuit: `List[RotationGateMap]`
"""
def __init__(
self,
cost_hamiltonian: Hamiltonian,
mixer_block: Union[List[RotationGateMap], Hamiltonian],
p: int,
mixer_coeffs: List[float] = [],
routing_function: Optional[Callable] = None,
device: Optional["DeviceBase"] = None,
):
"""
Parameters
----------
cost_hamiltonian: `Hamiltonian`
The cost hamiltonian of the problem the user is trying to solve.
mixer_block: Union[List[RotationGateMap], Hamiltonian]
The mixer hamiltonian or a list of initialised RotationGateMap objects
that defines the gates to be used within the "mixer part" of the circuit.
p: `int`
Number of QAOA layers; defaults to 1 if not specified
mixer_coeffs: `List[float]`
A list containing coefficients for each mixer GateMap. The order of the
coefficients should follow the order of the GateMaps provided in the relevant gate block.
This input isnt required if the input mixer block is of type Hamiltonian.
routing_function Optional[Callable]
A callable function running the routing algorithm on the problem
device: DeviceBase
The device on which to run the Quantum Circuit
"""
super().__init__(algorithm="QAOA")
self.p = p
self.cost_block_coeffs = cost_hamiltonian.coeffs
try:
self.mixer_block_coeffs = mixer_block.coeffs
except AttributeError:
self.mixer_block_coeffs = mixer_coeffs
# Needed in the BaseBackend to compute exact_solution, cost_funtion method
# and bitstring_energy
self.cost_hamiltonian = cost_hamiltonian
self.cost_block = self.block_setter(cost_hamiltonian, GateMapType.COST)
(
self.cost_single_qubit_coeffs,
self.cost_pair_qubit_coeffs,
self.cost_qubits_singles,
self.cost_qubits_pairs,
) = self._assign_coefficients(self.cost_block, self.cost_block_coeffs)
# route the cost block and append SWAP gates
if isinstance(routing_function, Callable):
try:
(
self.cost_block,
self.initial_mapping,
self.final_mapping,
) = self.route_gates_list(self.cost_block, device, routing_function)
self.routed = True
except TypeError:
raise TypeError(
"The specified function can has a set signature that accepts"
" device, problem, and initial_mapping"
)
except Exception as e:
raise e
elif routing_function == None:
self.routed = False
else:
raise ValueError(
f"Routing function can only be a Callable not {type(routing_function)}"
)
self.mixer_block = self.block_setter(mixer_block, GateMapType.MIXER)
(
self.mixer_single_qubit_coeffs,
self.mixer_pair_qubit_coeffs,
self.mixer_qubits_singles,
self.mixer_qubits_pairs,
) = self._assign_coefficients(self.mixer_block, self.mixer_block_coeffs)
self.mixer_blocks = HamiltonianMapper.repeat_gate_maps(self.mixer_block, self.p)
self.cost_blocks = HamiltonianMapper.repeat_gate_maps(self.cost_block, self.p)
self.qureg = list(range(self.n_qubits))
@property
def n_qubits(self) -> int:
if self.routed == True:
return len(self.final_mapping)
else:
return self.cost_hamiltonian.n_qubits
def __repr__(self):
"""Return an overview over the parameters and hyperparameters
Todo
----
Split this into ``__repr__`` and ``__str__`` with a more verbose
output in ``__repr__``.
"""
string = "Circuit Parameters:\n"
string += "\tp: " + str(self.p) + "\n"
string += "\tregister: " + str(self.qureg) + "\n" + "\n"
string += "Cost Hamiltonian:\n"
string += "\tcost_qubits_singles: " + str(self.cost_qubits_singles) + "\n"
string += (
"\tcost_single_qubit_coeffs: " + str(self.cost_single_qubit_coeffs) + "\n"
)
string += "\tcost_qubits_pairs: " + str(self.cost_qubits_pairs) + "\n"
string += (
"\tcost_pair_qubit_coeffs: "
+ str(self.cost_pair_qubit_coeffs)
+ "\n"
+ "\n"
)
string += "Mixer Hamiltonian:\n"
string += "\tmixer_qubits_singles: " + str(self.mixer_qubits_singles) + "\n"
string += (
"\tmixer_single_qubit_coeffs: " + str(self.mixer_single_qubit_coeffs) + "\n"
)
string += "\tmixer_qubits_pairs: " + str(self.mixer_qubits_pairs) + "\n"
string += (
"\tmixer_pair_qubit_coeffs: " + str(self.mixer_pair_qubit_coeffs) + "\n"
)
return string
def _assign_coefficients(
self, input_block: List[RotationGateMap], input_coeffs: List[float]
) -> None:
"""
Splits the coefficients and gatemaps into qubit singles and qubit pairs.
"""
single_qubit_coeffs = []
pair_qubit_coeffs = []
qubit_singles = []
qubit_pairs = []
if len(input_block) != len(input_coeffs):
raise ValueError(
"The number of terms/gatemaps must match the number of coefficients provided."
)
for each_gatemap, each_coeff in zip(input_block, input_coeffs):
if each_gatemap.gate_label.n_qubits == 1:
single_qubit_coeffs.append(each_coeff)
# Giving a string name to each gatemap (?)
qubit_singles.append(type(each_gatemap).__name__)
elif each_gatemap.gate_label.n_qubits == 2:
pair_qubit_coeffs.append(each_coeff)
qubit_pairs.append(type(each_gatemap).__name__)
return (single_qubit_coeffs, pair_qubit_coeffs, qubit_singles, qubit_pairs)
[docs] @staticmethod
def block_setter(
input_object: Union[List[RotationGateMap], Hamiltonian], block_type: Enum
) -> List[RotationGateMap]:
"""
Converts a Hamiltonian Object into a List of RotationGateMap Objects with
the appropriate block_type and sequence assigned to the GateLabel
OR
Remaps a list of RotationGateMap Objects with a block_type and sequence
implied from its position in the list.
Parameters
----------
input_object: `Union[List[RotationGateMap], Hamiltonian]`
A Hamiltonian Object or a list of RotationGateMap Objects (Ordered
according to their application order in the final circuit)
block_type: Enum
The type to be assigned to all the RotationGateMap Objects generated
from input_object
Returns
-------
`List[RotationGateMap]`
"""
if isinstance(input_object, Hamiltonian):
block = HamiltonianMapper.generate_gate_maps(input_object, block_type)
elif isinstance(input_object, list):
input_object = QAOADescriptor.set_block_sequence(input_object)
for each_gate in input_object:
if isinstance(each_gate, RotationGateMap):
each_gate.gate_label.update_gatelabel(new_gatemap_type=block_type)
else:
raise TypeError(
f"Input gate is of unsupported type {type(each_gate)}."
"Only RotationGateMaps are supported"
)
block = input_object
else:
raise ValueError(
"The input object defining mixer should be a List of RotationGateMaps or type Hamiltonian"
)
return block
[docs] @staticmethod
def set_block_sequence(
input_gatemap_list: List[RotationGateMap],
) -> List[RotationGateMap]:
"""
This method assigns the sequence attribute to all RotationGateMap objects in the list.
The sequence of the GateMaps are implied based on their positions in the list.
Parameters
----------
input_gatemap_list: `List[RotationGateMap]`
A list of RotationGateMap Objects
Returns
-------
`List[RotationGateMap]`
"""
one_qubit_count = 0
two_qubit_count = 0
for each_gate in input_gatemap_list:
if isinstance(each_gate, RotationGateMap):
if each_gate.gate_label.n_qubits == 1:
each_gate.gate_label.update_gatelabel(
new_application_sequence=one_qubit_count,
)
one_qubit_count += 1
elif each_gate.gate_label.n_qubits == 2:
each_gate.gate_label.update_gatelabel(
new_application_sequence=two_qubit_count,
)
two_qubit_count += 1
else:
raise TypeError(
f"Input gate is of unsupported type {type(each_gate)}."
"Only RotationGateMaps are supported"
)
return input_gatemap_list
[docs] def reorder_gates_block(self, gates_block, layer_number):
"""Update the qubits that the gates are acting on after application
of SWAPs in the cost layer
"""
for gate in gates_block:
if layer_number % 2 == 0:
mapping = self.final_mapping
gate.qubit_1 = mapping[gate.qubit_1]
if gate.gate_label.n_qubits == 2:
gate.qubit_2 = mapping[gate.qubit_2]
else:
pass
return gates_block
[docs] @staticmethod
def route_gates_list(
gates_to_route: List[GateMap],
device: "DeviceBase",
routing_function: Callable,
) -> List[GateMap]:
"""
Apply qubit routing to the abstract circuit gate list
based on device information
Parameters
----------
gates_to_route: `List[GateMap]`
The gates to route
device: `DeviceBase`
The device on which to run the circuit
routing_function: `Callable`
The function that accepts as input the device, problem, initial_mapping and
outputs the list of gates with swaps
"""
original_qubits_to_gate_mapping = {
(gate.qubit_1, gate.qubit_2): gate
for gate in gates_to_route
if gate.gate_label.n_qubits == 2
}
problem_to_solve = list(original_qubits_to_gate_mapping.keys())
(
gate_list_indices,
swap_mask,
initial_physical_to_logical_mapping,
final_mapping,
) = routing_function(device, problem_to_solve)
gates_list = [gate for gate in gates_to_route if gate.gate_label.n_qubits == 1]
swapped_history = []
for idx, pair_ij in enumerate(gate_list_indices):
mask = swap_mask[idx]
qi, qj = pair_ij
if mask == True:
swapped_history.append(pair_ij)
gates_list.append(SWAPGateMap(qi, qj))
elif mask == False:
old_qi, old_qj = qi, qj
# traverse each SWAP application in reverse order to obtain
# the original location of the current qubit
for swap_pair in swapped_history[::-1]:
if old_qi in swap_pair:
old_qi = (
swap_pair[0] if swap_pair[1] == old_qi else swap_pair[1]
)
if old_qj in swap_pair:
old_qj = (
swap_pair[0] if swap_pair[1] == old_qj else swap_pair[1]
)
try:
ising_gate = original_qubits_to_gate_mapping[
tuple([old_qi, old_qj])
]
except KeyError:
ising_gate = original_qubits_to_gate_mapping[
tuple([old_qj, old_qi])
]
except Exception as e:
raise e
ising_gate.qubit_1, ising_gate.qubit_2 = qi, qj
gates_list.append(ising_gate)
return (
gates_list,
list(initial_physical_to_logical_mapping.keys()),
final_mapping,
)
@property
def abstract_circuit(self):
# even layer inversion if the circuit contains SWAP gates
even_layer_inversion = -1 if self.routed == True else 1
_abstract_circuit = []
for each_p in range(self.p):
# apply each cost_block with reversed order to maintain the SWAP sequence
_abstract_circuit.extend(
self.cost_blocks[each_p][:: (even_layer_inversion) ** each_p]
)
# apply the mixer block
if self.routed == True:
mixer_block = self.reorder_gates_block(
self.mixer_blocks[each_p], each_p
)
else:
mixer_block = self.mixer_blocks[each_p]
_abstract_circuit.extend(mixer_block)
return _abstract_circuit