diff --git a/src/qibo/hamiltonians/__init__.py b/src/qibo/hamiltonians/__init__.py index 35c2673e32..f91e1e8bc0 100644 --- a/src/qibo/hamiltonians/__init__.py +++ b/src/qibo/hamiltonians/__init__.py @@ -1,6 +1,2 @@ -from qibo.hamiltonians.hamiltonians import ( - Hamiltonian, - SymbolicHamiltonian, - TrotterHamiltonian, -) +from qibo.hamiltonians.hamiltonians import Hamiltonian, SymbolicHamiltonian from qibo.hamiltonians.models import TFIM, XXX, XXZ, Heisenberg, MaxCut, X, Y, Z diff --git a/src/qibo/hamiltonians/hamiltonians.py b/src/qibo/hamiltonians/hamiltonians.py index 5bc78f5d30..1a87905b8c 100644 --- a/src/qibo/hamiltonians/hamiltonians.py +++ b/src/qibo/hamiltonians/hamiltonians.py @@ -1,5 +1,6 @@ """Module defining Hamiltonian classes.""" +from functools import cache, cached_property, reduce from itertools import chain from math import prod from typing import Optional @@ -7,6 +8,7 @@ import numpy as np import sympy +from qibo.backends import Backend, _check_backend from qibo.config import EINSUM_CHARS, log, raise_error from qibo.hamiltonians.abstract import AbstractHamiltonian from qibo.symbols import Z @@ -68,34 +70,6 @@ def matrix(self, mat): ) self._matrix = mat - @classmethod - def from_symbolic(cls, symbolic_hamiltonian, symbol_map, backend=None): - """Creates a :class:`qibo.hamiltonian.Hamiltonian` from a symbolic Hamiltonian. - - We refer to :ref:`How to define custom Hamiltonians using symbols? ` - for more details. - - Args: - symbolic_hamiltonian (sympy.Expr): full Hamiltonian written with ``sympy`` symbols. - symbol_map (dict): Dictionary that maps each symbol that appears in - the Hamiltonian to a pair ``(target, matrix)``. - backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used - in the execution. If ``None``, it uses the current backend. - Defaults to ``None``. - - Returns: - :class:`qibo.hamiltonians.SymbolicHamiltonian`: object that implements the - Hamiltonian represented by the given symbolic expression. - """ - log.warning( - "`Hamiltonian.from_symbolic` and the use of symbol maps is " - "deprecated. Please use `SymbolicHamiltonian` and Qibo symbols " - "to construct Hamiltonians using symbols." - ) - return SymbolicHamiltonian( - symbolic_hamiltonian, symbol_map=symbol_map, backend=backend - ) - def eigenvalues(self, k=6): if self._eigenvalues is None: self._eigenvalues = self.backend.calculate_eigenvalues(self.matrix, k) @@ -206,7 +180,9 @@ def __add__(self, o): "Only hamiltonians with the same number of qubits can be added.", ) new_matrix = self.matrix + o.matrix - elif isinstance(o, self.backend.numeric_types): + elif isinstance(o, self.backend.numeric_types) or isinstance( + o, self.backend.tensor_types + ): new_matrix = self.matrix + o * self.eye() else: raise_error( @@ -309,65 +285,62 @@ class SymbolicHamiltonian(AbstractHamiltonian): Hamiltonian should be written using Qibo symbols. See :ref:`How to define custom Hamiltonians using symbols? ` example for more details. - symbol_map (dict): Dictionary that maps each ``sympy.Symbol`` to a tuple - of (target qubit, matrix representation). This feature is kept for - compatibility with older versions where Qibo symbols were not available - and may be deprecated in the future. - It is not required if the Hamiltonian is constructed using Qibo symbols. - The symbol_map can also be used to pass non-quantum operator arguments - to the symbolic Hamiltonian, such as the parameters in the - :meth:`qibo.hamiltonians.models.MaxCut` Hamiltonian. backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used in the execution. If ``None``, it uses the current backend. Defaults to ``None``. """ - def __init__(self, form=None, nqubits=None, symbol_map={}, backend=None): + def __init__( + self, + form: sympy.Expr, + nqubits: Optional[int] = None, + backend: Optional[Backend] = None, + ): super().__init__() - self._form = None - self._terms = None + if not isinstance(form, sympy.Expr): + raise_error( + TypeError, + f"The ``form`` of a ``SymbolicHamiltonian`` has to be a ``sympy.Expr``, but a ``{type(form)}`` was passed.", + ) + self._form = form self.constant = 0 # used only when we perform calculations using ``_terms`` - self._dense = None - self.symbol_map = symbol_map - # if a symbol in the given form is not a Qibo symbol it must be - # included in the ``symbol_map`` from qibo.symbols import Symbol # pylint: disable=import-outside-toplevel self._qiboSymbol = Symbol # also used in ``self._get_symbol_matrix`` - from qibo.backends import _check_backend - self.backend = _check_backend(backend) - if form is not None: - self.form = form - if nqubits is not None: - self.nqubits = nqubits + self.nqubits = ( + self._calculate_nqubits_from_form(form) if nqubits is None else nqubits + ) - @property + @cached_property def dense(self) -> "MatrixHamiltonian": """Creates the equivalent Hamiltonian matrix.""" - if self._dense is None: - log.warning( - "Calculating the dense form of a symbolic Hamiltonian. " - "This operation is memory inefficient." - ) - self.dense = self.calculate_dense() - return self._dense - - @dense.setter - def dense(self, hamiltonian): - assert isinstance(hamiltonian, Hamiltonian) - self._dense = hamiltonian - self._eigenvalues = hamiltonian._eigenvalues - self._eigenvectors = hamiltonian._eigenvectors - self._exp = hamiltonian._exp + return self.calculate_dense() @property def form(self): return self._form + def _calculate_nqubits_from_form(self, form): + """Calculate number of qubits in the system described by the given + Hamiltonian formula + """ + nqubits = 0 + for symbol in form.free_symbols: + if isinstance(symbol, self._qiboSymbol): + q = symbol.target_qubit + else: + raise_error( + RuntimeError, + f"Symbol {symbol} is not a ``qibo.symbols.Symbol``, you can define a custom symbol for {symbol} by subclassing ``qibo.symbols.Symbol``.", + ) + if q > nqubits: + nqubits = q + return nqubits + 1 + @form.setter def form(self, form): # Check that given form is a ``sympy`` expression @@ -376,53 +349,31 @@ def form(self, form): TypeError, f"Symbolic Hamiltonian should be a ``sympy`` expression but is {type(form)}.", ) - # Calculate number of qubits in the system described by the given - # Hamiltonian formula - nqubits = 0 - for symbol in form.free_symbols: - if isinstance(symbol, self._qiboSymbol): - q = symbol.target_qubit - elif isinstance(symbol, sympy.Expr): - if symbol not in self.symbol_map: - raise_error(ValueError, f"Symbol {symbol} is not in symbol map.") - q, matrix = self.symbol_map.get(symbol) - if not isinstance(matrix, self.backend.tensor_types): - # ignore symbols that do not correspond to quantum operators - # for example parameters in the MaxCut Hamiltonian - q = 0 - if q > nqubits: - nqubits = q - self._form = form - self.nqubits = nqubits + 1 + self.nqubits = self._calculate_nqubits_from_form(form) - @property + @cached_property def terms(self): """List of terms of which the Hamiltonian is a sum of. Terms will be objects of type :class:`qibo.core.terms.HamiltonianTerm`. """ - if self._terms is None: - # Calculate terms based on ``self.form`` - from qibo.hamiltonians.terms import ( # pylint: disable=import-outside-toplevel - SymbolicTerm, - ) + # Calculate terms based on ``self.form`` + from qibo.hamiltonians.terms import ( # pylint: disable=import-outside-toplevel + SymbolicTerm, + ) - form = sympy.expand(self.form) - terms = [] - for f, c in form.as_coefficients_dict().items(): - term = SymbolicTerm(c, f, self.symbol_map) - if term.target_qubits: - terms.append(term) - else: - self.constant += term.coefficient - self._terms = terms - return self._terms - - @terms.setter - def terms(self, terms): - self._terms = terms - self.nqubits = max(q for term in self._terms for q in term.target_qubits) + 1 + self.constant = 0.0 + + form = sympy.expand(self.form) + terms = [] + for f, c in form.as_coefficients_dict().items(): + term = SymbolicTerm(c, f, backend=self.backend) + if term.target_qubits: + terms.append(term) + else: + self.constant += term.coefficient + return terms @property def matrix(self): @@ -444,6 +395,7 @@ def ground_state(self): def exp(self, a): return self.dense.exp(a) + @cache def _get_symbol_matrix(self, term): """Calculates numerical matrix corresponding to symbolic expression. @@ -472,42 +424,34 @@ def _get_symbol_matrix(self, term): # note that we need to use matrix multiplication even though # we use scalar symbols for convenience factors = term.as_ordered_factors() - result = self._get_symbol_matrix(factors[0]) - for subterm in factors[1:]: - result = result @ self._get_symbol_matrix(subterm) + result = reduce( + self.backend.np.matmul, + [self._get_symbol_matrix(subterm) for subterm in factors], + ) elif isinstance(term, sympy.Pow): # symbolic op for power base, exponent = term.as_base_exp() matrix = self._get_symbol_matrix(base) - # multiply ``base`` matrix ``exponent`` times to itself - result = matrix - for _ in range(exponent - 1): - result = result @ matrix - - elif isinstance(term, sympy.Symbol): - # if the term is a ``Symbol`` then it corresponds to a quantum - # operator for which we can construct the full matrix directly - if isinstance(term, self._qiboSymbol): - # if we have a Qibo symbol the matrix construction is - # implemented in :meth:`qibo.core.terms.SymbolicTerm.full_matrix`. - result = term.full_matrix(self.nqubits) - else: - q, matrix = self.symbol_map.get(term) - if not isinstance(matrix, self.backend.tensor_types): - # symbols that do not correspond to quantum operators - # for example parameters in the MaxCut Hamiltonian - result = complex(matrix) * np.eye(2**self.nqubits) - else: - # if we do not have a Qibo symbol we construct one and use - # :meth:`qibo.core.terms.SymbolicTerm.full_matrix`. - result = self._qiboSymbol(q, matrix).full_matrix(self.nqubits) + matrix_power = ( + np.linalg.matrix_power + if self.backend.name == "tensorflow" + else self.backend.np.linalg.matrix_power + ) + result = matrix_power(matrix, int(exponent)) + + elif isinstance(term, self._qiboSymbol): + # if we have a Qibo symbol the matrix construction is + # implemented in :meth:`qibo.core.terms.SymbolicTerm.full_matrix`. + # I have to force the symbol's backend + term.backend = self.backend + result = term.full_matrix(self.nqubits) elif term.is_number: # if the term is number we should return in the form of identity # matrix because in expressions like `1 + Z`, `1` is not correspond # to the float 1 but the identity operator (matrix) - result = complex(term) * np.eye(2**self.nqubits) + result = complex(term) * self.backend.np.eye(2**self.nqubits, dtype=float) else: raise_error( @@ -525,32 +469,46 @@ def _calculate_dense_from_form(self) -> Hamiltonian: matrix = self._get_symbol_matrix(self.form) return Hamiltonian(self.nqubits, matrix, backend=self.backend) + """ def _calculate_dense_from_terms(self) -> Hamiltonian: - """Calculates equivalent Hamiltonian using the term representation.""" - if 2 * self.nqubits > len(EINSUM_CHARS): # pragma: no cover - # case not tested because it only happens in large examples - raise_error(NotImplementedError, "Not enough einsum characters.") - + "Calculates equivalent Hamiltonian using the term representation." matrix = 0 - chars = EINSUM_CHARS[: 2 * self.nqubits] + indices = list(range(2 * self.nqubits)) + einsum = ( + np.einsum + if self.backend.platform == "tensorflow" + else self.backend.np.einsum + ) for term in self.terms: ntargets = len(term.target_qubits) - tmat = np.reshape(term.matrix, 2 * ntargets * (2,)) + tmat = self.backend.np.reshape(term.matrix, 2 * ntargets * (2,)) n = self.nqubits - ntargets - emat = np.reshape(np.eye(2**n, dtype=tmat.dtype), 2 * n * (2,)) - gen = lambda x: (chars[i + x] for i in term.target_qubits) - tc = "".join(chain(gen(0), gen(self.nqubits))) - ec = "".join(c for c in chars if c not in tc) - matrix += np.einsum(f"{tc},{ec}->{chars}", tmat, emat) - matrix = np.reshape(matrix, 2 * (2**self.nqubits,)) + emat = self.backend.np.reshape( + self.backend.np.eye(2**n, dtype=tmat.dtype), 2 * n * (2,) + ) + gen = lambda x: (indices[i + x] for i in term.target_qubits) + tc = list(chain(gen(0), gen(self.nqubits))) + ec = list(c for c in indices if c not in tc) + matrix += einsum(tmat, tc, emat, ec, indices) + + matrix = ( + self.backend.np.reshape(matrix, 2 * (2**self.nqubits,)) + if len(self.terms) > 0 + else self.backend.np.zeros(2 * (2**self.nqubits,)) + ) return Hamiltonian(self.nqubits, matrix, backend=self.backend) + self.constant + """ - def calculate_dense(self): - if self._terms is None: - # calculate dense matrix directly using the form to avoid the - # costly ``sympy.expand`` call - return self._calculate_dense_from_form() - return self._calculate_dense_from_terms() + def calculate_dense(self) -> Hamiltonian: + log.warning( + "Calculating the dense form of a symbolic Hamiltonian. " + "This operation is memory inefficient." + ) + # calculate dense matrix directly using the form to avoid the + # costly ``sympy.expand`` call + # if len(self.terms) > 40: + return self._calculate_dense_from_form() + # return self._calculate_dense_from_terms() def expectation(self, state, normalize=False): return Hamiltonian.expectation(self, state, normalize) @@ -663,120 +621,40 @@ def expectation_from_samples(self, freq: dict, qubit_map: list = None) -> float: ) return self.backend.np.sum(expvals @ counts.T) + self.constant.real - def __add__(self, o): + def _compose(self, o, operator): + form = self._form + if isinstance(o, self.__class__): if self.nqubits != o.nqubits: raise_error( RuntimeError, - "Only hamiltonians with the same number of qubits can be added.", + "Only hamiltonians with the same number of qubits can be composed.", ) - new_ham = self.__class__( - symbol_map=dict(self.symbol_map), backend=self.backend - ) - if self._form is not None and o._form is not None: - new_ham.form = self.form + o.form - new_ham.symbol_map.update(o.symbol_map) - if self._terms is not None and o._terms is not None: - new_ham.terms = self.terms + o.terms - new_ham.constant = self.constant + o.constant - if self._dense is not None and o._dense is not None: - new_ham.dense = self.dense + o.dense - elif isinstance(o, self.backend.numeric_types): - new_ham = self.__class__( - symbol_map=dict(self.symbol_map), backend=self.backend - ) - if self._form is not None: - new_ham.form = self.form + o - if self._terms is not None: - new_ham.terms = self.terms - new_ham.constant = self.constant + o - if self._dense is not None: - new_ham.dense = self.dense + o + if o._form is not None: + form = operator(form, o._form) if form is not None else o._form + elif isinstance(o, (self.backend.numeric_types, self.backend.tensor_types)): + form = operator(form, complex(o)) if form is not None else complex(o) else: raise_error( NotImplementedError, - f"SymbolicHamiltonian addition to {type(o)} not implemented.", + f"SymbolicHamiltonian composition to {type(o)} not implemented.", ) - return new_ham - def __sub__(self, o): - if isinstance(o, self.__class__): - if self.nqubits != o.nqubits: - raise_error( - RuntimeError, - "Only hamiltonians with the same number of qubits can be subtracted.", - ) - new_ham = self.__class__( - symbol_map=dict(self.symbol_map), backend=self.backend - ) - if self._form is not None and o._form is not None: - new_ham.form = self.form - o.form - new_ham.symbol_map.update(o.symbol_map) - if self._terms is not None and o._terms is not None: - new_ham.terms = self.terms + [-1 * x for x in o.terms] - new_ham.constant = self.constant - o.constant - if self._dense is not None and o._dense is not None: - new_ham.dense = self.dense - o.dense + return self.__class__(form=form, nqubits=self.nqubits, backend=self.backend) - elif isinstance(o, self.backend.numeric_types): - new_ham = self.__class__( - symbol_map=dict(self.symbol_map), backend=self.backend - ) - if self._form is not None: - new_ham.form = self.form - o - if self._terms is not None: - new_ham.terms = self.terms - new_ham.constant = self.constant - o - if self._dense is not None: - new_ham.dense = self.dense - o + def __add__(self, o): + return self._compose(o, lambda x, y: x + y) - else: - raise_error( - NotImplementedError, - f"Hamiltonian subtraction to {type(o)} " "not implemented.", - ) - return new_ham + def __sub__(self, o): + return self._compose(o, lambda x, y: x - y) def __rsub__(self, o): - if isinstance(o, self.backend.numeric_types): - new_ham = self.__class__( - symbol_map=dict(self.symbol_map), backend=self.backend - ) - if self._form is not None: - new_ham.form = o - self.form - if self._terms is not None: - new_ham.terms = [-1 * x for x in self.terms] - new_ham.constant = o - self.constant - if self._dense is not None: - new_ham.dense = o - self.dense - else: - raise_error( - NotImplementedError, - f"Hamiltonian subtraction to {type(o)} not implemented.", - ) - return new_ham + return self._compose(o, lambda x, y: y - x) def __mul__(self, o): - if not isinstance(o, (self.backend.numeric_types, self.backend.tensor_types)): - raise_error( - NotImplementedError, - f"Hamiltonian multiplication to {type(o)} not implemented.", - ) - o = complex(o) - new_ham = self.__class__(symbol_map=dict(self.symbol_map), backend=self.backend) - if self._form is not None: - new_ham.form = o * self.form - if self._terms is not None: - new_ham.terms = [o * x for x in self.terms] - new_ham.constant = self.constant * o - if self._dense is not None: - new_ham.dense = o * self._dense - - new_ham.nqubits = self.nqubits - - return new_ham + return self._compose(o, lambda x, y: y * x) def apply_gates(self, state, density_matrix=False): """Applies gates corresponding to the Hamiltonian terms. @@ -800,21 +678,7 @@ def apply_gates(self, state, density_matrix=False): def __matmul__(self, o): """Matrix multiplication with other Hamiltonians or state vectors.""" if isinstance(o, self.__class__): - if self._form is None or o._form is None: - raise_error( - NotImplementedError, - "Multiplication of symbolic Hamiltonians " - "without symbolic form is not implemented.", - ) - new_form = self.form * o.form - new_symbol_map = dict(self.symbol_map) - new_symbol_map.update(o.symbol_map) - new_ham = self.__class__( - new_form, symbol_map=new_symbol_map, backend=self.backend - ) - if self._dense is not None and o._dense is not None: - new_ham.dense = self.dense @ o.dense - return new_ham + return o * self if isinstance(o, self.backend.tensor_types): rank = len(tuple(o.shape)) @@ -860,19 +724,3 @@ def circuit(self, dt, accelerators=None): ) return circuit - - -class TrotterHamiltonian: - """""" - - def __init__(self, *parts): - raise_error( - NotImplementedError, - "`TrotterHamiltonian` is substituted by `SymbolicHamiltonian` " - + "and is no longer supported. Please check the documentation " - + "of `SymbolicHamiltonian` for more details.", - ) - - @classmethod - def from_symbolic(cls, symbolic_hamiltonian, symbol_map): - return cls() diff --git a/src/qibo/hamiltonians/models.py b/src/qibo/hamiltonians/models.py index 61fa918021..1c25fb731a 100644 --- a/src/qibo/hamiltonians/models.py +++ b/src/qibo/hamiltonians/models.py @@ -1,9 +1,10 @@ from functools import reduce -from typing import Union +from typing import Optional, Union import numpy as np -from qibo.backends import _check_backend, matrices +from qibo import symbols +from qibo.backends import Backend, _check_backend, matrices from qibo.config import raise_error from qibo.hamiltonians.hamiltonians import Hamiltonian, SymbolicHamiltonian from qibo.hamiltonians.terms import HamiltonianTerm @@ -25,7 +26,7 @@ def X(nqubits, dense: bool = True, backend=None): in the execution. If ``None``, it uses the current backend. Defaults to ``None``. """ - return _OneBodyPauli(nqubits, matrices.X, dense, backend=backend) + return _OneBodyPauli(nqubits, symbols.X, dense, backend=backend) def Y(nqubits, dense: bool = True, backend=None): @@ -43,7 +44,7 @@ def Y(nqubits, dense: bool = True, backend=None): in the execution. If ``None``, it uses the current backend. Defaults to ``None``. """ - return _OneBodyPauli(nqubits, matrices.Y, dense, backend=backend) + return _OneBodyPauli(nqubits, symbols.Y, dense, backend=backend) def Z(nqubits, dense: bool = True, backend=None): @@ -61,7 +62,7 @@ def Z(nqubits, dense: bool = True, backend=None): in the execution. If ``None``, it uses the current backend. Defaults to ``None``. """ - return _OneBodyPauli(nqubits, matrices.Z, dense, backend=backend) + return _OneBodyPauli(nqubits, symbols.Z, dense, backend=backend) def TFIM(nqubits, h: float = 0.0, dense: bool = True, backend=None): @@ -80,25 +81,31 @@ def TFIM(nqubits, h: float = 0.0, dense: bool = True, backend=None): """ if nqubits < 2: raise_error(ValueError, "Number of qubits must be larger than one.") + backend = _check_backend(backend) if dense: condition = lambda i, j: i in {j % nqubits, (j + 1) % nqubits} - ham = -_build_spin_model(nqubits, matrices.Z, condition) + ham = -_build_spin_model(nqubits, backend.matrices.Z, condition, backend) if h != 0: condition = lambda i, j: i == j % nqubits - ham -= h * _build_spin_model(nqubits, matrices.X, condition) + ham -= h * _build_spin_model( + nqubits, backend.matrices.X, condition, backend + ) return Hamiltonian(nqubits, ham, backend=backend) - matrix = -( - _multikron([matrices.Z, matrices.Z]) + h * _multikron([matrices.X, matrices.I]) - ) - terms = [HamiltonianTerm(matrix, i, i + 1) for i in range(nqubits - 1)] - terms.append(HamiltonianTerm(matrix, nqubits - 1, 0)) - ham = SymbolicHamiltonian(backend=backend) - ham.terms = terms + term = lambda q1, q2: symbols.Z(q1, backend=backend) * symbols.Z( + q2, backend=backend + ) + h * symbols.X(q1, backend=backend) + form = -1 * sum(term(i, i + 1) for i in range(nqubits - 1)) - term(nqubits - 1, 0) + ham = SymbolicHamiltonian(form=form, nqubits=nqubits, backend=backend) return ham -def MaxCut(nqubits, dense: bool = True, backend=None): +def MaxCut( + nqubits, + dense: bool = True, + adj_matrix: Optional[Union[list[list[float]], np.ndarray]] = None, + backend: Optional[Backend] = None, +): """Max Cut Hamiltonian. .. math:: @@ -109,26 +116,29 @@ def MaxCut(nqubits, dense: bool = True, backend=None): dense (bool): If ``True`` it creates the Hamiltonian as a :class:`qibo.core.hamiltonians.Hamiltonian`, otherwise it creates a :class:`qibo.core.hamiltonians.SymbolicHamiltonian`. + adj_matrix (list[list[float]] | np.ndarray): Adjecency matrix of the graph. Defaults to a + homogeneous fully connected graph with all edges having an equal 1.0 weight. backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used in the execution. If ``None``, it uses the current backend. Defaults to ``None``. """ - import sympy as sp + if adj_matrix is None: + adj_matrix = np.ones((nqubits, nqubits)) + elif len(adj_matrix) != nqubits: + raise_error( + RuntimeError, + f"Expected an adjacency matrix of shape ({nqubits},{nqubits}) for a {nqubits}-qubits system.", + ) - Z = sp.symbols(f"Z:{nqubits}") - V = sp.symbols(f"V:{nqubits**2}") - sham = -sum( - V[i * nqubits + j] * (1 - Z[i] * Z[j]) + form = -sum( + adj_matrix[i][j] + * (1 - symbols.Z(i, backend=backend) * symbols.Z(j, backend=backend)) for i in range(nqubits) for j in range(nqubits) ) - sham /= 2 + form /= 2 - v = np.ones(nqubits**2) - smap = {s: (i, matrices.Z) for i, s in enumerate(Z)} - smap.update({s: (i, v[i]) for i, s in enumerate(V)}) - - ham = SymbolicHamiltonian(sham, symbol_map=smap, backend=backend) + ham = SymbolicHamiltonian(form, nqubits=nqubits, backend=backend) if dense: return ham.dense return ham @@ -203,14 +213,16 @@ def Heisenberg( backend = _check_backend(backend) - paulis = [matrices.X, matrices.Y, matrices.Z] + paulis = (symbols.X, symbols.Y, symbols.Z) if dense: condition = lambda i, j: i in {j % nqubits, (j + 1) % nqubits} matrix = np.zeros((2**nqubits, 2**nqubits), dtype=complex) matrix = backend.cast(matrix, dtype=matrix.dtype) for ind, pauli in enumerate(paulis): - double_term = _build_spin_model(nqubits, pauli, condition) + double_term = _build_spin_model( + nqubits, pauli(0, backend=backend).matrix, condition, backend + ) double_term = backend.cast(double_term, dtype=double_term.dtype) matrix = matrix - coupling_constants[ind] * double_term matrix = ( @@ -221,31 +233,24 @@ def Heisenberg( return Hamiltonian(nqubits, matrix, backend=backend) - hx = _multikron([matrices.X, matrices.X]) - hy = _multikron([matrices.Y, matrices.Y]) - hz = _multikron([matrices.Z, matrices.Z]) - - matrix = ( - -coupling_constants[0] * hx - - coupling_constants[1] * hy - - coupling_constants[2] * hz - ) + def h(symbol): + return lambda q1, q2: symbol(q1, backend=backend) * symbol(q2, backend=backend) - terms = [HamiltonianTerm(matrix, i, i + 1) for i in range(nqubits - 1)] - terms.append(HamiltonianTerm(matrix, nqubits - 1, 0)) + def term(q1, q2): + return sum( + coeff * h(operator)(q1, q2) + for coeff, operator in zip(coupling_constants, paulis) + ) - terms.extend( - [ - -field_strength * HamiltonianTerm(pauli, qubit) - for qubit in range(nqubits) - for field_strength, pauli in zip(external_field_strengths, paulis) - if field_strength != 0.0 - ] + form = -1 * sum(term(i, i + 1) for i in range(nqubits - 1)) - term(nqubits - 1, 0) + form -= sum( + field_strength * pauli(qubit) + for qubit in range(nqubits) + for field_strength, pauli in zip(external_field_strengths, paulis) + if field_strength != 0.0 ) - ham = SymbolicHamiltonian(backend=backend) - ham.terms = terms - + ham = SymbolicHamiltonian(form=form, backend=backend) return ham @@ -342,7 +347,7 @@ def XXZ(nqubits, delta=0.5, dense: bool = True, backend=None): return Heisenberg(nqubits, [-1, -1, -delta], 0, dense=dense, backend=backend) -def _multikron(matrix_list): +def _multikron(matrix_list, backend): """Calculates Kronecker product of a list of matrices. Args: @@ -351,28 +356,90 @@ def _multikron(matrix_list): Returns: ndarray: Kronecker product of all matrices in ``matrix_list``. """ - return reduce(np.kron, matrix_list) + # TO DO: check whether this scales better on gpu + """ + indices = list(range(2 * len(matrix_list))) + even, odd = indices[::2], indices[1::2] + lhs = zip(even, odd) + rhs = even + odd + einsum_args = [item for pair in zip(matrix_list, lhs) for item in pair] + dim = 2 ** len(matrix_list) + if backend.platform != "tensorflow": + h = backend.np.einsum(*einsum_args, rhs) + else: + h = np.einsum(*einsum_args, rhs) + h = backend.np.sum(backend.np.reshape(h, (-1, dim, dim)), axis=0) + return h + """ + # reduce appears to be faster especially when matrix_list is long + return reduce(backend.np.kron, matrix_list) -def _build_spin_model(nqubits, matrix, condition): +def _build_spin_model(nqubits, matrix, condition, backend): """Helper method for building nearest-neighbor spin model Hamiltonians.""" h = sum( - _multikron(matrix if condition(i, j) else matrices.I for j in range(nqubits)) + reduce( + backend.np.kron, + ( + matrix if condition(i, j) else backend.matrices.I() + for j in range(nqubits) + ), + ) for i in range(nqubits) ) + """ + indices = list(range(2 * nqubits)) + even, odd = indices[::2], indices[1::2] + lhs = zip( + nqubits + * [ + len(indices), + ], + even, + odd, + ) + rhs = ( + [ + len(indices), + ] + + even + + odd + ) + columns = [ + backend.np.reshape( + backend.np.concatenate( + [ + matrix if condition(i, j) else backend.matrices.I() + for i in range(nqubits) + ], + axis=0, + ), + (nqubits, 2, 2), + ) + for j in range(nqubits) + ] + einsum_args = [item for pair in zip(columns, lhs) for item in pair] + dim = 2**nqubits + if backend.platform == "tensorflow": + h = np.einsum(*einsum_args, rhs) + else: + h = backend.np.einsum(*einsum_args, rhs, optimize=True) + h = backend.np.sum(backend.np.reshape(h, (nqubits, dim, dim)), axis=0) + """ return h -def _OneBodyPauli(nqubits, matrix, dense: bool = True, backend=None): - """Helper method for constracting non-interacting +def _OneBodyPauli(nqubits, operator, dense: bool = True, backend=None): + """Helper method for constructing non-interacting :math:`X`, :math:`Y`, and :math:`Z` Hamiltonians.""" + backend = _check_backend(backend) if dense: condition = lambda i, j: i == j % nqubits - ham = -_build_spin_model(nqubits, matrix, condition) + ham = -_build_spin_model( + nqubits, operator(0, backend=backend).matrix, condition, backend + ) return Hamiltonian(nqubits, ham, backend=backend) - matrix = -matrix - terms = [HamiltonianTerm(matrix, i) for i in range(nqubits)] - ham = SymbolicHamiltonian(backend=backend) - ham.terms = terms + form = sum([-1 * operator(i, backend=backend) for i in range(nqubits)]) + ham = SymbolicHamiltonian(form=form, backend=backend) return ham diff --git a/src/qibo/hamiltonians/terms.py b/src/qibo/hamiltonians/terms.py index c46557abeb..a1d9015938 100644 --- a/src/qibo/hamiltonians/terms.py +++ b/src/qibo/hamiltonians/terms.py @@ -1,7 +1,11 @@ +from functools import cached_property, reduce +from typing import Optional + import numpy as np import sympy from qibo import gates, symbols +from qibo.backends import Backend, _check_backend from qibo.config import raise_error from qibo.symbols import I, X, Y, Z @@ -21,14 +25,15 @@ class HamiltonianTerm: q (list): List of target qubit ids. """ - def __init__(self, matrix, *q): + def __init__(self, matrix, *q, backend: Optional[Backend] = None): + self.backend = _check_backend(backend) for qi in q: if qi < 0: raise_error( ValueError, f"Invalid qubit id {qi} < 0 was given in Hamiltonian term.", ) - if not isinstance(matrix, np.ndarray): + if not isinstance(matrix, self.backend.tensor_types): raise_error(TypeError, f"Invalid type {type(matrix)} of symbol matrix.") dim = int(matrix.shape[0]) if 2 ** len(q) != dim: @@ -78,8 +83,10 @@ def merge(self, term): "Cannot merge HamiltonianTerm acting on " + f"qubits {term.target_qubits} to term on qubits {self.target_qubits}.", ) - matrix = np.kron(term.matrix, np.eye(2 ** (len(self) - len(term)))) - matrix = np.reshape(matrix, 2 * len(self) * (2,)) + matrix = self.backend.np.kron( + term.matrix, self.backend.np.eye(2 ** (len(self) - len(term))) + ) + matrix = self.backend.np.reshape(matrix, 2 * len(self) * (2,)) order = [] i = len(term) for qubit in self.target_qubits: @@ -89,15 +96,19 @@ def merge(self, term): order.append(i) i += 1 order.extend([x + len(order) for x in order]) - matrix = np.transpose(matrix, order) - matrix = np.reshape(matrix, 2 * (2 ** len(self),)) - return HamiltonianTerm(self.matrix + matrix, *self.target_qubits) + matrix = self.backend.np.transpose(matrix, order) + matrix = self.backend.np.reshape(matrix, 2 * (2 ** len(self),)) + return HamiltonianTerm( + self.matrix + matrix, *self.target_qubits, backend=self.backend + ) def __len__(self): return len(self.target_qubits) def __mul__(self, x): - return HamiltonianTerm(x * self.matrix, *self.target_qubits) + return HamiltonianTerm( + x * self.matrix, *self.target_qubits, backend=self.backend + ) def __rmul__(self, x): return self.__mul__(x) @@ -128,18 +139,13 @@ class SymbolicTerm(HamiltonianTerm): coefficient (complex): Complex number coefficient of the underlying term in the Hamiltonian. factors (sympy.Expr): Sympy expression for the underlying term. - symbol_map (dict): Dictionary that maps symbols in the given ``factors`` - expression to tuples of (target qubit id, matrix). - This is required only if the expression is not created using Qibo - symbols and to keep compatibility with older versions where Qibo - symbols were not available. """ - def __init__(self, coefficient, factors=1, symbol_map={}): - self.coefficient = complex(coefficient) - self._matrix = None + def __init__(self, coefficient, factors=1, backend: Optional[Backend] = None): self._gate = None self.hamiltonian = None + self.backend = _check_backend(backend) + self.coefficient = complex(coefficient) # List of :class:`qibo.symbols.Symbol` that represent the term factors self.factors = [] @@ -166,16 +172,12 @@ def __init__(self, coefficient, factors=1, symbol_map={}): else: pow = 1 - # if the user is using ``symbol_map`` instead of qibo symbols, - # create the corresponding symbols - if factor in symbol_map: - from qibo.symbols import Symbol - - q, matrix = symbol_map.get(factor) - factor = Symbol(q, matrix, name=factor.name) - if isinstance(factor, sympy.Symbol): - if isinstance(factor.matrix, np.ndarray): + # forces the backend of the factor + # this way it is not necessary to explicitely define the + # backend of a symbol, i.e. Z(q, backend=backend) + factor.backend = self.backend + if isinstance(factor.matrix, self.backend.tensor_types): self.factors.extend(pow * [factor]) q = factor.target_qubit # if pow > 1 the matrix should be multiplied multiple @@ -199,7 +201,7 @@ def __init__(self, coefficient, factors=1, symbol_map={}): self.target_qubits = tuple(sorted(self.matrix_map.keys())) - @property + @cached_property def matrix(self): """Calculates the full matrix corresponding to this term. @@ -208,27 +210,46 @@ def matrix(self): where ``ntargets`` is the number of qubits included in the factors of this term. """ - if self._matrix is None: - - def matrices_product(matrices): - """Product of matrices that act on the same tuple of qubits. - - Args: - matrices (list): List of matrices to multiply, as exists in - the values of ``SymbolicTerm.matrix_map``. - """ - if len(matrices) == 1: - return matrices[0] - matrix = np.copy(matrices[0]) - for m in matrices[1:]: - matrix = matrix @ m - return matrix - - self._matrix = self.coefficient - for q in self.target_qubits: - matrix = matrices_product(self.matrix_map.get(q)) - self._matrix = np.kron(self._matrix, matrix) - return self._matrix + # from qibo.hamiltonians.models import _multikron + + """ + einsum = ( + self.backend.np.einsum + if self.backend.platform != "tensorflow" + else np.einsum + ) + + # find the max number of matrices for each qubit + max_len = max(len(v) for v in self.matrix_map.values()) + nqubits = len(self.matrix_map) + # pad each list with identity to max_len + matrix = [] + for qubit, matrices in self.matrix_map.items(): + matrix.append( + self.backend.np.concatenate( + self.matrix_map[qubit] + + (max_len - len(matrices)) * [self.backend.np.eye(2)], + axis=0, + ) + ) + # separate in `max_len`-column tensors of shape (`nqubits`, 2, 2) + matrix = self.backend.np.transpose( + self.backend.np.reshape( + self.backend.np.concatenate(matrix, axis=0), (nqubits, max_len, 2, 2) + ), + (1, 0, 2, 3), + ) + indices = list(zip(max_len * [0], range(1, max_len + 1), range(2, max_len + 2))) + lhs = zip(matrix, indices) + lhs = [el for item in lhs for el in item] + matrix = einsum(*lhs, (0, 1, max_len + 1)) + return self.coefficient * _multikron(matrix, self.backend) + """ + matrices = [ + reduce(self.backend.np.matmul, self.matrix_map.get(q)) + for q in self.target_qubits + ] + return complex(self.coefficient) * reduce(self.backend.np.kron, matrices) def copy(self): """Creates a shallow copy of the term with the same attributes.""" @@ -242,8 +263,6 @@ def __mul__(self, x): """Multiplication of scalar to the Hamiltonian term.""" new = self.copy() new.coefficient *= x - if self._matrix is not None: - new._matrix = x * self._matrix return new def __call__(self, backend, state, nqubits, density_matrix=False): diff --git a/src/qibo/models/tsp.py b/src/qibo/models/tsp.py index 7fcfc92439..c50a9dd71a 100644 --- a/src/qibo/models/tsp.py +++ b/src/qibo/models/tsp.py @@ -20,8 +20,8 @@ def tsp_phaser(distance_matrix, backend=None): if u != v: form += ( distance_matrix[u, v] - * Z(int(two_to_one[u, i])) - * Z(int(two_to_one[v, (i + 1) % num_cities])) + * Z(int(two_to_one[u, i]), backend=backend) + * Z(int(two_to_one[v, (i + 1) % num_cities]), backend=backend) ) ham = SymbolicHamiltonian(form, backend=backend) return ham @@ -29,8 +29,12 @@ def tsp_phaser(distance_matrix, backend=None): def tsp_mixer(num_cities, backend=None): two_to_one = calculate_two_to_one(num_cities) - splus = lambda u, i: X(int(two_to_one[u, i])) + 1j * Y(int(two_to_one[u, i])) - sminus = lambda u, i: X(int(two_to_one[u, i])) - 1j * Y(int(two_to_one[u, i])) + splus = lambda u, i: X(int(two_to_one[u, i]), backend=backend) + 1j * Y( + int(two_to_one[u, i]), backend=backend + ) + sminus = lambda u, i: X(int(two_to_one[u, i]), backend=backend) - 1j * Y( + int(two_to_one[u, i]), backend=backend + ) form = 0 for i in range(num_cities): for u in range(num_cities): diff --git a/src/qibo/symbols.py b/src/qibo/symbols.py index edb03eb677..c5b7ef289d 100644 --- a/src/qibo/symbols.py +++ b/src/qibo/symbols.py @@ -1,8 +1,10 @@ +from typing import Optional + import numpy as np import sympy from qibo import gates -from qibo.backends import matrices +from qibo.backends import Backend, _check_backend, get_backend, matrices from qibo.config import raise_error @@ -37,17 +39,25 @@ class Symbol(sympy.Symbol): (for example when the Hamiltonian consists of Z terms only). """ - def __new__(cls, q, matrix=None, name="Symbol", commutative=False, **assumptions): + def __new__(cls, q, matrix, name="Symbol", commutative=False, **assumptions): name = f"{name}{q}" assumptions["commutative"] = commutative return super().__new__(cls=cls, name=name, **assumptions) - def __init__(self, q, matrix=None, name="Symbol", commutative=False): + def __init__( + self, + q, + matrix, + name="Symbol", + commutative=False, + backend: Optional[Backend] = None, + ): self.target_qubit = q + self.backend = _check_backend(backend) self._gate = None if not ( - matrix is None - or isinstance(matrix, np.ndarray) + isinstance(matrix, np.ndarray) + or isinstance(matrix, self.backend.tensor_types) or isinstance( matrix, ( @@ -64,7 +74,7 @@ def __init__(self, q, matrix=None, name="Symbol", commutative=False): ) ): raise_error(TypeError, f"Invalid type {type(matrix)} of symbol matrix.") - self.matrix = matrix + self._matrix = matrix def __getstate__(self): return { @@ -78,6 +88,7 @@ def __setstate__(self, data): self.matrix = data.get("matrix") self.name = data.get("name") self._gate = None + self.backend = get_backend() @property def gate(self): @@ -89,11 +100,20 @@ def gate(self): def calculate_gate(self): # pragma: no cover return gates.Unitary(self.matrix, self.target_qubit) + @property + def matrix(self): + return self.backend.cast(self._matrix) + + @matrix.setter + def matrix(self, matrix): + self._matrix = matrix + def full_matrix(self, nqubits): """Calculates the full dense matrix corresponding to the symbol as part of a bigger system. Args: nqubits (int): Total number of qubits in the system. + backend (Backend): Optional backend to represent the matrix with. By default the global backend is used. Returns: Matrix of dimension (2^nqubits, 2^nqubits) composed of the Kronecker @@ -101,11 +121,11 @@ def full_matrix(self, nqubits): """ from qibo.hamiltonians.models import _multikron - matrix_list = self.target_qubit * [matrices.I] + matrix_list = self.target_qubit * [self.backend.matrices.I()] matrix_list.append(self.matrix) n = nqubits - self.target_qubit - 1 - matrix_list.extend(matrices.I for _ in range(n)) - return _multikron(matrix_list) + matrix_list.extend(self.backend.matrices.I() for _ in range(n)) + return _multikron(matrix_list, backend=self.backend) class PauliSymbol(Symbol): @@ -113,10 +133,10 @@ def __new__(cls, q, commutative=False, **assumptions): matrix = getattr(matrices, cls.__name__) return super().__new__(cls, q, matrix, cls.__name__, commutative, **assumptions) - def __init__(self, q, commutative=False): + def __init__(self, q, commutative=False, backend: Optional[Backend] = None): name = self.__class__.__name__ matrix = getattr(matrices, name) - super().__init__(q, matrix, name, commutative) + super().__init__(q, matrix, name, commutative, backend=backend) def calculate_gate(self): name = self.__class__.__name__ diff --git a/tests/test_hamiltonians.py b/tests/test_hamiltonians.py index 8806bd94bb..33fa884767 100644 --- a/tests/test_hamiltonians.py +++ b/tests/test_hamiltonians.py @@ -341,14 +341,14 @@ def test_hamiltonian_eigenvalues(backend, dtype, sparse_type, dense): c1 = dtype(2.5) H2 = c1 * H1 - H2_eigen = sorted(backend.to_numpy(H2._eigenvalues)) + H2_eigen = sorted(backend.to_numpy(H2.eigenvalues())) hH2_eigen = sorted(backend.to_numpy(backend.calculate_eigenvalues(c1 * H1.matrix))) backend.assert_allclose(H2_eigen, hH2_eigen) c2 = dtype(-11.1) H3 = H1 * c2 if sparse_type is None: - H3_eigen = sorted(backend.to_numpy(H3._eigenvalues)) + H3_eigen = sorted(backend.to_numpy(H3.eigenvalues())) hH3_eigen = sorted( backend.to_numpy(backend.calculate_eigenvalues(H1.matrix * c2)) ) @@ -369,20 +369,20 @@ def test_hamiltonian_eigenvectors(backend, dtype, dense): c1 = dtype(2.5) H2 = c1 * H1 - V2 = backend.to_numpy(H2._eigenvectors) - U2 = backend.to_numpy(H2._eigenvalues) + V2 = backend.to_numpy(H2.eigenvectors()) + U2 = backend.to_numpy(H2.eigenvalues()) backend.assert_allclose(H2.matrix, V2 @ np.diag(U2) @ V2.T) c2 = dtype(-11.1) H3 = H1 * c2 V3 = backend.to_numpy(H3.eigenvectors()) - U3 = backend.to_numpy(H3._eigenvalues) + U3 = backend.to_numpy(H3.eigenvalues()) backend.assert_allclose(H3.matrix, V3 @ np.diag(U3) @ V3.T) c3 = dtype(0) H4 = c3 * H1 - V4 = backend.to_numpy(H4._eigenvectors) - U4 = backend.to_numpy(H4._eigenvalues) + V4 = backend.to_numpy(H4.eigenvectors()) + U4 = backend.to_numpy(H4.eigenvalues()) backend.assert_allclose(H4.matrix, V4 @ np.diag(U4) @ V4.T) diff --git a/tests/test_hamiltonians_from_symbols.py b/tests/test_hamiltonians_from_symbols.py index 4e45fec904..d3aecd88d0 100644 --- a/tests/test_hamiltonians_from_symbols.py +++ b/tests/test_hamiltonians_from_symbols.py @@ -23,59 +23,34 @@ def test_symbols_pickling(symbol): @pytest.mark.parametrize("nqubits", [4, 5]) -@pytest.mark.parametrize("hamtype", ["normal", "symbolic"]) -@pytest.mark.parametrize("calcterms", [False, True]) -def test_tfim_hamiltonian_from_symbols(backend, nqubits, hamtype, calcterms): +@pytest.mark.parametrize("hamtype", ["symbolic"]) +def test_tfim_hamiltonian_from_symbols(backend, nqubits, hamtype): """Check creating TFIM Hamiltonian using sympy.""" - if hamtype == "symbolic": - h = 0.5 - symham = sum(Z(i) * Z(i + 1) for i in range(nqubits - 1)) - symham += Z(0) * Z(nqubits - 1) - symham += h * sum(X(i) for i in range(nqubits)) - ham = hamiltonians.SymbolicHamiltonian(-symham, backend=backend) - else: - h = 0.5 - z_symbols = sympy.symbols(" ".join(f"Z{i}" for i in range(nqubits))) - x_symbols = sympy.symbols(" ".join(f"X{i}" for i in range(nqubits))) - - symham = sum(z_symbols[i] * z_symbols[i + 1] for i in range(nqubits - 1)) - symham += z_symbols[0] * z_symbols[-1] - symham += h * sum(x_symbols) - symmap = {z: (i, matrices.Z) for i, z in enumerate(z_symbols)} - symmap.update({x: (i, matrices.X) for i, x in enumerate(x_symbols)}) - ham = hamiltonians.Hamiltonian.from_symbolic(-symham, symmap, backend=backend) - - if calcterms: - _ = ham.terms + h = 0.5 + symham = sum( + Z(i, backend=backend) * Z(i + 1, backend=backend) for i in range(nqubits - 1) + ) + symham += Z(0, backend=backend) * Z(nqubits - 1, backend=backend) + symham += h * sum(X(i, backend=backend) for i in range(nqubits)) + ham = hamiltonians.SymbolicHamiltonian(-symham, backend=backend) final_matrix = ham.matrix target_matrix = hamiltonians.TFIM(nqubits, h=h, backend=backend).matrix backend.assert_allclose(final_matrix, target_matrix) -@pytest.mark.parametrize("hamtype", ["normal", "symbolic"]) -@pytest.mark.parametrize("calcterms", [False, True]) -def test_from_symbolic_with_power(backend, hamtype, calcterms): +@pytest.mark.parametrize("hamtype", ["symbolic"]) +def test_from_symbolic_with_power(backend, hamtype): """Check ``from_symbolic`` when the expression contains powers.""" npbackend = NumpyBackend() - if hamtype == "symbolic": - matrix = random_hermitian(2, backend=npbackend) - symham = ( - Symbol(0, matrix) ** 2 - - Symbol(1, matrix) ** 2 - + 3 * Symbol(1, matrix) - - 2 * Symbol(0, matrix) * Symbol(2, matrix) - + 1 - ) - ham = hamiltonians.SymbolicHamiltonian(symham, backend=backend) - else: - z = sympy.symbols(" ".join(f"Z{i}" for i in range(3))) - symham = z[0] ** 2 - z[1] ** 2 + 3 * z[1] - 2 * z[0] * z[2] + 1 - matrix = random_hermitian(2, backend=npbackend) - symmap = {x: (i, matrix) for i, x in enumerate(z)} - ham = hamiltonians.Hamiltonian.from_symbolic(symham, symmap, backend=backend) - - if calcterms: - _ = ham.terms + matrix = random_hermitian(2, backend=npbackend) + symham = ( + Symbol(0, matrix, backend=backend) ** 2 + - Symbol(1, matrix, backend=backend) ** 2 + + 3 * Symbol(1, matrix, backend=backend) + - 2 * Symbol(0, matrix, backend=backend) * Symbol(2, matrix, backend=backend) + + 1 + ) + ham = hamiltonians.SymbolicHamiltonian(symham, backend=backend) final_matrix = ham.matrix matrix2 = matrix.dot(matrix) @@ -88,160 +63,100 @@ def test_from_symbolic_with_power(backend, hamtype, calcterms): backend.assert_allclose(final_matrix, target_matrix) -@pytest.mark.parametrize("hamtype", ["normal", "symbolic"]) -@pytest.mark.parametrize("calcterms", [False, True]) -def test_from_symbolic_with_complex_numbers(backend, hamtype, calcterms): +@pytest.mark.parametrize("hamtype", ["symbolic"]) +def test_from_symbolic_with_complex_numbers(backend, hamtype): """Check ``from_symbolic`` when the expression contains imaginary unit.""" - if hamtype == "symbolic": - symham = ( - (1 + 2j) * X(0) * X(1) - + 2 * Y(0) * Y(1) - - 3j * X(0) * Y(1) - + 1j * Y(0) * X(1) - ) - ham = hamiltonians.SymbolicHamiltonian(symham, backend=backend) - else: - x = sympy.symbols(" ".join(f"X{i}" for i in range(2))) - y = sympy.symbols(" ".join(f"Y{i}" for i in range(2))) - symham = ( - (1 + 2j) * x[0] * x[1] - + 2 * y[0] * y[1] - - 3j * x[0] * y[1] - + 1j * y[0] * x[1] - ) - symmap = {s: (i, matrices.X) for i, s in enumerate(x)} - symmap.update({s: (i, matrices.Y) for i, s in enumerate(y)}) - ham = hamiltonians.Hamiltonian.from_symbolic(symham, symmap, backend=backend) - - if calcterms: - _ = ham.terms - final_matrix = ham.matrix - target_matrix = (1 + 2j) * np.kron(matrices.X, matrices.X) - target_matrix += 2 * np.kron(matrices.Y, matrices.Y) - target_matrix -= 3j * np.kron(matrices.X, matrices.Y) - target_matrix += 1j * np.kron(matrices.Y, matrices.X) - backend.assert_allclose(final_matrix, target_matrix) - - -@pytest.mark.parametrize("calcterms", [False, True]) -def test_from_symbolic_application_hamiltonian(backend, calcterms): - """Check ``from_symbolic`` for a specific four-qubit Hamiltonian.""" - z1, z2, z3, z4 = sympy.symbols("z1 z2 z3 z4") - symmap = {z: (i, matrices.Z) for i, z in enumerate([z1, z2, z3, z4])} symham = ( - z1 * z2 - - 0.5 * z1 * z3 - + 2 * z2 * z3 - + 0.35 * z2 - + 0.25 * z3 * z4 - + 0.5 * z3 - + z4 - - z1 + (1 + 2j) * X(0, backend=backend) * X(1, backend=backend) + + 2 * Y(0, backend=backend) * Y(1, backend=backend) + - 3j * X(0, backend=backend) * Y(1, backend=backend) + + 1j * Y(0, backend=backend) * X(1, backend=backend) ) - # Check that Trotter dense matrix agrees will full Hamiltonian matrix - fham = hamiltonians.Hamiltonian.from_symbolic(symham, symmap, backend=backend) - symham = ( - Z(0) * Z(1) - - 0.5 * Z(0) * Z(2) - + 2 * Z(1) * Z(2) - + 0.35 * Z(1) - + 0.25 * Z(2) * Z(3) - + 0.5 * Z(2) - + Z(3) - - Z(0) - ) - sham = hamiltonians.SymbolicHamiltonian(symham, backend=backend) - if calcterms: - _ = sham.terms - backend.assert_allclose(sham.matrix, fham.matrix) + ham = hamiltonians.SymbolicHamiltonian(symham, backend=backend) + + final_matrix = ham.matrix + target_matrix = (1 + 2j) * backend.np.kron(backend.matrices.X, backend.matrices.X) + target_matrix += 2 * backend.np.kron(backend.matrices.Y, backend.matrices.Y) + target_matrix -= 3j * backend.np.kron(backend.matrices.X, backend.matrices.Y) + target_matrix += 1j * backend.np.kron(backend.matrices.Y, backend.matrices.X) + backend.assert_allclose(final_matrix, target_matrix) @pytest.mark.parametrize("nqubits", [4, 5]) -@pytest.mark.parametrize("hamtype", ["normal", "symbolic"]) -@pytest.mark.parametrize("calcterms", [False, True]) -def test_x_hamiltonian_from_symbols(backend, nqubits, hamtype, calcterms): +@pytest.mark.parametrize("hamtype", ["symbolic"]) +def test_x_hamiltonian_from_symbols(backend, nqubits, hamtype): """Check creating sum(X) Hamiltonian using sympy.""" - if hamtype == "symbolic": - symham = -sum(X(i) for i in range(nqubits)) - ham = hamiltonians.SymbolicHamiltonian(symham, backend=backend) - else: - x_symbols = sympy.symbols(" ".join(f"X{i}" for i in range(nqubits))) - symham = -sum(x_symbols) - symmap = {x: (i, matrices.X) for i, x in enumerate(x_symbols)} - ham = hamiltonians.Hamiltonian.from_symbolic(symham, symmap, backend=backend) - if calcterms: - _ = ham.terms + symham = -sum(X(i, backend=backend) for i in range(nqubits)) + ham = hamiltonians.SymbolicHamiltonian(symham, backend=backend) final_matrix = ham.matrix target_matrix = hamiltonians.X(nqubits, backend=backend).matrix backend.assert_allclose(final_matrix, target_matrix) -@pytest.mark.parametrize("hamtype", ["normal", "symbolic"]) -@pytest.mark.parametrize("calcterms", [False, True]) -def test_three_qubit_term_hamiltonian_from_symbols(backend, hamtype, calcterms): +@pytest.mark.parametrize("hamtype", ["symbolic"]) +def test_three_qubit_term_hamiltonian_from_symbols(backend, hamtype): """Check creating Hamiltonian with three-qubit interaction using sympy.""" - if hamtype == "symbolic": - symham = X(0) * Y(1) * Z(2) + 0.5 * Y(0) * Z(1) * X(3) + Z(0) * X(2) - symham += Y(2) + 1.5 * Z(1) - 2 - 3 * X(1) * Y(3) - ham = hamiltonians.SymbolicHamiltonian(symham, backend=backend) - else: - x_symbols = sympy.symbols(" ".join(f"X{i}" for i in range(4))) - y_symbols = sympy.symbols(" ".join(f"Y{i}" for i in range(4))) - z_symbols = sympy.symbols(" ".join(f"Z{i}" for i in range(4))) - symmap = {x: (i, matrices.X) for i, x in enumerate(x_symbols)} - symmap.update({x: (i, matrices.Y) for i, x in enumerate(y_symbols)}) - symmap.update({x: (i, matrices.Z) for i, x in enumerate(z_symbols)}) - - symham = x_symbols[0] * y_symbols[1] * z_symbols[2] - symham += 0.5 * y_symbols[0] * z_symbols[1] * x_symbols[3] - symham += z_symbols[0] * x_symbols[2] - symham += -3 * x_symbols[1] * y_symbols[3] - symham += y_symbols[2] - symham += 1.5 * z_symbols[1] - symham -= 2 - ham = hamiltonians.Hamiltonian.from_symbolic(symham, symmap, backend=backend) - - if calcterms: - _ = ham.terms + symham = ( + X(0, backend=backend) * Y(1, backend=backend) * Z(2, backend=backend) + + 0.5 * Y(0, backend=backend) * Z(1, backend=backend) * X(3, backend=backend) + + Z(0, backend=backend) * X(2, backend=backend) + ) + symham += ( + Y(2, backend=backend) + + 1.5 * Z(1, backend=backend) + - 2 + - 3 * X(1, backend=backend) * Y(3, backend=backend) + ) + ham = hamiltonians.SymbolicHamiltonian(symham, backend=backend) final_matrix = ham.matrix - target_matrix = np.kron( - np.kron(matrices.X, matrices.Y), np.kron(matrices.Z, matrices.I) + target_matrix = backend.np.kron( + backend.np.kron(backend.matrices.X, backend.matrices.Y), + backend.np.kron(backend.matrices.Z, backend.matrices.I()), ) - target_matrix += 0.5 * np.kron( - np.kron(matrices.Y, matrices.Z), np.kron(matrices.I, matrices.X) + target_matrix += 0.5 * backend.np.kron( + backend.np.kron(backend.matrices.Y, backend.matrices.Z), + backend.np.kron(backend.matrices.I(), backend.matrices.X), ) - target_matrix += np.kron( - np.kron(matrices.Z, matrices.I), np.kron(matrices.X, matrices.I) + target_matrix += backend.np.kron( + backend.np.kron(backend.matrices.Z, backend.matrices.I()), + backend.np.kron(backend.matrices.X, backend.matrices.I()), ) - target_matrix += -3 * np.kron( - np.kron(matrices.I, matrices.X), np.kron(matrices.I, matrices.Y) + target_matrix += -3 * backend.np.kron( + backend.np.kron(backend.matrices.I(), backend.matrices.X), + backend.np.kron(backend.matrices.I(), backend.matrices.Y), ) - target_matrix += np.kron( - np.kron(matrices.I, matrices.I), np.kron(matrices.Y, matrices.I) + target_matrix += backend.np.kron( + backend.np.kron(backend.matrices.I(), backend.matrices.I()), + backend.np.kron(backend.matrices.Y, backend.matrices.I()), ) - target_matrix += 1.5 * np.kron( - np.kron(matrices.I, matrices.Z), np.kron(matrices.I, matrices.I) + target_matrix += 1.5 * backend.np.kron( + backend.np.kron(backend.matrices.I(), backend.matrices.Z), + backend.np.kron(backend.matrices.I(), backend.matrices.I()), ) - target_matrix -= 2 * np.eye(2**4, dtype=target_matrix.dtype) + target_matrix -= 2 * backend.np.eye(2**4, dtype=target_matrix.dtype) backend.assert_allclose(final_matrix, target_matrix) -@pytest.mark.parametrize("calcterms", [False, True]) -def test_hamiltonian_with_identity_symbol(backend, calcterms): +def test_hamiltonian_with_identity_symbol(backend): """Check creating Hamiltonian from expression which contains the identity symbol.""" - symham = X(0) * I(1) * Z(2) + 0.5 * Y(0) * Z(1) * I(3) + Z(0) * I(1) * X(2) + symham = ( + X(0, backend=backend) * I(1, backend=backend) * Z(2, backend=backend) + + 0.5 * Y(0, backend=backend) * Z(1, backend=backend) * I(3, backend=backend) + + Z(0, backend=backend) * I(1, backend=backend) * X(2, backend=backend) + ) ham = hamiltonians.SymbolicHamiltonian(symham, backend=backend) - if calcterms: - _ = ham.terms final_matrix = ham.matrix - target_matrix = np.kron( - np.kron(matrices.X, matrices.I), np.kron(matrices.Z, matrices.I) + target_matrix = backend.np.kron( + backend.np.kron(backend.matrices.X, backend.matrices.I()), + backend.np.kron(backend.matrices.Z, backend.matrices.I()), ) target_matrix += 0.5 * np.kron( - np.kron(matrices.Y, matrices.Z), np.kron(matrices.I, matrices.I) + backend.np.kron(backend.matrices.Y, backend.matrices.Z), + backend.np.kron(backend.matrices.I(), backend.matrices.I()), ) - target_matrix += np.kron( - np.kron(matrices.Z, matrices.I), np.kron(matrices.X, matrices.I) + target_matrix += backend.np.kron( + backend.np.kron(backend.matrices.Z, backend.matrices.I()), + backend.np.kron(backend.matrices.X, backend.matrices.I()), ) backend.assert_allclose(final_matrix, target_matrix) diff --git a/tests/test_hamiltonians_models.py b/tests/test_hamiltonians_models.py index 5d2a2017d2..a5b52f5baf 100644 --- a/tests/test_hamiltonians_models.py +++ b/tests/test_hamiltonians_models.py @@ -32,28 +32,32 @@ def test_hamiltonian_models(backend, model, kwargs, filename): assert_regression_fixture(backend, matrix, filename) -@pytest.mark.parametrize("nqubits", [3, 4]) -@pytest.mark.parametrize( - "dense,calcterms", [(True, False), (False, False), (False, True)] -) -def test_maxcut(backend, nqubits, dense, calcterms): - size = 2**nqubits - ham = np.zeros(shape=(size, size), dtype=np.complex128) - for i in range(nqubits): - for j in range(nqubits): - h = np.eye(1) - for k in range(nqubits): - if (k == i) ^ (k == j): - h = np.kron(h, matrices.Z) - else: - h = np.kron(h, matrices.I) - M = np.eye(2**nqubits) - h - ham += M - target_ham = backend.cast(-ham / 2) - final_ham = hamiltonians.MaxCut(nqubits, dense, backend=backend) - if (not dense) and calcterms: - _ = final_ham.terms - backend.assert_allclose(final_ham.matrix, target_ham) +@pytest.mark.parametrize("nqubits,adj_matrix", zip([3, 4], [None, [[0, 1], [2, 3]]])) +@pytest.mark.parametrize("dense", [True, False]) +def test_maxcut(backend, nqubits, adj_matrix, dense): + if adj_matrix is not None: + with pytest.raises(RuntimeError): + final_ham = hamiltonians.MaxCut( + nqubits, dense, adj_matrix=adj_matrix, backend=backend + ) + else: + size = 2**nqubits + ham = np.zeros(shape=(size, size), dtype=np.complex128) + for i in range(nqubits): + for j in range(nqubits): + h = np.eye(1) + for k in range(nqubits): + if (k == i) ^ (k == j): + h = np.kron(h, matrices.Z) + else: + h = np.kron(h, matrices.I) + M = np.eye(2**nqubits) - h + ham += M + target_ham = backend.cast(-ham / 2) + final_ham = hamiltonians.MaxCut( + nqubits, dense, adj_matrix=adj_matrix, backend=backend + ) + backend.assert_allclose(final_ham.matrix, target_ham) @pytest.mark.parametrize("model", ["XXZ", "TFIM"]) diff --git a/tests/test_hamiltonians_symbolic.py b/tests/test_hamiltonians_symbolic.py index ff91e9fb1e..ebb99c1f73 100644 --- a/tests/test_hamiltonians_symbolic.py +++ b/tests/test_hamiltonians_symbolic.py @@ -10,13 +10,15 @@ from qibo.symbols import I, Symbol, X, Y, Z -def symbolic_tfim(nqubits, h=1.0): +def symbolic_tfim(nqubits, backend, h=1.0): """Constructs symbolic Hamiltonian for TFIM.""" from qibo.symbols import X, Z - sham = -sum(Z(i) * Z(i + 1) for i in range(nqubits - 1)) - sham -= Z(0) * Z(nqubits - 1) - sham -= h * sum(X(i) for i in range(nqubits)) + sham = -sum( + Z(i, backend=backend) * Z(i + 1, backend=backend) for i in range(nqubits - 1) + ) + sham -= Z(0, backend=backend) * Z(nqubits - 1, backend=backend) + sham -= h * sum(X(i, backend=backend) for i in range(nqubits)) return sham @@ -27,128 +29,126 @@ def test_symbolic_hamiltonian_errors(backend): # Wrong type of symbolic expression with pytest.raises(TypeError): ham = SymbolicHamiltonian("test", backend=backend) - # Passing form with symbol that is not in ``symbol_map`` + # Passing form with symbol that is not a ``qibo.symbols.Symbol`` from qibo import matrices - Z, X = sympy.Symbol("Z"), sympy.Symbol("X") - symbol_map = {Z: (0, matrices.Z)} - with pytest.raises(ValueError): - ham = SymbolicHamiltonian(Z * X, symbol_map=symbol_map, backend=backend) + z, x = sympy.Symbol("z"), sympy.Symbol("x") + with pytest.raises(RuntimeError): + ham = SymbolicHamiltonian(z * x, backend=backend) # Invalid operation in Hamiltonian expresion - ham = SymbolicHamiltonian(sympy.cos(Z), symbol_map=symbol_map, backend=backend) + ham = SymbolicHamiltonian(sympy.cos(Z(0, backend=backend)), backend=backend) with pytest.raises(TypeError): dense = ham.dense +def test_symbolic_hamiltonian_form_setter(backend): + h = SymbolicHamiltonian(Z(0), backend=backend) + new_form = Z(0) * X(1) * Y(3) + h.form = new_form + assert h.form == new_form + assert h.nqubits == 4 + + +def test_symbolic_hamiltonian_dense(backend): + target_matrix = backend.cast( + Z(0).matrix @ Z(0).matrix + X(0).matrix @ Y(0).matrix + np.eye(2) + ) + form = Z(0) ** 2 + X(0) * Y(0) + 1 + sham = SymbolicHamiltonian(form, nqubits=1, backend=backend) + backend.assert_allclose(sham.dense.matrix, target_matrix) + + @pytest.mark.parametrize("nqubits", [3, 4]) -@pytest.mark.parametrize("calcterms", [False, True]) -def test_symbolictfim_hamiltonian_to_dense(backend, nqubits, calcterms): - final_ham = SymbolicHamiltonian(symbolic_tfim(nqubits, h=1), backend=backend) +def test_symbolictfim_hamiltonian_to_dense(backend, nqubits): + final_ham = SymbolicHamiltonian( + symbolic_tfim(nqubits, backend, h=1), backend=backend + ) target_ham = TFIM(nqubits, h=1, backend=backend) - if calcterms: - _ = final_ham.terms backend.assert_allclose(final_ham.matrix, target_ham.matrix, atol=1e-15) @pytest.mark.parametrize("nqubits", [3, 4]) -@pytest.mark.parametrize("calcterms", [False, True]) -def test_symbolicxxz_hamiltonian_to_dense(backend, nqubits, calcterms): - sham = sum(X(i) * X(i + 1) for i in range(nqubits - 1)) - sham += sum(Y(i) * Y(i + 1) for i in range(nqubits - 1)) - sham += 0.5 * sum(Z(i) * Z(i + 1) for i in range(nqubits - 1)) - sham += X(0) * X(nqubits - 1) + Y(0) * Y(nqubits - 1) + 0.5 * Z(0) * Z(nqubits - 1) +def test_symbolicxxz_hamiltonian_to_dense(backend, nqubits): + sham = sum( + X(i, backend=backend) * X(i + 1, backend=backend) for i in range(nqubits - 1) + ) + sham += sum( + Y(i, backend=backend) * Y(i + 1, backend=backend) for i in range(nqubits - 1) + ) + sham += 0.5 * sum( + Z(i, backend=backend) * Z(i + 1, backend=backend) for i in range(nqubits - 1) + ) + sham += ( + X(0, backend=backend) * X(nqubits - 1, backend=backend) + + Y(0, backend=backend) * Y(nqubits - 1, backend=backend) + + 0.5 * Z(0, backend=backend) * Z(nqubits - 1, backend=backend) + ) final_ham = SymbolicHamiltonian(sham, backend=backend) target_ham = XXZ(nqubits, backend=backend) - if calcterms: - _ = final_ham.terms backend.assert_allclose(final_ham.matrix, target_ham.matrix, atol=1e-15) @pytest.mark.parametrize("nqubits", [3]) -@pytest.mark.parametrize("calcterms", [False, True]) -@pytest.mark.parametrize("calcdense", [False, True]) -def test_symbolic_hamiltonian_scalar_mul(backend, nqubits, calcterms, calcdense): +def test_symbolic_hamiltonian_scalar_mul(backend, nqubits): """Test multiplication of Trotter Hamiltonian with scalar.""" - local_ham = SymbolicHamiltonian(symbolic_tfim(nqubits, h=1.0), backend=backend) + local_ham = SymbolicHamiltonian( + symbolic_tfim(nqubits, backend, h=1.0), backend=backend + ) target_ham = 2 * TFIM(nqubits, h=1.0, backend=backend) - if calcterms: - _ = local_ham.terms - if calcdense: - _ = local_ham.dense local_dense = (2 * local_ham).dense backend.assert_allclose(local_dense.matrix, target_ham.matrix) - local_ham = SymbolicHamiltonian(symbolic_tfim(nqubits, h=1.0), backend=backend) - if calcterms: - _ = local_ham.terms - if calcdense: - _ = local_ham.dense + local_ham = SymbolicHamiltonian( + symbolic_tfim(nqubits, backend, h=1.0), backend=backend + ) local_dense = (local_ham * 2).dense backend.assert_allclose(local_dense.matrix, target_ham.matrix) @pytest.mark.parametrize("nqubits", [4]) -@pytest.mark.parametrize("calcterms", [False, True]) -@pytest.mark.parametrize("calcdense", [False, True]) -def test_symbolic_hamiltonian_scalar_add(backend, nqubits, calcterms, calcdense): +def test_symbolic_hamiltonian_scalar_add(backend, nqubits): """Test addition of Trotter Hamiltonian with scalar.""" - local_ham = SymbolicHamiltonian(symbolic_tfim(nqubits, h=1.0), backend=backend) + local_ham = SymbolicHamiltonian( + symbolic_tfim(nqubits, backend, h=1.0), backend=backend + ) target_ham = 2 + TFIM(nqubits, h=1.0, backend=backend) - if calcterms: - _ = local_ham.terms - if calcdense: - _ = local_ham.dense local_dense = (2 + local_ham).dense backend.assert_allclose(local_dense.matrix, target_ham.matrix) - local_ham = SymbolicHamiltonian(symbolic_tfim(nqubits, h=1.0), backend=backend) - if calcterms: - _ = local_ham.terms - if calcdense: - _ = local_ham.dense + local_ham = SymbolicHamiltonian( + symbolic_tfim(nqubits, backend, h=1.0), backend=backend + ) local_dense = (local_ham + 2).dense backend.assert_allclose(local_dense.matrix, target_ham.matrix) @pytest.mark.parametrize("nqubits", [3]) -@pytest.mark.parametrize("calcterms", [False, True]) -@pytest.mark.parametrize("calcdense", [False, True]) -def test_symbolic_hamiltonian_scalar_sub(backend, nqubits, calcterms, calcdense): +def test_symbolic_hamiltonian_scalar_sub(backend, nqubits): """Test subtraction of Trotter Hamiltonian with scalar.""" - local_ham = SymbolicHamiltonian(symbolic_tfim(nqubits, h=1.0), backend=backend) + local_ham = SymbolicHamiltonian( + symbolic_tfim(nqubits, backend, h=1.0), backend=backend + ) target_ham = 2 - TFIM(nqubits, h=1.0, backend=backend) - if calcterms: - _ = local_ham.terms - if calcdense: - _ = local_ham.dense local_dense = (2 - local_ham).dense backend.assert_allclose(local_dense.matrix, target_ham.matrix) target_ham = TFIM(nqubits, h=1.0, backend=backend) - 2 - local_ham = SymbolicHamiltonian(symbolic_tfim(nqubits, h=1.0), backend=backend) - if calcterms: - _ = local_ham.terms - if calcdense: - _ = local_ham.dense + local_ham = SymbolicHamiltonian( + symbolic_tfim(nqubits, backend, h=1.0), backend=backend + ) local_dense = (local_ham - 2).dense backend.assert_allclose(local_dense.matrix, target_ham.matrix) @pytest.mark.parametrize("nqubits", [3]) -@pytest.mark.parametrize("calcterms", [False, True]) -@pytest.mark.parametrize("calcdense", [False, True]) -def test_symbolic_hamiltonian_operator_add_and_sub( - backend, nqubits, calcterms, calcdense -): +def test_symbolic_hamiltonian_operator_add_and_sub(backend, nqubits): """Test addition and subtraction between Trotter Hamiltonians.""" - local_ham1 = SymbolicHamiltonian(symbolic_tfim(nqubits, h=1.0), backend=backend) - local_ham2 = SymbolicHamiltonian(symbolic_tfim(nqubits, h=0.5), backend=backend) - if calcterms: - _ = local_ham1.terms - _ = local_ham2.terms - if calcdense: - _ = local_ham1.dense - _ = local_ham2.dense + local_ham1 = SymbolicHamiltonian( + symbolic_tfim(nqubits, backend, h=1.0), backend=backend + ) + local_ham2 = SymbolicHamiltonian( + symbolic_tfim(nqubits, backend, h=0.5), backend=backend + ) local_ham = local_ham1 + local_ham2 target_ham = TFIM(nqubits, h=1.0, backend=backend) + TFIM( nqubits, h=0.5, backend=backend @@ -156,14 +156,12 @@ def test_symbolic_hamiltonian_operator_add_and_sub( dense = local_ham.dense backend.assert_allclose(dense.matrix, target_ham.matrix) - local_ham1 = SymbolicHamiltonian(symbolic_tfim(nqubits, h=1.0), backend=backend) - local_ham2 = SymbolicHamiltonian(symbolic_tfim(nqubits, h=0.5), backend=backend) - if calcterms: - _ = local_ham1.terms - _ = local_ham2.terms - if calcdense: - _ = local_ham1.dense - _ = local_ham2.dense + local_ham1 = SymbolicHamiltonian( + symbolic_tfim(nqubits, backend, h=1.0), backend=backend + ) + local_ham2 = SymbolicHamiltonian( + symbolic_tfim(nqubits, backend, h=0.5), backend=backend + ) local_ham = local_ham1 - local_ham2 target_ham = TFIM(nqubits, h=1.0, backend=backend) - TFIM( nqubits, h=0.5, backend=backend @@ -174,13 +172,22 @@ def test_symbolic_hamiltonian_operator_add_and_sub( # Test multiplication and sum target = XXZ(nqubits, backend=backend) term_1 = SymbolicHamiltonian( - X(0) * X(1) + X(1) * X(2) + X(0) * X(2), backend=backend + X(0, backend=backend) * X(1, backend=backend) + + X(1, backend=backend) * X(2, backend=backend) + + X(0, backend=backend) * X(2, backend=backend), + backend=backend, ) term_2 = SymbolicHamiltonian( - Y(0) * Y(1) + Y(1) * Y(2) + Y(0) * Y(2), backend=backend + Y(0, backend=backend) * Y(1, backend=backend) + + Y(1, backend=backend) * Y(2, backend=backend) + + Y(0, backend=backend) * Y(2, backend=backend), + backend=backend, ) term_3 = SymbolicHamiltonian( - Z(0) * Z(1) + Z(1) * Z(2) + Z(0) * Z(2), backend=backend + Z(0, backend=backend) * Z(1, backend=backend) + + Z(1, backend=backend) * Z(2, backend=backend) + + Z(0, backend=backend) * Z(2, backend=backend), + backend=backend, ) hamiltonian = term_1 + term_2 + 0.5 * term_3 matrix = hamiltonian.dense.matrix @@ -189,19 +196,15 @@ def test_symbolic_hamiltonian_operator_add_and_sub( @pytest.mark.parametrize("nqubits", [5]) -@pytest.mark.parametrize("calcterms", [False, True]) -@pytest.mark.parametrize("calcdense", [False, True]) -def test_symbolic_hamiltonian_hamiltonianmatmul(backend, nqubits, calcterms, calcdense): - local_ham1 = SymbolicHamiltonian(symbolic_tfim(nqubits, h=1.0), backend=backend) - local_ham2 = SymbolicHamiltonian(symbolic_tfim(nqubits, h=0.5), backend=backend) +def test_symbolic_hamiltonian_hamiltonianmatmul(backend, nqubits): + local_ham1 = SymbolicHamiltonian( + symbolic_tfim(nqubits, backend, h=1.0), backend=backend + ) + local_ham2 = SymbolicHamiltonian( + symbolic_tfim(nqubits, backend, h=0.5), backend=backend + ) dense_ham1 = TFIM(nqubits, h=1.0, backend=backend) dense_ham2 = TFIM(nqubits, h=0.5, backend=backend) - if calcterms: - _ = local_ham1.terms - _ = local_ham2.terms - if calcdense: - _ = local_ham1.dense - _ = local_ham2.dense local_matmul = local_ham1 @ local_ham2 target_matmul = dense_ham1 @ dense_ham2 backend.assert_allclose(local_matmul.matrix, target_matmul.matrix) @@ -209,33 +212,26 @@ def test_symbolic_hamiltonian_hamiltonianmatmul(backend, nqubits, calcterms, cal @pytest.mark.parametrize("nqubits", [3, 4]) @pytest.mark.parametrize("density_matrix", [False, True]) -@pytest.mark.parametrize("calcterms", [False, True]) -def test_symbolic_hamiltonian_matmul(backend, nqubits, density_matrix, calcterms): +def test_symbolic_hamiltonian_matmul(backend, nqubits, density_matrix): state = ( random_density_matrix(2**nqubits, backend=backend) if density_matrix else random_statevector(2**nqubits, backend=backend) ) - local_ham = SymbolicHamiltonian(symbolic_tfim(nqubits, h=1.0), backend=backend) + local_ham = SymbolicHamiltonian( + symbolic_tfim(nqubits, backend, h=1.0), backend=backend + ) dense_ham = TFIM(nqubits, h=1.0, backend=backend) - if calcterms: - _ = local_ham.terms local_matmul = local_ham @ state target_matmul = dense_ham @ state backend.assert_allclose(local_matmul, target_matmul) @pytest.mark.parametrize("nqubits,normalize", [(3, False), (4, False)]) -@pytest.mark.parametrize("calcterms", [False, True]) -@pytest.mark.parametrize("calcdense", [False, True]) -def test_symbolic_hamiltonian_state_expectation( - backend, nqubits, normalize, calcterms, calcdense -): - local_ham = SymbolicHamiltonian(symbolic_tfim(nqubits, h=1.0), backend=backend) + 2 - if calcterms: - _ = local_ham.terms - if calcdense: - _ = local_ham.dense +def test_symbolic_hamiltonian_state_expectation(backend, nqubits, normalize): + local_ham = ( + SymbolicHamiltonian(symbolic_tfim(nqubits, backend, h=1.0), backend=backend) + 2 + ) dense_ham = TFIM(nqubits, h=1.0, backend=backend) + 2 state = random_statevector(2**nqubits, backend=backend) @@ -250,20 +246,14 @@ def test_symbolic_hamiltonian_state_expectation( @pytest.mark.parametrize("give_nqubits", [False, True]) -@pytest.mark.parametrize("calcterms", [False, True]) -@pytest.mark.parametrize("calcdense", [False, True]) def test_symbolic_hamiltonian_state_expectation_different_nqubits( - backend, give_nqubits, calcterms, calcdense + backend, give_nqubits ): - expr = symbolic_tfim(3, h=1.0) + expr = symbolic_tfim(3, backend, h=1.0) if give_nqubits: local_ham = SymbolicHamiltonian(expr, nqubits=5, backend=backend) else: local_ham = SymbolicHamiltonian(expr, backend=backend) - if calcterms: - _ = local_ham.terms - if calcdense: - _ = local_ham.dense dense_ham = TFIM(3, h=1.0, backend=backend) dense_matrix = np.kron(backend.to_numpy(dense_ham.matrix), np.eye(4)) @@ -312,15 +302,14 @@ def test_hamiltonian_expectation_from_samples(backend, observable, qubit_map): @pytest.mark.parametrize("density_matrix", [False, True]) -@pytest.mark.parametrize("calcterms", [False, True]) -def test_symbolic_hamiltonian_abstract_symbol_ev(backend, density_matrix, calcterms): +def test_symbolic_hamiltonian_abstract_symbol_ev(backend, density_matrix): from qibo.symbols import Symbol, X matrix = np.random.random((2, 2)) - form = X(0) * Symbol(1, matrix) + Symbol(0, matrix) * X(1) + form = X(0, backend=backend) * Symbol(1, matrix, backend=backend) + Symbol( + 0, matrix, backend=backend + ) * X(1, backend=backend) local_ham = SymbolicHamiltonian(form, backend=backend) - if calcterms: - _ = local_ham.terms state = ( random_density_matrix(4, backend=backend) @@ -334,8 +323,8 @@ def test_symbolic_hamiltonian_abstract_symbol_ev(backend, density_matrix, calcte def test_trotter_hamiltonian_operation_errors(backend): """Test errors in ``SymbolicHamiltonian`` addition and subtraction.""" - h1 = SymbolicHamiltonian(symbolic_tfim(3, h=1.0), backend=backend) - h2 = SymbolicHamiltonian(symbolic_tfim(4, h=1.0), backend=backend) + h1 = SymbolicHamiltonian(symbolic_tfim(3, backend, h=1.0), backend=backend) + h2 = SymbolicHamiltonian(symbolic_tfim(4, backend, h=1.0), backend=backend) with pytest.raises(RuntimeError): h = h1 + h2 with pytest.raises(RuntimeError): @@ -355,8 +344,6 @@ def test_trotter_hamiltonian_operation_errors(backend): with pytest.raises(NotImplementedError): h = h1 @ np.ones((2, 2, 2, 2)) h2 = XXZ(3, dense=False, backend=backend) - with pytest.raises(NotImplementedError): - h = h1 @ h2 def test_symbolic_hamiltonian_with_constant(backend): diff --git a/tests/test_hamiltonians_terms.py b/tests/test_hamiltonians_terms.py index 0addb08adc..c3dbe6b2d3 100644 --- a/tests/test_hamiltonians_terms.py +++ b/tests/test_hamiltonians_terms.py @@ -99,21 +99,12 @@ def test_hamiltonian_term_merge(backend): term1.merge(term2) -@pytest.mark.parametrize("use_symbols", [True, False]) -def test_symbolic_term_creation(backend, use_symbols): +def test_symbolic_term_creation(backend): """Test creating ``SymbolicTerm`` from sympy expression.""" - if use_symbols: - from qibo.symbols import X, Y - - expression = X(0) * Y(1) * X(1) - symbol_map = {} - else: - import sympy - - x0, x1, y1 = sympy.symbols("X0 X1 Y1", commutative=False) - expression = x0 * y1 * x1 - symbol_map = {x0: (0, matrices.X), x1: (1, matrices.X), y1: (1, matrices.Y)} - term = terms.SymbolicTerm(2, expression, symbol_map) + from qibo.symbols import X, Y + + expression = X(0) * Y(1) * X(1) + term = terms.SymbolicTerm(2, expression) assert term.target_qubits == (0, 1) assert len(term.matrix_map) == 2 backend.assert_allclose(term.matrix_map.get(0)[0], matrices.X) diff --git a/tests/test_hamiltonians_trotter.py b/tests/test_hamiltonians_trotter.py index 0c2d2f1756..844d59e10e 100644 --- a/tests/test_hamiltonians_trotter.py +++ b/tests/test_hamiltonians_trotter.py @@ -96,55 +96,6 @@ def test_trotter_hamiltonian_matmul(backend, nqubits, normalize): backend.assert_allclose(trotter_matmul, target_matmul) -def test_trotter_hamiltonian_three_qubit_term(backend): - """Test creating ``TrotterHamiltonian`` with three qubit term.""" - from scipy.linalg import expm - - from qibo.hamiltonians.terms import HamiltonianTerm - - numpy_backend = NumpyBackend() - - m1 = random_hermitian(2**3, backend=numpy_backend) - m2 = random_hermitian(2**2, backend=numpy_backend) - m3 = random_hermitian(2**1, backend=numpy_backend) - - terms = [ - HamiltonianTerm(m1, 0, 1, 2), - HamiltonianTerm(m2, 2, 3), - HamiltonianTerm(m3, 1), - ] - m1 = backend.cast(m1, dtype=m1.dtype) - m2 = backend.cast(m2, dtype=m2.dtype) - m3 = backend.cast(m3, dtype=m3.dtype) - - ham = hamiltonians.SymbolicHamiltonian(backend=backend) - ham.terms = terms - - # Test that the `TrotterHamiltonian` dense matrix is correct - eye = np.eye(2, dtype=complex) - eye = backend.cast(eye, dtype=eye.dtype) - mm1 = backend.np.kron(m1, eye) - mm2 = backend.np.kron(backend.np.kron(eye, eye), m2) - mm3 = backend.np.kron(backend.np.kron(eye, m3), backend.np.kron(eye, eye)) - target_ham = hamiltonians.Hamiltonian(4, mm1 + mm2 + mm3, backend=backend) - backend.assert_allclose(ham.matrix, target_ham.matrix) - - dt = 1e-2 - initial_state = random_statevector(2**4, backend=backend) - circuit = ham.circuit(dt=dt) - final_state = backend.execute_circuit( - circuit, backend.np.copy(initial_state) - ).state() - mm1 = backend.to_numpy(mm1) - mm2 = backend.to_numpy(mm2) - mm3 = backend.to_numpy(mm3) - u = [expm(-0.5j * dt * (mm1 + mm3)), expm(-0.5j * dt * mm2)] - u = backend.cast(u) - target_state = backend.np.matmul(u[1], backend.np.matmul(u[0], initial_state)) - target_state = backend.np.matmul(u[0], backend.np.matmul(u[1], target_state)) - backend.assert_allclose(final_state, target_state) - - def test_symbolic_hamiltonian_circuit_different_dts(backend): """Issue: https://github.com/qiboteam/qibo/issues/1357.""" ham = hamiltonians.SymbolicHamiltonian(symbols.Z(0)) @@ -153,11 +104,3 @@ def test_symbolic_hamiltonian_circuit_different_dts(backend): matrix1 = ham.circuit(0.2).unitary(backend) matrix2 = (a + b).unitary(backend) backend.assert_allclose(matrix1, matrix2) - - -def test_old_trotter_hamiltonian_errors(): - """Check errors when creating the deprecated ``TrotterHamiltonian`` object.""" - with pytest.raises(NotImplementedError): - h = hamiltonians.TrotterHamiltonian() - with pytest.raises(NotImplementedError): - h = hamiltonians.TrotterHamiltonian.from_symbolic(0, 1) diff --git a/tests/test_models_error_mitigation.py b/tests/test_models_error_mitigation.py index 62bd804f5e..a73f70cf7a 100644 --- a/tests/test_models_error_mitigation.py +++ b/tests/test_models_error_mitigation.py @@ -342,7 +342,7 @@ def test_ics(backend, nqubits, noise, full_output, readout): # Define the circuit c = get_circuit(nqubits, nqubits - 1) # Define the observable - obs = np.prod([Z(i) for i in range(nqubits - 1)]) + obs = np.prod([Z(i, backend=backend) for i in range(nqubits - 1)]) obs_exact = SymbolicHamiltonian(obs, nqubits=nqubits, backend=backend) obs = SymbolicHamiltonian(obs, backend=backend) # Noise-free expected value diff --git a/tests/test_states.py b/tests/test_states.py index 3c885c13fd..0bf559648a 100644 --- a/tests/test_states.py +++ b/tests/test_states.py @@ -91,8 +91,12 @@ def test_state_probabilities(backend, density_matrix): def test_expectation_from_samples(backend): # fix seed to use the same samples in every execution np.random.seed(123) - obs0 = 2 * Z(0) * Z(1) + Z(0) * Z(2) - obs1 = 2 * Z(0) * Z(1) + Z(0) * Z(2) * I(3) + obs0 = 2 * Z(0, backend=backend) * Z(1, backend=backend) + Z( + 0, backend=backend + ) * Z(2, backend=backend) + obs1 = 2 * Z(0, backend=backend) * Z(1, backend=backend) + Z( + 0, backend=backend + ) * Z(2, backend=backend) * I(3, backend=backend) h_sym = hamiltonians.SymbolicHamiltonian(obs0, backend=backend) h_dense = hamiltonians.Hamiltonian(3, h_sym.matrix, backend=backend) h1 = hamiltonians.SymbolicHamiltonian(obs1, backend=backend)