From 042b92117ab177c335b316e0532add7e4e0d654f Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Thu, 25 Mar 2021 16:30:06 +0400 Subject: [PATCH 01/23] Refactor backend creation --- src/qibo/backends/__init__.py | 108 ++++++---------- src/qibo/backends/abstract.py | 24 +--- src/qibo/backends/numpy.py | 18 +++ src/qibo/backends/tensorflow.py | 121 +++++++++++------- src/qibo/tests_new/test_backends_agreement.py | 2 +- src/qibo/tests_new/test_backends_init.py | 16 +-- .../test_measurement_gate_collapse.py | 8 +- .../test_measurement_gate_probabilistic.py | 20 +-- 8 files changed, 157 insertions(+), 160 deletions(-) diff --git a/src/qibo/backends/__init__.py b/src/qibo/backends/__init__.py index 1af00b70db..a06f1f086e 100644 --- a/src/qibo/backends/__init__.py +++ b/src/qibo/backends/__init__.py @@ -1,59 +1,65 @@ 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(_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} + K = AVAILABLE_BACKENDS.get("numpy")() K.qnp = numpy_backend @@ -71,26 +77,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(): @@ -99,23 +91,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'): diff --git a/src/qibo/backends/abstract.py b/src/qibo/backends/abstract.py index b0d8464098..945fd9abc2 100644 --- a/src/qibo/backends/abstract.py +++ b/src/qibo/backends/abstract.py @@ -4,17 +4,12 @@ class AbstractBackend(ABC): - base_methods = {"assign", "set_gates", "dtypes", - "set_precision"} + base_methods = {"assign", "dtypes", "set_precision"} def __init__(self): self.backend = None self.name = "base" - self.gates = "custom" - self.custom_gates = True - self.custom_einsum = None - self.precision = 'double' self._dtypes = {'DTYPEINT': 'int64', 'DTYPE': 'float64', 'DTYPECPX': 'complex128'} @@ -31,7 +26,6 @@ def __init__(self): self.newaxis = None self.oom_error = None self.optimization = None - self.op = None def __str__(self): return self.name @@ -53,22 +47,6 @@ def assign(self, backend): self.newaxis = backend.newaxis self.oom_error = backend.oom_error self.optimization = backend.optimization - self.op = backend.op - - def set_gates(self, name): - if name == 'custom': - self.custom_gates = True - self.custom_einsum = None - elif name == 'defaulteinsum': - self.custom_gates = False - self.custom_einsum = "DefaultEinsum" - elif name == 'matmuleinsum': - self.custom_gates = False - self.custom_einsum = "MatmulEinsum" - else: # pragma: no cover - # this case is captured by `backends.__init__.set_backend` checks - raise_error(ValueError, f"Gate backend '{name}' not supported.") - self.gates = name def dtypes(self, name): if name in self._dtypes: diff --git a/src/qibo/backends/numpy.py b/src/qibo/backends/numpy.py index 991c531973..19b9e51a3d 100644 --- a/src/qibo/backends/numpy.py +++ b/src/qibo/backends/numpy.py @@ -232,3 +232,21 @@ def __exit__(self, *args): def set_seed(self, seed): self.backend.random.seed(seed) + + +class NumpyDefaultEinsumBackend(NumpyBackend): + + def __init__(self): + super().__init__() + self.name = "numpy_defaulteinsum" + self.custom_gates = False + self.custom_einsum = "DefaultEinsum" + + +class NumpyMatmulEinsumBackend(NumpyBackend): + + def __init__(self): + super().__init__() + self.name = "numpy_matmuleinsum" + self.custom_gates = False + self.custom_einsum = "MatmulEinsum" diff --git a/src/qibo/backends/tensorflow.py b/src/qibo/backends/tensorflow.py index 244f103b79..3b5c20e3a0 100644 --- a/src/qibo/backends/tensorflow.py +++ b/src/qibo/backends/tensorflow.py @@ -1,6 +1,6 @@ import os from qibo.backends import abstract, numpy -from qibo.config import raise_error, LOG_LEVEL +from qibo.config import raise_error, log, LOG_LEVEL class Optimization: @@ -43,11 +43,6 @@ def __init__(self): self.oom_error = errors_impl.ResourceExhaustedError self.optimization = Optimization() - from qibo.tensorflow import custom_operators as op - self.op = None - if op._custom_operators_loaded: - self.op = op - # seed to use in the measurement frequency custom op self._seed = None # seed can be modified using ``K.set_seed`` @@ -145,29 +140,18 @@ def gather(self, x, indices=None, condition=None, axis=0): def gather_nd(self, x, indices): return self.backend.gather_nd(x, indices) - def initial_state(self, nqubits, is_matrix=False): - if self.op is None: # pragma: no cover - dim = 1 + is_matrix - shape = dim * (2 ** nqubits,) - idx = self.backend.constant([dim * [0]], dtype=self.dtypes('DTYPEINT')) - state = self.backend.zeros(shape, dtype=self.dtypes('DTYPECPX')) - update = self.backend.constant([1], dtype=self.dtypes('DTYPECPX')) - state = self.backend.tensor_scatter_nd_update(state, idx, update) - return state - else: - from qibo.config import get_threads - return self.op.initial_state(nqubits, self.dtypes('DTYPECPX'), - is_matrix=is_matrix, - omp_num_threads=get_threads()) + def initial_state(self, nqubits, is_matrix=False): # pragma: no cover + dim = 1 + is_matrix + shape = dim * (2 ** nqubits,) + idx = self.backend.constant([dim * [0]], dtype=self.dtypes('DTYPEINT')) + state = self.backend.zeros(shape, dtype=self.dtypes('DTYPECPX')) + update = self.backend.constant([1], dtype=self.dtypes('DTYPECPX')) + state = self.backend.tensor_scatter_nd_update(state, idx, update) + return state - def transpose_state(self, pieces, state, nqubits, order): - if self.op is None: # pragma: no cover - pieces = self.reshape(self.backend.stack(pieces), nqubits * (2,)) - return self.reshape(self.transpose(pieces, order), state.shape) - else: - from qibo.config import get_threads - return self.op.transpose_state(pieces, state, nqubits, order, - get_threads()) + def transpose_state(self, pieces, state, nqubits, order): # pragma: no cover + pieces = self.reshape(self.backend.stack(pieces), nqubits * (2,)) + return self.reshape(self.transpose(pieces, order), state.shape) def random_uniform(self, shape, dtype='DTYPE'): return self.backend.random.uniform(shape, dtype=self.dtypes(dtype)) @@ -184,24 +168,11 @@ def sample_shots(self, probs, nshots): return self.concatenate(samples, axis=0) def sample_frequencies(self, probs, nshots): - from qibo.config import SHOT_CUSTOM_OP_THREASHOLD - if self.op is None or nshots < SHOT_CUSTOM_OP_THREASHOLD: - logits = self.log(probs)[self.newaxis] - samples = self.random.categorical(logits, nshots, dtype=self.dtypes('DTYPEINT'))[0] - res, counts = self.unique(samples, return_counts=True) - frequencies = self.zeros(int(probs.shape[0]), dtype=self.dtypes('DTYPEINT')) - frequencies = self.backend.tensor_scatter_nd_add(frequencies, res[:, self.newaxis], counts) - else: - from qibo.config import get_threads - # Generate random seed using tf - dtype = self.dtypes('DTYPEINT') - seed = self.backend.random.uniform( - shape=tuple(), maxval=int(1e8), dtype=dtype) - nqubits = int(self.np.log2(tuple(probs.shape)[0])) - shape = self.cast(2 ** nqubits, dtype='DTYPEINT') - frequencies = self.zeros(shape, dtype='DTYPEINT') - frequencies = self.op.measure_frequencies( - frequencies, probs, nshots, nqubits, seed, get_threads()) + logits = self.log(probs)[self.newaxis] + samples = self.random.categorical(logits, nshots, dtype=self.dtypes('DTYPEINT'))[0] + res, counts = self.unique(samples, return_counts=True) + frequencies = self.zeros(int(probs.shape[0]), dtype=self.dtypes('DTYPEINT')) + frequencies = self.backend.tensor_scatter_nd_add(frequencies, res[:, self.newaxis], counts) return frequencies def compile(self, func): @@ -216,3 +187,61 @@ def executing_eagerly(self): def set_seed(self, seed): self._seed = seed self.backend.random.set_seed(seed) + + +class TensorflowCustomBackend(TensorflowBackend): + + def __init__(self): + from qibo.tensorflow import custom_operators as op + if not op._custom_operators_loaded: + raise_error(RuntimeError) + + super().__init__() + self.name = "custom" + self.custom_gates = True + self.custom_einsum = None + self.op = op + from qibo.config import get_threads + self.get_threads = get_threads + + def initial_state(self, nqubits, is_matrix=False): + return self.op.initial_state(nqubits, self.dtypes('DTYPECPX'), + is_matrix=is_matrix, + omp_num_threads=self.get_threads()) + + def transpose_state(self, pieces, state, nqubits, order): + return self.op.transpose_state(pieces, state, nqubits, order, + self.get_threads()) + + def sample_frequencies(self, probs, nshots): + from qibo.config import SHOT_CUSTOM_OP_THREASHOLD + if nshots < SHOT_CUSTOM_OP_THREASHOLD: + return super().sample_frequencies(probs, nshots) + # Generate random seed using tf + dtype = self.dtypes('DTYPEINT') + seed = self.backend.random.uniform( + shape=tuple(), maxval=int(1e8), dtype=dtype) + nqubits = int(self.np.log2(tuple(probs.shape)[0])) + shape = self.cast(2 ** nqubits, dtype='DTYPEINT') + frequencies = self.zeros(shape, dtype='DTYPEINT') + frequencies = self.op.measure_frequencies( + frequencies, probs, nshots, nqubits, seed, self.get_threads()) + return frequencies + + +class TensorflowDefaultEinsumBackend(TensorflowBackend): + + def __init__(self): + super().__init__() + self.name = "tensorflow_defaulteinsum" + self.custom_gates = False + self.custom_einsum = "DefaultEinsum" + + +class TensorflowMatmulEinsumBackend(TensorflowBackend): + + def __init__(self): + super().__init__() + self.name = "tensorflow_matmuleinsum" + self.custom_gates = False + self.custom_einsum = "MatmulEinsum" diff --git a/src/qibo/tests_new/test_backends_agreement.py b/src/qibo/tests_new/test_backends_agreement.py index d67988d914..1f8d1f1715 100644 --- a/src/qibo/tests_new/test_backends_agreement.py +++ b/src/qibo/tests_new/test_backends_agreement.py @@ -60,7 +60,7 @@ def test_backend_methods(tested_backend, target_backend, method, kwargs): if isinstance(kwargs, dict): np.testing.assert_allclose(tested_func(**kwargs), target_func(**kwargs)) else: - if method in {"kron", "inv"} and tested_backend.name == "tensorflow": + if method in {"kron", "inv"} and "numpy" not in tested_backend.name: with pytest.raises(NotImplementedError): tested_func(*kwargs) else: diff --git a/src/qibo/tests_new/test_backends_init.py b/src/qibo/tests_new/test_backends_init.py index 37597117f3..5683497c26 100644 --- a/src/qibo/tests_new/test_backends_init.py +++ b/src/qibo/tests_new/test_backends_init.py @@ -2,9 +2,12 @@ from qibo import K, backends, models, gates -def test_construct_backend(engine): - backend = backends._construct_backend(engine) - assert backend.name == engine +def test_construct_backend(backend): + bk = backends._construct_backend(backend) + try: + assert bk.name == backend + except AssertionError: + assert bk.name.split("_")[-1] == backend with pytest.raises(ValueError): bk = backends._construct_backend("test") @@ -13,13 +16,6 @@ def test_set_backend(backend): """Check ``set_backend`` for switching gate backends.""" original_backend = backends.get_backend() backends.set_backend(backend) - assert backends.get_backend() == backend - - target_name = "numpy" if "numpy" in backend else "tensorflow" - assert K.name == target_name - assert str(K) == target_name - assert K.__repr__() == "{}Backend".format(target_name.capitalize()) - if backend == "custom": assert K.custom_gates assert K.custom_einsum is None diff --git a/src/qibo/tests_new/test_measurement_gate_collapse.py b/src/qibo/tests_new/test_measurement_gate_collapse.py index ff4b03bbf2..fff8c9ddc4 100644 --- a/src/qibo/tests_new/test_measurement_gate_collapse.py +++ b/src/qibo/tests_new/test_measurement_gate_collapse.py @@ -73,12 +73,12 @@ def test_measurement_collapse_bitflip_noise(backend, accelerators): c = models.Circuit(4, accelerators) output = c.add(gates.M(0, 1, p0=0.2, collapse=True)) result = c(nshots=20) - if K.name == "tensorflow": - target_samples = [2, 2, 2, 1, 1, 0, 1, 2, 1, 2, 2, 3, 3, 0, 3, - 0, 0, 3, 0, 1] - elif K.name == "numpy": + if "numpy" in K.name: target_samples = [3, 3, 0, 3, 2, 0, 1, 2, 2, 2, 2, 0, 0, 2, 0, 2, 3, 1, 1, 0] + else: + target_samples = [2, 2, 2, 1, 1, 0, 1, 2, 1, 2, 2, 3, 3, 0, 3, + 0, 0, 3, 0, 1] np.testing.assert_allclose(output.samples(binary=False), target_samples) _, target_frequencies = np.unique(target_samples, return_counts=True) target_frequencies = {i: v for i, v in enumerate(target_frequencies)} diff --git a/src/qibo/tests_new/test_measurement_gate_probabilistic.py b/src/qibo/tests_new/test_measurement_gate_probabilistic.py index d67fe53c14..dd4176eb61 100644 --- a/src/qibo/tests_new/test_measurement_gate_probabilistic.py +++ b/src/qibo/tests_new/test_measurement_gate_probabilistic.py @@ -27,14 +27,14 @@ def test_probabilistic_measurement(backend, accelerators, use_samples): _ = result.samples() # update reference values based on backend and device - if K.name == "tensorflow": + if "numpy" in K.name: + decimal_frequencies = {0: 249, 1: 231, 2: 253, 3: 267} + else: if K.gpu_devices: # pragma: no cover # CI does not use GPU decimal_frequencies = {0: 273, 1: 233, 2: 242, 3: 252} else: decimal_frequencies = {0: 271, 1: 239, 2: 242, 3: 248} - elif K.name == "numpy": - decimal_frequencies = {0: 249, 1: 231, 2: 253, 3: 267} assert sum(result.frequencies().values()) == 1000 assert_result(result, decimal_frequencies=decimal_frequencies) qibo.set_backend(original_backend) @@ -60,14 +60,14 @@ def test_unbalanced_probabilistic_measurement(backend, use_samples): # otherwise it uses the frequency-only calculation _ = result.samples() # update reference values based on backend and device - if K.name == "tensorflow": + if "numpy" in K.name: + decimal_frequencies = {0: 171, 1: 148, 2: 161, 3: 520} + else: if K.gpu_devices: # pragma: no cover # CI does not use GPU decimal_frequencies = {0: 196, 1: 153, 2: 156, 3: 495} else: decimal_frequencies = {0: 168, 1: 188, 2: 154, 3: 490} - elif K.name == "numpy": - decimal_frequencies = {0: 171, 1: 148, 2: 161, 3: 520} assert sum(result.frequencies().values()) == 1000 assert_result(result, decimal_frequencies=decimal_frequencies) qibo.set_backend(original_backend) @@ -118,12 +118,12 @@ def test_post_measurement_bitflips_on_circuit(backend, accelerators, i, probs): c.add(gates.M(0, 1, p0={0: probs[0], 1: probs[1]})) c.add(gates.M(3, p0=probs[2])) result = c(nshots=30).frequencies(binary=False) - if K.name == "tensorflow": - targets = [{5: 30}, {5: 16, 7: 10, 6: 2, 3: 1, 4: 1}, - {3: 6, 5: 6, 7: 5, 2: 4, 4: 3, 0: 2, 1: 2, 6: 2}] - elif K.name == "numpy": + if "numpy" in K.name: targets = [{5: 30}, {5: 18, 4: 5, 7: 4, 1: 2, 6: 1}, {4: 8, 2: 6, 5: 5, 1: 3, 3: 3, 6: 2, 7: 2, 0: 1}] + else: + targets = [{5: 30}, {5: 16, 7: 10, 6: 2, 3: 1, 4: 1}, + {3: 6, 5: 6, 7: 5, 2: 4, 4: 3, 0: 2, 1: 2, 6: 2}] assert result == targets[i] qibo.set_backend(original_backend) From 7e2420596b4387199a9f5dfceca36cfda69b8bf0 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Fri, 26 Mar 2021 14:19:15 +0400 Subject: [PATCH 02/23] Move gate application from gates.py to backends --- src/qibo/{core => backends}/einsum.py | 65 ---------- src/qibo/backends/numpy.py | 146 ++++++++++++++++++++++- src/qibo/backends/tensorflow.py | 22 +++- src/qibo/core/gates.py | 84 +------------ src/qibo/tests_new/test_backends_init.py | 5 - 5 files changed, 166 insertions(+), 156 deletions(-) rename src/qibo/{core => backends}/einsum.py (81%) diff --git a/src/qibo/core/einsum.py b/src/qibo/backends/einsum.py similarity index 81% rename from src/qibo/core/einsum.py rename to src/qibo/backends/einsum.py index 8e05cb58e3..285b2f0f02 100644 --- a/src/qibo/core/einsum.py +++ b/src/qibo/backends/einsum.py @@ -18,7 +18,6 @@ examples. """ from abc import ABC, abstractmethod -from qibo import K from qibo.config import raise_error from typing import Dict, List, Optional, Sequence @@ -220,70 +219,6 @@ def _calculate_density_matrix_controlled(self): # pragma: no cover "MatmulEinsum backend is not implemented when multicontrol " "gates are used on density matrices.") -class DefaultEinsum: - """Gate application backend that based on default ``einsum``. - - This is the most efficient implementation for GPU, however its - backpropagation is not working properly for complex numbers. - The user should switch to :class:`qibo.core.einsum.MatmulEinsum` - if automatic differentiation is required. - """ - - def __call__(self, cache: str, state: K.Tensor, gate: K.Tensor) -> K.Tensor: - return K.einsum(cache, state, gate) - - @staticmethod - def create_cache(qubits: Sequence[int], nqubits: int, - ncontrol: Optional[int] = None): - return DefaultEinsumCache(qubits, nqubits, ncontrol) - - -class MatmulEinsum: - """Gate application backend based on ``matmul``. - - For Tensorflow this is more efficient than ``einsum`` on CPU but slower on GPU. - The matmul version implemented here is not the most efficient possible. - The implementation algorithm is the following. - - Assume that we are applying - a two qubit gate of shape (4, 4) to qubits 0 and 3 of a five qubit state - vector of shape 5 * (2,). We perform the following steps: - - * Reshape the state to (2, 4, 2, 2) - * Transpose to (2, 2, 4, 2) to bring the target qubits in the beginning. - * Reshape to (4, 8). - * Apply the gate using the matmul (4, 4) x (4, 8). - * Reshape to the original shape 5 * (2,) and traspose so that the final - qubit order agrees with the initial. - """ - - def __call__(self, cache: Dict, state: K.Tensor, gate: K.Tensor) -> K.Tensor: - shapes = cache["shapes"] - - state = K.reshape(state, shapes[0]) - state = K.transpose(state, cache["ids"]) - if cache["conjugate"]: - state = K.reshape(K.conj(state), shapes[1]) - else: - state = K.reshape(state, shapes[1]) - - n = len(tuple(gate.shape)) - if n > 2: - dim = 2 ** (n // 2) - state = K.matmul(K.reshape(gate, (dim, dim)), state) - else: - state = K.matmul(gate, state) - - state = K.reshape(state, shapes[2]) - state = K.transpose(state, cache["inverse_ids"]) - state = K.reshape(state, shapes[3]) - return state - - @staticmethod - def create_cache(qubits: Sequence[int], nqubits: int, - ncontrol: Optional[int] = None): - return MatmulEinsumCache(qubits, nqubits, ncontrol) - class ControlCache: """Helper tools for `controlled_by` gates. diff --git a/src/qibo/backends/numpy.py b/src/qibo/backends/numpy.py index 19b9e51a3d..21b894c606 100644 --- a/src/qibo/backends/numpy.py +++ b/src/qibo/backends/numpy.py @@ -22,7 +22,7 @@ def __init__(self): self.newaxis = np.newaxis self.oom_error = MemoryError self.optimization = None - self.op = None + self.einsum_module = None def set_device(self, name): log.warning("Numpy does not support device placement. " @@ -233,20 +233,160 @@ def __exit__(self, *args): def set_seed(self, seed): self.backend.random.seed(seed) + def create_cache(self, qubits, nqubits, ncontrol=None): # pragma: no cover + raise_error(NotImplementedError) + + def gate_call(self, cache, state, matrix): # pragma: no cover + raise_error(NotImplementedError) + + def prepare_gate(self, gate): + s = 1 + gate.density_matrix + gate.tensor_shape = self.cast(s * gate.nqubits * (2,), dtype='DTYPEINT') + gate.flat_shape = self.cast(s * (2 ** gate.nqubits,), dtype='DTYPEINT') + if gate.is_controlled_by: + if gate.density_matrix: + # FIXME: fall back to the 'defaulteinsum' backend when using + # density matrices with `controlled_by` gates because + # 'matmuleinsum' is not properly implemented for this case + gate.einsum = self.einsum_module.DefaultEinsum() + gate.control_cache = self.einsum_module.ControlCache(gate) + nactive = gate.nqubits - len(gate.control_qubits) + targets = gate.control_cache.targets + gate.calculation_cache = self.create_cache( + targets, nactive, ncontrol=len(gate.control_qubits)) + else: + gate.calculation_cache = self.create_cache(gate.qubits, gate.nqubits) + gate.calculation_cache.cast_shapes( + lambda x: self.cast(x, dtype='DTYPEINT')) + + def state_vector_call(self, gate, state): + state = self.reshape(state, gate.tensor_shape) + if gate.is_controlled_by: + ncontrol = len(gate.control_qubits) + nactive = gate.nqubits - ncontrol + state = self.transpose(state, gate.control_cache.order(False)) + # Apply `einsum` only to the part of the state where all controls + # are active. This should be `state[-1]` + state = self.reshape(state, (2 ** ncontrol,) + nactive * (2,)) + updates = self.gate_call(gate.calculation_cache.vector, state[-1], + gate.matrix) + # Concatenate the updated part of the state `updates` with the + # part of of the state that remained unaffected `state[:-1]`. + state = self.concatenate([state[:-1], updates[self.newaxis]], axis=0) + state = self.reshape(state, gate.nqubits * (2,)) + # Put qubit indices back to their proper places + state = self.transpose(state, gate.control_cache.reverse(False)) + else: + einsum_str = gate.calculation_cache.vector + state = self.gate_call(einsum_str, state, gate.matrix) + return self.reshape(state, gate.flat_shape) + + def density_matrix_call(self, gate, state): + state = self.reshape(state, gate.tensor_shape) + if gate.is_controlled_by: + ncontrol = len(gate.control_qubits) + nactive = gate.nqubits - ncontrol + n = 2 ** ncontrol + state = self.transpose(state, gate.control_cache.order(True)) + state = self.reshape(state, 2 * (n,) + 2 * nactive * (2,)) + state01 = self.gather(state, indices=range(n - 1), axis=0) + state01 = self.squeeze(self.gather(state01, indices=[n - 1], axis=1), axis=1) + state01 = self.gate_call(gate.calculation_cache.right0, + state01, self.conj(gate.matrix)) + state10 = self.gather(state, indices=range(n - 1), axis=1) + state10 = self.squeeze(self.gather(state10, indices=[n - 1], axis=0), axis=0) + state10 = self.gate_call(gate.calculation_cache.left0, + state10, gate.matrix) + + state11 = self.squeeze(self.gather(state, indices=[n - 1], axis=0), axis=0) + state11 = self.squeeze(self.gather(state11, indices=[n - 1], axis=0), axis=0) + state11 = self.gate_call(gate.calculation_cache.right, state11, self.conj(gate.matrix)) + state11 = self.gate_call(gate.calculation_cache.left, state11, gate.matrix) + + state00 = self.gather(state, indices=range(n - 1), axis=0) + state00 = self.gather(state00, indices=range(n - 1), axis=1) + state01 = self.concatenate([state00, state01[:, self.newaxis]], axis=1) + state10 = self.concatenate([state10, state11[self.newaxis]], axis=0) + state = self.concatenate([state01, state10[self.newaxis]], axis=0) + state = self.reshape(state, 2 * gate.nqubits * (2,)) + state = self.transpose(state, gate.control_cache.reverse(True)) + else: + state = self.gate_call(gate.calculation_cache.right, state, + self.conj(gate.matrix)) + state = self.gate_call(gate.calculation_cache.left, state, gate.matrix) + return self.reshape(state, gate.flat_shape) + class NumpyDefaultEinsumBackend(NumpyBackend): + """Gate application backend that based on default ``einsum``. + + This is the most efficient implementation for GPU, however its + backpropagation is not working properly for complex numbers. + The user should switch to :class:`qibo.core.einsum.MatmulEinsum` + if automatic differentiation is required. + """ def __init__(self): super().__init__() + from qibo.backends import einsum self.name = "numpy_defaulteinsum" self.custom_gates = False - self.custom_einsum = "DefaultEinsum" + self.einsum_module = einsum + + def create_cache(self, qubits, nqubits, ncontrol=None): + return self.einsum_module.DefaultEinsumCache(qubits, nqubits, ncontrol) + + def gate_call(self, cache, state, matrix): + return self.einsum(cache, state, matrix) class NumpyMatmulEinsumBackend(NumpyBackend): + """Gate application backend based on ``matmul``. + + For Tensorflow this is more efficient than ``einsum`` on CPU but slower on GPU. + The matmul version implemented here is not the most efficient possible. + The implementation algorithm is the following. + + Assume that we are applying + a two qubit gate of shape (4, 4) to qubits 0 and 3 of a five qubit state + vector of shape 5 * (2,). We perform the following steps: + + * Reshape the state to (2, 4, 2, 2) + * Transpose to (2, 2, 4, 2) to bring the target qubits in the beginning. + * Reshape to (4, 8). + * Apply the gate using the matmul (4, 4) x (4, 8). + * Reshape to the original shape 5 * (2,) and traspose so that the final + qubit order agrees with the initial. + """ def __init__(self): super().__init__() + from qibo.backends import einsum self.name = "numpy_matmuleinsum" self.custom_gates = False - self.custom_einsum = "MatmulEinsum" + self.einsum_module = einsum + + def create_cache(self, qubits, nqubits, ncontrol=None): + return self.einsum_module.MatmulEinsumCache(qubits, nqubits, ncontrol) + + def gate_call(self, cache, state, matrix): + shapes = cache["shapes"] + + state = self.reshape(state, shapes[0]) + state = self.transpose(state, cache["ids"]) + if cache["conjugate"]: + state = self.reshape(self.conj(state), shapes[1]) + else: + state = self.reshape(state, shapes[1]) + + n = len(tuple(matrix.shape)) + if n > 2: + dim = 2 ** (n // 2) + state = self.matmul(self.reshape(matrix, (dim, dim)), state) + else: + state = self.matmul(matrix, state) + + state = self.reshape(state, shapes[2]) + state = self.transpose(state, cache["inverse_ids"]) + state = self.reshape(state, shapes[3]) + return state diff --git a/src/qibo/backends/tensorflow.py b/src/qibo/backends/tensorflow.py index 3b5c20e3a0..1bf806a67f 100644 --- a/src/qibo/backends/tensorflow.py +++ b/src/qibo/backends/tensorflow.py @@ -233,15 +233,33 @@ class TensorflowDefaultEinsumBackend(TensorflowBackend): def __init__(self): super().__init__() + from qibo.backends import einsum self.name = "tensorflow_defaulteinsum" self.custom_gates = False - self.custom_einsum = "DefaultEinsum" + self.einsum_module = einsum + + def create_cache(self, qubits, nqubits, ncontrol=None): + return numpy.NumpyDefaultEinsumBackend.create_cache( + self, qubits, nqubits, ncontrol) + + def gate_call(self, cache, state, matrix): + return numpy.NumpyDefaultEinsumBackend.gate_call( + self, cache, state, matrix) class TensorflowMatmulEinsumBackend(TensorflowBackend): def __init__(self): + from qibo.backends import einsum super().__init__() self.name = "tensorflow_matmuleinsum" self.custom_gates = False - self.custom_einsum = "MatmulEinsum" + self.einsum_module = einsum + + def create_cache(self, qubits, nqubits, ncontrol=None): + return numpy.NumpyMatmulEinsumBackend.create_cache( + self, qubits, nqubits, ncontrol) + + def gate_call(self, cache, state, matrix): + return numpy.NumpyMatmulEinsumBackend.gate_call( + self, cache, state, matrix) diff --git a/src/qibo/core/gates.py b/src/qibo/core/gates.py index 8e43221f42..468b9705c9 100644 --- a/src/qibo/core/gates.py +++ b/src/qibo/core/gates.py @@ -21,12 +21,6 @@ def __init__(self): self.control_cache = None # Gate matrices self.matrix = None - # Einsum backend - from qibo.core import einsum - self.einsum_module = einsum - self.einsum = getattr(einsum, K.custom_einsum)() - self.tensor_shape = None - self.flat_shape = None @staticmethod def control_unitary(unitary): @@ -42,25 +36,7 @@ def prepare(self): if self.well_defined: self.reprepare() try: - s = 1 + self.density_matrix - self.tensor_shape = K.cast(s * self.nqubits * (2,), dtype='DTYPEINT') - self.flat_shape = K.cast(s * (2 ** self.nqubits,), dtype='DTYPEINT') - if self.is_controlled_by: - if self.density_matrix: - # fall back to the 'defaulteinsum' backend when using - # density matrices with `controlled_by` gates because - # 'matmuleinsum' is not properly implemented for this case - self.einsum = self.einsum_module.DefaultEinsum() - self.control_cache = self.einsum_module.ControlCache(self) - nactive = self.nqubits - len(self.control_qubits) - targets = self.control_cache.targets - self.calculation_cache = self.einsum.create_cache( - targets, nactive, ncontrol=len(self.control_qubits)) - else: - self.calculation_cache = self.einsum.create_cache( - self.qubits, self.nqubits) - self.calculation_cache.cast_shapes( - lambda x: K.cast(x, dtype='DTYPEINT')) + K.prepare_gate(self) except (ValueError, OverflowError): pass @@ -68,63 +44,10 @@ def set_nqubits(self, state): cgates.BackendGate.set_nqubits(self, state) def state_vector_call(self, state): - state = K.reshape(state, self.tensor_shape) - if self.is_controlled_by: - ncontrol = len(self.control_qubits) - nactive = self.nqubits - ncontrol - state = K.transpose(state, self.control_cache.order(False)) - # Apply `einsum` only to the part of the state where all controls - # are active. This should be `state[-1]` - state = K.reshape(state, (2 ** ncontrol,) + nactive * (2,)) - updates = self.einsum(self.calculation_cache.vector, state[-1], - self.matrix) - # Concatenate the updated part of the state `updates` with the - # part of of the state that remained unaffected `state[:-1]`. - state = K.concatenate([state[:-1], updates[K.newaxis]], axis=0) - state = K.reshape(state, self.nqubits * (2,)) - # Put qubit indices back to their proper places - state = K.transpose(state, self.control_cache.reverse(False)) - else: - einsum_str = self.calculation_cache.vector - state = self.einsum(einsum_str, state, self.matrix) - return K.reshape(state, self.flat_shape) + return K.state_vector_call(self, state) def density_matrix_call(self, state): - state = K.reshape(state, self.tensor_shape) - if self.is_controlled_by: - ncontrol = len(self.control_qubits) - nactive = self.nqubits - ncontrol - n = 2 ** ncontrol - state = K.transpose(state, self.control_cache.order(True)) - state = K.reshape(state, 2 * (n,) + 2 * nactive * (2,)) - state01 = K.gather(state, indices=range(n - 1), axis=0) - state01 = K.squeeze(K.gather(state01, indices=[n - 1], axis=1), axis=1) - state01 = self.einsum(self.calculation_cache.right0, - state01, K.conj(self.matrix)) - state10 = K.gather(state, indices=range(n - 1), axis=1) - state10 = K.squeeze(K.gather(state10, indices=[n - 1], axis=0), axis=0) - state10 = self.einsum(self.calculation_cache.left0, - state10, self.matrix) - - state11 = K.squeeze(K.gather(state, indices=[n - 1], axis=0), axis=0) - state11 = K.squeeze(K.gather(state11, indices=[n - 1], axis=0), axis=0) - state11 = self.einsum(self.calculation_cache.right, state11, - K.conj(self.matrix)) - state11 = self.einsum(self.calculation_cache.left, - state11, self.matrix) - - state00 = K.gather(state, indices=range(n - 1), axis=0) - state00 = K.gather(state00, indices=range(n - 1), axis=1) - state01 = K.concatenate([state00, state01[:, K.newaxis]], axis=1) - state10 = K.concatenate([state10, state11[K.newaxis]], axis=0) - state = K.concatenate([state01, state10[K.newaxis]], axis=0) - state = K.reshape(state, 2 * self.nqubits * (2,)) - state = K.transpose(state, self.control_cache.reverse(True)) - else: - state = self.einsum(self.calculation_cache.right, state, - K.conj(self.matrix)) - state = self.einsum(self.calculation_cache.left, state, self.matrix) - return K.reshape(state, self.flat_shape) + return K.density_matrix_call(self, state) class H(BackendGate, gates.H): @@ -150,7 +73,6 @@ def controlled_by(self, *q): """Fall back to CNOT and Toffoli if controls are one or two.""" # FIXME: This method can probably be removed gate = gates.X.controlled_by(self, *q) - gate.einsum = self.einsum return gate diff --git a/src/qibo/tests_new/test_backends_init.py b/src/qibo/tests_new/test_backends_init.py index 5683497c26..e58e72fd51 100644 --- a/src/qibo/tests_new/test_backends_init.py +++ b/src/qibo/tests_new/test_backends_init.py @@ -22,11 +22,6 @@ def test_set_backend(backend): from qibo.core import cgates as custom_gates assert isinstance(gates.H(0), custom_gates.BackendGate) else: - assert not K.custom_gates - if "defaulteinsum" in backend: - assert K.custom_einsum == "DefaultEinsum" - else: - assert K.custom_einsum == "MatmulEinsum" from qibo.core import gates as native_gates h = gates.H(0) assert isinstance(h, native_gates.BackendGate) From 2c0b0db82cd7abb6ba521443b15315da1b740f9b Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Fri, 26 Mar 2021 14:30:57 +0400 Subject: [PATCH 03/23] Fix matmul einsum fallback --- src/qibo/backends/numpy.py | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/src/qibo/backends/numpy.py b/src/qibo/backends/numpy.py index 21b894c606..d65d9347ce 100644 --- a/src/qibo/backends/numpy.py +++ b/src/qibo/backends/numpy.py @@ -239,21 +239,34 @@ def create_cache(self, qubits, nqubits, ncontrol=None): # pragma: no cover def gate_call(self, cache, state, matrix): # pragma: no cover raise_error(NotImplementedError) + def _get_default_einsum(self): + # finds default einsum backend of the same engine to use for fall back + # in the case of `controlled_by` gates used on density matrices where + # matmul einsum does not work properly + import sys + module = sys.modules[self.__class__.__module__] + engine = self.name.split("_")[0].capitalize() + return getattr(module, f"{engine}DefaultEinsumBackend") + def prepare_gate(self, gate): s = 1 + gate.density_matrix gate.tensor_shape = self.cast(s * gate.nqubits * (2,), dtype='DTYPEINT') gate.flat_shape = self.cast(s * (2 ** gate.nqubits,), dtype='DTYPEINT') if gate.is_controlled_by: - if gate.density_matrix: - # FIXME: fall back to the 'defaulteinsum' backend when using - # density matrices with `controlled_by` gates because - # 'matmuleinsum' is not properly implemented for this case - gate.einsum = self.einsum_module.DefaultEinsum() gate.control_cache = self.einsum_module.ControlCache(gate) nactive = gate.nqubits - len(gate.control_qubits) targets = gate.control_cache.targets - gate.calculation_cache = self.create_cache( - targets, nactive, ncontrol=len(gate.control_qubits)) + ncontrol = len(gate.control_qubits) + if gate.density_matrix: + # fall back to the 'defaulteinsum' backend when using + # density matrices with `controlled_by` gates because + # 'matmuleinsum' is not properly implemented for this case + backend = self._get_default_einsum() + gate.calculation_cache = backend.create_cache( + self, targets, nactive, ncontrol) + else: + gate.calculation_cache = self.create_cache( + targets, nactive, ncontrol) else: gate.calculation_cache = self.create_cache(gate.qubits, gate.nqubits) gate.calculation_cache.cast_shapes( @@ -370,6 +383,14 @@ def create_cache(self, qubits, nqubits, ncontrol=None): return self.einsum_module.MatmulEinsumCache(qubits, nqubits, ncontrol) def gate_call(self, cache, state, matrix): + if isinstance(cache, str): + # `controlled_by` gate acting on density matrices + # fall back to defaulteinsum because matmuleinsum is not properly + # implemented. See `qibo.backends.numpy.NumpyBackend.prepare_gate` + # for more details + backend = self._get_default_einsum() + return backend.gate_call(self, cache, state, matrix) + shapes = cache["shapes"] state = self.reshape(state, shapes[0]) From 65b38de7582ec0ebdd6ef89774954bd75169e6b1 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Fri, 26 Mar 2021 14:37:53 +0400 Subject: [PATCH 04/23] Move abstract methods in abstract backend --- src/qibo/backends/__init__.py | 2 +- src/qibo/backends/abstract.py | 21 +++++++++++++++++++++ src/qibo/backends/numpy.py | 7 +------ src/qibo/backends/tensorflow.py | 15 +++++++++++++++ 4 files changed, 38 insertions(+), 7 deletions(-) diff --git a/src/qibo/backends/__init__.py b/src/qibo/backends/__init__.py index a06f1f086e..40682da082 100644 --- a/src/qibo/backends/__init__.py +++ b/src/qibo/backends/__init__.py @@ -39,7 +39,7 @@ def _construct_backend(name): _available_names = ", ".join(list(AVAILABLE_BACKENDS.keys())) raise_error(ValueError, "Environment variable `QIBO_BACKEND` has " "unknown value {}. Please select one of {}." - "".format(_available_names)) + "".format(_BACKEND_NAME, _available_names)) K = AVAILABLE_BACKENDS.get(_BACKEND_NAME)() else: try: diff --git a/src/qibo/backends/abstract.py b/src/qibo/backends/abstract.py index 945fd9abc2..36ce624bff 100644 --- a/src/qibo/backends/abstract.py +++ b/src/qibo/backends/abstract.py @@ -389,3 +389,24 @@ def executing_eagerly(self): @abstractmethod def set_seed(self, seed): # pragma: no cover raise_error(NotImplementedError) + + # TODO: Add docstrings here + @abstractmethod + def create_cache(self, qubits, nqubits, ncontrol=None): # pragma: no cover + raise_error(NotImplementedError) + + @abstractmethod + def gate_call(self, cache, state, matrix): # pragma: no cover + raise_error(NotImplementedError) + + @abstractmethod + def prepare_gate(self, gate): # pragma: no cover + raise_error(NotImplementedError) + + @abstractmethod + def state_vector_call(self, gate, state): # pragma: no cover + raise_error(NotImplementedError) + + @abstractmethod + def density_matrix_call(self, gate, state): # pragma: no cover + raise_error(NotImplementedError) diff --git a/src/qibo/backends/numpy.py b/src/qibo/backends/numpy.py index d65d9347ce..5a9c332ee2 100644 --- a/src/qibo/backends/numpy.py +++ b/src/qibo/backends/numpy.py @@ -1,3 +1,4 @@ +from abc import abstractmethod from qibo.backends import abstract from qibo.config import raise_error, log @@ -233,12 +234,6 @@ def __exit__(self, *args): def set_seed(self, seed): self.backend.random.seed(seed) - def create_cache(self, qubits, nqubits, ncontrol=None): # pragma: no cover - raise_error(NotImplementedError) - - def gate_call(self, cache, state, matrix): # pragma: no cover - raise_error(NotImplementedError) - def _get_default_einsum(self): # finds default einsum backend of the same engine to use for fall back # in the case of `controlled_by` gates used on density matrices where diff --git a/src/qibo/backends/tensorflow.py b/src/qibo/backends/tensorflow.py index 1bf806a67f..eccede7db4 100644 --- a/src/qibo/backends/tensorflow.py +++ b/src/qibo/backends/tensorflow.py @@ -228,6 +228,21 @@ def sample_frequencies(self, probs, nshots): frequencies, probs, nshots, nqubits, seed, self.get_threads()) return frequencies + def create_cache(self, qubits, nqubits, ncontrol=None): # pragma: no cover + raise_error(NotImplementedError) + + def gate_call(self, cache, state, matrix): # pragma: no cover + raise_error(NotImplementedError) + + def prepare_gate(self, gate): # pragma: no cover + raise_error(NotImplementedError) + + def state_vector_call(self, gate, state): # pragma: no cover + raise_error(NotImplementedError) + + def density_matrix_call(self, gate, state): # pragma: no cover + raise_error(NotImplementedError) + class TensorflowDefaultEinsumBackend(TensorflowBackend): From 2159f4ecf5f0b99fe659e76763032f1f3ce4244e Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Fri, 26 Mar 2021 14:57:27 +0400 Subject: [PATCH 05/23] Fix relaxation channel B --- src/qibo/backends/numpy.py | 8 +++++++- src/qibo/core/gates.py | 20 +++----------------- 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/src/qibo/backends/numpy.py b/src/qibo/backends/numpy.py index 5a9c332ee2..c9d7de4390 100644 --- a/src/qibo/backends/numpy.py +++ b/src/qibo/backends/numpy.py @@ -263,7 +263,13 @@ def prepare_gate(self, gate): gate.calculation_cache = self.create_cache( targets, nactive, ncontrol) else: - gate.calculation_cache = self.create_cache(gate.qubits, gate.nqubits) + from qibo.abstractions.gates import _ThermalRelaxationChannelB + if isinstance(gate, _ThermalRelaxationChannelB): + # TODO: Handle this case otherwise + qubits = gate.qubits + tuple(q + gate.nqubits for q in gate.qubits) + gate.calculation_cache = self.create_cache(qubits, 2 * gate.nqubits) + else: + gate.calculation_cache = self.create_cache(gate.qubits, gate.nqubits) gate.calculation_cache.cast_shapes( lambda x: self.cast(x, dtype='DTYPEINT')) diff --git a/src/qibo/core/gates.py b/src/qibo/core/gates.py index 468b9705c9..b1a0947d3e 100644 --- a/src/qibo/core/gates.py +++ b/src/qibo/core/gates.py @@ -603,20 +603,6 @@ def __init__(self, q, t1, t2, time, excited_population=0, seed=None): self, q, t1, t2, time, excited_population=excited_population, seed=seed) - def prepare(self): - self.is_prepared = True - try: - self.tensor_shape = K.cast(2 * self.nqubits * (2,), dtype='DTYPEINT') - self.flat_shape = K.cast(2 * (2 ** self.nqubits,), dtype='DTYPEINT') - self.reprepare() - qubits = self.qubits + tuple(q + self.nqubits for q in self.qubits) - self.calculation_cache = self.einsum.create_cache( - qubits, 2 * self.nqubits) - self.calculation_cache.cast_shapes( - lambda x: K.cast(x, dtype='DTYPEINT')) - except (ValueError, OverflowError): # pragma: no cover - pass - def construct_unitary(self): return cgates._ThermalRelaxationChannelB.construct_unitary(self) @@ -626,6 +612,6 @@ def state_vector_call(self, state): def density_matrix_call(self, state): einsum_str = self.calculation_cache.vector - state = K.reshape(state, self.tensor_shape) - state = self.einsum(einsum_str, state, self.matrix) - return K.reshape(state, self.flat_shape) + state = K.reshape(state, self.tensor_shape) # pylint: disable=no-member + state = K.gate_call(einsum_str, state, self.matrix) + return K.reshape(state, self.flat_shape) # pylint: disable=no-member From 120b35bea3f989721eb5b05c3b930362a3b896ef Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Sun, 28 Mar 2021 17:50:51 +0400 Subject: [PATCH 06/23] Remove einsum specific methods from abstract backend --- src/qibo/backends/abstract.py | 8 ------ src/qibo/backends/numpy.py | 50 ++++++++++++++++++--------------- src/qibo/backends/tensorflow.py | 19 ++++++------- 3 files changed, 36 insertions(+), 41 deletions(-) diff --git a/src/qibo/backends/abstract.py b/src/qibo/backends/abstract.py index 36ce624bff..19c4130a44 100644 --- a/src/qibo/backends/abstract.py +++ b/src/qibo/backends/abstract.py @@ -391,14 +391,6 @@ def set_seed(self, seed): # pragma: no cover raise_error(NotImplementedError) # TODO: Add docstrings here - @abstractmethod - def create_cache(self, qubits, nqubits, ncontrol=None): # pragma: no cover - raise_error(NotImplementedError) - - @abstractmethod - def gate_call(self, cache, state, matrix): # pragma: no cover - raise_error(NotImplementedError) - @abstractmethod def prepare_gate(self, gate): # pragma: no cover raise_error(NotImplementedError) diff --git a/src/qibo/backends/numpy.py b/src/qibo/backends/numpy.py index c9d7de4390..9e9f344be2 100644 --- a/src/qibo/backends/numpy.py +++ b/src/qibo/backends/numpy.py @@ -23,7 +23,9 @@ def __init__(self): self.newaxis = np.newaxis self.oom_error = MemoryError self.optimization = None - self.einsum_module = None + + from qibo.backends import einsum + self.einsum_module = einsum def set_device(self, name): log.warning("Numpy does not support device placement. " @@ -234,6 +236,12 @@ def __exit__(self, *args): def set_seed(self, seed): self.backend.random.seed(seed) + def create_einsum_cache(self, qubits, nqubits, ncontrol=None): # pragma: no cover + raise_error(NotImplementedError) + + def einsum_call(self, cache, state, matrix): # pragma: no cover + raise_error(NotImplementedError) + def _get_default_einsum(self): # finds default einsum backend of the same engine to use for fall back # in the case of `controlled_by` gates used on density matrices where @@ -257,19 +265,19 @@ def prepare_gate(self, gate): # density matrices with `controlled_by` gates because # 'matmuleinsum' is not properly implemented for this case backend = self._get_default_einsum() - gate.calculation_cache = backend.create_cache( + gate.calculation_cache = backend.create_einsum_cache( self, targets, nactive, ncontrol) else: - gate.calculation_cache = self.create_cache( + gate.calculation_cache = self.create_einsum_cache( targets, nactive, ncontrol) else: from qibo.abstractions.gates import _ThermalRelaxationChannelB if isinstance(gate, _ThermalRelaxationChannelB): # TODO: Handle this case otherwise qubits = gate.qubits + tuple(q + gate.nqubits for q in gate.qubits) - gate.calculation_cache = self.create_cache(qubits, 2 * gate.nqubits) + gate.calculation_cache = self.create_einsum_cache(qubits, 2 * gate.nqubits) else: - gate.calculation_cache = self.create_cache(gate.qubits, gate.nqubits) + gate.calculation_cache = self.create_einsum_cache(gate.qubits, gate.nqubits) gate.calculation_cache.cast_shapes( lambda x: self.cast(x, dtype='DTYPEINT')) @@ -282,7 +290,7 @@ def state_vector_call(self, gate, state): # Apply `einsum` only to the part of the state where all controls # are active. This should be `state[-1]` state = self.reshape(state, (2 ** ncontrol,) + nactive * (2,)) - updates = self.gate_call(gate.calculation_cache.vector, state[-1], + updates = self.einsum_call(gate.calculation_cache.vector, state[-1], gate.matrix) # Concatenate the updated part of the state `updates` with the # part of of the state that remained unaffected `state[:-1]`. @@ -292,7 +300,7 @@ def state_vector_call(self, gate, state): state = self.transpose(state, gate.control_cache.reverse(False)) else: einsum_str = gate.calculation_cache.vector - state = self.gate_call(einsum_str, state, gate.matrix) + state = self.einsum_call(einsum_str, state, gate.matrix) return self.reshape(state, gate.flat_shape) def density_matrix_call(self, gate, state): @@ -305,17 +313,17 @@ def density_matrix_call(self, gate, state): state = self.reshape(state, 2 * (n,) + 2 * nactive * (2,)) state01 = self.gather(state, indices=range(n - 1), axis=0) state01 = self.squeeze(self.gather(state01, indices=[n - 1], axis=1), axis=1) - state01 = self.gate_call(gate.calculation_cache.right0, + state01 = self.einsum_call(gate.calculation_cache.right0, state01, self.conj(gate.matrix)) state10 = self.gather(state, indices=range(n - 1), axis=1) state10 = self.squeeze(self.gather(state10, indices=[n - 1], axis=0), axis=0) - state10 = self.gate_call(gate.calculation_cache.left0, - state10, gate.matrix) + state10 = self.einsum_call(gate.calculation_cache.left0, + state10, gate.matrix) state11 = self.squeeze(self.gather(state, indices=[n - 1], axis=0), axis=0) state11 = self.squeeze(self.gather(state11, indices=[n - 1], axis=0), axis=0) - state11 = self.gate_call(gate.calculation_cache.right, state11, self.conj(gate.matrix)) - state11 = self.gate_call(gate.calculation_cache.left, state11, gate.matrix) + state11 = self.einsum_call(gate.calculation_cache.right, state11, self.conj(gate.matrix)) + state11 = self.einsum_call(gate.calculation_cache.left, state11, gate.matrix) state00 = self.gather(state, indices=range(n - 1), axis=0) state00 = self.gather(state00, indices=range(n - 1), axis=1) @@ -325,9 +333,9 @@ def density_matrix_call(self, gate, state): state = self.reshape(state, 2 * gate.nqubits * (2,)) state = self.transpose(state, gate.control_cache.reverse(True)) else: - state = self.gate_call(gate.calculation_cache.right, state, + state = self.einsum_call(gate.calculation_cache.right, state, self.conj(gate.matrix)) - state = self.gate_call(gate.calculation_cache.left, state, gate.matrix) + state = self.einsum_call(gate.calculation_cache.left, state, gate.matrix) return self.reshape(state, gate.flat_shape) @@ -342,15 +350,13 @@ class NumpyDefaultEinsumBackend(NumpyBackend): def __init__(self): super().__init__() - from qibo.backends import einsum self.name = "numpy_defaulteinsum" self.custom_gates = False - self.einsum_module = einsum - def create_cache(self, qubits, nqubits, ncontrol=None): + def create_einsum_cache(self, qubits, nqubits, ncontrol=None): return self.einsum_module.DefaultEinsumCache(qubits, nqubits, ncontrol) - def gate_call(self, cache, state, matrix): + def einsum_call(self, cache, state, matrix): return self.einsum(cache, state, matrix) @@ -375,22 +381,20 @@ class NumpyMatmulEinsumBackend(NumpyBackend): def __init__(self): super().__init__() - from qibo.backends import einsum self.name = "numpy_matmuleinsum" self.custom_gates = False - self.einsum_module = einsum - def create_cache(self, qubits, nqubits, ncontrol=None): + def create_einsum_cache(self, qubits, nqubits, ncontrol=None): return self.einsum_module.MatmulEinsumCache(qubits, nqubits, ncontrol) - def gate_call(self, cache, state, matrix): + def einsum_call(self, cache, state, matrix): if isinstance(cache, str): # `controlled_by` gate acting on density matrices # fall back to defaulteinsum because matmuleinsum is not properly # implemented. See `qibo.backends.numpy.NumpyBackend.prepare_gate` # for more details backend = self._get_default_einsum() - return backend.gate_call(self, cache, state, matrix) + return backend.einsum_call(self, cache, state, matrix) shapes = cache["shapes"] diff --git a/src/qibo/backends/tensorflow.py b/src/qibo/backends/tensorflow.py index eccede7db4..54f5f379eb 100644 --- a/src/qibo/backends/tensorflow.py +++ b/src/qibo/backends/tensorflow.py @@ -203,6 +203,7 @@ def __init__(self): self.op = op from qibo.config import get_threads self.get_threads = get_threads + self.einsum_module = None def initial_state(self, nqubits, is_matrix=False): return self.op.initial_state(nqubits, self.dtypes('DTYPECPX'), @@ -251,14 +252,13 @@ def __init__(self): from qibo.backends import einsum self.name = "tensorflow_defaulteinsum" self.custom_gates = False - self.einsum_module = einsum - def create_cache(self, qubits, nqubits, ncontrol=None): - return numpy.NumpyDefaultEinsumBackend.create_cache( + def create_einsum_cache(self, qubits, nqubits, ncontrol=None): + return numpy.NumpyDefaultEinsumBackend.create_einsum_cache( self, qubits, nqubits, ncontrol) - def gate_call(self, cache, state, matrix): - return numpy.NumpyDefaultEinsumBackend.gate_call( + def einsum_call(self, cache, state, matrix): + return numpy.NumpyDefaultEinsumBackend.einsum_call( self, cache, state, matrix) @@ -269,12 +269,11 @@ def __init__(self): super().__init__() self.name = "tensorflow_matmuleinsum" self.custom_gates = False - self.einsum_module = einsum - def create_cache(self, qubits, nqubits, ncontrol=None): - return numpy.NumpyMatmulEinsumBackend.create_cache( + def create_einsum_cache(self, qubits, nqubits, ncontrol=None): + return numpy.NumpyMatmulEinsumBackend.create_einsum_cache( self, qubits, nqubits, ncontrol) - def gate_call(self, cache, state, matrix): - return numpy.NumpyMatmulEinsumBackend.gate_call( + def einsum_call(self, cache, state, matrix): + return numpy.NumpyMatmulEinsumBackend.einsum_call( self, cache, state, matrix) From cb84d060500903ee0f9ffd1428396d57f7d348bc Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Sun, 28 Mar 2021 18:14:55 +0400 Subject: [PATCH 07/23] Create gate cache --- src/qibo/abstractions/abstract_gates.py | 6 +++ src/qibo/backends/abstract.py | 2 +- src/qibo/backends/numpy.py | 57 ++++++++++++++----------- src/qibo/backends/tensorflow.py | 6 --- src/qibo/core/cgates.py | 6 +++ src/qibo/core/circuit.py | 3 ++ src/qibo/core/gates.py | 23 +++++----- 7 files changed, 57 insertions(+), 46 deletions(-) diff --git a/src/qibo/abstractions/abstract_gates.py b/src/qibo/abstractions/abstract_gates.py index 952ebbf22e..156dc95fd6 100644 --- a/src/qibo/abstractions/abstract_gates.py +++ b/src/qibo/abstractions/abstract_gates.py @@ -386,6 +386,7 @@ class BaseBackendGate(Gate, ABC): def __init__(self): Gate.__init__(self) 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 @@ -432,6 +433,11 @@ def construct_unitary(self): # pragma: no cover """Constructs the gate's unitary matrix.""" return raise_error(NotImplementedError) + @property + @abstractmethod + def cache(self): # pragma: no cover + raise_error(NotImplementedError) + @abstractmethod def prepare(self): # pragma: no cover """Prepares gate for application to states. diff --git a/src/qibo/backends/abstract.py b/src/qibo/backends/abstract.py index 19c4130a44..c27562c848 100644 --- a/src/qibo/backends/abstract.py +++ b/src/qibo/backends/abstract.py @@ -392,7 +392,7 @@ def set_seed(self, seed): # pragma: no cover # TODO: Add docstrings here @abstractmethod - def prepare_gate(self, gate): # pragma: no cover + def create_gate_cache(self, gate): # pragma: no cover raise_error(NotImplementedError) @abstractmethod diff --git a/src/qibo/backends/numpy.py b/src/qibo/backends/numpy.py index 9e9f344be2..af869d0a76 100644 --- a/src/qibo/backends/numpy.py +++ b/src/qibo/backends/numpy.py @@ -251,79 +251,84 @@ def _get_default_einsum(self): engine = self.name.split("_")[0].capitalize() return getattr(module, f"{engine}DefaultEinsumBackend") - def prepare_gate(self, gate): + class GateCache: + pass + + def create_gate_cache(self, gate): + cache = self.GateCache() s = 1 + gate.density_matrix - gate.tensor_shape = self.cast(s * gate.nqubits * (2,), dtype='DTYPEINT') - gate.flat_shape = self.cast(s * (2 ** gate.nqubits,), dtype='DTYPEINT') + cache.tensor_shape = self.cast(s * gate.nqubits * (2,), dtype='DTYPEINT') + cache.flat_shape = self.cast(s * (2 ** gate.nqubits,), dtype='DTYPEINT') if gate.is_controlled_by: - gate.control_cache = self.einsum_module.ControlCache(gate) + cache.control_cache = self.einsum_module.ControlCache(gate) nactive = gate.nqubits - len(gate.control_qubits) - targets = gate.control_cache.targets + targets = cache.control_cache.targets ncontrol = len(gate.control_qubits) if gate.density_matrix: # fall back to the 'defaulteinsum' backend when using # density matrices with `controlled_by` gates because # 'matmuleinsum' is not properly implemented for this case backend = self._get_default_einsum() - gate.calculation_cache = backend.create_einsum_cache( + cache.calculation_cache = backend.create_einsum_cache( self, targets, nactive, ncontrol) else: - gate.calculation_cache = self.create_einsum_cache( + cache.calculation_cache = self.create_einsum_cache( targets, nactive, ncontrol) else: from qibo.abstractions.gates import _ThermalRelaxationChannelB if isinstance(gate, _ThermalRelaxationChannelB): # TODO: Handle this case otherwise qubits = gate.qubits + tuple(q + gate.nqubits for q in gate.qubits) - gate.calculation_cache = self.create_einsum_cache(qubits, 2 * gate.nqubits) + cache.calculation_cache = self.create_einsum_cache(qubits, 2 * gate.nqubits) else: - gate.calculation_cache = self.create_einsum_cache(gate.qubits, gate.nqubits) - gate.calculation_cache.cast_shapes( + cache.calculation_cache = self.create_einsum_cache(gate.qubits, gate.nqubits) + cache.calculation_cache.cast_shapes( lambda x: self.cast(x, dtype='DTYPEINT')) + return cache def state_vector_call(self, gate, state): - state = self.reshape(state, gate.tensor_shape) + state = self.reshape(state, gate.cache.tensor_shape) if gate.is_controlled_by: ncontrol = len(gate.control_qubits) nactive = gate.nqubits - ncontrol - state = self.transpose(state, gate.control_cache.order(False)) + state = self.transpose(state, gate.cache.control_cache.order(False)) # Apply `einsum` only to the part of the state where all controls # are active. This should be `state[-1]` state = self.reshape(state, (2 ** ncontrol,) + nactive * (2,)) - updates = self.einsum_call(gate.calculation_cache.vector, state[-1], + updates = self.einsum_call(gate.cache.calculation_cache.vector, state[-1], gate.matrix) # Concatenate the updated part of the state `updates` with the # part of of the state that remained unaffected `state[:-1]`. state = self.concatenate([state[:-1], updates[self.newaxis]], axis=0) state = self.reshape(state, gate.nqubits * (2,)) # Put qubit indices back to their proper places - state = self.transpose(state, gate.control_cache.reverse(False)) + state = self.transpose(state, gate.cache.control_cache.reverse(False)) else: - einsum_str = gate.calculation_cache.vector + einsum_str = gate.cache.calculation_cache.vector state = self.einsum_call(einsum_str, state, gate.matrix) - return self.reshape(state, gate.flat_shape) + return self.reshape(state, gate.cache.flat_shape) def density_matrix_call(self, gate, state): - state = self.reshape(state, gate.tensor_shape) + state = self.reshape(state, gate.cache.tensor_shape) if gate.is_controlled_by: ncontrol = len(gate.control_qubits) nactive = gate.nqubits - ncontrol n = 2 ** ncontrol - state = self.transpose(state, gate.control_cache.order(True)) + state = self.transpose(state, gate.cache.control_cache.order(True)) state = self.reshape(state, 2 * (n,) + 2 * nactive * (2,)) state01 = self.gather(state, indices=range(n - 1), axis=0) state01 = self.squeeze(self.gather(state01, indices=[n - 1], axis=1), axis=1) - state01 = self.einsum_call(gate.calculation_cache.right0, + state01 = self.einsum_call(gate.cache.calculation_cache.right0, state01, self.conj(gate.matrix)) state10 = self.gather(state, indices=range(n - 1), axis=1) state10 = self.squeeze(self.gather(state10, indices=[n - 1], axis=0), axis=0) - state10 = self.einsum_call(gate.calculation_cache.left0, + state10 = self.einsum_call(gate.cache.calculation_cache.left0, state10, gate.matrix) state11 = self.squeeze(self.gather(state, indices=[n - 1], axis=0), axis=0) state11 = self.squeeze(self.gather(state11, indices=[n - 1], axis=0), axis=0) - state11 = self.einsum_call(gate.calculation_cache.right, state11, self.conj(gate.matrix)) - state11 = self.einsum_call(gate.calculation_cache.left, state11, gate.matrix) + state11 = self.einsum_call(gate.cache.calculation_cache.right, state11, self.conj(gate.matrix)) + state11 = self.einsum_call(gate.cache.calculation_cache.left, state11, gate.matrix) state00 = self.gather(state, indices=range(n - 1), axis=0) state00 = self.gather(state00, indices=range(n - 1), axis=1) @@ -331,12 +336,12 @@ def density_matrix_call(self, gate, state): state10 = self.concatenate([state10, state11[self.newaxis]], axis=0) state = self.concatenate([state01, state10[self.newaxis]], axis=0) state = self.reshape(state, 2 * gate.nqubits * (2,)) - state = self.transpose(state, gate.control_cache.reverse(True)) + state = self.transpose(state, gate.cache.control_cache.reverse(True)) else: - state = self.einsum_call(gate.calculation_cache.right, state, + state = self.einsum_call(gate.cache.calculation_cache.right, state, self.conj(gate.matrix)) - state = self.einsum_call(gate.calculation_cache.left, state, gate.matrix) - return self.reshape(state, gate.flat_shape) + state = self.einsum_call(gate.cache.calculation_cache.left, state, gate.matrix) + return self.reshape(state, gate.cache.flat_shape) class NumpyDefaultEinsumBackend(NumpyBackend): diff --git a/src/qibo/backends/tensorflow.py b/src/qibo/backends/tensorflow.py index 54f5f379eb..77667e15c8 100644 --- a/src/qibo/backends/tensorflow.py +++ b/src/qibo/backends/tensorflow.py @@ -229,12 +229,6 @@ def sample_frequencies(self, probs, nshots): frequencies, probs, nshots, nqubits, seed, self.get_threads()) return frequencies - def create_cache(self, qubits, nqubits, ncontrol=None): # pragma: no cover - raise_error(NotImplementedError) - - def gate_call(self, cache, state, matrix): # pragma: no cover - raise_error(NotImplementedError) - def prepare_gate(self, gate): # pragma: no cover raise_error(NotImplementedError) diff --git a/src/qibo/core/cgates.py b/src/qibo/core/cgates.py index 075e77d4a3..77c031ffce 100644 --- a/src/qibo/core/cgates.py +++ b/src/qibo/core/cgates.py @@ -42,6 +42,12 @@ def control_unitary(unitary): part2 = K.concatenate([zeros, unitary], axis=0) return K.concatenate([part1, part2], axis=1) + @property + def cache(self): + if self._cache is None: + self._cache = K.create_gate_cache(self) + return self._cache + def reprepare(self): raise_error(RuntimeError, "Cannot reprepare non-parametrized gate.") diff --git a/src/qibo/core/circuit.py b/src/qibo/core/circuit.py index 65226a951a..25da3a37a4 100644 --- a/src/qibo/core/circuit.py +++ b/src/qibo/core/circuit.py @@ -133,6 +133,9 @@ def compile(self): if K.custom_gates: raise_error(RuntimeError, "Cannot compile circuit that uses custom " "operators.") + for gate in self.queue: + # create gate cache before compilation + _ = gate.cache self._compiled_execute = K.compile(self._execute_for_compile) def _execute(self, initial_state=None): diff --git a/src/qibo/core/gates.py b/src/qibo/core/gates.py index b1a0947d3e..3bca7f8cdd 100644 --- a/src/qibo/core/gates.py +++ b/src/qibo/core/gates.py @@ -15,17 +15,18 @@ class BackendGate(BaseBackendGate): def __init__(self): super().__init__() - self.calculation_cache = None - # For ``controlled_by`` gates - # (see ``core.einsum.ControlCache`` for more details) - self.control_cache = None - # Gate matrices self.matrix = None @staticmethod def control_unitary(unitary): return cgates.BackendGate.control_unitary(unitary) + @property + def cache(self): + if self._cache is None: + self._cache = K.create_gate_cache(self) + return self._cache + def reprepare(self): matrix = self.construct_unitary() rank = int(math.log2(int(matrix.shape[0]))) @@ -35,10 +36,6 @@ def prepare(self): self.is_prepared = True if self.well_defined: self.reprepare() - try: - K.prepare_gate(self) - except (ValueError, OverflowError): - pass def set_nqubits(self, state): cgates.BackendGate.set_nqubits(self, state) @@ -611,7 +608,7 @@ def state_vector_call(self, state): "state vectors when T1 < T2.") def density_matrix_call(self, state): - einsum_str = self.calculation_cache.vector - state = K.reshape(state, self.tensor_shape) # pylint: disable=no-member - state = K.gate_call(einsum_str, state, self.matrix) - return K.reshape(state, self.flat_shape) # pylint: disable=no-member + einsum_str = self.cache.calculation_cache.vector + state = K.reshape(state, self.cache.tensor_shape) # pylint: disable=no-member + state = K.einsum_call(einsum_str, state, self.matrix) + return K.reshape(state, self.cache.flat_shape) # pylint: disable=no-member From 682c0b1bf17c32e11ff6eb772c0850615ff9b6f1 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Sun, 28 Mar 2021 18:22:33 +0400 Subject: [PATCH 08/23] Fix pylint --- src/qibo/backends/numpy.py | 2 ++ src/qibo/backends/tensorflow.py | 8 +++++++- src/qibo/models.py | 2 ++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/qibo/backends/numpy.py b/src/qibo/backends/numpy.py index af869d0a76..2ace88bae1 100644 --- a/src/qibo/backends/numpy.py +++ b/src/qibo/backends/numpy.py @@ -236,9 +236,11 @@ def __exit__(self, *args): def set_seed(self, seed): self.backend.random.seed(seed) + @abstractmethod def create_einsum_cache(self, qubits, nqubits, ncontrol=None): # pragma: no cover raise_error(NotImplementedError) + @abstractmethod def einsum_call(self, cache, state, matrix): # pragma: no cover raise_error(NotImplementedError) diff --git a/src/qibo/backends/tensorflow.py b/src/qibo/backends/tensorflow.py index 77667e15c8..83c1fab7b4 100644 --- a/src/qibo/backends/tensorflow.py +++ b/src/qibo/backends/tensorflow.py @@ -229,7 +229,13 @@ def sample_frequencies(self, probs, nshots): frequencies, probs, nshots, nqubits, seed, self.get_threads()) return frequencies - def prepare_gate(self, gate): # pragma: no cover + def create_einsum_cache(self, qubits, nqubits, ncontrol=None): # pragma: no cover + raise_error(NotImplementedError) + + def einsum_call(self, cache, state, matrix): # pragma: no cover + raise_error(NotImplementedError) + + def create_gate_cache(self, gate): raise_error(NotImplementedError) def state_vector_call(self, gate, state): # pragma: no cover diff --git a/src/qibo/models.py b/src/qibo/models.py index f46bba1caf..0e11d1fb33 100644 --- a/src/qibo/models.py +++ b/src/qibo/models.py @@ -213,6 +213,8 @@ def _loss(params, circuit, hamiltonian): raise_error(RuntimeError, "Cannot compile VQE that uses custom operators. " "Set the compile flag to False.") from qibo import K + for gate in self.circuit.queue: + _ = gate.cache loss = K.compile(_loss) if method == 'sgd': From bc8692ef1b063cf9a9c2969048f830d70ca70480 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Sun, 28 Mar 2021 18:52:28 +0400 Subject: [PATCH 09/23] Move cgates calls to backends --- src/qibo/backends/tensorflow.py | 39 ++++++++++++++++++++++----- src/qibo/core/cgates.py | 47 +++++++++------------------------ 2 files changed, 46 insertions(+), 40 deletions(-) diff --git a/src/qibo/backends/tensorflow.py b/src/qibo/backends/tensorflow.py index 83c1fab7b4..bcdc5c1994 100644 --- a/src/qibo/backends/tensorflow.py +++ b/src/qibo/backends/tensorflow.py @@ -236,13 +236,40 @@ def einsum_call(self, cache, state, matrix): # pragma: no cover raise_error(NotImplementedError) def create_gate_cache(self, gate): - raise_error(NotImplementedError) - - def state_vector_call(self, gate, state): # pragma: no cover - raise_error(NotImplementedError) + cache = self.GateCache() + qubits = [gate.nqubits - q - 1 for q in gate.control_qubits] + qubits.extend(gate.nqubits - q - 1 for q in gate.target_qubits) + cache.qubits_tensor = self.cast(sorted(qubits), "int32") + if gate.density_matrix: + cache.target_qubits_dm = [q + gate.nqubits for q in gate.target_qubits] + return cache + + def state_vector_call(self, gate, state): + return gate.gate_op(state, gate.cache.qubits_tensor, gate.nqubits, + *gate.target_qubits, self.get_threads()) + + def state_vector_matrix_call(self, gate, state): + return gate.gate_op(state, gate.matrix, gate.cache.qubits_tensor, # pylint: disable=E1121 + gate.nqubits, *gate.target_qubits, + self.get_threads()) + + def density_matrix_call(self, gate, state): + state = gate.gate_op(state, gate.cache.qubits_tensor + gate.nqubits, + 2 * gate.nqubits, *gate.target_qubits, + self.get_threads()) + state = gate.gate_op(state, gate.cache.qubits_tensor, 2 * gate.nqubits, + *gate.cache.target_qubits_dm, self.get_threads()) + return state - def density_matrix_call(self, gate, state): # pragma: no cover - raise_error(NotImplementedError) + def density_matrix_matrix_call(self, gate, state): + state = gate.gate_op(state, gate.matrix, gate.cache.qubits_tensor + gate.nqubits, # pylint: disable=E1121 + 2 * gate.nqubits, *gate.target_qubits, + self.get_threads()) + adjmatrix = self.conj(gate.matrix) + state = gate.gate_op(state, adjmatrix, gate.cache.qubits_tensor, + 2 * gate.nqubits, *gate.cache.target_qubits_dm, + self.get_threads()) + return state class TensorflowDefaultEinsumBackend(TensorflowBackend): diff --git a/src/qibo/core/cgates.py b/src/qibo/core/cgates.py index 77c031ffce..b3e83ebb66 100644 --- a/src/qibo/core/cgates.py +++ b/src/qibo/core/cgates.py @@ -55,32 +55,15 @@ def prepare(self): """Prepares the gate for application to state vectors.""" self.is_prepared = True - @property - def qubits_tensor(self): - if self._qubits_tensor is None: - qubits = [self.nqubits - q - 1 for q in self.control_qubits] - qubits.extend(self.nqubits - q - 1 for q in self.target_qubits) - self._qubits_tensor = K.cast(sorted(qubits), "int32") - return self._qubits_tensor - - @property - def target_qubits_dm(self): - return [q + self.nqubits for q in self.target_qubits] - def set_nqubits(self, state): self.nqubits = int(math.log2(tuple(state.shape)[0])) self.prepare() def state_vector_call(self, state): - return self.gate_op(state, self.qubits_tensor, self.nqubits, - *self.target_qubits, get_threads()) + return K.state_vector_call(self, state) def density_matrix_call(self, state): - state = self.gate_op(state, self.qubits_tensor + self.nqubits, 2 * self.nqubits, - *self.target_qubits, get_threads()) - state = self.gate_op(state, self.qubits_tensor, 2 * self.nqubits, - *self.target_qubits_dm, get_threads()) - return state + return K.density_matrix_call(self, state) class MatrixGate(BackendGate): @@ -99,16 +82,10 @@ def prepare(self): self.reprepare() def state_vector_call(self, state): - return self.gate_op(state, self.matrix, self.qubits_tensor, # pylint: disable=E1121 - self.nqubits, *self.target_qubits, get_threads()) + return K.state_vector_matrix_call(self, state) def density_matrix_call(self, state): - state = self.gate_op(state, self.matrix, self.qubits_tensor + self.nqubits, # pylint: disable=E1121 - 2 * self.nqubits, *self.target_qubits, get_threads()) - adjmatrix = K.conj(self.matrix) - state = self.gate_op(state, adjmatrix, self.qubits_tensor, - 2 * self.nqubits, *self.target_qubits_dm, get_threads()) - return state + return K.density_matrix_matrix_call(self, state) class H(MatrixGate, gates.H): @@ -143,11 +120,12 @@ def construct_unitary(self): return K.matrices.Y def density_matrix_call(self, state): - state = self.gate_op(state, self.qubits_tensor + self.nqubits, 2 * self.nqubits, - *self.target_qubits, get_threads()) + state = self.gate_op(state, self.cache.qubits_tensor + self.nqubits, + 2 * self.nqubits, *self.target_qubits, + get_threads()) matrix = K.conj(K.matrices.Y) - state = K.op.apply_gate(state, matrix, self.qubits_tensor, - 2 * self.nqubits, *self.target_qubits_dm, + state = K.op.apply_gate(state, matrix, self.cache.qubits_tensor, + 2 * self.nqubits, *self.cache.target_qubits_dm, get_threads()) return state @@ -230,13 +208,13 @@ def symbol(self): return self._symbol def state_vector_collapse(self, state, result): - return self.gate_op(state, self.qubits_tensor, result, + return self.gate_op(state, self.cache.qubits_tensor, result, self.nqubits, True, get_threads()) def density_matrix_collapse(self, state, result): - state = self.gate_op(state, self.qubits_tensor + self.nqubits, result, + state = self.gate_op(state, self.cache.qubits_tensor + self.nqubits, result, 2 * self.nqubits, False, get_threads()) - state = self.gate_op(state, self.qubits_tensor, result, + state = self.gate_op(state, self.cache.qubits_tensor, result, 2 * self.nqubits, False, get_threads()) return state / K.trace(state) @@ -1040,6 +1018,7 @@ def __init__(self, q, t1, t2, time, excited_population=0, seed=None): seed=seed) self.gate_op = K.op.apply_two_qubit_gate + # TODO: Remove this property and `target_qubits_dm` @property def qubits_tensor(self): if self._qubits_tensor is None: From 912c054a62a35ff6899cd3f28967a07b470f4291 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Mon, 29 Mar 2021 16:02:29 +0400 Subject: [PATCH 10/23] Remove core/gates.py --- src/qibo/backends/abstract.py | 16 + src/qibo/backends/numpy.py | 38 ++ src/qibo/backends/tensorflow.py | 11 + src/qibo/core/cgates.py | 219 ++++--- src/qibo/core/gates.py | 614 ------------------ src/qibo/models.py | 10 +- src/qibo/optimizers.py | 14 +- src/qibo/tensorflow/distcircuit.py | 2 +- src/qibo/tests_new/test_backends_init.py | 9 +- src/qibo/tests_new/test_core_circuit.py | 6 +- src/qibo/tests_new/test_core_gates.py | 2 - .../tests_new/test_core_gates_features.py | 8 +- 12 files changed, 203 insertions(+), 746 deletions(-) delete mode 100644 src/qibo/core/gates.py diff --git a/src/qibo/backends/abstract.py b/src/qibo/backends/abstract.py index c27562c848..4d2939ef42 100644 --- a/src/qibo/backends/abstract.py +++ b/src/qibo/backends/abstract.py @@ -399,6 +399,22 @@ def create_gate_cache(self, gate): # pragma: no cover def state_vector_call(self, gate, state): # pragma: no cover raise_error(NotImplementedError) + @abstractmethod + def state_vector_matrix_call(self, gate, state): # pragma: no cover + raise_error(NotImplementedError) + @abstractmethod def density_matrix_call(self, gate, state): # pragma: no cover raise_error(NotImplementedError) + + @abstractmethod + def density_matrix_matrix_call(self, gate, state): # pragma: no cover + raise_error(NotImplementedError) + + @abstractmethod + def state_vector_collapse(self, gate, state, result): # pragma: no cover + raise_error(NotImplementedError) + + @abstractmethod + def density_matrix_collapse(self, gate, state, result): # pragma: no cover + raise_error(NotImplementedError) diff --git a/src/qibo/backends/numpy.py b/src/qibo/backends/numpy.py index 2ace88bae1..f33d2099ad 100644 --- a/src/qibo/backends/numpy.py +++ b/src/qibo/backends/numpy.py @@ -310,6 +310,10 @@ def state_vector_call(self, gate, state): state = self.einsum_call(einsum_str, state, gate.matrix) return self.reshape(state, gate.cache.flat_shape) + def state_vector_matrix_call(self, gate, state): + print(gate, gate.matrix.shape) + return self.state_vector_call(gate, state) + def density_matrix_call(self, gate, state): state = self.reshape(state, gate.cache.tensor_shape) if gate.is_controlled_by: @@ -345,6 +349,40 @@ def density_matrix_call(self, gate, state): state = self.einsum_call(gate.cache.calculation_cache.left, state, gate.matrix) return self.reshape(state, gate.cache.flat_shape) + def density_matrix_matrix_call(self, gate, state): + return self.density_matrix_call(gate, state) + + def _append_zeros(self, state, qubits, results): + """Helper method for `state_vector_collapse` and `density_matrix_collapse`.""" + for q, r in zip(qubits, results): + state = self.expand_dims(state, axis=q) + if r: + state = self.concatenate([self.zeros_like(state), state], axis=q) + else: + state = self.concatenate([state, self.zeros_like(state)], axis=q) + return state + + def state_vector_collapse(self, gate, state, result): + state = self.reshape(state, gate.tensor_shape) + substate = self.gather_nd(self.transpose(state, gate.order), result) + norm = self.sum(self.square(self.abs(substate))) + state = substate / self.cast(self.sqrt(norm), dtype=state.dtype) + state = self._append_zeros(state, sorted(gate.target_qubits), result) + return self.reshape(state, gate.flat_shape) + + def density_matrix_collapse(self, gate, state, result): + density_matrix_result = 2 * result + sorted_qubits = sorted(gate.target_qubits) + sorted_qubits = sorted_qubits + [q + gate.nqubits for q in sorted_qubits] + state = self.reshape(state, gate.tensor_shape) + substate = self.gather_nd(self.transpose(state, gate.order), + density_matrix_result) + n = 2 ** (len(tuple(substate.shape)) // 2) + norm = self.trace(self.reshape(substate, (n, n))) + state = substate / norm + state = self._append_zeros(state, sorted_qubits, density_matrix_result) + return self.reshape(state, gate.flat_shape) + class NumpyDefaultEinsumBackend(NumpyBackend): """Gate application backend that based on default ``einsum``. diff --git a/src/qibo/backends/tensorflow.py b/src/qibo/backends/tensorflow.py index bcdc5c1994..e2bcd9fe65 100644 --- a/src/qibo/backends/tensorflow.py +++ b/src/qibo/backends/tensorflow.py @@ -271,6 +271,17 @@ def density_matrix_matrix_call(self, gate, state): self.get_threads()) return state + def state_vector_collapse(self, gate, state, result): + return gate.gate_op(state, gate.cache.qubits_tensor, result, + gate.nqubits, True, self.get_threads()) + + def density_matrix_collapse(self, gate, state, result): + state = gate.gate_op(state, gate.cache.qubits_tensor + gate.nqubits, result, + 2 * gate.nqubits, False, self.get_threads()) + state = gate.gate_op(state, gate.cache.qubits_tensor, result, + 2 * gate.nqubits, False, self.get_threads()) + return state / self.trace(state) + class TensorflowDefaultEinsumBackend(TensorflowBackend): diff --git a/src/qibo/core/cgates.py b/src/qibo/core/cgates.py index b3e83ebb66..966afa32f7 100644 --- a/src/qibo/core/cgates.py +++ b/src/qibo/core/cgates.py @@ -10,26 +10,19 @@ class BackendGate(BaseBackendGate): - module = sys.modules[__name__] - - def __new__(cls, *args, **kwargs): - cgate_only = {"I", "ZPow", "CZPow", "Flatten", "CallbackGate"} - # TODO: Move these to a different file and refactor - if K.custom_gates or cls.__name__ in cgate_only: - return super(BackendGate, cls).__new__(cls) - else: - from qibo.core import gates - return getattr(gates, cls.__name__)(*args, **kwargs) # pylint: disable=E0110 + module = sys.modules[__name__] # FIXME: This is probably not needed def __init__(self): - if not K.executing_eagerly(): - raise_error(NotImplementedError, - "Custom operator gates should not be used in compiled " - "mode.") - super().__init__() - if K.op: + if K.name == "custom": + if not K.executing_eagerly(): + raise_error(NotImplementedError, + "Custom operator gates should not be used in " + "compiled mode.") self.gate_op = K.op.apply_gate - self._qubits_tensor = None + else: + self.gate_op = None + super().__init__() + self.matrix = None @staticmethod def control_unitary(unitary): @@ -49,11 +42,16 @@ def cache(self): return self._cache def reprepare(self): - raise_error(RuntimeError, "Cannot reprepare non-parametrized gate.") + self.matrix = self.construct_unitary() + if K.name != "custom": + rank = int(math.log2(int(self.matrix.shape[0]))) + self.matrix = K.reshape(self.matrix, 2 * rank * (2,)) def prepare(self): """Prepares the gate for application to state vectors.""" self.is_prepared = True + if self.well_defined: + self.reprepare() def set_nqubits(self, state): self.nqubits = int(math.log2(tuple(state.shape)[0])) @@ -69,18 +67,6 @@ def density_matrix_call(self, state): class MatrixGate(BackendGate): """Gate that uses matrix multiplication to be applied to states.""" - def __init__(self): - super().__init__() - self.matrix = None - - def reprepare(self): - self.matrix = self.construct_unitary() - - def prepare(self): - super().prepare() - if self.well_defined: - self.reprepare() - def state_vector_call(self, state): return K.state_vector_matrix_call(self, state) @@ -103,7 +89,8 @@ class X(BackendGate, gates.X): def __init__(self, q): BackendGate.__init__(self) gates.X.__init__(self, q) - self.gate_op = K.op.apply_x + if self.gate_op: + self.gate_op = K.op.apply_x def construct_unitary(self): return K.matrices.X @@ -114,12 +101,14 @@ class Y(BackendGate, gates.Y): def __init__(self, q): BackendGate.__init__(self) gates.Y.__init__(self, q) - self.gate_op = K.op.apply_y + if self.gate_op: + self.gate_op = K.op.apply_y + self.density_matrix_call = lambda state: self._custom_density_matrix_call(state) def construct_unitary(self): return K.matrices.Y - def density_matrix_call(self, state): + def _custom_density_matrix_call(self, state): state = self.gate_op(state, self.cache.qubits_tensor + self.nqubits, 2 * self.nqubits, *self.target_qubits, get_threads()) @@ -176,6 +165,7 @@ def __init__(self, *q, register_name: Optional[str] = None, if collapse: self.result = self.measurements.MeasurementResult(self.qubits) self.gate_op = K.op.collapse_state + self.order = None def add(self, gate: gates.M): if self.is_prepared: @@ -184,6 +174,7 @@ def add(self, gate: gates.M): gates.M.add(self, gate) def prepare(self): + # FIXME: Move these to `self.cache` BackendGate.prepare(self) target_qubits = set(self.target_qubits) unmeasured_qubits = [] @@ -197,6 +188,25 @@ def prepare(self): self.reduced_target_qubits = list( reduced_target_qubits[i] for i in self.target_qubits) + if K.name != "custom": + sorted_qubits = sorted(self.target_qubits) + self.order = list(sorted_qubits) + s = 1 + self.density_matrix + self.tensor_shape = K.cast(s * self.nqubits * (2,), dtype='DTYPEINT') + self.flat_shape = K.cast(s * (2 ** self.nqubits,), dtype='DTYPEINT') + if self.density_matrix: + self.order.extend((q + self.nqubits for q in sorted_qubits)) + self.order.extend((q for q in range(self.nqubits) + if q not in sorted_qubits)) + self.order.extend((q + self.nqubits for q in range(self.nqubits) + if q not in sorted_qubits)) + else: + self.order.extend((q for q in range(self.nqubits) + if q not in sorted_qubits)) + + def reprepare(self): + pass + def construct_unitary(self): raise_error(ValueError, "Measurement gate does not have unitary " "representation.") @@ -207,17 +217,6 @@ def symbol(self): self._symbol = MeasurementSymbol(self.result) return self._symbol - def state_vector_collapse(self, state, result): - return self.gate_op(state, self.cache.qubits_tensor, result, - self.nqubits, True, get_threads()) - - def density_matrix_collapse(self, state, result): - state = self.gate_op(state, self.cache.qubits_tensor + self.nqubits, result, - 2 * self.nqubits, False, get_threads()) - state = self.gate_op(state, self.cache.qubits_tensor, result, - 2 * self.nqubits, False, get_threads()) - return state / K.trace(state) - def result_list(self): if self._result_list is None: pairs = zip(self.target_qubits, self.result.binary[-1]) @@ -273,10 +272,18 @@ def calculate_probs(): return result def state_vector_call(self, state): - return self.state_vector_collapse(state, self.result_tensor()) + if K.name == "custom": + result = self.result_tensor() + else: + result = self.result.binary[-1] + return K.state_vector_collapse(self, state, result) def density_matrix_call(self, state): - return self.density_matrix_collapse(state, self.result_tensor()) + if K.name == "custom": + result = self.result_tensor() + else: + result = self.result_list() + return K.density_matrix_collapse(self, state, result) def __call__(self, state, nshots=1): self.result = self.measure(state, nshots) @@ -328,10 +335,14 @@ class U1(MatrixGate, gates.U1): def __init__(self, q, theta, trainable=True): MatrixGate.__init__(self) gates.U1.__init__(self, q, theta, trainable) - self.gate_op = K.op.apply_z_pow + if self.gate_op: + self.gate_op = K.op.apply_z_pow def reprepare(self): - self.matrix = K.cast(K.qnp.exp(1j * self.parameters)) + if K.name == "custom": + self.matrix = K.cast(K.qnp.exp(1j * self.parameters)) + else: + MatrixGate.reprepare(self) def construct_unitary(self): return K.qnp.diag([1, K.qnp.exp(1j * self.parameters)]) @@ -369,12 +380,8 @@ def construct_unitary(self): class ZPow(gates.ZPow): - def __new__(cls, q, theta, trainable=True): - if K.custom_gates: - return U1(q, theta, trainable) - else: - from qibo.core import gates - return gates.U1(q, theta, trainable) + def __new__(cls, q, theta, trainable=True): + return U1(q, theta, trainable) class CNOT(BackendGate, gates.CNOT): @@ -382,7 +389,8 @@ class CNOT(BackendGate, gates.CNOT): def __init__(self, q0, q1): BackendGate.__init__(self) gates.CNOT.__init__(self, q0, q1) - self.gate_op = K.op.apply_x + if self.gate_op: + self.gate_op = K.op.apply_x def construct_unitary(self): return K.matrices.CNOT @@ -393,7 +401,8 @@ class CZ(BackendGate, gates.CZ): def __init__(self, q0, q1): BackendGate.__init__(self) gates.CZ.__init__(self, q0, q1) - self.gate_op = K.op.apply_z + if self.gate_op: + self.gate_op = K.op.apply_z def construct_unitary(self): return K.matrices.CZ @@ -409,6 +418,8 @@ def __init__(self, q0, q1, **params): def reprepare(self): self.matrix = self.base.construct_unitary(self) + if K.name != "custom": + self.matrix = K.reshape(self.control_unitary(self.matrix), 4 * (2,)) def construct_unitary(self): return MatrixGate.control_unitary(self.base.construct_unitary(self)) @@ -440,7 +451,8 @@ class CU1(_CUn_, gates.CU1): def __init__(self, q0, q1, theta, trainable=True): _CUn_.__init__(self, q0, q1, theta=theta, trainable=trainable) - self.gate_op = K.op.apply_z_pow + if self.gate_op: + self.gate_op = K.op.apply_z_pow def reprepare(self): U1.reprepare(self) @@ -463,12 +475,8 @@ def __init__(self, q0, q1, theta, phi, lam, trainable=True): class CZPow(gates.CZPow): - def __new__(cls, q0, q1, theta, trainable=True): - if K.custom_gates: - return CU1(q0, q1, theta, trainable) - else: - from qibo.core import gates - return gates.CU1(q0, q1, theta, trainable) + def __new__(cls, q0, q1, theta, trainable=True): + return CU1(q0, q1, theta, trainable) class SWAP(BackendGate, gates.SWAP): @@ -476,7 +484,8 @@ class SWAP(BackendGate, gates.SWAP): def __init__(self, q0, q1): BackendGate.__init__(self) gates.SWAP.__init__(self, q0, q1) - self.gate_op = K.op.apply_swap + if self.gate_op: + self.gate_op = K.op.apply_swap def construct_unitary(self): return K.matrices.SWAP @@ -487,13 +496,17 @@ class fSim(MatrixGate, gates.fSim): def __init__(self, q0, q1, theta, phi, trainable=True): MatrixGate.__init__(self) gates.fSim.__init__(self, q0, q1, theta, phi, trainable) - self.gate_op = K.op.apply_fsim + if self.gate_op: + self.gate_op = K.op.apply_fsim def reprepare(self): - theta, phi = self.parameters - cos, isin = K.qnp.cos(theta) + 0j, -1j * K.qnp.sin(theta) - phase = K.qnp.exp(-1j * phi) - self.matrix = K.cast([cos, isin, isin, cos, phase]) + if K.name == "custom": + theta, phi = self.parameters + cos, isin = K.qnp.cos(theta) + 0j, -1j * K.qnp.sin(theta) + phase = K.qnp.exp(-1j * phi) + self.matrix = K.cast([cos, isin, isin, cos, phase]) + else: + MatrixGate.reprepare(self) def construct_unitary(self): theta, phi = self.parameters @@ -510,14 +523,18 @@ class GeneralizedfSim(MatrixGate, gates.GeneralizedfSim): def __init__(self, q0, q1, unitary, phi, trainable=True): BackendGate.__init__(self) gates.GeneralizedfSim.__init__(self, q0, q1, unitary, phi, trainable) - self.gate_op = K.op.apply_fsim + if self.gate_op: + self.gate_op = K.op.apply_fsim def reprepare(self): - unitary, phi = self.parameters - matrix = K.qnp.zeros(5) - matrix[:4] = K.qnp.reshape(unitary, (4,)) - matrix[4] = K.qnp.exp(-1j * phi) - self.matrix = K.cast(matrix) + if K.name == "custom": + unitary, phi = self.parameters + matrix = K.qnp.zeros(5) + matrix[:4] = K.qnp.reshape(unitary, (4,)) + matrix[4] = K.qnp.exp(-1j * phi) + self.matrix = K.cast(matrix) + else: + MatrixGate.reprepare(self) def construct_unitary(self): unitary, phi = self.parameters @@ -541,7 +558,8 @@ class TOFFOLI(BackendGate, gates.TOFFOLI): def __init__(self, q0, q1, q2): BackendGate.__init__(self) gates.TOFFOLI.__init__(self, q0, q1, q2) - self.gate_op = K.op.apply_x + if self.gate_op: + self.gate_op = K.op.apply_x def construct_unitary(self): return K.matrices.TOFFOLI @@ -562,18 +580,19 @@ def __init__(self, unitary, *q, trainable=True, name: Optional[str] = None): MatrixGate.__init__(self) gates.Unitary.__init__(self, unitary, *q, trainable=trainable, name=name) rank = self.rank - if rank == 1: - self.gate_op = K.op.apply_gate - elif rank == 2: - self.gate_op = K.op.apply_two_qubit_gate - else: - n = len(self.target_qubits) - raise_error(NotImplementedError, "Unitary gate supports one or two-" - "qubit gates when using custom " - "operators, but {} target qubits " - "were given. Please switch to a " - "different backend to execute " - "this operation.".format(n)) + if self.gate_op: + if rank == 1: + self.gate_op = K.op.apply_gate + elif rank == 2: + self.gate_op = K.op.apply_two_qubit_gate + else: + n = len(self.target_qubits) + raise_error(NotImplementedError, "Unitary gate supports one or two-" + "qubit gates when using custom " + "operators, but {} target qubits " + "were given. Please switch to a " + "different backend to execute " + "this operation.".format(n)) def construct_unitary(self): unitary = self.parameters @@ -710,6 +729,9 @@ def __init__(self, coefficients): gates.Flatten.__init__(self, coefficients) self.swap_reset = [] + def reprepare(self): + pass + def construct_unitary(self): raise_error(ValueError, "Flatten gate does not have unitary " "representation.") @@ -735,6 +757,9 @@ def density_matrix(self, x): BaseBackendGate.density_matrix.fset(self, x) # pylint: disable=no-member self.callback.density_matrix = x + def reprepare(self): + pass + def construct_unitary(self): raise_error(ValueError, "Callback gate does not have unitary " "representation.") @@ -934,11 +959,11 @@ def _invert(gate): def state_vector_call(self, state): not_collapsed = True if K.qnp.random.random() < self.probs[-2]: - state = self.gates[-2].state_vector_collapse(state, [0]) + state = K.state_vector_collapse(self.gates[-2], state, [0]) not_collapsed = False if K.qnp.random.random() < self.probs[-1]: if not_collapsed: - state = self.gates[-2].state_vector_collapse(state, [0]) + state = K.state_vector_collapse(self.gates[-2], state, [0]) state = self.gates[-1](state) return state @@ -946,7 +971,7 @@ def density_matrix_call(self, state): new_state = (1 - self.psum) * state for p, gate, inv_gate in zip(self.probs, self.gates, self.inv_gates): if isinstance(gate, M): - state = gate.density_matrix_collapse(state, [0]) + state = K.density_matrix_collapse(gate, state, [0]) else: state = gate(state) new_state += p * state @@ -958,17 +983,10 @@ def density_matrix_call(self, state): class ThermalRelaxationChannel(gates.ThermalRelaxationChannel): def __new__(cls, q, t1, t2, time, excited_population=0, seed=None): - if K.custom_gates: - cls_a = _ThermalRelaxationChannelA - cls_b = _ThermalRelaxationChannelB - else: - from qibo.core import gates - cls_a = gates._ThermalRelaxationChannelA - cls_b = gates._ThermalRelaxationChannelB if t2 > t1: - cls_s = cls_b + cls_s = _ThermalRelaxationChannelB else: - cls_s = cls_a + cls_s = _ThermalRelaxationChannelA return cls_s( q, t1, t2, time, excited_population=excited_population, seed=seed) @@ -1017,6 +1035,7 @@ def __init__(self, q, t1, t2, time, excited_population=0, seed=None): self, q, t1, t2, time, excited_population=excited_population, seed=seed) self.gate_op = K.op.apply_two_qubit_gate + self._qubits_tensor = None # TODO: Remove this property and `target_qubits_dm` @property diff --git a/src/qibo/core/gates.py b/src/qibo/core/gates.py deleted file mode 100644 index 3bca7f8cdd..0000000000 --- a/src/qibo/core/gates.py +++ /dev/null @@ -1,614 +0,0 @@ -# -*- coding: utf-8 -*- -# @authors: S. Efthymiou -import math -import sys -from qibo import K -from qibo.abstractions import gates -from qibo.abstractions.abstract_gates import BaseBackendGate, ParametrizedGate -from qibo.core import cgates -from qibo.config import raise_error -from typing import Dict, List, Optional, Tuple - - -class BackendGate(BaseBackendGate): - module = sys.modules[__name__] - - def __init__(self): - super().__init__() - self.matrix = None - - @staticmethod - def control_unitary(unitary): - return cgates.BackendGate.control_unitary(unitary) - - @property - def cache(self): - if self._cache is None: - self._cache = K.create_gate_cache(self) - return self._cache - - def reprepare(self): - matrix = self.construct_unitary() - rank = int(math.log2(int(matrix.shape[0]))) - self.matrix = K.reshape(matrix, 2 * rank * (2,)) - - def prepare(self): - self.is_prepared = True - if self.well_defined: - self.reprepare() - - def set_nqubits(self, state): - cgates.BackendGate.set_nqubits(self, state) - - def state_vector_call(self, state): - return K.state_vector_call(self, state) - - def density_matrix_call(self, state): - return K.density_matrix_call(self, state) - - -class H(BackendGate, gates.H): - - def __init__(self, q): - BackendGate.__init__(self) - gates.H.__init__(self, q) - - def construct_unitary(self): - return K.matrices.H - - -class X(BackendGate, gates.X): - - def __init__(self, q): - BackendGate.__init__(self) - gates.X.__init__(self, q) - - def construct_unitary(self): - return K.matrices.X - - def controlled_by(self, *q): - """Fall back to CNOT and Toffoli if controls are one or two.""" - # FIXME: This method can probably be removed - gate = gates.X.controlled_by(self, *q) - return gate - - -class Y(BackendGate, gates.Y): - - def __init__(self, q): - BackendGate.__init__(self) - gates.Y.__init__(self, q) - - def construct_unitary(self): - return K.matrices.Y - - -class Z(BackendGate, gates.Z): - - def __init__(self, q): - BackendGate.__init__(self) - gates.Z.__init__(self, q) - - def construct_unitary(self): - return K.matrices.Z - - -class M(BackendGate, gates.M): - from qibo.core import measurements, states - - def __init__(self, *q, register_name: Optional[str] = None, - collapse: bool = False, - p0: Optional["ProbsType"] = None, - p1: Optional["ProbsType"] = None): - BackendGate.__init__(self) - gates.M.__init__(self, *q, register_name=register_name, - collapse=collapse, p0=p0, p1=p1) - self.unmeasured_qubits = None # Tuple - self.reduced_target_qubits = None # List - - self.result = None - self._result_list = None - self._result_tensor = None - if collapse: - self.result = self.measurements.MeasurementResult(self.qubits) - self.order = None - - def add(self, gate: gates.M): - cgates.M.add(self, gate) - - def prepare(self): - cgates.M.prepare(self) - try: - sorted_qubits = sorted(self.target_qubits) - self.order = list(sorted_qubits) - s = 1 + self.density_matrix - self.tensor_shape = K.cast(s * self.nqubits * (2,), dtype='DTYPEINT') - self.flat_shape = K.cast(s * (2 ** self.nqubits,), dtype='DTYPEINT') - if self.density_matrix: - self.order.extend((q + self.nqubits for q in sorted_qubits)) - self.order.extend((q for q in range(self.nqubits) - if q not in sorted_qubits)) - self.order.extend((q + self.nqubits for q in range(self.nqubits) - if q not in sorted_qubits)) - else: - self.order.extend((q for q in range(self.nqubits) - if q not in sorted_qubits)) - except (ValueError, OverflowError): # pragma: no cover - pass - - def construct_unitary(self): - cgates.M.construct_unitary(self) - - def symbol(self): - return cgates.M.symbol(self) - - @staticmethod - def _append_zeros(state, qubits: List[int], results: List[int]): - for q, r in zip(qubits, results): - state = K.expand_dims(state, axis=q) - if r: - state = K.concatenate([K.zeros_like(state), state], axis=q) - else: - state = K.concatenate([state, K.zeros_like(state)], axis=q) - return state - - def state_vector_collapse(self, state, result): - state = K.reshape(state, self.tensor_shape) - substate = K.gather_nd(K.transpose(state, self.order), result) - norm = K.sum(K.square(K.abs(substate))) - state = substate / K.cast(K.sqrt(norm), dtype=state.dtype) - state = self._append_zeros(state, sorted(self.target_qubits), result) - return K.reshape(state, self.flat_shape) - - def density_matrix_collapse(self, state, result): - density_matrix_result = 2 * result - sorted_qubits = sorted(self.target_qubits) - sorted_qubits = sorted_qubits + [q + self.nqubits for q in sorted_qubits] - state = K.reshape(state, self.tensor_shape) - substate = K.gather_nd(K.transpose(state, self.order), - density_matrix_result) - n = 2 ** (len(tuple(substate.shape)) // 2) - norm = K.trace(K.reshape(substate, (n, n))) - state = substate / norm - state = self._append_zeros(state, sorted_qubits, density_matrix_result) - return K.reshape(state, self.flat_shape) - - def result_list(self): - return cgates.M.result_list(self) - - def measure(self, state, nshots): - return cgates.M.measure(self, state, nshots) - - def state_vector_call(self, state): - return self.state_vector_collapse(state, self.result.binary[-1]) - - def density_matrix_call(self, state): - return self.density_matrix_collapse(state, self.result_list()) - - def __call__(self, state, nshots=1): - return cgates.M.__call__(self, state, nshots) - - -class RX(BackendGate, gates.RX): - - def __init__(self, q, theta, trainable=True): - BackendGate.__init__(self) - gates.RX.__init__(self, q, theta, trainable) - - def construct_unitary(self): - t = K.cast(self.parameters) - return K.cos(t / 2.0) * K.matrices.I - 1j * K.sin(t / 2.0) * K.matrices.X - - -class RY(BackendGate, gates.RY): - - def __init__(self, q, theta, trainable=True): - BackendGate.__init__(self) - gates.RY.__init__(self, q, theta, trainable) - - def construct_unitary(self): - t = K.cast(self.parameters) - return K.cos(t / 2.0) * K.matrices.I - 1j * K.sin(t / 2.0) * K.matrices.Y - - -class RZ(BackendGate, gates.RZ): - - def __init__(self, q, theta, trainable=True): - BackendGate.__init__(self) - gates.RZ.__init__(self, q, theta, trainable) - - def construct_unitary(self): - t = K.cast(self.parameters) - phase = K.exp(1j * t / 2.0)[K.newaxis] - diag = K.concatenate([K.conj(phase), phase], axis=0) - return K.diag(diag) - - -class U1(BackendGate, gates.U1): - - def __init__(self, q, theta, trainable=True): - BackendGate.__init__(self) - gates.U1.__init__(self, q, theta, trainable) - - def construct_unitary(self): - t = K.cast(self.parameters) - phase = K.exp(1j * t) - return K.diag([1, phase]) - - -class U2(BackendGate, gates.U2): - - def __init__(self, q, phi, lam, trainable=True): - BackendGate.__init__(self) - gates.U2.__init__(self, q, phi, lam, trainable) - - def construct_unitary(self): - return cgates.U2.construct_unitary(self) - - -class U3(BackendGate, gates.U3): - - def __init__(self, q, theta, phi, lam, trainable=True): - BackendGate.__init__(self) - gates.U3.__init__(self, q, theta, phi, lam, trainable=trainable) - - def construct_unitary(self): - return cgates.U3.construct_unitary(self) - - -class CNOT(BackendGate, gates.CNOT): - - def __init__(self, q0, q1): - BackendGate.__init__(self) - gates.CNOT.__init__(self, q0, q1) - - def construct_unitary(self): - return K.matrices.CNOT - - -class CZ(BackendGate, gates.CZ): - - def __init__(self, q0, q1): - BackendGate.__init__(self) - gates.CZ.__init__(self, q0, q1) - - def construct_unitary(self): - return K.matrices.CZ - - -class _CUn_(BackendGate): - base = U1 - - def __init__(self, q0, q1, **params): - cbase = "C{}".format(self.base.__name__) - BackendGate.__init__(self) - getattr(gates, cbase).__init__(self, q0, q1, **params) - - def construct_unitary(self): - return BackendGate.control_unitary(self.base.construct_unitary(self)) - - -class CRX(_CUn_, gates.CRX): - base = RX - - def __init__(self, q0, q1, theta, trainable=True): - _CUn_.__init__(self, q0, q1, theta=theta, trainable=trainable) - - -class CRY(_CUn_, gates.CRY): - base = RY - - def __init__(self, q0, q1, theta, trainable=True): - _CUn_.__init__(self, q0, q1, theta=theta, trainable=trainable) - - -class CRZ(_CUn_, gates.CRZ): - base = RZ - - def __init__(self, q0, q1, theta, trainable=True): - _CUn_.__init__(self, q0, q1, theta=theta, trainable=trainable) - - -class CU1(_CUn_, gates.CU1): - base = U1 - - def __init__(self, q0, q1, theta, trainable=True): - _CUn_.__init__(self, q0, q1, theta=theta, trainable=trainable) - - -class CU2(_CUn_, gates.CU2): - base = U2 - - def __init__(self, q0, q1, phi, lam, trainable=True): - _CUn_.__init__(self, q0, q1, phi=phi, lam=lam, trainable=trainable) - - -class CU3(_CUn_, gates.CU3): - base = U3 - - def __init__(self, q0, q1, theta, phi, lam, trainable=True): - _CUn_.__init__(self, q0, q1, theta=theta, phi=phi, lam=lam, - trainable=trainable) - - -class SWAP(BackendGate, gates.SWAP): - - def __init__(self, q0, q1): - BackendGate.__init__(self) - gates.SWAP.__init__(self, q0, q1) - - def construct_unitary(self): - return K.cast([[1, 0, 0, 0], [0, 0, 1, 0], - [0, 1, 0, 0], [0, 0, 0, 1]]) - - -class fSim(BackendGate, gates.fSim): - - def __init__(self, q0, q1, theta, phi, trainable=True): - BackendGate.__init__(self) - gates.fSim.__init__(self, q0, q1, theta, phi, trainable) - - def construct_unitary(self): - return cgates.fSim.construct_unitary(self) - - -class GeneralizedfSim(BackendGate, gates.GeneralizedfSim): - - def __init__(self, q0, q1, unitary, phi, trainable=True): - BackendGate.__init__(self) - gates.GeneralizedfSim.__init__(self, q0, q1, unitary, phi, trainable) - - def construct_unitary(self): - return cgates.GeneralizedfSim.construct_unitary(self) - - def _dagger(self) -> "GeneralizedfSim": - return cgates.GeneralizedfSim._dagger(self) - - -class TOFFOLI(BackendGate, gates.TOFFOLI): - - def __init__(self, q0, q1, q2): - BackendGate.__init__(self) - gates.TOFFOLI.__init__(self, q0, q1, q2) - - def construct_unitary(self): - return K.matrices.TOFFOLI - - @property - def unitary(self): - if self._unitary is None: - self._unitary = self.construct_unitary() - return self._unitary - - -class Unitary(BackendGate, gates.Unitary): - - def __init__(self, unitary, *q, trainable=True, name: Optional[str] = None): - if not isinstance(unitary, K.tensor_types): - raise_error(TypeError, "Unknown type {} of unitary matrix." - "".format(type(unitary))) - BackendGate.__init__(self) - gates.Unitary.__init__(self, unitary, *q, trainable=trainable, name=name) - - def construct_unitary(self): - unitary = self.parameters - rank = int(math.log2(int(unitary.shape[0]))) - matrix = K.copy(K.cast(unitary)) - return matrix - - def _dagger(self) -> "Unitary": - return cgates.Unitary._dagger(self) - - @ParametrizedGate.parameters.setter - def parameters(self, x): - cgates.Unitary.parameters.fset(self, x) # pylint: disable=no-member - - -class VariationalLayer(BackendGate, gates.VariationalLayer): - - @staticmethod - def _tfkron(m1, m2): - m = K.transpose(K.tensordot(m1, m2, axes=0), [0, 2, 1, 3]) - return K.reshape(m, (4, 4)) - - def _calculate_unitaries(self): - matrices = K.stack([self._tfkron( - self.one_qubit_gate(q1, theta=self.params[q1]).unitary, - self.one_qubit_gate(q2, theta=self.params[q2]).unitary) - for q1, q2 in self.pairs], axis=0) - entangling_matrix = self.two_qubit_gate(0, 1).unitary - matrices = K.matmul(entangling_matrix, matrices) - - additional_matrix = None - q = self.additional_target - if q is not None: - additional_matrix = self.one_qubit_gate( - q, theta=self.params[q]).unitary - - if self.params2: - matrices2 = K.stack([self._tfkron( - self.one_qubit_gate(q1, theta=self.params2[q1]).unitary, - self.one_qubit_gate(q2, theta=self.params2[q2]).unitary) - for q1, q2 in self.pairs], axis=0) - matrices = K.matmul(matrices2, matrices) - - q = self.additional_target - if q is not None: - additional_matrix = K.matmul( - self.one_qubit_gate(q, theta=self.params2[q]).unitary, - additional_matrix) - return matrices, additional_matrix - - def __init__(self, qubits: List[int], pairs: List[Tuple[int, int]], - one_qubit_gate, two_qubit_gate, - params: List[float], - params2: Optional[List[float]] = None, - trainable: bool = True, - name: Optional[str] = None): - cgates.VariationalLayer.__init__(self, qubits, pairs, - one_qubit_gate, two_qubit_gate, - params, params2, - trainable=trainable, name=name) - - @BaseBackendGate.density_matrix.setter - def density_matrix(self, x: bool): - cgates.VariationalLayer.density_matrix.fset(self, x) # pylint: disable=no-member - - def _dagger(self): - return cgates.VariationalLayer._dagger(self) - - def construct_unitary(self): - return cgates.VariationalLayer.construct_unitary(self) - - def reprepare(self): - cgates.VariationalLayer.reprepare(self) - - def prepare(self): - cgates.VariationalLayer.prepare(self) - - def state_vector_call(self, state): - return cgates.VariationalLayer.state_vector_call(self, state) - - def density_matrix_call(self, state): - return cgates.VariationalLayer.density_matrix_call(self, state) - - -class PartialTrace(BackendGate, gates.PartialTrace): - - def __init__(self, *q): - BackendGate.__init__(self) - gates.PartialTrace.__init__(self, *q) - - self.traceout_string = None - self.zero_matrix = None - self.transpose_order = None - self.output_shape = None - - def prepare(self): - cgates.PartialTrace.prepare(self) - - def construct_unitary(self): - cgates.PartialTrace.construct_unitary(self) - - def state_vector_partial_trace(self, state): - return cgates.PartialTrace.state_vector_partial_trace(self, state) - - def density_matrix_partial_trace(self, state): - return cgates.PartialTrace.density_matrix_partial_trace(self, state) - - def state_vector_call(self, state): - return cgates.PartialTrace.state_vector_call(self, state) - - def density_matrix_call(self, state): - return cgates.PartialTrace.density_matrix_call(self, state) - - -class KrausChannel(BackendGate, gates.KrausChannel): - - def __init__(self, ops): - BackendGate.__init__(self) - gates.KrausChannel.__init__(self, ops) - - def prepare(self): - self.is_prepared = True - for gate in self.gates: - gate.density_matrix = self.density_matrix - gate.device = self.device - gate.nqubits = self.nqubits - gate.prepare() - - def construct_unitary(self): - cgates.KrausChannel.construct_unitary(self) - - def state_vector_call(self, state): - ccls = getattr(cgates, self.__class__.__name__) - return ccls.state_vector_call(self, state) - - def density_matrix_call(self, state): - new_state = K.zeros_like(state) - for gate in self.gates: - new_state += gate(state) - return new_state - - -class UnitaryChannel(KrausChannel, gates.UnitaryChannel): - - def __init__(self, p: List[float], ops: List["Gate"], - seed: Optional[int] = None): - BackendGate.__init__(self) - gates.UnitaryChannel.__init__(self, p, ops, seed=seed) - - def prepare(self): - KrausChannel.prepare(self) - cgates.UnitaryChannel.set_seed(self) - - def density_matrix_call(self, state): - new_state = (1 - self.psum) * state - for p, gate in zip(self.probs, self.gates): - new_state += p * gate(state) - return new_state - - -class PauliNoiseChannel(UnitaryChannel, gates.PauliNoiseChannel): - - def __init__(self, q: int, px: float = 0, py: float = 0, pz: float = 0, - seed: Optional[int] = None): - BackendGate.__init__(self) - gates.PauliNoiseChannel.__init__(self, q, px, py, pz, seed=seed) - - -class ResetChannel(UnitaryChannel, gates.ResetChannel): - - def __init__(self, q: int, p0: float = 0.0, p1: float = 0.0, - seed: Optional[int] = None): - BackendGate.__init__(self) - gates.ResetChannel.__init__(self, q, p0=p0, p1=p1, seed=seed) - - def density_matrix_call(self, state): - new_state = (1 - self.psum) * state - for p, gate in zip(self.probs, self.gates): - if isinstance(gate, M): - state = gate.density_matrix_collapse(state, [0]) - else: - state = gate(state) - new_state += p * state - return new_state - - -class _ThermalRelaxationChannelA(ResetChannel, gates._ThermalRelaxationChannelA): - - def calculate_probabilities(self, t1, t2, time, excited_population): - cls = cgates.ThermalRelaxationChannel - return cls.calculate_probabilities(self, t1, t2, time, excited_population) - - def __init__(self, q, t1, t2, time, excited_population=0, seed=None): - BackendGate.__init__(self) - gates._ThermalRelaxationChannelA.__init__( - self, q, t1, t2, time, excited_population=excited_population, - seed=seed) - - -class _ThermalRelaxationChannelB(BackendGate, gates._ThermalRelaxationChannelB): - - def calculate_probabilities(self, t1, t2, time, excited_population): - cls = cgates.ThermalRelaxationChannel - return cls.calculate_probabilities(self, t1, t2, time, excited_population) - - def __init__(self, q, t1, t2, time, excited_population=0, seed=None): - BackendGate.__init__(self) - gates._ThermalRelaxationChannelB.__init__( - self, q, t1, t2, time, excited_population=excited_population, - seed=seed) - - def construct_unitary(self): - return cgates._ThermalRelaxationChannelB.construct_unitary(self) - - def state_vector_call(self, state): - raise_error(ValueError, "Thermal relaxation cannot be applied to " - "state vectors when T1 < T2.") - - def density_matrix_call(self, state): - einsum_str = self.cache.calculation_cache.vector - state = K.reshape(state, self.cache.tensor_shape) # pylint: disable=no-member - state = K.einsum_call(einsum_str, state, self.matrix) - return K.reshape(state, self.cache.flat_shape) # pylint: disable=no-member diff --git a/src/qibo/models.py b/src/qibo/models.py index 0e11d1fb33..270ba7b3b5 100644 --- a/src/qibo/models.py +++ b/src/qibo/models.py @@ -219,12 +219,10 @@ def _loss(params, circuit, hamiltonian): if method == 'sgd': # check if gates are using the MatmulEinsum backend - from qibo.core.gates import BackendGate - for gate in self.circuit.queue: - if not isinstance(gate, BackendGate): - raise_error(RuntimeError, 'SGD VQE requires native Tensorflow ' - 'gates because gradients are not ' - 'supported in the custom kernels.') + if K.name == "custom": + raise_error(RuntimeError, 'SGD VQE requires native Tensorflow ' + 'gates because gradients are not ' + 'supported in the custom kernels.') loss = _loss else: loss = lambda p, c, h: _loss(p, c, h).numpy() diff --git a/src/qibo/optimizers.py b/src/qibo/optimizers.py index 8f8145ecb9..f3658022ec 100644 --- a/src/qibo/optimizers.py +++ b/src/qibo/optimizers.py @@ -161,17 +161,15 @@ def sgd(loss, initial_parameters, args=(), options=None, compile=False): a message of the loss function. """ # check if gates are using the MatmulEinsum backend - from qibo.core.gates import BackendGate from qibo.core.circuit import Circuit for argument in args: if isinstance(argument, Circuit): - circuit = argument - for gate in circuit.queue: - if not isinstance(gate, BackendGate): # pragma: no cover - from qibo.config import raise_error - raise_error(RuntimeError, 'SGD requires native Tensorflow ' - 'gates because gradients are not ' - 'supported in the custom kernels.') + from qibo import K + if K.name == "custom": # pragma: no cover + from qibo.config import raise_error + raise_error(RuntimeError, 'SGD requires native Tensorflow ' + 'gates because gradients are not ' + 'supported in the custom kernels.') from qibo import K from qibo.config import log diff --git a/src/qibo/tensorflow/distcircuit.py b/src/qibo/tensorflow/distcircuit.py index c9418f96b4..d95967f347 100644 --- a/src/qibo/tensorflow/distcircuit.py +++ b/src/qibo/tensorflow/distcircuit.py @@ -98,7 +98,7 @@ def _add(self, gate: gates.Gate): Also checks that there are sufficient qubits to use as global. """ - if not isinstance(gate, gate_module.BackendGate): + if K.name != "custom": raise_error(NotImplementedError, "Distributed circuit does not " "support native tensorflow gates.") if isinstance(gate, gates.KrausChannel): diff --git a/src/qibo/tests_new/test_backends_init.py b/src/qibo/tests_new/test_backends_init.py index e58e72fd51..6f94f25463 100644 --- a/src/qibo/tests_new/test_backends_init.py +++ b/src/qibo/tests_new/test_backends_init.py @@ -16,15 +16,12 @@ def test_set_backend(backend): """Check ``set_backend`` for switching gate backends.""" original_backend = backends.get_backend() backends.set_backend(backend) + h = gates.H(0) if backend == "custom": - assert K.custom_gates assert K.custom_einsum is None - from qibo.core import cgates as custom_gates - assert isinstance(gates.H(0), custom_gates.BackendGate) + assert h.gate_op else: - from qibo.core import gates as native_gates - h = gates.H(0) - assert isinstance(h, native_gates.BackendGate) + assert h.gate_op is None backends.set_backend(original_backend) diff --git a/src/qibo/tests_new/test_core_circuit.py b/src/qibo/tests_new/test_core_circuit.py index 7ea2736c73..f69370cc8b 100644 --- a/src/qibo/tests_new/test_core_circuit.py +++ b/src/qibo/tests_new/test_core_circuit.py @@ -39,12 +39,8 @@ def test_circuit_add_layer(backend, nqubits, accelerators): params = nqubits * [0.1] c.add(gates.VariationalLayer(qubits, pairs, gates.RY, gates.CZ, params)) assert len(c.queue) == nqubits // 2 + nqubits % 2 - if backend == "custom": - target_gate_cls = gates.Unitary - else: - from qibo.core.gates import Unitary as target_gate_cls for gate in c.queue: - assert isinstance(gate, target_gate_cls) + assert isinstance(gate, gates.Unitary) qibo.set_backend(original_backend) # TODO: Test `_fuse_copy` diff --git a/src/qibo/tests_new/test_core_gates.py b/src/qibo/tests_new/test_core_gates.py index dc5093d179..9c446fe026 100644 --- a/src/qibo/tests_new/test_core_gates.py +++ b/src/qibo/tests_new/test_core_gates.py @@ -451,8 +451,6 @@ def test_flatten(backend): gate = gates.Flatten(target_state) with pytest.raises(ValueError): gate.construct_unitary() - with pytest.raises(RuntimeError): - gate.reprepare() qibo.set_backend(original_backend) diff --git a/src/qibo/tests_new/test_core_gates_features.py b/src/qibo/tests_new/test_core_gates_features.py index e9a2d49c60..b6a56e0321 100644 --- a/src/qibo/tests_new/test_core_gates_features.py +++ b/src/qibo/tests_new/test_core_gates_features.py @@ -347,9 +347,9 @@ def test_reset_channel_repeated(backend): for _ in range(30): state = np.copy(initial_state) if np.random.random() < 0.3: - state = collapse.state_vector_collapse(state, [0]) + state = K.state_vector_collapse(collapse, state, [0]) if np.random.random() < 0.3: - state = collapse.state_vector_collapse(state, [0]) + state = K.state_vector_collapse(collapse, state, [0]) state = xgate(state) target_state.append(np.copy(state)) np.testing.assert_allclose(final_state, target_state) @@ -378,9 +378,9 @@ def test_thermal_relaxation_channel_repeated(backend): if np.random.random() < pz: state = zgate(state) if np.random.random() < p0: - state = collapse.state_vector_collapse(state, [0]) + state = K.state_vector_collapse(collapse, state, [0]) if np.random.random() < p1: - state = collapse.state_vector_collapse(state, [0]) + state = K.state_vector_collapse(collapse, state, [0]) state = xgate(state) target_state.append(np.copy(state)) np.testing.assert_allclose(final_state, target_state) From caa02dbf19c6b31a4da4279d4d1956e4bbc3c9be Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Mon, 29 Mar 2021 16:35:39 +0400 Subject: [PATCH 11/23] Fix backpropagation issues --- src/qibo/backends/numpy.py | 1 - src/qibo/core/{cgates.py => gates.py} | 78 ++++++++++++++++++--------- src/qibo/gates.py | 2 +- src/qibo/models.py | 1 + src/qibo/tests/test_variational.py | 1 + 5 files changed, 57 insertions(+), 26 deletions(-) rename src/qibo/core/{cgates.py => gates.py} (94%) diff --git a/src/qibo/backends/numpy.py b/src/qibo/backends/numpy.py index f33d2099ad..075f4d7c09 100644 --- a/src/qibo/backends/numpy.py +++ b/src/qibo/backends/numpy.py @@ -311,7 +311,6 @@ def state_vector_call(self, gate, state): return self.reshape(state, gate.cache.flat_shape) def state_vector_matrix_call(self, gate, state): - print(gate, gate.matrix.shape) return self.state_vector_call(gate, state) def density_matrix_call(self, gate, state): diff --git a/src/qibo/core/cgates.py b/src/qibo/core/gates.py similarity index 94% rename from src/qibo/core/cgates.py rename to src/qibo/core/gates.py index 966afa32f7..ef451d4cac 100644 --- a/src/qibo/core/cgates.py +++ b/src/qibo/core/gates.py @@ -10,7 +10,7 @@ class BackendGate(BaseBackendGate): - module = sys.modules[__name__] # FIXME: This is probably not needed + module = sys.modules[__name__] def __init__(self): if K.name == "custom": @@ -303,7 +303,11 @@ def __init__(self, q, theta, trainable=True): def construct_unitary(self): theta = self.parameters - cos, isin = K.qnp.cos(theta / 2.0) + 0j, -1j * K.qnp.sin(theta / 2.0) + if isinstance(theta, K.Tensor): # pragma: no cover + p = K + else: + p = K.qnp + cos, isin = p.cos(theta / 2.0) + 0j, -1j * p.sin(theta / 2.0) return K.cast([[cos, isin], [isin, cos]]) @@ -315,7 +319,11 @@ def __init__(self, q, theta, trainable=True): def construct_unitary(self): theta = self.parameters - cos, sin = K.qnp.cos(theta / 2.0), K.qnp.sin(theta / 2.0) + if isinstance(theta, K.Tensor): + p = K + else: + p = K.qnp + cos, sin = p.cos(theta / 2.0), p.sin(theta / 2.0) return K.cast([[cos, -sin], [sin, cos]]) @@ -326,8 +334,12 @@ def __init__(self, q, theta, trainable=True): gates.RZ.__init__(self, q, theta, trainable) def construct_unitary(self): - phase = K.qnp.exp(1j * self.parameters / 2.0) - return K.cast(K.qnp.diag([K.qnp.conj(phase), phase])) + if isinstance(self.parameters, K.Tensor): # pragma: no cover + p = K + else: + p = K.qnp + phase = p.exp(1j * self.parameters / 2.0) + return K.cast(p.diag([p.conj(phase), phase])) class U1(MatrixGate, gates.U1): @@ -345,7 +357,11 @@ def reprepare(self): MatrixGate.reprepare(self) def construct_unitary(self): - return K.qnp.diag([1, K.qnp.exp(1j * self.parameters)]) + if isinstance(self.parameters, K.Tensor): # pragma: no cover + p = K + else: + p = K.qnp + return p.diag([1, p.exp(1j * self.parameters)]) class U2(MatrixGate, gates.U2): @@ -356,10 +372,14 @@ def __init__(self, q, phi, lam, trainable=True): def construct_unitary(self): phi, lam = self.parameters - eplus = K.qnp.exp(1j * (phi + lam) / 2.0) - eminus = K.qnp.exp(1j * (phi - lam) / 2.0) - return K.cast([[eplus.conj(), - eminus.conj()], - [eminus, eplus]]) / K.qnp.sqrt(2) + if isinstance(phi, K.Tensor) or isinstance(lam, K.Tensor): # pragma: no cover + p = K + else: + p = K.qnp + eplus = p.exp(1j * (phi + lam) / 2.0) + eminus = p.exp(1j * (phi - lam) / 2.0) + return K.cast([[p.conj(eplus), - p.conj(eminus)], + [eminus, eplus]]) / p.sqrt(2) class U3(MatrixGate, gates.U3): @@ -370,11 +390,13 @@ def __init__(self, q, theta, phi, lam, trainable=True): def construct_unitary(self): theta, phi, lam = self.parameters - cost = K.qnp.cos(theta / 2) - sint = K.qnp.sin(theta / 2) - eplus = K.qnp.exp(1j * (phi + lam) / 2.0) - eminus = K.qnp.exp(1j * (phi - lam) / 2.0) - return K.cast([[eplus.conj() * cost, - eminus.conj() * sint], + if isinstance(theta, K.Tensor) or isinstance(phi, K.Tensor) or isinstance(lam, K.Tensor): # pragma: no cover + p = K + else: + p = K.qnp + cost, sint = p.cos(theta / 2), p.sin(theta / 2) + eplus, eminus = p.exp(1j * (phi + lam) / 2.0), p.exp(1j * (phi - lam) / 2.0) + return K.cast([[p.conj(eplus) * cost, - p.conj(eminus) * sint], [eminus * sint, eplus * cost]]) @@ -510,11 +532,15 @@ def reprepare(self): def construct_unitary(self): theta, phi = self.parameters - cos, isin = K.qnp.cos(theta), -1j * K.qnp.sin(theta) - matrix = K.qnp.eye(4) + if isinstance(theta, K.Tensor) or isinstance(phi, K.Tensor): # pragma: no cover + p = K + else: + p = K.qnp + cos, isin = p.cos(theta), -1j * p.sin(theta) + matrix = p.eye(4) matrix[1, 1], matrix[2, 2] = cos, cos matrix[1, 2], matrix[2, 1] = isin, isin - matrix[3, 3] = K.qnp.exp(-1j * phi) + matrix[3, 3] = p.exp(-1j * phi) return K.cast(matrix) @@ -538,9 +564,13 @@ def reprepare(self): def construct_unitary(self): unitary, phi = self.parameters - matrix = K.qnp.eye(4) - matrix[1:3, 1:3] = K.qnp.reshape(unitary, (2, 2)) - matrix[3, 3] = K.qnp.exp(-1j * phi) + if isinstance(unitary, K.Tensor) or isinstance(phi, K.Tensor): # pragma: no cover + p = K + else: + p = K.qnp + matrix = p.eye(4) + matrix[1:3, 1:3] = p.reshape(unitary, (2, 2)) + matrix[3, 3] = p.exp(-1j * phi) return K.cast(matrix) def _dagger(self) -> "GenerelizedfSim": @@ -661,7 +691,7 @@ def __init__(self, qubits: List[int], pairs: List[Tuple[int, int]], params: List[float], params2: Optional[List[float]] = None, trainable: bool = True, name: Optional[str] = None): - self.module.BackendGate.__init__(self) + BackendGate.__init__(self) gates.VariationalLayer.__init__(self, qubits, pairs, one_qubit_gate, two_qubit_gate, params, params2, @@ -670,10 +700,10 @@ def __init__(self, qubits: List[int], pairs: List[Tuple[int, int]], matrices, additional_matrix = self._calculate_unitaries() self.unitaries = [] for targets, matrix in zip(self.pairs, matrices): - unitary = self.module.Unitary(matrix, *targets) + unitary = Unitary(matrix, *targets) self.unitaries.append(unitary) if self.additional_target is not None: - self.additional_unitary = self.module.Unitary( + self.additional_unitary = Unitary( additional_matrix, self.additional_target) self.additional_unitary.density_matrix = self.density_matrix else: diff --git a/src/qibo/gates.py b/src/qibo/gates.py index 7559679f0e..e4c1d080c4 100644 --- a/src/qibo/gates.py +++ b/src/qibo/gates.py @@ -1 +1 @@ -from qibo.core.cgates import * +from qibo.core.gates import * diff --git a/src/qibo/models.py b/src/qibo/models.py index 270ba7b3b5..25ee4b9029 100644 --- a/src/qibo/models.py +++ b/src/qibo/models.py @@ -219,6 +219,7 @@ def _loss(params, circuit, hamiltonian): if method == 'sgd': # check if gates are using the MatmulEinsum backend + from qibo import K if K.name == "custom": raise_error(RuntimeError, 'SGD VQE requires native Tensorflow ' 'gates because gradients are not ' diff --git a/src/qibo/tests/test_variational.py b/src/qibo/tests/test_variational.py index d71e4d061a..60f1df24b8 100644 --- a/src/qibo/tests/test_variational.py +++ b/src/qibo/tests/test_variational.py @@ -81,6 +81,7 @@ def test_vqe(method, options, compile, filename): original_threads = get_threads() nqubits = 6 layers = 4 + print(qibo.get_backend()) circuit = Circuit(nqubits) for l in range(layers): From 37cde0003c9c7445f6365d254ac86c6c9a11f670 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Wed, 31 Mar 2021 15:18:52 +0400 Subject: [PATCH 12/23] Remove prepare method from gates --- src/qibo/abstractions/abstract_gates.py | 65 ++-- src/qibo/abstractions/gates.py | 13 +- src/qibo/backends/numpy.py | 15 +- src/qibo/core/circuit.py | 4 +- src/qibo/core/gates.py | 289 ++++++++---------- src/qibo/tensorflow/distutils.py | 2 - src/qibo/tests_new/test_core_circuit.py | 4 - .../tests_new/test_core_gates_features.py | 2 - 8 files changed, 172 insertions(+), 222 deletions(-) diff --git a/src/qibo/abstractions/abstract_gates.py b/src/qibo/abstractions/abstract_gates.py index 156dc95fd6..f462e839c9 100644 --- a/src/qibo/abstractions/abstract_gates.py +++ b/src/qibo/abstractions/abstract_gates.py @@ -290,6 +290,45 @@ 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() + 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: + gate.nqubits = n + + 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. @@ -350,8 +389,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 @@ -385,6 +423,7 @@ 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 @@ -438,28 +477,6 @@ def construct_unitary(self): # pragma: no cover def cache(self): # pragma: no cover raise_error(NotImplementedError) - @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. - """ - raise_error(NotImplementedError) - @abstractmethod def set_nqubits(self, state): # pragma: no cover """Sets ``gate.nqubits`` and prepares gates for application to states. diff --git a/src/qibo/abstractions/gates.py b/src/qibo/abstractions/gates.py index c16c17123d..8560fb367f 100644 --- a/src/qibo/abstractions/gates.py +++ b/src/qibo/abstractions/gates.py @@ -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", @@ -1290,7 +1290,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: @@ -1354,15 +1354,6 @@ def _from_matrices(self, matrices): gatelist.append(self.module.Unitary(matrix, *list(qubits))) 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. diff --git a/src/qibo/backends/numpy.py b/src/qibo/backends/numpy.py index 075f4d7c09..ae6bc17851 100644 --- a/src/qibo/backends/numpy.py +++ b/src/qibo/backends/numpy.py @@ -362,25 +362,25 @@ def _append_zeros(self, state, qubits, results): return state def state_vector_collapse(self, gate, state, result): - state = self.reshape(state, gate.tensor_shape) - substate = self.gather_nd(self.transpose(state, gate.order), result) + state = self.reshape(state, gate.cache.tensor_shape) + substate = self.gather_nd(self.transpose(state, gate.cache.order), result) norm = self.sum(self.square(self.abs(substate))) state = substate / self.cast(self.sqrt(norm), dtype=state.dtype) state = self._append_zeros(state, sorted(gate.target_qubits), result) - return self.reshape(state, gate.flat_shape) + return self.reshape(state, gate.cache.flat_shape) def density_matrix_collapse(self, gate, state, result): density_matrix_result = 2 * result sorted_qubits = sorted(gate.target_qubits) sorted_qubits = sorted_qubits + [q + gate.nqubits for q in sorted_qubits] - state = self.reshape(state, gate.tensor_shape) - substate = self.gather_nd(self.transpose(state, gate.order), + state = self.reshape(state, gate.cache.tensor_shape) + substate = self.gather_nd(self.transpose(state, gate.cache.order), density_matrix_result) n = 2 ** (len(tuple(substate.shape)) // 2) norm = self.trace(self.reshape(substate, (n, n))) state = substate / norm state = self._append_zeros(state, sorted_qubits, density_matrix_result) - return self.reshape(state, gate.flat_shape) + return self.reshape(state, gate.cache.flat_shape) class NumpyDefaultEinsumBackend(NumpyBackend): @@ -435,8 +435,7 @@ def einsum_call(self, cache, state, matrix): if isinstance(cache, str): # `controlled_by` gate acting on density matrices # fall back to defaulteinsum because matmuleinsum is not properly - # implemented. See `qibo.backends.numpy.NumpyBackend.prepare_gate` - # for more details + # implemented. backend = self._get_default_einsum() return backend.einsum_call(self, cache, state, matrix) diff --git a/src/qibo/core/circuit.py b/src/qibo/core/circuit.py index 25da3a37a4..2c0d1dab64 100644 --- a/src/qibo/core/circuit.py +++ b/src/qibo/core/circuit.py @@ -32,16 +32,14 @@ def __init__(self, nqubits): self.state_cls = states.VectorState def set_nqubits(self, gate): - if gate.is_prepared and gate.nqubits != self.nqubits: + if gate._nqubits is not None and gate.nqubits != self.nqubits: raise_error(RuntimeError, "Cannot add gate {} that acts on {} " "qubits to circuit that contains {}" "qubits.".format( gate, gate.nqubits, self.nqubits)) gate.nqubits = self.nqubits - gate.prepare() def _add_layer(self, gate): - gate.prepare() for unitary in gate.unitaries: self.set_nqubits(unitary) self.queue.append(unitary) diff --git a/src/qibo/core/gates.py b/src/qibo/core/gates.py index ef451d4cac..8fa38805a5 100644 --- a/src/qibo/core/gates.py +++ b/src/qibo/core/gates.py @@ -22,7 +22,6 @@ def __init__(self): else: self.gate_op = None super().__init__() - self.matrix = None @staticmethod def control_unitary(unitary): @@ -41,21 +40,21 @@ def cache(self): self._cache = K.create_gate_cache(self) return self._cache - def reprepare(self): - self.matrix = self.construct_unitary() - if K.name != "custom": - rank = int(math.log2(int(self.matrix.shape[0]))) - self.matrix = K.reshape(self.matrix, 2 * rank * (2,)) + @property + def matrix(self): + if self._matrix is None: + self._matrix = self.calculate_matrix() + return self._matrix - def prepare(self): - """Prepares the gate for application to state vectors.""" - self.is_prepared = True - if self.well_defined: - self.reprepare() + def calculate_matrix(self): + matrix = self.construct_unitary() + if K.name != "custom": + rank = int(math.log2(int(matrix.shape[0]))) + matrix = K.reshape(matrix, 2 * rank * (2,)) + return matrix def set_nqubits(self, state): self.nqubits = int(math.log2(tuple(state.shape)[0])) - self.prepare() def state_vector_call(self, state): return K.state_vector_call(self, state) @@ -168,44 +167,41 @@ def __init__(self, *q, register_name: Optional[str] = None, self.order = None def add(self, gate: gates.M): - if self.is_prepared: - raise_error(RuntimeError, "Cannot add qubits to a measurement " - "gate that is prepared.") gates.M.add(self, gate) - def prepare(self): - # FIXME: Move these to `self.cache` - BackendGate.prepare(self) - target_qubits = set(self.target_qubits) - unmeasured_qubits = [] - reduced_target_qubits = dict() - for i in range(self.nqubits): - if i in target_qubits: - reduced_target_qubits[i] = i - len(unmeasured_qubits) - else: - unmeasured_qubits.append(i) - self.unmeasured_qubits = tuple(unmeasured_qubits) - self.reduced_target_qubits = list( - reduced_target_qubits[i] for i in self.target_qubits) - - if K.name != "custom": - sorted_qubits = sorted(self.target_qubits) - self.order = list(sorted_qubits) - s = 1 + self.density_matrix - self.tensor_shape = K.cast(s * self.nqubits * (2,), dtype='DTYPEINT') - self.flat_shape = K.cast(s * (2 ** self.nqubits,), dtype='DTYPEINT') - if self.density_matrix: - self.order.extend((q + self.nqubits for q in sorted_qubits)) - self.order.extend((q for q in range(self.nqubits) - if q not in sorted_qubits)) - self.order.extend((q + self.nqubits for q in range(self.nqubits) - if q not in sorted_qubits)) - else: - self.order.extend((q for q in range(self.nqubits) - if q not in sorted_qubits)) - - def reprepare(self): - pass + @property + def cache(self): + if self._cache is None: + cache = K.create_gate_cache(self) + target_qubits = set(self.target_qubits) + unmeasured_qubits = [] + reduced_target_qubits = dict() + for i in range(self.nqubits): + if i in target_qubits: + reduced_target_qubits[i] = i - len(unmeasured_qubits) + else: + unmeasured_qubits.append(i) + cache.unmeasured_qubits = tuple(unmeasured_qubits) + cache.reduced_target_qubits = list( + reduced_target_qubits[i] for i in self.target_qubits) + + if K.name != "custom": + sorted_qubits = sorted(self.target_qubits) + cache.order = list(sorted_qubits) + s = 1 + self.density_matrix + cache.tensor_shape = K.cast(s * self.nqubits * (2,), dtype='DTYPEINT') + cache.flat_shape = K.cast(s * (2 ** self.nqubits,), dtype='DTYPEINT') + if self.density_matrix: + cache.order.extend((q + self.nqubits for q in sorted_qubits)) + cache.order.extend((q for q in range(self.nqubits) + if q not in sorted_qubits)) + cache.order.extend((q + self.nqubits for q in range(self.nqubits) + if q not in sorted_qubits)) + else: + cache.order.extend((q for q in range(self.nqubits) + if q not in sorted_qubits)) + self._cache = cache + return self._cache def construct_unitary(self): raise_error(ValueError, "Measurement gate does not have unitary " @@ -350,11 +346,11 @@ def __init__(self, q, theta, trainable=True): if self.gate_op: self.gate_op = K.op.apply_z_pow - def reprepare(self): + def calculate_matrix(self): if K.name == "custom": - self.matrix = K.cast(K.qnp.exp(1j * self.parameters)) + return K.cast(K.qnp.exp(1j * self.parameters)) else: - MatrixGate.reprepare(self) + return MatrixGate.calculate_matrix(self) def construct_unitary(self): if isinstance(self.parameters, K.Tensor): # pragma: no cover @@ -438,10 +434,11 @@ def __init__(self, q0, q1, **params): cbase = "C{}".format(self.base.__name__) getattr(gates, cbase).__init__(self, q0, q1, **params) - def reprepare(self): - self.matrix = self.base.construct_unitary(self) + def calculate_matrix(self): + matrix = self.base.construct_unitary(self) if K.name != "custom": - self.matrix = K.reshape(self.control_unitary(self.matrix), 4 * (2,)) + matrix = K.reshape(self.control_unitary(matrix), 4 * (2,)) + return matrix def construct_unitary(self): return MatrixGate.control_unitary(self.base.construct_unitary(self)) @@ -476,8 +473,8 @@ def __init__(self, q0, q1, theta, trainable=True): if self.gate_op: self.gate_op = K.op.apply_z_pow - def reprepare(self): - U1.reprepare(self) + def calculate_matrix(self): + return U1.calculate_matrix(self) class CU2(_CUn_, gates.CU2): @@ -521,14 +518,13 @@ def __init__(self, q0, q1, theta, phi, trainable=True): if self.gate_op: self.gate_op = K.op.apply_fsim - def reprepare(self): + def calculate_matrix(self): if K.name == "custom": theta, phi = self.parameters cos, isin = K.qnp.cos(theta) + 0j, -1j * K.qnp.sin(theta) phase = K.qnp.exp(-1j * phi) - self.matrix = K.cast([cos, isin, isin, cos, phase]) - else: - MatrixGate.reprepare(self) + return K.cast([cos, isin, isin, cos, phase]) + return MatrixGate.calculate_matrix(self) def construct_unitary(self): theta, phi = self.parameters @@ -552,15 +548,14 @@ def __init__(self, q0, q1, unitary, phi, trainable=True): if self.gate_op: self.gate_op = K.op.apply_fsim - def reprepare(self): + def calculate_matrix(self): if K.name == "custom": unitary, phi = self.parameters matrix = K.qnp.zeros(5) matrix[:4] = K.qnp.reshape(unitary, (4,)) matrix[4] = K.qnp.exp(-1j * phi) - self.matrix = K.cast(matrix) - else: - MatrixGate.reprepare(self) + return K.cast(matrix) + return MatrixGate.calculate_matrix(self) def construct_unitary(self): unitary, phi = self.parameters @@ -729,18 +724,6 @@ def construct_unitary(self): raise_error(ValueError, "VariationalLayer gate does not have unitary " "representation.") - def reprepare(self): - matrices, additional_matrix = self._calculate_unitaries() - for unitary, matrix in zip(self.unitaries, matrices): - unitary.parameters = matrix - unitary.reprepare() - if additional_matrix is not None: - self.additional_unitary.parameters = additional_matrix - self.additional_unitary.reprepare() - - def prepare(self): - self.is_prepared = True - def state_vector_call(self, state): for i, unitary in enumerate(self.unitaries): state = unitary(state) @@ -759,9 +742,6 @@ def __init__(self, coefficients): gates.Flatten.__init__(self, coefficients) self.swap_reset = [] - def reprepare(self): - pass - def construct_unitary(self): raise_error(ValueError, "Flatten gate does not have unitary " "representation.") @@ -787,9 +767,6 @@ def density_matrix(self, x): BaseBackendGate.density_matrix.fset(self, x) # pylint: disable=no-member self.callback.density_matrix = x - def reprepare(self): - pass - def construct_unitary(self): raise_error(ValueError, "Callback gate does not have unitary " "representation.") @@ -808,38 +785,38 @@ def __init__(self, *q): BackendGate.__init__(self) gates.PartialTrace.__init__(self, *q) - self.zero_matrix = None - self.einsum_order = None - self.final_order = None - self.einsum_shape = None - self.output_shape = None - self.reduced_shape = None - - def prepare(self): - self.is_prepared = True - qubits = set(self.target_qubits) - # Create |00...0><00...0| for qubits that are traced out - n = len(self.target_qubits) - row0 = K.cast([1] + (2 ** n - 1) * [0], dtype='DTYPECPX') - shape = K.cast((2 ** n - 1, 2 ** n), dtype='DTYPEINT') - rows = K.zeros(shape, dtype='DTYPECPX') - self.zero_matrix = K.concatenate([row0[K.newaxis], rows], axis=0) - self.zero_matrix = K.reshape(self.zero_matrix, 2 * n * (2,)) - # Calculate initial transpose order - order = tuple(sorted(self.target_qubits)) - order += tuple(i for i in range(self.nqubits) if i not in qubits) - order += tuple(i + self.nqubits for i in order) - self.einsum_order = order - # Calculate final transpose order - order1 = tuple(i for i in range(self.nqubits) if i not in qubits) - order2 = tuple(self.target_qubits) - order = (order1 + tuple(i + self.nqubits for i in order1) + - order2 + tuple(i + self.nqubits for i in order2)) - self.final_order = tuple(order.index(i) for i in range(2 * self.nqubits)) - # Shapes - self.einsum_shape = K.cast(2 * (2 ** n, 2 ** (self.nqubits - n)), dtype='DTYPEINT') - self.output_shape = K.cast(2 * (2 ** self.nqubits,), dtype='DTYPEINT') - self.reduced_shape = K.cast(2 * (2 ** (self.nqubits - n),), dtype='DTYPEINT') + class GateCache: + pass + + @property + def cache(self): + if self._cache is None: + cache = self.GateCache() + qubits = set(self.target_qubits) + # Create |00...0><00...0| for qubits that are traced out + n = len(self.target_qubits) + row0 = K.cast([1] + (2 ** n - 1) * [0], dtype='DTYPECPX') + shape = K.cast((2 ** n - 1, 2 ** n), dtype='DTYPEINT') + rows = K.zeros(shape, dtype='DTYPECPX') + cache.zero_matrix = K.concatenate([row0[K.newaxis], rows], axis=0) + cache.zero_matrix = K.reshape(cache.zero_matrix, 2 * n * (2,)) + # Calculate initial transpose order + order = tuple(sorted(self.target_qubits)) + order += tuple(i for i in range(self.nqubits) if i not in qubits) + order += tuple(i + self.nqubits for i in order) + cache.einsum_order = order + # Calculate final transpose order + order1 = tuple(i for i in range(self.nqubits) if i not in qubits) + order2 = tuple(self.target_qubits) + order = (order1 + tuple(i + self.nqubits for i in order1) + + order2 + tuple(i + self.nqubits for i in order2)) + cache.final_order = tuple(order.index(i) for i in range(2 * self.nqubits)) + # Shapes + cache.einsum_shape = K.cast(2 * (2 ** n, 2 ** (self.nqubits - n)), dtype='DTYPEINT') + cache.output_shape = K.cast(2 * (2 ** self.nqubits,), dtype='DTYPEINT') + cache.reduced_shape = K.cast(2 * (2 ** (self.nqubits - n),), dtype='DTYPEINT') + self._cache = cache + return self._cache def construct_unitary(self): raise_error(ValueError, "Partial trace gate does not have unitary " @@ -850,13 +827,13 @@ def state_vector_partial_trace(self, state): state = K.reshape(state, self.nqubits * (2,)) axes = 2 * [list(self.target_qubits)] rho = K.tensordot(state, K.conj(state), axes=axes) - return K.reshape(rho, self.reduced_shape) + return K.reshape(rho, self.cache.reduced_shape) def density_matrix_partial_trace(self, state): self.set_nqubits(state) state = K.reshape(state, 2 * self.nqubits * (2,)) - state = K.transpose(state, self.einsum_order) - state = K.reshape(state, self.einsum_shape) + state = K.transpose(state, self.cache.einsum_order) + state = K.reshape(state, self.cache.einsum_shape) return K.einsum("abac->bc", state) def state_vector_call(self, state): @@ -868,9 +845,9 @@ def density_matrix_call(self, state): substate = self.density_matrix_partial_trace(state) n = self.nqubits - len(self.target_qubits) substate = K.reshape(substate, 2 * n * (2,)) - state = K.tensordot(substate, self.zero_matrix, axes=0) - state = K.transpose(state, self.final_order) - return K.reshape(state, self.output_shape) + state = K.tensordot(substate, self.cache.zero_matrix, axes=0) + state = K.transpose(state, self.cache.final_order) + return K.reshape(state, self.cache.output_shape) class KrausChannel(BackendGate, gates.KrausChannel): @@ -878,33 +855,16 @@ class KrausChannel(BackendGate, gates.KrausChannel): def __init__(self, ops): BackendGate.__init__(self) gates.KrausChannel.__init__(self, ops) - # create inversion gates to rest to the original state vector - # because of the in-place updates used in custom operators - self.inv_gates = tuple() - @staticmethod - def _invert(gate): - """Creates invert gates of each Ak to reset to the original state.""" - matrix = gate.parameters - if isinstance(matrix, K.tensor_types): - inv_matrix = K.qnp.inv(matrix) - return Unitary(inv_matrix, *gate.target_qubits) - - def prepare(self): - self.is_prepared = True + def calculate_inverse_gates(self): inv_gates = [] - for gate in self.gates: - inv_gate = self._invert(gate) - # use a ``set`` for this loop because it may be ``inv_gate == gate`` - for g in {gate, inv_gate}: - if g is not None: - g.density_matrix = self.density_matrix - g.device = self.device - g.nqubits = self.nqubits - g.prepare() - inv_gates.append(inv_gate) - inv_gates[-1] = None - self.inv_gates = tuple(inv_gates) + for gate in self.gates[:-1]: + matrix = gate.parameters + if isinstance(matrix, K.tensor_types): + inv_matrix = K.qnp.inv(matrix) + inv_gates.append(Unitary(inv_matrix, *gate.target_qubits)) + inv_gates.append(None) + return tuple(inv_gates) def construct_unitary(self): raise_error(ValueError, "Channels do not have unitary representation.") @@ -915,7 +875,7 @@ def state_vector_call(self, state): def density_matrix_call(self, state): new_state = K.zeros_like(state) - for gate, inv_gate in zip(self.gates, self.inv_gates): + for gate, inv_gate in zip(self.gates, self.inverse_gates): new_state += gate(state) if inv_gate is not None: inv_gate(state) @@ -928,20 +888,16 @@ def __init__(self, p: List[float], ops: List["Gate"], seed: Optional[int] = None): BackendGate.__init__(self) gates.UnitaryChannel.__init__(self, p, ops, seed=seed) - self.inv_gates = tuple() + self.set_seed() - @staticmethod - def _invert(gate): - return gate.dagger() + def calculate_inverse_gates(self): + inv_gates = tuple(gate.dagger() for gate in self.gates[:-1]) + return inv_gates + (None,) def set_seed(self): if self.seed is not None: K.qnp.random.seed(self.seed) - def prepare(self): - KrausChannel.prepare(self) - self.set_seed() - def state_vector_call(self, state): for p, gate in zip(self.probs, self.gates): if K.qnp.random.random() < p: @@ -950,7 +906,7 @@ def state_vector_call(self, state): def density_matrix_call(self, state): new_state = (1 - self.psum) * state - for p, gate, inv_gate in zip(self.probs, self.gates, self.inv_gates): + for p, gate, inv_gate in zip(self.probs, self.gates, self.inverse_gates): state = gate(state) new_state += p * state if inv_gate is not None: @@ -964,12 +920,10 @@ def __init__(self, q: int, px: float = 0, py: float = 0, pz: float = 0, seed: Optional[int] = None): BackendGate.__init__(self) gates.PauliNoiseChannel.__init__(self, q, px, py, pz, seed=seed) - self.inv_gates = tuple() + self.set_seed() - @staticmethod - def _invert(gate): - # for Pauli gates we can use same gate as inverse for efficiency - return gate + def calculate_inverse_gates(self): + return tuple(self.gates[:-1]) + (None,) class ResetChannel(UnitaryChannel, gates.ResetChannel): @@ -978,13 +932,12 @@ def __init__(self, q: int, p0: float = 0.0, p1: float = 0.0, seed: Optional[int] = None): BackendGate.__init__(self) gates.ResetChannel.__init__(self, q, p0=p0, p1=p1, seed=seed) - self.inv_gates = tuple() + self.set_seed() - @staticmethod - def _invert(gate): - if isinstance(gate, gates.M): - return None - return gate + def calculate_inverse_gates(self): + inv_gates = tuple(gate.dagger() if not isinstance(gate, gates.M) else None + for gate in self.gates[:-1]) + return inv_gates + (None,) def state_vector_call(self, state): not_collapsed = True @@ -999,7 +952,7 @@ def state_vector_call(self, state): def density_matrix_call(self, state): new_state = (1 - self.psum) * state - for p, gate, inv_gate in zip(self.probs, self.gates, self.inv_gates): + for p, gate, inv_gate in zip(self.probs, self.gates, self.inverse_gates): if isinstance(gate, M): state = K.density_matrix_collapse(gate, state, [0]) else: @@ -1045,7 +998,7 @@ def __init__(self, q, t1, t2, time, excited_population=0, seed=None): gates._ThermalRelaxationChannelA.__init__( self, q, t1, t2, time, excited_population=excited_population, seed=seed) - self.inv_gates = tuple() + self.set_seed() def state_vector_call(self, state): if K.qnp.random.random() < self.probs[0]: diff --git a/src/qibo/tensorflow/distutils.py b/src/qibo/tensorflow/distutils.py index 66df7987e5..e625793c81 100644 --- a/src/qibo/tensorflow/distutils.py +++ b/src/qibo/tensorflow/distutils.py @@ -273,7 +273,6 @@ def create(self, queue: List[gates.Gate]): is_collapse = isinstance(gate, gates.M) and gate.collapse if not gate.target_qubits or is_collapse: # special gate gate.nqubits = self.nqubits - gate.prepare() self.special_queue.append(gate) self.queues.append([]) @@ -303,7 +302,6 @@ def create(self, queue: List[gates.Gate]): # device otherwise device parallelization will break devgate.device = device devgate.nqubits = self.nlocal - devgate.prepare() for i in ids: flag = True # If there are control qubits that are global then diff --git a/src/qibo/tests_new/test_core_circuit.py b/src/qibo/tests_new/test_core_circuit.py index f69370cc8b..630f1b5c53 100644 --- a/src/qibo/tests_new/test_core_circuit.py +++ b/src/qibo/tests_new/test_core_circuit.py @@ -23,7 +23,6 @@ def test_set_nqubits(backend): assert c.queue[0].nqubits == 4 gate = gates.H(1) gate.nqubits = 3 - gate.prepare() with pytest.raises(RuntimeError): c.add(gate) qibo.set_backend(original_backend) @@ -84,10 +83,7 @@ def create_circuit(theta = 0.1234): c2.compile() r2 = c2() init_state = c2.get_initial_state() - r3, _ = c2._execute_for_compile(np.reshape(init_state, (2, 2))) - r3 = np.reshape(r3, (4,)) np.testing.assert_allclose(r1, r2) - np.testing.assert_allclose(r1, r3) qibo.set_backend(original_backend) diff --git a/src/qibo/tests_new/test_core_gates_features.py b/src/qibo/tests_new/test_core_gates_features.py index b6a56e0321..5aa5b56ebc 100644 --- a/src/qibo/tests_new/test_core_gates_features.py +++ b/src/qibo/tests_new/test_core_gates_features.py @@ -342,7 +342,6 @@ def test_reset_channel_repeated(backend): target_state = [] collapse = gates.M(2, collapse=True) collapse.nqubits = 5 - collapse.prepare() xgate = gates.X(2) for _ in range(30): state = np.copy(initial_state) @@ -371,7 +370,6 @@ def test_thermal_relaxation_channel_repeated(backend): target_state = [] collapse = gates.M(4, collapse=True) collapse.nqubits = 5 - collapse.prepare() zgate, xgate = gates.Z(4), gates.X(4) for _ in range(30): state = np.copy(initial_state) From 1e1c87aa4e2850ba827532886f6fa296cb3e798f Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Wed, 31 Mar 2021 15:48:32 +0400 Subject: [PATCH 13/23] Fix variational layer --- src/qibo/abstractions/gates.py | 7 +++---- src/qibo/core/gates.py | 20 +++++++++++++------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/qibo/abstractions/gates.py b/src/qibo/abstractions/gates.py index 8560fb367f..1c2e9ed248 100644 --- a/src/qibo/abstractions/gates.py +++ b/src/qibo/abstractions/gates.py @@ -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) @@ -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: diff --git a/src/qibo/core/gates.py b/src/qibo/core/gates.py index 8fa38805a5..4188980b25 100644 --- a/src/qibo/core/gates.py +++ b/src/qibo/core/gates.py @@ -693,16 +693,12 @@ def __init__(self, qubits: List[int], pairs: List[Tuple[int, int]], trainable=trainable, name=name) matrices, additional_matrix = self._calculate_unitaries() - self.unitaries = [] - for targets, matrix in zip(self.pairs, matrices): - unitary = Unitary(matrix, *targets) - self.unitaries.append(unitary) + self.unitaries = [Unitary(matrix, *targets) + for targets, matrix in zip(self.pairs, matrices)] if self.additional_target is not None: self.additional_unitary = Unitary( additional_matrix, self.additional_target) self.additional_unitary.density_matrix = self.density_matrix - else: - self.additional_unitary = None @BaseBackendGate.density_matrix.setter def density_matrix(self, x: bool): @@ -712,6 +708,16 @@ def density_matrix(self, x: bool): if self.additional_unitary is not None: self.additional_unitary.density_matrix = x + @ParametrizedGate.parameters.setter + def parameters(self, x): + gates.VariationalLayer.parameters.fset(self, x) # pylint: disable=no-member + if self.unitaries: + matrices, additional_matrix = self._calculate_unitaries() + for gate, matrix in zip(self.unitaries, matrices): + gate.parameters = matrix + if self.additional_unitary is not None: + self.additional_unitary.parameters = additional_matrix + def _dagger(self): import copy varlayer = copy.copy(self) @@ -722,7 +728,7 @@ def _dagger(self): def construct_unitary(self): raise_error(ValueError, "VariationalLayer gate does not have unitary " - "representation.") + "representation.") def state_vector_call(self, state): for i, unitary in enumerate(self.unitaries): From f412f7ec2076206996ebbdd186f2a07dc5ea7e14 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Wed, 31 Mar 2021 15:59:44 +0400 Subject: [PATCH 14/23] Fix measurements --- src/qibo/core/gates.py | 17 +++++------------ src/qibo/tests_new/test_measurement_gate.py | 7 ------- 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/src/qibo/core/gates.py b/src/qibo/core/gates.py index 4188980b25..7785a40da3 100644 --- a/src/qibo/core/gates.py +++ b/src/qibo/core/gates.py @@ -54,7 +54,8 @@ def calculate_matrix(self): return matrix def set_nqubits(self, state): - self.nqubits = int(math.log2(tuple(state.shape)[0])) + if self._nqubits is None: + self.nqubits = int(math.log2(tuple(state.shape)[0])) def state_vector_call(self, state): return K.state_vector_call(self, state) @@ -155,9 +156,6 @@ def __init__(self, *q, register_name: Optional[str] = None, BackendGate.__init__(self) gates.M.__init__(self, *q, register_name=register_name, collapse=collapse, p0=p0, p1=p1) - self.unmeasured_qubits = None # Tuple - self.reduced_target_qubits = None # List - self.result = None self._result_list = None self._result_tensor = None @@ -166,9 +164,6 @@ def __init__(self, *q, register_name: Optional[str] = None, self.gate_op = K.op.collapse_state self.order = None - def add(self, gate: gates.M): - gates.M.add(self, gate) - @property def cache(self): if self._cache is None: @@ -230,15 +225,13 @@ def result_tensor(self): def measure(self, state, nshots): if isinstance(state, K.tensor_types): - if not self.is_prepared: - self.set_nqubits(state) + self.set_nqubits(state) if self.density_matrix: state = self.states.MatrixState.from_tensor(state) else: state = self.states.VectorState.from_tensor(state) elif isinstance(state, self.states.AbstractState): - if not self.is_prepared: - self.set_nqubits(state.tensor) + self.set_nqubits(state.tensor) else: raise_error(TypeError, "Measurement gate called on state of type " "{} that is not supported." @@ -247,7 +240,7 @@ def measure(self, state, nshots): def calculate_probs(): probs_dim = K.cast((2 ** len(self.target_qubits),), dtype='DTYPEINT') probs = state.probabilities(measurement_gate=self) - probs = K.transpose(probs, axes=self.reduced_target_qubits) + probs = K.transpose(probs, axes=self.cache.reduced_target_qubits) probs = K.reshape(probs, probs_dim) return probs diff --git a/src/qibo/tests_new/test_measurement_gate.py b/src/qibo/tests_new/test_measurement_gate.py index 5907b65201..4cb4022bf9 100644 --- a/src/qibo/tests_new/test_measurement_gate.py +++ b/src/qibo/tests_new/test_measurement_gate.py @@ -53,13 +53,6 @@ def test_measurement_gate_errors(backend): # attempting to construct unitary with pytest.raises(ValueError): matrix = gate.unitary - # add targets after calling - state = np.zeros(4) - state[-1] = 1 - gate = gates.M(1) - result = gate(state, nshots=100) - with pytest.raises(RuntimeError): - gate.add(gates.M(2)) # calling on bad state with pytest.raises(TypeError): gate("test", 100) From 4bf27ac56186297284396e6a0db729a1fff91c4b Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Wed, 31 Mar 2021 16:36:12 +0400 Subject: [PATCH 15/23] Fix density matrices --- src/qibo/abstractions/abstract_gates.py | 13 +++++++++++++ src/qibo/abstractions/gates.py | 1 + src/qibo/tests/test_density_matrix.py | 6 ------ 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/qibo/abstractions/abstract_gates.py b/src/qibo/abstractions/abstract_gates.py index f462e839c9..e83253b14c 100644 --- a/src/qibo/abstractions/abstract_gates.py +++ b/src/qibo/abstractions/abstract_gates.py @@ -304,6 +304,10 @@ def __init__(self): 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: + gate.nqubits = self.nqubits + gate.density_matrix = self.density_matrix return self._inverse_gates @abstractmethod @@ -319,6 +323,15 @@ def nqubits(self, n: int): for gate in self._inverse_gates: 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: + gate.density_matrix = x + def controlled_by(self, *q): """""" raise_error(ValueError, "Noise channel cannot be controlled on qubits.") diff --git a/src/qibo/abstractions/gates.py b/src/qibo/abstractions/gates.py index 1c2e9ed248..12e15d8ef9 100644 --- a/src/qibo/abstractions/gates.py +++ b/src/qibo/abstractions/gates.py @@ -1351,6 +1351,7 @@ 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)) diff --git a/src/qibo/tests/test_density_matrix.py b/src/qibo/tests/test_density_matrix.py index 696857ea7b..ec9a38726d 100644 --- a/src/qibo/tests/test_density_matrix.py +++ b/src/qibo/tests/test_density_matrix.py @@ -526,12 +526,6 @@ def test_entanglement_entropy(backend): def test_density_matrix_circuit_errors(): """Check errors of circuits that simulate density matrices.""" - # Switch `gate.density_matrix` to `True` after setting `nqubits` - gate = gates.X(0) - gate.nqubits = 2 - gate.prepare() - with pytest.raises(RuntimeError): - gate.density_matrix = True # Attempt to distribute density matrix circuit with pytest.raises(NotImplementedError): c = models.Circuit(5, accelerators={"/GPU:0": 2}, density_matrix=True) From f7531d795b759795368e81d9b6d6a7fc045fb3eb Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Wed, 31 Mar 2021 16:53:53 +0400 Subject: [PATCH 16/23] Fix numpy tests --- src/qibo/backends/__init__.py | 2 ++ src/qibo/core/gates.py | 9 ++++++--- src/qibo/tests/test_variational.py | 5 ++--- src/qibo/tests_new/test_backends_init.py | 2 +- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/qibo/backends/__init__.py b/src/qibo/backends/__init__.py index 40682da082..d3209083a9 100644 --- a/src/qibo/backends/__init__.py +++ b/src/qibo/backends/__init__.py @@ -59,6 +59,8 @@ def _construct_backend(name): "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")() diff --git a/src/qibo/core/gates.py b/src/qibo/core/gates.py index 7785a40da3..15677b4714 100644 --- a/src/qibo/core/gates.py +++ b/src/qibo/core/gates.py @@ -124,7 +124,8 @@ class Z(BackendGate, gates.Z): def __init__(self, q): BackendGate.__init__(self) gates.Z.__init__(self, q) - self.gate_op = K.op.apply_z + if self.gate_op: + self.gate_op = K.op.apply_z def construct_unitary(self): return K.matrices.Z @@ -161,7 +162,8 @@ def __init__(self, *q, register_name: Optional[str] = None, self._result_tensor = None if collapse: self.result = self.measurements.MeasurementResult(self.qubits) - self.gate_op = K.op.collapse_state + if self.gate_op: + self.gate_op = K.op.collapse_state self.order = None @property @@ -1016,8 +1018,9 @@ def __init__(self, q, t1, t2, time, excited_population=0, seed=None): gates._ThermalRelaxationChannelB.__init__( self, q, t1, t2, time, excited_population=excited_population, seed=seed) - self.gate_op = K.op.apply_two_qubit_gate self._qubits_tensor = None + if self.gate_op: + self.gate_op = K.op.apply_two_qubit_gate # TODO: Remove this property and `target_qubits_dm` @property diff --git a/src/qibo/tests/test_variational.py b/src/qibo/tests/test_variational.py index 60f1df24b8..502eeb42b9 100644 --- a/src/qibo/tests/test_variational.py +++ b/src/qibo/tests/test_variational.py @@ -61,7 +61,8 @@ def myloss(parameters, circuit, target): ("parallel_L-BFGS-B", {'maxiter': 1}, False, None), ("cma", {"maxfevals": 2}, False, None), ("sgd", {"nepochs": 5}, False, None), - ("sgd", {"nepochs": 5}, True, None)] + #("sgd", {"nepochs": 5}, True, None) + ] @pytest.mark.parametrize(test_names, test_values) def test_vqe(method, options, compile, filename): """Performs a VQE circuit minimization test.""" @@ -81,8 +82,6 @@ def test_vqe(method, options, compile, filename): original_threads = get_threads() nqubits = 6 layers = 4 - print(qibo.get_backend()) - circuit = Circuit(nqubits) for l in range(layers): for q in range(nqubits): diff --git a/src/qibo/tests_new/test_backends_init.py b/src/qibo/tests_new/test_backends_init.py index 6f94f25463..106f993117 100644 --- a/src/qibo/tests_new/test_backends_init.py +++ b/src/qibo/tests_new/test_backends_init.py @@ -35,7 +35,7 @@ def test_set_backend_errors(): backends.set_backend("numpy_badgates") h = gates.H(0) with pytest.warns(RuntimeWarning): - backends.set_backend("numpy_defaulteinsum") + backends.set_backend("numpy_matmuleinsum") backends.set_backend(original_backend) From 5790aad9fd3272f4854753e408c1143264f17352 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Wed, 31 Mar 2021 17:10:54 +0400 Subject: [PATCH 17/23] Fix thermal relaxation channel --- src/qibo/backends/numpy.py | 8 +------- src/qibo/core/gates.py | 34 +++++++++++++++++++++------------- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/src/qibo/backends/numpy.py b/src/qibo/backends/numpy.py index ae6bc17851..8f2717e258 100644 --- a/src/qibo/backends/numpy.py +++ b/src/qibo/backends/numpy.py @@ -277,13 +277,7 @@ def create_gate_cache(self, gate): cache.calculation_cache = self.create_einsum_cache( targets, nactive, ncontrol) else: - from qibo.abstractions.gates import _ThermalRelaxationChannelB - if isinstance(gate, _ThermalRelaxationChannelB): - # TODO: Handle this case otherwise - qubits = gate.qubits + tuple(q + gate.nqubits for q in gate.qubits) - cache.calculation_cache = self.create_einsum_cache(qubits, 2 * gate.nqubits) - else: - cache.calculation_cache = self.create_einsum_cache(gate.qubits, gate.nqubits) + cache.calculation_cache = self.create_einsum_cache(gate.qubits, gate.nqubits) cache.calculation_cache.cast_shapes( lambda x: self.cast(x, dtype='DTYPEINT')) return cache diff --git a/src/qibo/core/gates.py b/src/qibo/core/gates.py index 15677b4714..5ffb7eb7c4 100644 --- a/src/qibo/core/gates.py +++ b/src/qibo/core/gates.py @@ -2,7 +2,7 @@ # @authors: S. Efthymiou import sys import math -from qibo import K, get_threads +from qibo import K from qibo.abstractions import gates from qibo.abstractions.abstract_gates import BaseBackendGate, ParametrizedGate from qibo.config import raise_error @@ -111,11 +111,11 @@ def construct_unitary(self): def _custom_density_matrix_call(self, state): state = self.gate_op(state, self.cache.qubits_tensor + self.nqubits, 2 * self.nqubits, *self.target_qubits, - get_threads()) + K.get_threads()) matrix = K.conj(K.matrices.Y) state = K.op.apply_gate(state, matrix, self.cache.qubits_tensor, 2 * self.nqubits, *self.cache.target_qubits_dm, - get_threads()) + K.get_threads()) return state @@ -1022,17 +1022,22 @@ def __init__(self, q, t1, t2, time, excited_population=0, seed=None): if self.gate_op: self.gate_op = K.op.apply_two_qubit_gate - # TODO: Remove this property and `target_qubits_dm` @property - def qubits_tensor(self): - if self._qubits_tensor is None: + def cache(self): + if self._cache is None: + cache = K.create_gate_cache(self) + qubits = sorted(self.nqubits - q - 1 for q in self.target_qubits) - self._qubits_tensor = qubits + [q + self.nqubits for q in qubits] - return self._qubits_tensor + cache.qubits_tensor = qubits + [q + self.nqubits for q in qubits] + cache.target_qubits_dm = self.qubits + tuple(q + self.nqubits for q in self.qubits) - @property - def target_qubits_dm(self): - return self.target_qubits + tuple(q + self.nqubits for q in self.target_qubits) + if K.name != "custom": + cache.calculation_cache = K.create_einsum_cache( + cache.target_qubits_dm, 2 * self.nqubits) + + self._cache = cache + + return self._cache def construct_unitary(self): matrix = K.qnp.diag([1 - self.preset1, self.exp_t2, self.exp_t2, @@ -1046,5 +1051,8 @@ def state_vector_call(self, state): "state vectors when T1 < T2.") def density_matrix_call(self, state): - return self.gate_op(state, self.matrix, self.qubits_tensor, - 2 * self.nqubits, *self.target_qubits_dm, get_threads()) + if K.name == "custom": + return self.gate_op(state, self.matrix, self.cache.qubits_tensor, + 2 * self.nqubits, *self.cache.target_qubits_dm, + K.get_threads()) + return K.state_vector_call(self, state) From 72c37f2f1874a46d74dc666d42904f84d93d5171 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Wed, 31 Mar 2021 19:09:34 +0400 Subject: [PATCH 18/23] Fix backprop tests --- src/qibo/backends/abstract.py | 1 + src/qibo/backends/numpy.py | 1 + src/qibo/backends/tensorflow.py | 1 + src/qibo/core/gates.py | 32 ++++++++++++------- .../test_core_circuit_backpropagation.py | 4 +-- 5 files changed, 26 insertions(+), 13 deletions(-) diff --git a/src/qibo/backends/abstract.py b/src/qibo/backends/abstract.py index 4d2939ef42..ebac943e35 100644 --- a/src/qibo/backends/abstract.py +++ b/src/qibo/backends/abstract.py @@ -21,6 +21,7 @@ def __init__(self): self.matrices = None self.numeric_types = None self.tensor_types = None + self.native_types = None self.Tensor = None self.random = None self.newaxis = None diff --git a/src/qibo/backends/numpy.py b/src/qibo/backends/numpy.py index 8f2717e258..433c2ac3e0 100644 --- a/src/qibo/backends/numpy.py +++ b/src/qibo/backends/numpy.py @@ -18,6 +18,7 @@ def __init__(self): np.int64, np.float32, np.float64, np.complex64, np.complex128) self.tensor_types = (np.ndarray,) + self.native_types = (np.ndarray,) self.Tensor = np.ndarray self.random = np.random self.newaxis = np.newaxis diff --git a/src/qibo/backends/tensorflow.py b/src/qibo/backends/tensorflow.py index e2bcd9fe65..efd36a3825 100644 --- a/src/qibo/backends/tensorflow.py +++ b/src/qibo/backends/tensorflow.py @@ -36,6 +36,7 @@ def __init__(self): self.matrices = matrices.TensorflowMatrices(self.dtypes('DTYPECPX')) self.tensor_types = (self.np.ndarray, tf.Tensor, tf.Variable) + self.native_types = (tf.Tensor, tf.Variable) self.Tensor = tf.Tensor self.random = tf.random self.newaxis = tf.newaxis diff --git a/src/qibo/core/gates.py b/src/qibo/core/gates.py index 5ffb7eb7c4..1c64d1adec 100644 --- a/src/qibo/core/gates.py +++ b/src/qibo/core/gates.py @@ -294,8 +294,9 @@ def __init__(self, q, theta, trainable=True): def construct_unitary(self): theta = self.parameters - if isinstance(theta, K.Tensor): # pragma: no cover + if isinstance(theta, K.native_types): # pragma: no cover p = K + theta = K.cast(theta) else: p = K.qnp cos, isin = p.cos(theta / 2.0) + 0j, -1j * p.sin(theta / 2.0) @@ -310,8 +311,9 @@ def __init__(self, q, theta, trainable=True): def construct_unitary(self): theta = self.parameters - if isinstance(theta, K.Tensor): + if isinstance(theta, K.native_types): p = K + theta = K.cast(theta) else: p = K.qnp cos, sin = p.cos(theta / 2.0), p.sin(theta / 2.0) @@ -325,11 +327,17 @@ def __init__(self, q, theta, trainable=True): gates.RZ.__init__(self, q, theta, trainable) def construct_unitary(self): - if isinstance(self.parameters, K.Tensor): # pragma: no cover + print(self.parameters) + if isinstance(self.parameters, K.native_types): # pragma: no cover p = K + theta = K.cast(self.parameters) + print(theta) else: p = K.qnp - phase = p.exp(1j * self.parameters / 2.0) + theta = self.parameters + print(theta) + print(p) + phase = p.exp(1j * theta / 2.0) return K.cast(p.diag([p.conj(phase), phase])) @@ -348,11 +356,13 @@ def calculate_matrix(self): return MatrixGate.calculate_matrix(self) def construct_unitary(self): - if isinstance(self.parameters, K.Tensor): # pragma: no cover + if isinstance(self.parameters, K.native_types): # pragma: no cover p = K + theta = K.cast(self.parameters) else: p = K.qnp - return p.diag([1, p.exp(1j * self.parameters)]) + theta = self.parameters + return p.diag([1, p.exp(1j * theta)]) class U2(MatrixGate, gates.U2): @@ -363,7 +373,7 @@ def __init__(self, q, phi, lam, trainable=True): def construct_unitary(self): phi, lam = self.parameters - if isinstance(phi, K.Tensor) or isinstance(lam, K.Tensor): # pragma: no cover + if isinstance(phi, K.native_types) or isinstance(lam, K.native_types): # pragma: no cover p = K else: p = K.qnp @@ -381,7 +391,7 @@ def __init__(self, q, theta, phi, lam, trainable=True): def construct_unitary(self): theta, phi, lam = self.parameters - if isinstance(theta, K.Tensor) or isinstance(phi, K.Tensor) or isinstance(lam, K.Tensor): # pragma: no cover + if isinstance(theta, K.native_types) or isinstance(phi, K.native_types) or isinstance(lam, K.native_types): # pragma: no cover p = K else: p = K.qnp @@ -523,7 +533,7 @@ def calculate_matrix(self): def construct_unitary(self): theta, phi = self.parameters - if isinstance(theta, K.Tensor) or isinstance(phi, K.Tensor): # pragma: no cover + if isinstance(theta, K.native_types) or isinstance(phi, K.native_types): # pragma: no cover p = K else: p = K.qnp @@ -554,7 +564,7 @@ def calculate_matrix(self): def construct_unitary(self): unitary, phi = self.parameters - if isinstance(unitary, K.Tensor) or isinstance(phi, K.Tensor): # pragma: no cover + if isinstance(unitary, K.native_types) or isinstance(phi, K.native_types): # pragma: no cover p = K else: p = K.qnp @@ -565,7 +575,7 @@ def construct_unitary(self): def _dagger(self) -> "GenerelizedfSim": unitary, phi = self.parameters - if isinstance(unitary, K.Tensor): + if isinstance(unitary, K.native_types): ud = K.conj(K.transpose(unitary)) else: ud = unitary.conj().T diff --git a/src/qibo/tests_new/test_core_circuit_backpropagation.py b/src/qibo/tests_new/test_core_circuit_backpropagation.py index f6e96186e5..740e932bbc 100644 --- a/src/qibo/tests_new/test_core_circuit_backpropagation.py +++ b/src/qibo/tests_new/test_core_circuit_backpropagation.py @@ -11,7 +11,7 @@ def test_variable_backpropagation(backend): pytest.skip("Custom gates do not support automatic differentiation.") original_backend = qibo.get_backend() qibo.set_backend(backend) - if K.name != "tensorflow": + if "numpy" in K.name: qibo.set_backend(original_backend) pytest.skip("Backpropagation is not supported by {}.".format(K.name)) @@ -38,7 +38,7 @@ def test_two_variables_backpropagation(backend): pytest.skip("Custom gates do not support automatic differentiation.") original_backend = qibo.get_backend() qibo.set_backend(backend) - if K.name != "tensorflow": + if "numpy" in K.name: qibo.set_backend(original_backend) pytest.skip("Backpropagation is not supported by {}.".format(K.name)) From b5f601f770e4fa8301fbc1837a0df6679a7a30d9 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Wed, 31 Mar 2021 19:14:11 +0400 Subject: [PATCH 19/23] Fix backend coverage --- src/qibo/backends/tensorflow.py | 7 +++++-- src/qibo/tests_new/test_backends_init.py | 9 +++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/qibo/backends/tensorflow.py b/src/qibo/backends/tensorflow.py index efd36a3825..9a6f32346a 100644 --- a/src/qibo/backends/tensorflow.py +++ b/src/qibo/backends/tensorflow.py @@ -194,8 +194,11 @@ class TensorflowCustomBackend(TensorflowBackend): def __init__(self): from qibo.tensorflow import custom_operators as op - if not op._custom_operators_loaded: - raise_error(RuntimeError) + if not op._custom_operators_loaded: # pragma: no cover + # CI can compile custom operators so this case is not tested + raise_error(RuntimeError, "Cannot initialize Tensorflow custom " + "backend if custom operators are not " + "compiled.") super().__init__() self.name = "custom" diff --git a/src/qibo/tests_new/test_backends_init.py b/src/qibo/tests_new/test_backends_init.py index 106f993117..a137148439 100644 --- a/src/qibo/tests_new/test_backends_init.py +++ b/src/qibo/tests_new/test_backends_init.py @@ -16,6 +16,15 @@ def test_set_backend(backend): """Check ``set_backend`` for switching gate backends.""" original_backend = backends.get_backend() backends.set_backend(backend) + if backend == "defaulteinsum": + target_name = "tensorflow_defaulteinsum" + elif backend == "matmuleinsum": + target_name = "tensorflow_matmuleinsum" + else: + target_name = backend + assert K.name == target_name + assert str(K) == target_name + assert K.executing_eagerly() h = gates.H(0) if backend == "custom": assert K.custom_einsum is None From 2ccdea5d7f72a10ce09fdb254eeeffa929ae87ed Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Thu, 1 Apr 2021 16:19:21 +0400 Subject: [PATCH 20/23] Fix channel gate setters --- src/qibo/abstractions/abstract_gates.py | 9 ++++++--- src/qibo/core/states.py | 11 ++++------ .../test_core_gates_density_matrix.py | 20 +++++++++++++++++++ 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/src/qibo/abstractions/abstract_gates.py b/src/qibo/abstractions/abstract_gates.py index e83253b14c..18422d2448 100644 --- a/src/qibo/abstractions/abstract_gates.py +++ b/src/qibo/abstractions/abstract_gates.py @@ -306,7 +306,8 @@ def inverse_gates(self): self._inverse_gates = self.calculate_inverse_gates() for gate in self._inverse_gates: if gate is not None: - gate.nqubits = self.nqubits + if self._nqubits is not None: + gate.nqubits = self._nqubits gate.density_matrix = self.density_matrix return self._inverse_gates @@ -321,7 +322,8 @@ def nqubits(self, n: int): gate.nqubits = n if self._inverse_gates is not None: for gate in self._inverse_gates: - gate.nqubits = n + if gate is not None: + gate.nqubits = n @Gate.density_matrix.setter def density_matrix(self, x): @@ -330,7 +332,8 @@ def density_matrix(self, x): gate.density_matrix = x if self._inverse_gates is not None: for gate in self._inverse_gates: - gate.density_matrix = x + if gate is not None: + gate.density_matrix = x def controlled_by(self, *q): """""" diff --git a/src/qibo/core/states.py b/src/qibo/core/states.py index 57c660f0a0..3367fbaa82 100644 --- a/src/qibo/core/states.py +++ b/src/qibo/core/states.py @@ -19,13 +19,10 @@ def tensor(self, x): if not isinstance(x, K.tensor_types): raise_error(TypeError, "Initial state type {} is not recognized." "".format(type(x))) - try: - shape = tuple(x.shape) - if self._nqubits is None: - self.nqubits = int(K.np.log2(shape[0])) - except ValueError: # happens when using TensorFlow compiled mode - shape = None - if shape is not None and shape != self.shape: + shape = tuple(x.shape) + if self._nqubits is None: + self.nqubits = int(K.np.log2(shape[0])) + if shape != self.shape: raise_error(ValueError, "Invalid tensor shape {} for state of {} " "qubits.".format(shape, self.nqubits)) self._tensor = K.cast(x) diff --git a/src/qibo/tests_new/test_core_gates_density_matrix.py b/src/qibo/tests_new/test_core_gates_density_matrix.py index 11ccf39a6c..2924928764 100644 --- a/src/qibo/tests_new/test_core_gates_density_matrix.py +++ b/src/qibo/tests_new/test_core_gates_density_matrix.py @@ -294,3 +294,23 @@ def test_partial_trace_gate_errors(backend): with pytest.raises(RuntimeError): gate(state) qibo.set_backend(original_backend) + + +def test_channel_gate_setters(backend): + original_backend = qibo.get_backend() + qibo.set_backend(backend) + a1 = np.sqrt(0.4) * np.array([[0, 1], [1, 0]]) + a2 = np.sqrt(0.6) * np.array([[1, 0, 0, 0], [0, 1, 0, 0], + [0, 0, 0, 1], [0, 0, 1, 0]]) + channel = gates.KrausChannel([((1,), a1), ((0, 1), a2)]) + _ = channel.inverse_gates # create inverse gates + channel.nqubits = 5 + channel.density_matrix = True + for gate in channel.gates: + assert gate.nqubits == 5 + assert gate.density_matrix + for gate in channel.inverse_gates: + if gate is not None: + assert gate.nqubits == 5 + assert gate.density_matrix + qibo.set_backend(original_backend) From b317210e1d4869b135367fe5dbd1ef4d89d6484f Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Thu, 1 Apr 2021 17:35:12 +0400 Subject: [PATCH 21/23] Fix compilation issue --- src/qibo/optimizers.py | 1 + src/qibo/tests/test_variational.py | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/qibo/optimizers.py b/src/qibo/optimizers.py index f3658022ec..9f831dbc6e 100644 --- a/src/qibo/optimizers.py +++ b/src/qibo/optimizers.py @@ -193,6 +193,7 @@ def opt_step(): return l if compile: + loss = K.compile(loss) opt_step = K.compile(opt_step) for e in range(sgd_options["nepochs"]): diff --git a/src/qibo/tests/test_variational.py b/src/qibo/tests/test_variational.py index 502eeb42b9..c68946fcc0 100644 --- a/src/qibo/tests/test_variational.py +++ b/src/qibo/tests/test_variational.py @@ -61,8 +61,7 @@ def myloss(parameters, circuit, target): ("parallel_L-BFGS-B", {'maxiter': 1}, False, None), ("cma", {"maxfevals": 2}, False, None), ("sgd", {"nepochs": 5}, False, None), - #("sgd", {"nepochs": 5}, True, None) - ] + ("sgd", {"nepochs": 5}, True, None)] @pytest.mark.parametrize(test_names, test_values) def test_vqe(method, options, compile, filename): """Performs a VQE circuit minimization test.""" From 355f89d606771e954f115d7538365d26ee28df45 Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Thu, 1 Apr 2021 17:54:15 +0400 Subject: [PATCH 22/23] Add docstrings in abstract backend --- src/qibo/backends/abstract.py | 75 ++++++++++++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/src/qibo/backends/abstract.py b/src/qibo/backends/abstract.py index ebac943e35..c47c934ec8 100644 --- a/src/qibo/backends/abstract.py +++ b/src/qibo/backends/abstract.py @@ -385,37 +385,110 @@ def device(self, device_name): # pragma: no cover raise_error(NotImplementedError) def executing_eagerly(self): + """Checks if we are in eager or compiled mode. + + Relevant for the Tensorflow backends only. + """ return True @abstractmethod def set_seed(self, seed): # pragma: no cover + """Sets the seed for random number generation. + + Args: + seed (int): Integer to use as seed. + """ raise_error(NotImplementedError) - # TODO: Add docstrings here @abstractmethod def create_gate_cache(self, gate): # pragma: no cover + """Calculates data required for applying gates to states. + + These can be einsum index strings or tensors of qubit ids and it + depends on the underlying backend. + + Args: + gate (:class:`qibo.abstractions.abstract_gates.BackendGate`): Gate + object to calculate its cache. + + Returns: + Custom cache object that holds all the required data as + attributes. + """ raise_error(NotImplementedError) @abstractmethod def state_vector_call(self, gate, state): # pragma: no cover + """Applies gate to state vector. + + Args: + gate (:class:`qibo.abstractions.abstract_gates.BackendGate`): Gate + object to apply to state. + state (Tensor): State vector as a ``Tensor`` supported by this + backend. + + Returns: + State vector after applying the gate as a ``Tensor``. + """ raise_error(NotImplementedError) @abstractmethod def state_vector_matrix_call(self, gate, state): # pragma: no cover + """Applies gate to state vector using the gate's unitary matrix representation. + + This method is useful for the ``custom`` backend for which some gates + do not require the unitary matrix. + + Args: + gate (:class:`qibo.abstractions.abstract_gates.BackendGate`): Gate + object to apply to state. + state (Tensor): State vector as a ``Tensor`` supported by this + backend. + + Returns: + State vector after applying the gate as a ``Tensor``. + """ raise_error(NotImplementedError) @abstractmethod def density_matrix_call(self, gate, state): # pragma: no cover + """Applies gate to density matrix. + + Args: + gate (:class:`qibo.abstractions.abstract_gates.BackendGate`): Gate + object to apply to state. + state (Tensor): Density matrix as a ``Tensor`` supported by this + backend. + + Returns: + Density matrix after applying the gate as a ``Tensor``. + """ raise_error(NotImplementedError) @abstractmethod def density_matrix_matrix_call(self, gate, state): # pragma: no cover + """Applies gate to density matrix using the gate's unitary matrix representation. + + This method is useful for the ``custom`` backend for which some gates + do not require the unitary matrix. + + Args: + gate (:class:`qibo.abstractions.abstract_gates.BackendGate`): Gate + object to apply to state. + state (Tensor): Density matrix as a ``Tensor`` supported by this + backend. + + Returns: + Density matrix after applying the gate as a ``Tensor``. + """ raise_error(NotImplementedError) @abstractmethod def state_vector_collapse(self, gate, state, result): # pragma: no cover + """Collapses state vector to a given result.""" raise_error(NotImplementedError) @abstractmethod def density_matrix_collapse(self, gate, state, result): # pragma: no cover + """Collapses density matrix to a given result.""" raise_error(NotImplementedError) From 429f2fd6e8956c9a689a05b071fe513c0be85f3f Mon Sep 17 00:00:00 2001 From: Stavros Efthymiou <35475381+stavros11@users.noreply.github.com> Date: Thu, 1 Apr 2021 18:02:29 +0400 Subject: [PATCH 23/23] Fix actual backend in benchmark examples --- examples/benchmarks/main.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/examples/benchmarks/main.py b/examples/benchmarks/main.py index 74f4a4ba4f..119f4ad308 100644 --- a/examples/benchmarks/main.py +++ b/examples/benchmarks/main.py @@ -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()