Skip to content

Commit

Permalink
Merge pull request #372 from Quantum-TII/gatebackends
Browse files Browse the repository at this point in the history
Use single gates.py file for all backends
  • Loading branch information
scarrazza committed Apr 7, 2021
2 parents 4041aa5 + 429f2fd commit 002c172
Show file tree
Hide file tree
Showing 29 changed files with 1,328 additions and 1,699 deletions.
7 changes: 1 addition & 6 deletions examples/benchmarks/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,13 +149,8 @@ def main(nqubits_list: List[int],
circuit = circuit.fuse()
logs["creation_time"].append(time.time() - start_time)

try:
actual_backend = circuit.queue[0].einsum.__class__.__name__
except AttributeError:
actual_backend = "Custom"

print("\nBenchmark parameters:", kwargs)
print("Actual backend:", actual_backend)
print("Actual backend:", qibo.get_backend())
with tf.device(device):
if compile:
start_time = time.time()
Expand Down
81 changes: 60 additions & 21 deletions src/qibo/abstractions/abstract_gates.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,61 @@ def on_qubits(self, *q):
"Cannot use special gates on subroutines.")


class Channel(Gate):
"""Abstract class for channels."""

def __init__(self):
super().__init__()
self.gates = tuple()
# create inversion gates to rest to the original state vector
# because of the in-place updates used in custom operators
self._inverse_gates = None

@property
def inverse_gates(self):
if self._inverse_gates is None:
self._inverse_gates = self.calculate_inverse_gates()
for gate in self._inverse_gates:
if gate is not None:
if self._nqubits is not None:
gate.nqubits = self._nqubits
gate.density_matrix = self.density_matrix
return self._inverse_gates

@abstractmethod
def calculate_inverse_gates(self): # pragma: no cover
raise_error(NotImplementedError)

@Gate.nqubits.setter
def nqubits(self, n: int):
Gate.nqubits.fset(self, n) # pylint: disable=no-member
for gate in self.gates:
gate.nqubits = n
if self._inverse_gates is not None:
for gate in self._inverse_gates:
if gate is not None:
gate.nqubits = n

@Gate.density_matrix.setter
def density_matrix(self, x):
Gate.density_matrix.fset(self, x) # pylint: disable=no-member
for gate in self.gates:
gate.density_matrix = x
if self._inverse_gates is not None:
for gate in self._inverse_gates:
if gate is not None:
gate.density_matrix = x

def controlled_by(self, *q):
""""""
raise_error(ValueError, "Noise channel cannot be controlled on qubits.")

def on_qubits(self, *q): # pragma: no cover
# future TODO
raise_error(NotImplementedError, "`on_qubits` method is not available "
"for the `GeneralChannel` gate.")


class ParametrizedGate(Gate):
"""Base class for parametrized gates.
Expand Down Expand Up @@ -350,8 +405,7 @@ def parameters(self, x):
# pylint: disable=E1101
if isinstance(self, BaseBackendGate):
self._unitary = None
if self.is_prepared:
self.reprepare()
self._matrix = None
for devgate in self.device_gates:
devgate.parameters = x

Expand Down Expand Up @@ -385,7 +439,9 @@ class BaseBackendGate(Gate, ABC):

def __init__(self):
Gate.__init__(self)
self._matrix = None
self._unitary = None
self._cache = None
# Cast gate matrices to the proper device
self.device = get_device()
# Reference to copies of this gate that are casted in devices when
Expand Down Expand Up @@ -432,26 +488,9 @@ def construct_unitary(self): # pragma: no cover
"""Constructs the gate's unitary matrix."""
return raise_error(NotImplementedError)

@property
@abstractmethod
def prepare(self): # pragma: no cover
"""Prepares gate for application to states.
This method is called automatically when a gate is added to a circuit
or when called on a state. Note that the gate's ``nqubits`` should be
set before this method is called.
"""
raise_error(NotImplementedError)

@abstractmethod
def reprepare(self): # pragma: no cover
"""Recalculates gate matrix when the gate's parameters are changed.
Used in parametrized gates when the ``circuit.set_parameters`` method
is used to update the variational parameters.
This method is seperated from ``prepare`` because it is usually not
required to repeat the full preperation calculation from scratch when
the gate's parameters are updated.
"""
def cache(self): # pragma: no cover
raise_error(NotImplementedError)

@abstractmethod
Expand Down
21 changes: 6 additions & 15 deletions src/qibo/abstractions/gates.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from abc import abstractmethod
from qibo.config import raise_error, EINSUM_CHARS
from typing import Dict, List, Optional, Tuple
from qibo.abstractions.abstract_gates import Gate, ParametrizedGate, SpecialGate
from qibo.abstractions.abstract_gates import Gate, Channel, SpecialGate, ParametrizedGate

QASM_GATES = {"h": "H", "x": "X", "y": "Y", "z": "Z",
"rx": "RX", "ry": "RY", "rz": "RZ",
Expand Down Expand Up @@ -1178,6 +1178,9 @@ def __init__(self, qubits: List[int], pairs: List[Tuple[int, int]],
"trainable": trainable, "name": name}
self.name = "VariationalLayer" if name is None else name

self.unitaries = []
self.additional_unitary = None

self.target_qubits = tuple(qubits)
self.parameter_names = [f"theta{i}" for i, _ in enumerate(params)]
parameter_values = list(params)
Expand Down Expand Up @@ -1214,10 +1217,6 @@ def _create_params_dict(self, params: List[float]) -> Dict[int, float]:
"".format(len(self.target_qubits), len(params)))
return {q: p for q, p in zip(self.target_qubits, params)}

@abstractmethod
def _dagger(self) -> "Gate": # pragma: no cover
raise_error(NotImplementedError)

@ParametrizedGate.parameters.setter
def parameters(self, x):
if self.params2:
Expand Down Expand Up @@ -1290,7 +1289,7 @@ def __init__(self, *q):
self.init_kwargs = {}


class KrausChannel(Gate):
class KrausChannel(Channel):
"""General channel defined by arbitrary Krauss operators.
Implements the following transformation:
Expand Down Expand Up @@ -1352,17 +1351,9 @@ def _from_matrices(self, matrices):
"".format(shape, len(qubits)))
qubitset.update(qubits)
gatelist.append(self.module.Unitary(matrix, *list(qubits)))
gatelist[-1].density_matrix = True
return tuple(gatelist), tuple(sorted(qubitset))

def controlled_by(self, *q):
""""""
raise_error(ValueError, "Noise channel cannot be controlled on qubits.")

def on_qubits(self, *q): # pragma: no cover
# future TODO
raise_error(NotImplementedError, "`on_qubits` method is not available "
"for the `GeneralChannel` gate.")


class UnitaryChannel(KrausChannel):
"""Channel that is a probabilistic sum of unitary operations.
Expand Down
110 changes: 44 additions & 66 deletions src/qibo/backends/__init__.py
Original file line number Diff line number Diff line change
@@ -1,59 +1,67 @@
import os
from qibo import config
from qibo.config import raise_error, log, warnings
from qibo.backends.numpy import NumpyBackend
from qibo.backends.tensorflow import TensorflowBackend

from qibo.backends.numpy import NumpyDefaultEinsumBackend, NumpyMatmulEinsumBackend
from qibo.backends.tensorflow import TensorflowCustomBackend, TensorflowDefaultEinsumBackend, TensorflowMatmulEinsumBackend


AVAILABLE_BACKENDS = {
"custom": TensorflowCustomBackend,
"tensorflow": TensorflowCustomBackend,
"defaulteinsum": TensorflowDefaultEinsumBackend,
"matmuleinsum": TensorflowMatmulEinsumBackend,
"tensorflow_defaulteinsum": TensorflowDefaultEinsumBackend,
"tensorflow_matmuleinsum": TensorflowMatmulEinsumBackend,
"numpy": NumpyDefaultEinsumBackend,
"numpy_defaulteinsum": NumpyDefaultEinsumBackend,
"numpy_matmuleinsum": NumpyMatmulEinsumBackend
}

_CONSTRUCTED_BACKENDS = {}
def _construct_backend(name):
if name not in _CONSTRUCTED_BACKENDS:
if name == "numpy":
_CONSTRUCTED_BACKENDS["numpy"] = NumpyBackend()
elif name == "tensorflow":
_CONSTRUCTED_BACKENDS["tensorflow"] = TensorflowBackend()
else:
raise_error(ValueError, "Unknown backend name {}.".format(name))
if name not in AVAILABLE_BACKENDS:
available = ", ".join(list(AVAILABLE_BACKENDS.keys()))
raise_error(ValueError, "Unknown backend {}. Please select one of "
"the available backends: {}."
"".format(name, available))
_CONSTRUCTED_BACKENDS[name] = AVAILABLE_BACKENDS.get(name)()
return _CONSTRUCTED_BACKENDS.get(name)

numpy_backend = _construct_backend("numpy")
numpy_backend = _construct_backend("numpy_defaulteinsum")
numpy_matrices = numpy_backend.matrices

AVAILABLE_BACKENDS = ["custom", "defaulteinsum", "matmuleinsum",
"tensorflow_defaulteinsum", "tensorflow_matmuleinsum",
"numpy_defaulteinsum", "numpy_matmuleinsum"]


# Select the default backend engine
if "QIBO_BACKEND" in os.environ: # pragma: no cover
_BACKEND_NAME = os.environ.get("QIBO_BACKEND")
if _BACKEND_NAME == "tensorflow":
K = TensorflowBackend()
elif _BACKEND_NAME == "numpy": # pragma: no cover
# CI uses tensorflow as default backend
K = NumpyBackend()
else: # pragma: no cover
if _BACKEND_NAME not in AVAILABLE_BACKENDS: # pragma: no cover
_available_names = ", ".join(list(AVAILABLE_BACKENDS.keys()))
raise_error(ValueError, "Environment variable `QIBO_BACKEND` has "
"unknown value {}. Please select either "
"`tensorflow` or `numpy`."
"".format(_BACKEND_NAME))
"unknown value {}. Please select one of {}."
"".format(_BACKEND_NAME, _available_names))
K = AVAILABLE_BACKENDS.get(_BACKEND_NAME)()
else:
try:
os.environ["TF_CPP_MIN_LOG_LEVEL"] = str(config.LOG_LEVEL)
import tensorflow as tf
import qibo.tensorflow.custom_operators as op
_CUSTOM_OPERATORS_LOADED = op._custom_operators_loaded
if not _CUSTOM_OPERATORS_LOADED: # pragma: no cover
log.warning("Removing custom operators from available backends.")
AVAILABLE_BACKENDS.remove("custom")
K = TensorflowBackend()
if not op._custom_operators_loaded: # pragma: no cover
log.warning("Einsum will be used to apply gates with Tensorflow. "
"Removing custom operators from available backends.")
AVAILABLE_BACKENDS.pop("custom")
AVAILABLE_BACKENDS["tensorflow"] = TensorflowDefaultEinsumBackend
K = AVAILABLE_BACKENDS.get("tensorflow")()
except ModuleNotFoundError: # pragma: no cover
# case not tested because CI has tf installed
log.warning("Tensorflow is not installed. Falling back to numpy.")
K = NumpyBackend()
AVAILABLE_BACKENDS = [b for b in AVAILABLE_BACKENDS
if "tensorflow" not in b]
AVAILABLE_BACKENDS.remove("custom")
log.warning("Tensorflow is not installed. Falling back to numpy. "
"Numpy does not support Qibo custom operators and GPU. "
"Einsum will be used to apply gates on CPU.")
AVAILABLE_BACKENDS = {k: v for k, v in AVAILABLE_BACKENDS.items()
if "numpy" in k}
AVAILABLE_BACKENDS["defaulteinsum"] = NumpyDefaultEinsumBackend
AVAILABLE_BACKENDS["matmuleinsum"] = NumpyMatmulEinsumBackend
K = AVAILABLE_BACKENDS.get("numpy")()


K.qnp = numpy_backend
Expand All @@ -71,26 +79,12 @@ def set_backend(backend="custom"):
Args:
backend (str): A backend from the above options.
"""
if backend not in AVAILABLE_BACKENDS:
available = ", ".join(AVAILABLE_BACKENDS)
raise_error(ValueError, "Unknown backend {}. Please select one of the "
"available backends: {}."
"".format(backend, available))
if not config.ALLOW_SWITCHERS and backend != K.gates:
bk = _construct_backend(backend)
if not config.ALLOW_SWITCHERS and backend != K.name:
warnings.warn("Backend should not be changed after allocating gates.",
category=RuntimeWarning)

gate_backend = backend.split("_")
if len(gate_backend) == 1:
calc_backend, gate_backend = _BACKEND_NAME, gate_backend[0]
elif len(gate_backend) == 2:
calc_backend, gate_backend = gate_backend
if gate_backend == "custom":
calc_backend = "tensorflow"
bk = _construct_backend(calc_backend)
K.assign(bk)
K.qnp = numpy_backend
K.set_gates(gate_backend)


def get_backend():
Expand All @@ -99,23 +93,7 @@ def get_backend():
Returns:
A string with the backend name.
"""
if K.name == "tensorflow":
return K.gates
else:
return "_".join([K.name, K.gates])


if _BACKEND_NAME != "tensorflow": # pragma: no cover
# CI uses tensorflow as default backend
log.warning("{} does not support Qibo custom operators and GPU. "
"Einsum will be used to apply gates on CPU."
"".format(_BACKEND_NAME))
set_backend("defaulteinsum")


if _BACKEND_NAME == "tensorflow" and not _CUSTOM_OPERATORS_LOADED: # pragma: no cover
log.warning("Einsum will be used to apply gates with tensorflow.")
set_backend("defaulteinsum")
return K.name


def set_precision(dtype='double'):
Expand Down
Loading

0 comments on commit 002c172

Please sign in to comment.