Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use single gates.py file for all backends #372

Merged
merged 25 commits into from
Apr 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rest -> restore/reset?

# 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:
scarrazza marked this conversation as resolved.
Show resolved Hide resolved
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