From dcbb156d07fa11fa6bededf160755ea6303ef025 Mon Sep 17 00:00:00 2001 From: BrunoLiegiBastonLiegi Date: Wed, 18 Dec 2024 08:18:30 +0100 Subject: [PATCH 01/25] feat: removed terms and dense setter --- src/qibo/hamiltonians/hamiltonians.py | 54 ++++++++++++++------------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/src/qibo/hamiltonians/hamiltonians.py b/src/qibo/hamiltonians/hamiltonians.py index 0016ddb05b..1b975c3f94 100644 --- a/src/qibo/hamiltonians/hamiltonians.py +++ b/src/qibo/hamiltonians/hamiltonians.py @@ -1,5 +1,6 @@ """Module defining Hamiltonian classes.""" +from functools import cached_property from itertools import chain from math import prod from typing import Optional @@ -325,9 +326,7 @@ class SymbolicHamiltonian(AbstractHamiltonian): def __init__(self, form=None, nqubits=None, symbol_map={}, backend=None): super().__init__() self._form = None - self._terms = None 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`` @@ -342,32 +341,40 @@ def __init__(self, form=None, nqubits=None, symbol_map={}, backend=None): 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 + log.warning( + "Calculating the dense form of a symbolic Hamiltonian. " + "This operation is memory inefficient." + ) + return self.calculate_dense() @property def form(self): return self._form + def _calculate_nqubits_from_form(self, form): + 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 + return nqubits + 1 + @form.setter def form(self, form): # Check that given form is a ``sympy`` expression @@ -396,7 +403,7 @@ def form(self, form): self._form = form self.nqubits = nqubits + 1 - @property + @cached_property def terms(self): """List of terms of which the Hamiltonian is a sum of. @@ -419,11 +426,6 @@ def terms(self): 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 - @property def matrix(self): """Returns the full matrix representation. From d49ce6cb409bf1beacb9f712d7564ef9dc12000e Mon Sep 17 00:00:00 2001 From: BrunoLiegiBastonLiegi Date: Wed, 18 Dec 2024 09:28:44 +0100 Subject: [PATCH 02/25] feat: adapting hamiltonian --- src/qibo/hamiltonians/hamiltonians.py | 181 +++++++------------------- 1 file changed, 47 insertions(+), 134 deletions(-) diff --git a/src/qibo/hamiltonians/hamiltonians.py b/src/qibo/hamiltonians/hamiltonians.py index 1b975c3f94..5f35d0e46a 100644 --- a/src/qibo/hamiltonians/hamiltonians.py +++ b/src/qibo/hamiltonians/hamiltonians.py @@ -359,6 +359,9 @@ 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): @@ -383,25 +386,8 @@ 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) @cached_property def terms(self): @@ -409,22 +395,21 @@ def terms(self): 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 + self.constant = 0.0 + + 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 @property def matrix(self): @@ -548,10 +533,10 @@ def _calculate_dense_from_terms(self) -> Hamiltonian: 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() + # 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 expectation(self, state, normalize=False): @@ -672,120 +657,48 @@ def expectation_from_samples(self, freq: dict, qubit_map: dict = None) -> float: ) return self.backend.np.sum(expvals @ counts.T) + self.constant.real - def __add__(self, o): + def _compose(self, o, operator): + new_ham = self.__class__( + form=self._form, symbol_map=dict(self.symbol_map), backend=self.backend + ) + 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 + + if o._form is not None: 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 + new_ham.form = ( + operator(self._form, o._form) if self._form is not None else o._form + ) - elif isinstance(o, self.backend.numeric_types): - new_ham = self.__class__( - symbol_map=dict(self.symbol_map), backend=self.backend + elif isinstance(o, (self.backend.numeric_types, self.backend.tensor_types)): + new_ham.form = ( + operator(self._form, o) if self._form is not None else complex(o) ) - 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 - 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 new_ham - 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 + # o = complex(o) + return self._compose(o, lambda x, y: y * x) def apply_gates(self, state, density_matrix=False): """Applies gates corresponding to the Hamiltonian terms. @@ -821,8 +734,8 @@ def __matmul__(self, o): 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 + # if self._dense is not None and o._dense is not None: + # new_ham.dense = self.dense @ o.dense return new_ham if isinstance(o, self.backend.tensor_types): From 49ea30bb9e95295af5df5b95645d8151a5e76c47 Mon Sep 17 00:00:00 2001 From: BrunoLiegiBastonLiegi Date: Wed, 18 Dec 2024 12:58:46 +0100 Subject: [PATCH 03/25] feat: patching hamiltonian/models to build starting from forms --- src/qibo/hamiltonians/hamiltonians.py | 12 +-- src/qibo/hamiltonians/models.py | 134 +++++++++++++++++++++----- 2 files changed, 114 insertions(+), 32 deletions(-) diff --git a/src/qibo/hamiltonians/hamiltonians.py b/src/qibo/hamiltonians/hamiltonians.py index 5f35d0e46a..d663d10d05 100644 --- a/src/qibo/hamiltonians/hamiltonians.py +++ b/src/qibo/hamiltonians/hamiltonians.py @@ -348,10 +348,6 @@ def __init__(self, form=None, nqubits=None, symbol_map={}, backend=None): @cached_property def dense(self) -> "MatrixHamiltonian": """Creates the equivalent Hamiltonian matrix.""" - log.warning( - "Calculating the dense form of a symbolic Hamiltonian. " - "This operation is memory inefficient." - ) return self.calculate_dense() @property @@ -533,11 +529,15 @@ def _calculate_dense_from_terms(self) -> Hamiltonian: return Hamiltonian(self.nqubits, matrix, backend=self.backend) + self.constant def calculate_dense(self): + log.warning( + "Calculating the dense form of a symbolic Hamiltonian. " + "This operation is memory inefficient." + ) # 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() + return self._calculate_dense_from_form() + # return self._calculate_dense_from_terms() def expectation(self, state, normalize=False): return Hamiltonian.expectation(self, state, normalize) diff --git a/src/qibo/hamiltonians/models.py b/src/qibo/hamiltonians/models.py index 61fa918021..1e4f3ccca5 100644 --- a/src/qibo/hamiltonians/models.py +++ b/src/qibo/hamiltonians/models.py @@ -3,6 +3,7 @@ import numpy as np +from qibo import symbols from qibo.backends import _check_backend, matrices from qibo.config import raise_error from qibo.hamiltonians.hamiltonians import Hamiltonian, SymbolicHamiltonian @@ -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): @@ -82,19 +83,25 @@ def TFIM(nqubits, h: float = 0.0, dense: bool = True, backend=None): raise_error(ValueError, "Number of qubits must be larger than one.") 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]) + term = lambda q1, q2: symbols.Z(q1) * symbols.Z(q2) + h * symbols.X(q1) * symbols.I( + q2 ) - 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 + form = sum(term(i, i + 1) for i in range(nqubits - 1)) + # matrix = -( + # _multikron([backend.matrices.Z, backend.matrices.Z], backend) + h * _multikron([backend.matrices.X, backend.matrices.I], backend) + # ) + # terms = [HamiltonianTerm(matrix, i, i + 1) for i in range(nqubits - 1)] + # terms.append(HamiltonianTerm(matrix, nqubits - 1, 0)) + ham = SymbolicHamiltonian(form=form, nqubits=nqubits, backend=backend) + # ham.terms = terms return ham @@ -210,7 +217,7 @@ def Heisenberg( 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, condition, backend) double_term = backend.cast(double_term, dtype=double_term.dtype) matrix = matrix - coupling_constants[ind] * double_term matrix = ( @@ -221,6 +228,26 @@ def Heisenberg( return Hamiltonian(nqubits, matrix, backend=backend) + paulis = (symbols.X, symbols.Y, symbols.Z) + + def h(symbol): + return lambda q1, q2: symbol(q1) * symbol(q2) + + def term(q1, q2): + return -1 * sum( + coeff * h(operator)(q1, q2) + for coeff, operator in zip(coupling_constants, paulis) + ) + + form = sum(term(i, i + 1) for i in range(nqubits - 1)) + 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 + ) + + """ hx = _multikron([matrices.X, matrices.X]) hy = _multikron([matrices.Y, matrices.Y]) hz = _multikron([matrices.Z, matrices.Z]) @@ -242,9 +269,10 @@ def Heisenberg( if field_strength != 0.0 ] ) + """ - ham = SymbolicHamiltonian(backend=backend) - ham.terms = terms + ham = SymbolicHamiltonian(form=form, backend=backend) + # ham.terms = terms return ham @@ -342,7 +370,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=None): """Calculates Kronecker product of a list of matrices. Args: @@ -351,28 +379,82 @@ def _multikron(matrix_list): Returns: ndarray: Kronecker product of all matrices in ``matrix_list``. """ + """ + # this is a better implementation but requires the whole + # hamiltonian/symbols modules to be adapted + 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, (dim, dim)), axis=0) + return h + """ return reduce(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)) - 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 = backend.np.einsum(*einsum_args, rhs) + else: + h = np.einsum(*einsum_args, rhs) + h = backend.np.sum(backend.np.reshape(h, (nqubits, dim, dim)), axis=0) + # h = sum( + # _multikron(matrix if condition(i, j) else matrices.I for j in range(nqubits)) + # for i in range(nqubits) + # ) 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.""" if dense: condition = lambda i, j: i == j % nqubits - ham = -_build_spin_model(nqubits, matrix, condition) + ham = -_build_spin_model(nqubits, operator(0).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 + # matrix = -matrix + # terms = [HamiltonianTerm(matrix, i) for i in range(nqubits)] + form = sum([-1 * operator(i) for i in range(nqubits)]) + ham = SymbolicHamiltonian(form=form, backend=backend) + # ham.terms = terms return ham From 083ebba88718e377dcc07b82546d9f0885b26709 Mon Sep 17 00:00:00 2001 From: BrunoLiegiBastonLiegi Date: Wed, 18 Dec 2024 19:23:47 +0100 Subject: [PATCH 04/25] fix: some fixes for tests --- src/qibo/hamiltonians/hamiltonians.py | 1 + src/qibo/hamiltonians/models.py | 18 ++++++++++++------ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/qibo/hamiltonians/hamiltonians.py b/src/qibo/hamiltonians/hamiltonians.py index d663d10d05..b6a6e35e2c 100644 --- a/src/qibo/hamiltonians/hamiltonians.py +++ b/src/qibo/hamiltonians/hamiltonians.py @@ -406,6 +406,7 @@ def terms(self): terms.append(term) else: self.constant += term.coefficient + return terms @property def matrix(self): diff --git a/src/qibo/hamiltonians/models.py b/src/qibo/hamiltonians/models.py index 1e4f3ccca5..703200048f 100644 --- a/src/qibo/hamiltonians/models.py +++ b/src/qibo/hamiltonians/models.py @@ -210,14 +210,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, backend) + double_term = _build_spin_model( + nqubits, backend.cast(pauli(0).matrix), condition, backend + ) double_term = backend.cast(double_term, dtype=double_term.dtype) matrix = matrix - coupling_constants[ind] * double_term matrix = ( @@ -432,10 +434,12 @@ def _build_spin_model(nqubits, matrix, condition, backend): ] einsum_args = [item for pair in zip(columns, lhs) for item in pair] dim = 2**nqubits - if backend.platform != "tensorflow": - h = backend.np.einsum(*einsum_args, rhs) - else: + if backend.platform == "tensorflow": h = np.einsum(*einsum_args, rhs) + elif backend.platform == "cupy": + h = backend.cp.einsum(*einsum_args, rhs) + else: + h = backend.np.einsum(*einsum_args, rhs) h = backend.np.sum(backend.np.reshape(h, (nqubits, dim, dim)), axis=0) # h = sum( # _multikron(matrix if condition(i, j) else matrices.I for j in range(nqubits)) @@ -449,7 +453,9 @@ def _OneBodyPauli(nqubits, operator, dense: bool = True, backend=None): :math:`X`, :math:`Y`, and :math:`Z` Hamiltonians.""" if dense: condition = lambda i, j: i == j % nqubits - ham = -_build_spin_model(nqubits, operator(0).matrix, condition, backend) + ham = -_build_spin_model( + nqubits, backend.cast(operator(0).matrix), condition, backend + ) return Hamiltonian(nqubits, ham, backend=backend) # matrix = -matrix From 068993823e41f2bc2751feb96c76a66d619f7875 Mon Sep 17 00:00:00 2001 From: BrunoLiegiBastonLiegi Date: Wed, 18 Dec 2024 20:53:23 +0100 Subject: [PATCH 05/25] fix: some further fixes --- src/qibo/hamiltonians/hamiltonians.py | 16 +--------------- src/qibo/hamiltonians/models.py | 7 +++---- tests/test_hamiltonians.py | 14 +++++++------- 3 files changed, 11 insertions(+), 26 deletions(-) diff --git a/src/qibo/hamiltonians/hamiltonians.py b/src/qibo/hamiltonians/hamiltonians.py index b6a6e35e2c..9c8abe111f 100644 --- a/src/qibo/hamiltonians/hamiltonians.py +++ b/src/qibo/hamiltonians/hamiltonians.py @@ -723,21 +723,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)) diff --git a/src/qibo/hamiltonians/models.py b/src/qibo/hamiltonians/models.py index 703200048f..ad442684b0 100644 --- a/src/qibo/hamiltonians/models.py +++ b/src/qibo/hamiltonians/models.py @@ -91,10 +91,9 @@ def TFIM(nqubits, h: float = 0.0, dense: bool = True, backend=None): ) return Hamiltonian(nqubits, ham, backend=backend) - term = lambda q1, q2: symbols.Z(q1) * symbols.Z(q2) + h * symbols.X(q1) * symbols.I( - q2 - ) - form = sum(term(i, i + 1) for i in range(nqubits - 1)) + term = lambda q1, q2: symbols.Z(q1) * symbols.Z(q2) + h * symbols.X(q1) + form = sum(term(i, i + 1) for i in range(nqubits - 1)) + term(nqubits - 1, 0) + print(form) # matrix = -( # _multikron([backend.matrices.Z, backend.matrices.Z], backend) + h * _multikron([backend.matrices.X, backend.matrices.I], backend) # ) diff --git a/tests/test_hamiltonians.py b/tests/test_hamiltonians.py index e66c824da2..5d96ac464c 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) From 6fb5270bb823df51468f7c8a50d880f49227622e Mon Sep 17 00:00:00 2001 From: BrunoLiegiBastonLiegi Date: Thu, 19 Dec 2024 09:30:22 +0100 Subject: [PATCH 06/25] fix: fixing the form of some H models --- src/qibo/hamiltonians/models.py | 9 ++---- tests/test_hamiltonians_symbolic.py | 4 +-- tests/test_hamiltonians_trotter.py | 49 ----------------------------- 3 files changed, 5 insertions(+), 57 deletions(-) diff --git a/src/qibo/hamiltonians/models.py b/src/qibo/hamiltonians/models.py index ad442684b0..8707412cd5 100644 --- a/src/qibo/hamiltonians/models.py +++ b/src/qibo/hamiltonians/models.py @@ -92,8 +92,7 @@ def TFIM(nqubits, h: float = 0.0, dense: bool = True, backend=None): return Hamiltonian(nqubits, ham, backend=backend) term = lambda q1, q2: symbols.Z(q1) * symbols.Z(q2) + h * symbols.X(q1) - form = sum(term(i, i + 1) for i in range(nqubits - 1)) + term(nqubits - 1, 0) - print(form) + form = -1 * sum(term(i, i + 1) for i in range(nqubits - 1)) - term(nqubits - 1, 0) # matrix = -( # _multikron([backend.matrices.Z, backend.matrices.Z], backend) + h * _multikron([backend.matrices.X, backend.matrices.I], backend) # ) @@ -229,18 +228,16 @@ def Heisenberg( return Hamiltonian(nqubits, matrix, backend=backend) - paulis = (symbols.X, symbols.Y, symbols.Z) - def h(symbol): return lambda q1, q2: symbol(q1) * symbol(q2) def term(q1, q2): - return -1 * sum( + return sum( coeff * h(operator)(q1, q2) for coeff, operator in zip(coupling_constants, paulis) ) - form = sum(term(i, i + 1) for i in range(nqubits - 1)) + 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) diff --git a/tests/test_hamiltonians_symbolic.py b/tests/test_hamiltonians_symbolic.py index 0a0f225728..a3b82d7b66 100644 --- a/tests/test_hamiltonians_symbolic.py +++ b/tests/test_hamiltonians_symbolic.py @@ -351,8 +351,8 @@ 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 + # with pytest.raises(NotImplementedError): + # h = h1 @ h2 def test_symbolic_hamiltonian_with_constant(backend): diff --git a/tests/test_hamiltonians_trotter.py b/tests/test_hamiltonians_trotter.py index 0c2d2f1756..b63b3a0814 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)) From eceb4130a658f7a0c486549b6c3abee87f7b8df7 Mon Sep 17 00:00:00 2001 From: BrunoLiegiBastonLiegi Date: Thu, 19 Dec 2024 10:26:09 +0100 Subject: [PATCH 07/25] fix: some fixes to _compose --- src/qibo/hamiltonians/hamiltonians.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/qibo/hamiltonians/hamiltonians.py b/src/qibo/hamiltonians/hamiltonians.py index d844a85ac8..6e591dc675 100644 --- a/src/qibo/hamiltonians/hamiltonians.py +++ b/src/qibo/hamiltonians/hamiltonians.py @@ -652,9 +652,8 @@ def expectation_from_samples(self, freq: dict, qubit_map: list = None) -> float: return self.backend.np.sum(expvals @ counts.T) + self.constant.real def _compose(self, o, operator): - new_ham = self.__class__( - form=self._form, symbol_map=dict(self.symbol_map), backend=self.backend - ) + form = self._form + symbol_map = self.symbol_map if isinstance(o, self.__class__): if self.nqubits != o.nqubits: @@ -664,22 +663,20 @@ def _compose(self, o, operator): ) if o._form is not None: - new_ham.symbol_map.update(o.symbol_map) - new_ham.form = ( - operator(self._form, o._form) if self._form is not None else o._form - ) + symbol_map.update(o.symbol_map) + form = operator(form, o._form) if form is not None else o._form elif isinstance(o, (self.backend.numeric_types, self.backend.tensor_types)): - new_ham.form = ( - operator(self._form, o) if self._form is not None else complex(o) - ) + form = operator(form, complex(o)) if form is not None else complex(o) else: raise_error( NotImplementedError, f"SymbolicHamiltonian composition to {type(o)} not implemented.", ) - return new_ham + return self.__class__( + form=form, symbol_map=symbol_map, nqubits=self.nqubits, backend=self.backend + ) def __add__(self, o): return self._compose(o, lambda x, y: x + y) From f80f4028d59e810c604c7ae1632e19f6c208565f Mon Sep 17 00:00:00 2001 From: BrunoLiegiBastonLiegi Date: Thu, 19 Dec 2024 10:58:04 +0100 Subject: [PATCH 08/25] fix: add missing _check_backend --- doc/final_result.npy | Bin 0 -> 968 bytes src/qibo/hamiltonians/models.py | 2 ++ 2 files changed, 2 insertions(+) create mode 100644 doc/final_result.npy diff --git a/doc/final_result.npy b/doc/final_result.npy new file mode 100644 index 0000000000000000000000000000000000000000..38783b6eba11d6d233fb4575f34db51b7e1606a3 GIT binary patch literal 968 zcmbtSOK;Oa5O$ih1=I3=ziP}Q2?UcMfvO6rNRc=YHAR+?Oi^U5olU*Mui0ILqBIhh zsN~F_ZI{Md{e`6Hcut3ne3*ldL;;~JoBXv zTEWueVSz%Wr_ErgTk)WJ2sIm)8@)=iBn{;_r8=xQOPZ$bZo=yP$03}t;cTNbTZUQUX3H>H z%%n^BzQFwFcFX7t7RfK)%JjtKbe{=Hxmz}fvoJ)OpM)Vvg=#q})@yo1u)LMIr@HT% zs$JSfMp=_~%J(S(uEY^kx{!HH9{U0p*C%-r9)VOR*AIzML~}>AKcW*iE^z+{O-w#btpar=@R|2%>stwoBo?dMR z*Mq{9(I(tbuH3ZY*20xHaQnC`nuI&vyldcGJioV#=gA0nCEW97aNmXvXBF}C>%r(d zD|n4Xkf4hXc79czWxVJA2bj5_F5`a2q@7ELhfWP~U|s@`^3t~T((1f|$9bKQL{fON z&{kL48a6abZBJ2K70dn3Yj~!_wQP8f;)0X1ER^*Ysd!G8Rp6&bcp;(fWkf>9v;F~M Cj}1rw literal 0 HcmV?d00001 diff --git a/src/qibo/hamiltonians/models.py b/src/qibo/hamiltonians/models.py index 8707412cd5..f013e1bde2 100644 --- a/src/qibo/hamiltonians/models.py +++ b/src/qibo/hamiltonians/models.py @@ -81,6 +81,7 @@ 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, backend.matrices.Z, condition, backend) @@ -447,6 +448,7 @@ def _build_spin_model(nqubits, matrix, condition, backend): 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( From 74bc480e8f8d5e0b62c1617563045b51aa79bd9a Mon Sep 17 00:00:00 2001 From: BrunoLiegiBastonLiegi Date: Thu, 19 Dec 2024 12:29:30 +0100 Subject: [PATCH 09/25] feat: using indices in _dense_from_terms + check that form is sympy --- doc/final_result.npy | Bin 968 -> 0 bytes src/qibo/hamiltonians/hamiltonians.py | 54 +++++++++++++++++--------- src/qibo/hamiltonians/models.py | 2 - 3 files changed, 36 insertions(+), 20 deletions(-) delete mode 100644 doc/final_result.npy diff --git a/doc/final_result.npy b/doc/final_result.npy deleted file mode 100644 index 38783b6eba11d6d233fb4575f34db51b7e1606a3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 968 zcmbtSOK;Oa5O$ih1=I3=ziP}Q2?UcMfvO6rNRc=YHAR+?Oi^U5olU*Mui0ILqBIhh zsN~F_ZI{Md{e`6Hcut3ne3*ldL;;~JoBXv zTEWueVSz%Wr_ErgTk)WJ2sIm)8@)=iBn{;_r8=xQOPZ$bZo=yP$03}t;cTNbTZUQUX3H>H z%%n^BzQFwFcFX7t7RfK)%JjtKbe{=Hxmz}fvoJ)OpM)Vvg=#q})@yo1u)LMIr@HT% zs$JSfMp=_~%J(S(uEY^kx{!HH9{U0p*C%-r9)VOR*AIzML~}>AKcW*iE^z+{O-w#btpar=@R|2%>stwoBo?dMR z*Mq{9(I(tbuH3ZY*20xHaQnC`nuI&vyldcGJioV#=gA0nCEW97aNmXvXBF}C>%r(d zD|n4Xkf4hXc79czWxVJA2bj5_F5`a2q@7ELhfWP~U|s@`^3t~T((1f|$9bKQL{fON z&{kL48a6abZBJ2K70dn3Yj~!_wQP8f;)0X1ER^*Ysd!G8Rp6&bcp;(fWkf>9v;F~M Cj}1rw diff --git a/src/qibo/hamiltonians/hamiltonians.py b/src/qibo/hamiltonians/hamiltonians.py index 6e591dc675..0b42b2dc26 100644 --- a/src/qibo/hamiltonians/hamiltonians.py +++ b/src/qibo/hamiltonians/hamiltonians.py @@ -8,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 @@ -323,11 +324,22 @@ class SymbolicHamiltonian(AbstractHamiltonian): Defaults to ``None``. """ - def __init__(self, form=None, nqubits=None, symbol_map={}, backend=None): + def __init__( + self, + form: sympy.Expr, + nqubits: Optional[int] = None, + symbol_map: Optional[dict] = None, + backend: Optional[Backend] = None, + ): super().__init__() - self._form = 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.symbol_map = symbol_map + self.symbol_map = symbol_map if symbol_map is not None else {} # if a symbol in the given form is not a Qibo symbol it must be # included in the ``symbol_map`` @@ -335,15 +347,11 @@ def __init__(self, form=None, nqubits=None, symbol_map={}, backend=None): 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 - self.nqubits = ( - self._calculate_nqubits_from_form(form) if nqubits is None else nqubits - ) + self.nqubits = ( + self._calculate_nqubits_from_form(form) if nqubits is None else nqubits + ) @cached_property def dense(self) -> "MatrixHamiltonian": @@ -516,17 +524,27 @@ def _calculate_dense_from_terms(self) -> Hamiltonian: raise_error(NotImplementedError, "Not enough einsum characters.") matrix = 0 - chars = EINSUM_CHARS[: 2 * self.nqubits] + # chars = EINSUM_CHARS[: 2 * self.nqubits] + indices = list(range(2 * self.nqubits)) for term in self.terms: ntargets = len(term.target_qubits) tmat = 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,)) + # gen = lambda x: (chars[i + x] for i in term.target_qubits) + gen = lambda x: (indices[i + x] for i in term.target_qubits) + # tc = "".join(chain(gen(0), gen(self.nqubits))) + tc = list(chain(gen(0), gen(self.nqubits))) + # ec = "".join(c for c in chars if c not in tc) + ec = list(c for c in indices if c not in tc) + # matrix += np.einsum(f"{tc},{ec}->{chars}", tmat, emat) + matrix += np.einsum(tmat, tc, emat, ec, indices) + + matrix = ( + 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): @@ -537,8 +555,8 @@ 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() + # return self._calculate_dense_from_form() + return self._calculate_dense_from_terms() def expectation(self, state, normalize=False): return Hamiltonian.expectation(self, state, normalize) diff --git a/src/qibo/hamiltonians/models.py b/src/qibo/hamiltonians/models.py index f013e1bde2..be4593c696 100644 --- a/src/qibo/hamiltonians/models.py +++ b/src/qibo/hamiltonians/models.py @@ -433,8 +433,6 @@ def _build_spin_model(nqubits, matrix, condition, backend): dim = 2**nqubits if backend.platform == "tensorflow": h = np.einsum(*einsum_args, rhs) - elif backend.platform == "cupy": - h = backend.cp.einsum(*einsum_args, rhs) else: h = backend.np.einsum(*einsum_args, rhs) h = backend.np.sum(backend.np.reshape(h, (nqubits, dim, dim)), axis=0) From 6cc8435af939686c1c5097ad028adbb7bfe9a96b Mon Sep 17 00:00:00 2001 From: BrunoLiegiBastonLiegi Date: Thu, 19 Dec 2024 12:48:36 +0100 Subject: [PATCH 10/25] feat: made dense_from_terms backend aware + some comments --- src/qibo/hamiltonians/hamiltonians.py | 39 ++++++++++++++++----------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/src/qibo/hamiltonians/hamiltonians.py b/src/qibo/hamiltonians/hamiltonians.py index 0b42b2dc26..5c3df94783 100644 --- a/src/qibo/hamiltonians/hamiltonians.py +++ b/src/qibo/hamiltonians/hamiltonians.py @@ -335,7 +335,7 @@ def __init__( 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.", + 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`` @@ -436,6 +436,7 @@ def ground_state(self): def exp(self, a): return self.dense.exp(a) + # only useful for dense_from_form, which might not be needed in the end def _get_symbol_matrix(self, term): """Calculates numerical matrix corresponding to symbolic expression. @@ -509,6 +510,8 @@ def _get_symbol_matrix(self, term): return result + # not sure this is useful, it appears to be significantly slower than + # the from_terms counterpart def _calculate_dense_from_form(self) -> Hamiltonian: """Calculates equivalent Hamiltonian using symbolic form. @@ -519,35 +522,41 @@ def _calculate_dense_from_form(self) -> Hamiltonian: 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.") - matrix = 0 - # chars = EINSUM_CHARS[: 2 * self.nqubits] indices = list(range(2 * self.nqubits)) + # most likely the looped einsum could be avoided by preparing all the + # matrices first and performing a single einsum in the end with a suitable + # choice of indices for term in self.terms: ntargets = len(term.target_qubits) - tmat = np.reshape(term.matrix, 2 * ntargets * (2,)) + # I have to cast to a backend array because SymbolicTerm does not support + # backend declaration and just works with numpy, might be worth implementing + # a SymbolicTerm.matrix(backend=None) method that returns the matrix in the + # desired backend type and defaults to numpy or GlobalBackend + # A similar argument holds for qibo Symbols + tmat = self.backend.np.reshape( + self.backend.cast(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) + 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 = "".join(chain(gen(0), gen(self.nqubits))) tc = list(chain(gen(0), gen(self.nqubits))) - # ec = "".join(c for c in chars if c not in tc) ec = list(c for c in indices if c not in tc) - # matrix += np.einsum(f"{tc},{ec}->{chars}", tmat, emat) - matrix += np.einsum(tmat, tc, emat, ec, indices) + if self.backend.platform == "tensorflow": + matrix += np.einsum(tmat, tc, emat, ec, indices) + else: + matrix += self.backend.np.einsum(tmat, tc, emat, ec, indices) matrix = ( - np.reshape(matrix, 2 * (2**self.nqubits,)) + 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): + def calculate_dense(self) -> Hamiltonian: log.warning( "Calculating the dense form of a symbolic Hamiltonian. " "This operation is memory inefficient." From 977d8b53113ca6bb608fa8a7ca74d50033db50e9 Mon Sep 17 00:00:00 2001 From: BrunoLiegiBastonLiegi Date: Fri, 10 Jan 2025 10:34:51 +0100 Subject: [PATCH 11/25] feat: added backend to symbols + refactor maxcut --- src/qibo/hamiltonians/#hamiltonians.py# | 804 ++++++++++++++++++++++++ src/qibo/hamiltonians/.#hamiltonians.py | 1 + src/qibo/hamiltonians/hamiltonians.py | 3 +- src/qibo/hamiltonians/models.py | 66 +- src/qibo/symbols.py | 32 +- 5 files changed, 854 insertions(+), 52 deletions(-) create mode 100644 src/qibo/hamiltonians/#hamiltonians.py# create mode 120000 src/qibo/hamiltonians/.#hamiltonians.py diff --git a/src/qibo/hamiltonians/#hamiltonians.py# b/src/qibo/hamiltonians/#hamiltonians.py# new file mode 100644 index 0000000000..c2f1e4a150 --- /dev/null +++ b/src/qibo/hamiltonians/#hamiltonians.py# @@ -0,0 +1,804 @@ +"""Module defining Hamiltonian classes.""" + +from functools import cached_property +from itertools import chain +from math import prod +from typing import Optional + +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 + + +class Hamiltonian(AbstractHamiltonian): + """Hamiltonian based on a dense or sparse matrix representation. + + Args: + nqubits (int): number of quantum bits. + matrix (ndarray): Matrix representation of the Hamiltonian in the + computational basis as an array of shape :math:`2^{n} \\times 2^{n}`. + Sparse matrices based on ``scipy.sparse`` for ``numpy`` / ``qibojit`` backends + (or on ``tf.sparse`` for the ``tensorflow`` backend) are also supported. + 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, nqubits, matrix, backend=None): + from qibo.backends import _check_backend + + self.backend = _check_backend(backend) + + if not ( + isinstance(matrix, self.backend.tensor_types) + or self.backend.is_sparse(matrix) + ): + raise_error( + TypeError, + f"Matrix of invalid type {type(matrix)} given during Hamiltonian initialization", + ) + matrix = self.backend.cast(matrix) + + super().__init__() + self.nqubits = nqubits + self.matrix = matrix + self._eigenvalues = None + self._eigenvectors = None + self._exp = {"a": None, "result": None} + + @property + def matrix(self): + """Returns the full matrix representation. + + For :math:`n` qubits, can be a dense :math:`2^{n} \\times 2^{n}` array or a sparse + matrix, depending on how the Hamiltonian was created. + """ + return self._matrix + + @matrix.setter + def matrix(self, mat): + shape = tuple(mat.shape) + if shape != 2 * (2**self.nqubits,): + raise_error( + ValueError, + f"The Hamiltonian is defined for {self.nqubits} qubits " + + f"while the given matrix has shape {shape}.", + ) + 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) + return self._eigenvalues + + def eigenvectors(self, k=6): + if self._eigenvectors is None: + self._eigenvalues, self._eigenvectors = self.backend.calculate_eigenvectors( + self.matrix, k + ) + return self._eigenvectors + + def exp(self, a): + from qibo.quantum_info.linalg_operations import ( # pylint: disable=C0415 + matrix_exponentiation, + ) + + if self._exp.get("a") != a: + self._exp["a"] = a + self._exp["result"] = matrix_exponentiation( + a, self.matrix, self._eigenvectors, self._eigenvalues, self.backend + ) + return self._exp.get("result") + + def expectation(self, state, normalize=False): + if isinstance(state, self.backend.tensor_types): + state = self.backend.cast(state) + shape = tuple(state.shape) + if len(shape) == 1: # state vector + return self.backend.calculate_expectation_state(self, state, normalize) + + if len(shape) == 2: # density matrix + return self.backend.calculate_expectation_density_matrix( + self, state, normalize + ) + + raise_error( + ValueError, + "Cannot calculate Hamiltonian expectation value " + + f"for state of shape {shape}", + ) + + raise_error( + TypeError, + "Cannot calculate Hamiltonian expectation " + + f"value for state of type {type(state)}", + ) + + def expectation_from_samples(self, freq, qubit_map=None): + obs = self.matrix + if ( + self.backend.np.count_nonzero( + obs - self.backend.np.diag(self.backend.np.diagonal(obs)) + ) + != 0 + ): + raise_error( + NotImplementedError, + "Observable is not diagonal. Expectation of non diagonal observables starting from samples is currently supported for `qibo.hamiltonians.hamiltonians.SymbolicHamiltonian` only.", + ) + keys = list(freq.keys()) + if qubit_map is None: + qubit_map = list(range(int(np.log2(len(obs))))) + counts = np.array(list(freq.values())) / sum(freq.values()) + expval = 0 + size = len(qubit_map) + for j, k in enumerate(keys): + index = 0 + for i in qubit_map: + index += int(k[qubit_map.index(i)]) * 2 ** (size - 1 - i) + expval += obs[index, index] * counts[j] + return self.backend.np.real(expval) + + def eye(self, dim: Optional[int] = None): + """Generate Identity matrix with dimension ``dim``""" + if dim is None: + dim = int(self.matrix.shape[0]) + return self.backend.cast(self.backend.matrices.I(dim), dtype=self.matrix.dtype) + + def energy_fluctuation(self, state): + """ + Evaluate energy fluctuation: + + .. math:: + \\Xi_{k}(\\mu) = \\sqrt{\\bra{\\mu} \\, H^{2} \\, \\ket{\\mu} + - \\bra{\\mu} \\, H \\, \\ket{\\mu}^2} \\, . + + for a given state :math:`\\ket{\\mu}`. + + Args: + state (ndarray): quantum state to be used to compute the energy fluctuation. + + Returns: + float: Energy fluctuation value. + """ + state = self.backend.cast(state) + energy = self.expectation(state) + h = self.matrix + h2 = Hamiltonian(nqubits=self.nqubits, matrix=h @ h, backend=self.backend) + average_h2 = self.backend.calculate_expectation_state(h2, state, normalize=True) + return self.backend.np.sqrt(self.backend.np.abs(average_h2 - energy**2)) + + def __add__(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 added.", + ) + new_matrix = self.matrix + o.matrix + elif isinstance(o, self.backend.numeric_types): + new_matrix = self.matrix + o * self.eye() + else: + raise_error( + NotImplementedError, + f"Hamiltonian addition to {type(o)} not implemented.", + ) + return self.__class__(self.nqubits, new_matrix, backend=self.backend) + + 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_matrix = self.matrix - o.matrix + elif isinstance(o, self.backend.numeric_types): + new_matrix = self.matrix - o * self.eye() + else: + raise_error( + NotImplementedError, + f"Hamiltonian subtraction to {type(o)} not implemented.", + ) + return self.__class__(self.nqubits, new_matrix, backend=self.backend) + + def __rsub__(self, o): + if isinstance(o, self.__class__): # pragma: no cover + # impractical case because it will be handled by `__sub__` + if self.nqubits != o.nqubits: + raise_error( + RuntimeError, + "Only hamiltonians with the same number of qubits can be added.", + ) + new_matrix = o.matrix - self.matrix + elif isinstance(o, self.backend.numeric_types): + new_matrix = o * self.eye() - self.matrix + else: + raise_error( + NotImplementedError, + f"Hamiltonian subtraction to {type(o)} not implemented.", + ) + return self.__class__(self.nqubits, new_matrix, backend=self.backend) + + def __mul__(self, o): + if isinstance(o, self.backend.tensor_types): + o = complex(o) + elif not isinstance(o, self.backend.numeric_types): + raise_error( + NotImplementedError, + f"Hamiltonian multiplication to {type(o)} not implemented.", + ) + new_matrix = self.matrix * o + r = self.__class__(self.nqubits, new_matrix, backend=self.backend) + o = self.backend.cast(o) + if self._eigenvalues is not None: + if self.backend.np.real(o) >= 0: # TODO: check for side effects K.qnp + r._eigenvalues = o * self._eigenvalues + elif not self.backend.is_sparse(self.matrix): + axis = (0,) if (self.backend.platform == "pytorch") else 0 + r._eigenvalues = o * self.backend.np.flip(self._eigenvalues, axis) + if self._eigenvectors is not None: + if self.backend.np.real(o) > 0: # TODO: see above + r._eigenvectors = self._eigenvectors + elif o == 0: + r._eigenvectors = self.eye(int(self._eigenvectors.shape[0])) + return r + + def __matmul__(self, o): + if isinstance(o, self.__class__): + matrix = self.backend.calculate_hamiltonian_matrix_product( + self.matrix, o.matrix + ) + return self.__class__(self.nqubits, matrix, backend=self.backend) + + if isinstance(o, self.backend.tensor_types): + return self.backend.calculate_hamiltonian_state_product(self.matrix, o) + + raise_error( + NotImplementedError, + f"Hamiltonian matmul to {type(o)} not implemented.", + ) + + +class SymbolicHamiltonian(AbstractHamiltonian): + """Hamiltonian based on a symbolic representation. + + Calculations using symbolic Hamiltonians are either done directly using + the given ``sympy`` expression as it is (``form``) or by parsing the + corresponding ``terms`` (which are :class:`qibo.core.terms.SymbolicTerm` + objects). The latter approach is more computationally costly as it uses + a ``sympy.expand`` call on the given form before parsing the terms. + For this reason the ``terms`` are calculated only when needed, for example + during Trotterization. + The dense matrix of the symbolic Hamiltonian can be calculated directly + from ``form`` without requiring ``terms`` calculation (see + :meth:`qibo.core.hamiltonians.SymbolicHamiltonian.calculate_dense` for details). + + Args: + form (sympy.Expr): Hamiltonian form as a ``sympy.Expr``. Ideally the + 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: sympy.Expr, + nqubits: Optional[int] = None, + symbol_map: Optional[dict] = None, + backend: Optional[Backend] = None, + ): + super().__init__() + 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.symbol_map = symbol_map if symbol_map is not None else {} + # 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`` + + self.backend = _check_backend(backend) + + self.nqubits = ( + self._calculate_nqubits_from_form(form) if nqubits is None else nqubits + ) + + @cached_property + def dense(self) -> "MatrixHamiltonian": + """Creates the equivalent Hamiltonian matrix.""" + 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 + 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 + return nqubits + 1 + + @form.setter + def form(self, form): + # Check that given form is a ``sympy`` expression + if not isinstance(form, sympy.Expr): + raise_error( + TypeError, + f"Symbolic Hamiltonian should be a ``sympy`` expression but is {type(form)}.", + ) + self._form = form + self.nqubits = self._calculate_nqubits_from_form(form) + + @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`. + """ + # Calculate terms based on ``self.form`` + from qibo.hamiltonians.terms import ( # pylint: disable=import-outside-toplevel + SymbolicTerm, + ) + + self.constant = 0.0 + + 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 + return terms + + @property + def matrix(self): + """Returns the full matrix representation. + + Consisting of :math:`2^{n} \\times 2^{n}`` elements. + """ + return self.dense.matrix + + def eigenvalues(self, k=6): + return self.dense.eigenvalues(k) + + def eigenvectors(self, k=6): + return self.dense.eigenvectors(k) + + def ground_state(self): + return self.eigenvectors()[:, 0] + + def exp(self, a): + return self.dense.exp(a) + + # only useful for dense_from_form, which might not be needed in the end + def _get_symbol_matrix(self, term): + """Calculates numerical matrix corresponding to symbolic expression. + + This is partly equivalent to sympy's ``.subs``, which does not work + in our case as it does not allow us to substitute ``sympy.Symbol`` + with numpy arrays and there are different complication when switching + to ``sympy.MatrixSymbol``. Here we calculate the full numerical matrix + given the symbolic expression using recursion. + Helper method for ``_calculate_dense_from_form``. + + Args: + term (sympy.Expr): Symbolic expression containing local operators. + + Returns: + ndarray: matrix corresponding to the given expression as an array + of shape ``(2 ** self.nqubits, 2 ** self.nqubits)``. + """ + if isinstance(term, sympy.Add): + # symbolic op for addition + result = sum( + self._get_symbol_matrix(subterm) for subterm in term.as_ordered_terms() + ) + + elif isinstance(term, sympy.Mul): + # symbolic op for multiplication + # 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) + + 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) + + 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) + + else: + raise_error( + TypeError, + f"Cannot calculate matrix for symbolic term of type {type(term)}.", + ) + + return result + + # not sure this is useful, it appears to be significantly slower than + # the from_terms counterpart + def _calculate_dense_from_form(self) -> Hamiltonian: + """Calculates equivalent Hamiltonian using symbolic form. + + Useful when the term representation is not available. + """ + 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.""" + matrix = 0 + indices = list(range(2 * self.nqubits)) + # most likely the looped einsum could be avoided by preparing all the + # matrices first and performing a single einsum in the end with a suitable + # choice of indices + for term in self.terms: + ntargets = len(term.target_qubits) + # I have to cast to a backend array because SymbolicTerm does not support + # backend declaration and just works with numpy, might be worth implementing + # a SymbolicTerm.matrix(backend=None) method that returns the matrix in the + # desired backend type and defaults to numpy or GlobalBackend + # A similar argument holds for qibo Symbols + tmat = self.backend.np.reshape( + self.backend.cast(term.matrix), 2 * ntargets * (2,) + ) + n = self.nqubits - ntargets + 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) + if self.backend.platform == "tensorflow": + matrix += np.einsum(tmat, tc, emat, ec, indices) + else: + matrix += self.backend.np.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) -> 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) + + def expectation_from_circuit(self, circuit: "Circuit", nshots: int = 1000) -> float: + """ + Calculate the expectation value from a circuit. + This even works for observables not completely diagonal in the computational + basis, but only diagonal at a term level in a defined basis. Namely, for + an observable of the form :math:``H = \\sum_i H_i``, where each :math:``H_i`` + consists in a `n`-qubits pauli operator :math:`P_0 \\otimes P_1 \\otimes \\cdots \\otimes P_n`, + the expectation value is computed by rotating the input circuit in the suitable + basis for each term :math:``H_i`` thus extracting the `term-wise` expectations + that are then summed to build the global expectation value. + Each term of the observable is treated separately, by measuring in the correct + basis and re-executing the circuit. + + Args: + circuit (Circuit): input circuit. + nshots (int): number of shots, defaults to 1000. + + Returns: + (float): the calculated expectation value. + """ + from qibo import gates + + rotated_circuits = [] + coefficients = [] + Z_observables = [] + qubit_maps = [] + for term in self.terms: + # store coefficient + coefficients.append(term.coefficient) + # Only care about non-I terms + non_identity_factors = [ + factor for factor in term.factors if factor.name[0] != "I" + ] + # build diagonal observable + Z_observables.append( + SymbolicHamiltonian( + prod(Z(factor.target_qubit) for factor in non_identity_factors), + nqubits=circuit.nqubits, + backend=self.backend, + ) + ) + # Get the qubits we want to measure for each term + qubit_map = sorted(factor.target_qubit for factor in non_identity_factors) + # prepare the measurement basis and append it to the circuit + measurements = [ + gates.M(factor.target_qubit, basis=factor.gate.__class__) + for factor in non_identity_factors + ] + circ_copy = circuit.copy(True) + circ_copy.add(measurements) + rotated_circuits.append(circ_copy) + # for mapping the obtained sample frequencies to the original qubits + qubit_maps.append(qubit_map) + frequencies = [ + result.frequencies() + for result in self.backend.execute_circuits(rotated_circuits, nshots=nshots) + ] + return sum( + coeff * obs.expectation_from_samples(freq, qubit_map) + for coeff, freq, obs, qubit_map in zip( + coefficients, frequencies, Z_observables, qubit_maps + ) + ) + + def expectation_from_samples(self, freq: dict, qubit_map: list = None) -> float: + """ + Calculate the expectation value from the samples. + The observable has to be diagonal in the computational basis. + + Args: + freq (dict): input frequencies of the samples. + qubit_map (list): qubit map. + + Returns: + (float): the calculated expectation value. + """ + for term in self.terms: + # pylint: disable=E1101 + for factor in term.factors: + if not isinstance(factor, Z): + raise_error( + NotImplementedError, "Observable is not a Z Pauli string." + ) + + if qubit_map is None: + qubit_map = list(range(self.nqubits)) + + keys = list(freq.keys()) + counts = self.backend.cast(list(freq.values()), self.backend.precision) / sum( + freq.values() + ) + expvals = [] + for term in self.terms: + qubits = { + factor.target_qubit for factor in term.factors if factor.name[0] != "I" + } + expvals.extend( + [ + term.coefficient.real + * (-1) ** [state[qubit_map.index(q)] for q in qubits].count("1") + for state in keys + ] + ) + expvals = self.backend.cast(expvals, dtype=counts.dtype).reshape( + len(self.terms), len(freq) + ) + return self.backend.np.sum(expvals @ counts.T) + self.constant.real + + def _compose(self, o, operator): + form = self._form + symbol_map = self.symbol_map + + if isinstance(o, self.__class__): + if self.nqubits != o.nqubits: + raise_error( + RuntimeError, + "Only hamiltonians with the same number of qubits can be composed.", + ) + + if o._form is not None: + symbol_map.update(o.symbol_map) + 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 composition to {type(o)} not implemented.", + ) + + return self.__class__( + form=form, symbol_map=symbol_map, nqubits=self.nqubits, backend=self.backend + ) + + def __add__(self, o): + return self._compose(o, lambda x, y: x + y) + + def __sub__(self, o): + return self._compose(o, lambda x, y: x - y) + + def __rsub__(self, o): + return self._compose(o, lambda x, y: y - x) + + def __mul__(self, o): + # o = complex(o) + return self._compose(o, lambda x, y: y * x) + + def apply_gates(self, state, density_matrix=False): + """Applies gates corresponding to the Hamiltonian terms. + + Gates are applied to the given state. + + Helper method for :meth:`qibo.hamiltonians.SymbolicHamiltonian.__matmul__`. + """ + total = 0 + for term in self.terms: + total += term( + self.backend, + self.backend.cast(state, copy=True), + self.nqubits, + density_matrix=density_matrix, + ) + if self.constant: # pragma: no cover + total += self.constant * state + return total + + def __matmul__(self, o): + """Matrix multiplication with other Hamiltonians or state vectors.""" + if isinstance(o, self.__class__): + return o * self + + if isinstance(o, self.backend.tensor_types): + rank = len(tuple(o.shape)) + if rank not in (1, 2): + raise_error( + NotImplementedError, + f"Cannot multiply Hamiltonian with rank-{rank} tensor.", + ) + state_qubits = int(np.log2(int(o.shape[0]))) + if state_qubits != self.nqubits: + raise_error( + ValueError, + f"Cannot multiply Hamiltonian on {self.nqubits} qubits to " + + f"state of {state_qubits} qubits.", + ) + if rank == 1: # state vector + return self.apply_gates(o) + + return self.apply_gates(o, density_matrix=True) + + raise_error( + NotImplementedError, + f"Hamiltonian matmul to {type(o)} not implemented.", + ) + + def circuit(self, dt, accelerators=None): + """Circuit that implements a Trotter step of this Hamiltonian. + + Args: + dt (float): Time step used for Trotterization. + accelerators (dict, optional): Dictionary with accelerators for distributed circuits. + Defaults to ``None``. + """ + from qibo import Circuit # pylint: disable=import-outside-toplevel + from qibo.hamiltonians.terms import ( # pylint: disable=import-outside-toplevel + TermGroup, + ) + + groups = TermGroup.from_terms(self.terms) + circuit = Circuit(self.nqubits, accelerators=accelerators) + circuit.add( + group.term.expgate(dt / 2.0) for group in chain(groups, groups[::-1]) + ) + + 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/.#hamiltonians.py b/src/qibo/hamiltonians/.#hamiltonians.py new file mode 120000 index 0000000000..977a90663a --- /dev/null +++ b/src/qibo/hamiltonians/.#hamiltonians.py @@ -0,0 +1 @@ +andrea@MacBook-Pro-3.local.11311 \ No newline at end of file diff --git a/src/qibo/hamiltonians/hamiltonians.py b/src/qibo/hamiltonians/hamiltonians.py index 5c3df94783..6594a1db8e 100644 --- a/src/qibo/hamiltonians/hamiltonians.py +++ b/src/qibo/hamiltonians/hamiltonians.py @@ -564,7 +564,8 @@ def calculate_dense(self) -> Hamiltonian: # 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() + if len(self.terms) > 40: + return self._calculate_dense_from_form() return self._calculate_dense_from_terms() def expectation(self, state, normalize=False): diff --git a/src/qibo/hamiltonians/models.py b/src/qibo/hamiltonians/models.py index be4593c696..3826a8d140 100644 --- a/src/qibo/hamiltonians/models.py +++ b/src/qibo/hamiltonians/models.py @@ -1,5 +1,5 @@ from functools import reduce -from typing import Union +from typing import Optional, Union import numpy as np @@ -94,17 +94,16 @@ def TFIM(nqubits, h: float = 0.0, dense: bool = True, backend=None): term = lambda q1, q2: symbols.Z(q1) * symbols.Z(q2) + h * symbols.X(q1) form = -1 * sum(term(i, i + 1) for i in range(nqubits - 1)) - term(nqubits - 1, 0) - # matrix = -( - # _multikron([backend.matrices.Z, backend.matrices.Z], backend) + h * _multikron([backend.matrices.X, backend.matrices.I], backend) - # ) - # terms = [HamiltonianTerm(matrix, i, i + 1) for i in range(nqubits - 1)] - # terms.append(HamiltonianTerm(matrix, nqubits - 1, 0)) ham = SymbolicHamiltonian(form=form, nqubits=nqubits, backend=backend) - # ham.terms = terms 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=None, +): """Max Cut Hamiltonian. .. math:: @@ -115,26 +114,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 @@ -246,33 +248,7 @@ def term(q1, q2): if field_strength != 0.0 ) - """ - 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 - ) - - terms = [HamiltonianTerm(matrix, i, i + 1) for i in range(nqubits - 1)] - terms.append(HamiltonianTerm(matrix, nqubits - 1, 0)) - - 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 - ] - ) - """ - ham = SymbolicHamiltonian(form=form, backend=backend) - # ham.terms = terms - return ham diff --git a/src/qibo/symbols.py b/src/qibo/symbols.py index edb03eb677..4dbb7e28f6 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 @@ -42,7 +44,14 @@ def __new__(cls, q, matrix=None, name="Symbol", commutative=False, **assumptions 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=None, + name="Symbol", + commutative=False, + backend: Optional[Backend] = None, + ): self.target_qubit = q self._gate = None if not ( @@ -64,7 +73,8 @@ 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 + self.backend = _check_backend(backend) 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,7 +121,7 @@ def full_matrix(self, nqubits): """ from qibo.hamiltonians.models import _multikron - matrix_list = self.target_qubit * [matrices.I] + matrix_list = self.target_qubit * [backend.matrices.I] matrix_list.append(self.matrix) n = nqubits - self.target_qubit - 1 matrix_list.extend(matrices.I for _ in range(n)) @@ -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__ From febf76b969952605164ca59596f30aad2afbe212 Mon Sep 17 00:00:00 2001 From: BrunoLiegiBastonLiegi Date: Fri, 10 Jan 2025 11:18:21 +0100 Subject: [PATCH 12/25] fix: removed cache files --- src/qibo/hamiltonians/#hamiltonians.py# | 804 ------------------------ src/qibo/hamiltonians/.#hamiltonians.py | 1 - src/qibo/hamiltonians/hamiltonians.py | 5 +- src/qibo/hamiltonians/models.py | 4 +- 4 files changed, 5 insertions(+), 809 deletions(-) delete mode 100644 src/qibo/hamiltonians/#hamiltonians.py# delete mode 120000 src/qibo/hamiltonians/.#hamiltonians.py diff --git a/src/qibo/hamiltonians/#hamiltonians.py# b/src/qibo/hamiltonians/#hamiltonians.py# deleted file mode 100644 index c2f1e4a150..0000000000 --- a/src/qibo/hamiltonians/#hamiltonians.py# +++ /dev/null @@ -1,804 +0,0 @@ -"""Module defining Hamiltonian classes.""" - -from functools import cached_property -from itertools import chain -from math import prod -from typing import Optional - -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 - - -class Hamiltonian(AbstractHamiltonian): - """Hamiltonian based on a dense or sparse matrix representation. - - Args: - nqubits (int): number of quantum bits. - matrix (ndarray): Matrix representation of the Hamiltonian in the - computational basis as an array of shape :math:`2^{n} \\times 2^{n}`. - Sparse matrices based on ``scipy.sparse`` for ``numpy`` / ``qibojit`` backends - (or on ``tf.sparse`` for the ``tensorflow`` backend) are also supported. - 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, nqubits, matrix, backend=None): - from qibo.backends import _check_backend - - self.backend = _check_backend(backend) - - if not ( - isinstance(matrix, self.backend.tensor_types) - or self.backend.is_sparse(matrix) - ): - raise_error( - TypeError, - f"Matrix of invalid type {type(matrix)} given during Hamiltonian initialization", - ) - matrix = self.backend.cast(matrix) - - super().__init__() - self.nqubits = nqubits - self.matrix = matrix - self._eigenvalues = None - self._eigenvectors = None - self._exp = {"a": None, "result": None} - - @property - def matrix(self): - """Returns the full matrix representation. - - For :math:`n` qubits, can be a dense :math:`2^{n} \\times 2^{n}` array or a sparse - matrix, depending on how the Hamiltonian was created. - """ - return self._matrix - - @matrix.setter - def matrix(self, mat): - shape = tuple(mat.shape) - if shape != 2 * (2**self.nqubits,): - raise_error( - ValueError, - f"The Hamiltonian is defined for {self.nqubits} qubits " - + f"while the given matrix has shape {shape}.", - ) - 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) - return self._eigenvalues - - def eigenvectors(self, k=6): - if self._eigenvectors is None: - self._eigenvalues, self._eigenvectors = self.backend.calculate_eigenvectors( - self.matrix, k - ) - return self._eigenvectors - - def exp(self, a): - from qibo.quantum_info.linalg_operations import ( # pylint: disable=C0415 - matrix_exponentiation, - ) - - if self._exp.get("a") != a: - self._exp["a"] = a - self._exp["result"] = matrix_exponentiation( - a, self.matrix, self._eigenvectors, self._eigenvalues, self.backend - ) - return self._exp.get("result") - - def expectation(self, state, normalize=False): - if isinstance(state, self.backend.tensor_types): - state = self.backend.cast(state) - shape = tuple(state.shape) - if len(shape) == 1: # state vector - return self.backend.calculate_expectation_state(self, state, normalize) - - if len(shape) == 2: # density matrix - return self.backend.calculate_expectation_density_matrix( - self, state, normalize - ) - - raise_error( - ValueError, - "Cannot calculate Hamiltonian expectation value " - + f"for state of shape {shape}", - ) - - raise_error( - TypeError, - "Cannot calculate Hamiltonian expectation " - + f"value for state of type {type(state)}", - ) - - def expectation_from_samples(self, freq, qubit_map=None): - obs = self.matrix - if ( - self.backend.np.count_nonzero( - obs - self.backend.np.diag(self.backend.np.diagonal(obs)) - ) - != 0 - ): - raise_error( - NotImplementedError, - "Observable is not diagonal. Expectation of non diagonal observables starting from samples is currently supported for `qibo.hamiltonians.hamiltonians.SymbolicHamiltonian` only.", - ) - keys = list(freq.keys()) - if qubit_map is None: - qubit_map = list(range(int(np.log2(len(obs))))) - counts = np.array(list(freq.values())) / sum(freq.values()) - expval = 0 - size = len(qubit_map) - for j, k in enumerate(keys): - index = 0 - for i in qubit_map: - index += int(k[qubit_map.index(i)]) * 2 ** (size - 1 - i) - expval += obs[index, index] * counts[j] - return self.backend.np.real(expval) - - def eye(self, dim: Optional[int] = None): - """Generate Identity matrix with dimension ``dim``""" - if dim is None: - dim = int(self.matrix.shape[0]) - return self.backend.cast(self.backend.matrices.I(dim), dtype=self.matrix.dtype) - - def energy_fluctuation(self, state): - """ - Evaluate energy fluctuation: - - .. math:: - \\Xi_{k}(\\mu) = \\sqrt{\\bra{\\mu} \\, H^{2} \\, \\ket{\\mu} - - \\bra{\\mu} \\, H \\, \\ket{\\mu}^2} \\, . - - for a given state :math:`\\ket{\\mu}`. - - Args: - state (ndarray): quantum state to be used to compute the energy fluctuation. - - Returns: - float: Energy fluctuation value. - """ - state = self.backend.cast(state) - energy = self.expectation(state) - h = self.matrix - h2 = Hamiltonian(nqubits=self.nqubits, matrix=h @ h, backend=self.backend) - average_h2 = self.backend.calculate_expectation_state(h2, state, normalize=True) - return self.backend.np.sqrt(self.backend.np.abs(average_h2 - energy**2)) - - def __add__(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 added.", - ) - new_matrix = self.matrix + o.matrix - elif isinstance(o, self.backend.numeric_types): - new_matrix = self.matrix + o * self.eye() - else: - raise_error( - NotImplementedError, - f"Hamiltonian addition to {type(o)} not implemented.", - ) - return self.__class__(self.nqubits, new_matrix, backend=self.backend) - - 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_matrix = self.matrix - o.matrix - elif isinstance(o, self.backend.numeric_types): - new_matrix = self.matrix - o * self.eye() - else: - raise_error( - NotImplementedError, - f"Hamiltonian subtraction to {type(o)} not implemented.", - ) - return self.__class__(self.nqubits, new_matrix, backend=self.backend) - - def __rsub__(self, o): - if isinstance(o, self.__class__): # pragma: no cover - # impractical case because it will be handled by `__sub__` - if self.nqubits != o.nqubits: - raise_error( - RuntimeError, - "Only hamiltonians with the same number of qubits can be added.", - ) - new_matrix = o.matrix - self.matrix - elif isinstance(o, self.backend.numeric_types): - new_matrix = o * self.eye() - self.matrix - else: - raise_error( - NotImplementedError, - f"Hamiltonian subtraction to {type(o)} not implemented.", - ) - return self.__class__(self.nqubits, new_matrix, backend=self.backend) - - def __mul__(self, o): - if isinstance(o, self.backend.tensor_types): - o = complex(o) - elif not isinstance(o, self.backend.numeric_types): - raise_error( - NotImplementedError, - f"Hamiltonian multiplication to {type(o)} not implemented.", - ) - new_matrix = self.matrix * o - r = self.__class__(self.nqubits, new_matrix, backend=self.backend) - o = self.backend.cast(o) - if self._eigenvalues is not None: - if self.backend.np.real(o) >= 0: # TODO: check for side effects K.qnp - r._eigenvalues = o * self._eigenvalues - elif not self.backend.is_sparse(self.matrix): - axis = (0,) if (self.backend.platform == "pytorch") else 0 - r._eigenvalues = o * self.backend.np.flip(self._eigenvalues, axis) - if self._eigenvectors is not None: - if self.backend.np.real(o) > 0: # TODO: see above - r._eigenvectors = self._eigenvectors - elif o == 0: - r._eigenvectors = self.eye(int(self._eigenvectors.shape[0])) - return r - - def __matmul__(self, o): - if isinstance(o, self.__class__): - matrix = self.backend.calculate_hamiltonian_matrix_product( - self.matrix, o.matrix - ) - return self.__class__(self.nqubits, matrix, backend=self.backend) - - if isinstance(o, self.backend.tensor_types): - return self.backend.calculate_hamiltonian_state_product(self.matrix, o) - - raise_error( - NotImplementedError, - f"Hamiltonian matmul to {type(o)} not implemented.", - ) - - -class SymbolicHamiltonian(AbstractHamiltonian): - """Hamiltonian based on a symbolic representation. - - Calculations using symbolic Hamiltonians are either done directly using - the given ``sympy`` expression as it is (``form``) or by parsing the - corresponding ``terms`` (which are :class:`qibo.core.terms.SymbolicTerm` - objects). The latter approach is more computationally costly as it uses - a ``sympy.expand`` call on the given form before parsing the terms. - For this reason the ``terms`` are calculated only when needed, for example - during Trotterization. - The dense matrix of the symbolic Hamiltonian can be calculated directly - from ``form`` without requiring ``terms`` calculation (see - :meth:`qibo.core.hamiltonians.SymbolicHamiltonian.calculate_dense` for details). - - Args: - form (sympy.Expr): Hamiltonian form as a ``sympy.Expr``. Ideally the - 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: sympy.Expr, - nqubits: Optional[int] = None, - symbol_map: Optional[dict] = None, - backend: Optional[Backend] = None, - ): - super().__init__() - 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.symbol_map = symbol_map if symbol_map is not None else {} - # 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`` - - self.backend = _check_backend(backend) - - self.nqubits = ( - self._calculate_nqubits_from_form(form) if nqubits is None else nqubits - ) - - @cached_property - def dense(self) -> "MatrixHamiltonian": - """Creates the equivalent Hamiltonian matrix.""" - 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 - 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 - return nqubits + 1 - - @form.setter - def form(self, form): - # Check that given form is a ``sympy`` expression - if not isinstance(form, sympy.Expr): - raise_error( - TypeError, - f"Symbolic Hamiltonian should be a ``sympy`` expression but is {type(form)}.", - ) - self._form = form - self.nqubits = self._calculate_nqubits_from_form(form) - - @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`. - """ - # Calculate terms based on ``self.form`` - from qibo.hamiltonians.terms import ( # pylint: disable=import-outside-toplevel - SymbolicTerm, - ) - - self.constant = 0.0 - - 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 - return terms - - @property - def matrix(self): - """Returns the full matrix representation. - - Consisting of :math:`2^{n} \\times 2^{n}`` elements. - """ - return self.dense.matrix - - def eigenvalues(self, k=6): - return self.dense.eigenvalues(k) - - def eigenvectors(self, k=6): - return self.dense.eigenvectors(k) - - def ground_state(self): - return self.eigenvectors()[:, 0] - - def exp(self, a): - return self.dense.exp(a) - - # only useful for dense_from_form, which might not be needed in the end - def _get_symbol_matrix(self, term): - """Calculates numerical matrix corresponding to symbolic expression. - - This is partly equivalent to sympy's ``.subs``, which does not work - in our case as it does not allow us to substitute ``sympy.Symbol`` - with numpy arrays and there are different complication when switching - to ``sympy.MatrixSymbol``. Here we calculate the full numerical matrix - given the symbolic expression using recursion. - Helper method for ``_calculate_dense_from_form``. - - Args: - term (sympy.Expr): Symbolic expression containing local operators. - - Returns: - ndarray: matrix corresponding to the given expression as an array - of shape ``(2 ** self.nqubits, 2 ** self.nqubits)``. - """ - if isinstance(term, sympy.Add): - # symbolic op for addition - result = sum( - self._get_symbol_matrix(subterm) for subterm in term.as_ordered_terms() - ) - - elif isinstance(term, sympy.Mul): - # symbolic op for multiplication - # 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) - - 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) - - 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) - - else: - raise_error( - TypeError, - f"Cannot calculate matrix for symbolic term of type {type(term)}.", - ) - - return result - - # not sure this is useful, it appears to be significantly slower than - # the from_terms counterpart - def _calculate_dense_from_form(self) -> Hamiltonian: - """Calculates equivalent Hamiltonian using symbolic form. - - Useful when the term representation is not available. - """ - 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.""" - matrix = 0 - indices = list(range(2 * self.nqubits)) - # most likely the looped einsum could be avoided by preparing all the - # matrices first and performing a single einsum in the end with a suitable - # choice of indices - for term in self.terms: - ntargets = len(term.target_qubits) - # I have to cast to a backend array because SymbolicTerm does not support - # backend declaration and just works with numpy, might be worth implementing - # a SymbolicTerm.matrix(backend=None) method that returns the matrix in the - # desired backend type and defaults to numpy or GlobalBackend - # A similar argument holds for qibo Symbols - tmat = self.backend.np.reshape( - self.backend.cast(term.matrix), 2 * ntargets * (2,) - ) - n = self.nqubits - ntargets - 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) - if self.backend.platform == "tensorflow": - matrix += np.einsum(tmat, tc, emat, ec, indices) - else: - matrix += self.backend.np.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) -> 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) - - def expectation_from_circuit(self, circuit: "Circuit", nshots: int = 1000) -> float: - """ - Calculate the expectation value from a circuit. - This even works for observables not completely diagonal in the computational - basis, but only diagonal at a term level in a defined basis. Namely, for - an observable of the form :math:``H = \\sum_i H_i``, where each :math:``H_i`` - consists in a `n`-qubits pauli operator :math:`P_0 \\otimes P_1 \\otimes \\cdots \\otimes P_n`, - the expectation value is computed by rotating the input circuit in the suitable - basis for each term :math:``H_i`` thus extracting the `term-wise` expectations - that are then summed to build the global expectation value. - Each term of the observable is treated separately, by measuring in the correct - basis and re-executing the circuit. - - Args: - circuit (Circuit): input circuit. - nshots (int): number of shots, defaults to 1000. - - Returns: - (float): the calculated expectation value. - """ - from qibo import gates - - rotated_circuits = [] - coefficients = [] - Z_observables = [] - qubit_maps = [] - for term in self.terms: - # store coefficient - coefficients.append(term.coefficient) - # Only care about non-I terms - non_identity_factors = [ - factor for factor in term.factors if factor.name[0] != "I" - ] - # build diagonal observable - Z_observables.append( - SymbolicHamiltonian( - prod(Z(factor.target_qubit) for factor in non_identity_factors), - nqubits=circuit.nqubits, - backend=self.backend, - ) - ) - # Get the qubits we want to measure for each term - qubit_map = sorted(factor.target_qubit for factor in non_identity_factors) - # prepare the measurement basis and append it to the circuit - measurements = [ - gates.M(factor.target_qubit, basis=factor.gate.__class__) - for factor in non_identity_factors - ] - circ_copy = circuit.copy(True) - circ_copy.add(measurements) - rotated_circuits.append(circ_copy) - # for mapping the obtained sample frequencies to the original qubits - qubit_maps.append(qubit_map) - frequencies = [ - result.frequencies() - for result in self.backend.execute_circuits(rotated_circuits, nshots=nshots) - ] - return sum( - coeff * obs.expectation_from_samples(freq, qubit_map) - for coeff, freq, obs, qubit_map in zip( - coefficients, frequencies, Z_observables, qubit_maps - ) - ) - - def expectation_from_samples(self, freq: dict, qubit_map: list = None) -> float: - """ - Calculate the expectation value from the samples. - The observable has to be diagonal in the computational basis. - - Args: - freq (dict): input frequencies of the samples. - qubit_map (list): qubit map. - - Returns: - (float): the calculated expectation value. - """ - for term in self.terms: - # pylint: disable=E1101 - for factor in term.factors: - if not isinstance(factor, Z): - raise_error( - NotImplementedError, "Observable is not a Z Pauli string." - ) - - if qubit_map is None: - qubit_map = list(range(self.nqubits)) - - keys = list(freq.keys()) - counts = self.backend.cast(list(freq.values()), self.backend.precision) / sum( - freq.values() - ) - expvals = [] - for term in self.terms: - qubits = { - factor.target_qubit for factor in term.factors if factor.name[0] != "I" - } - expvals.extend( - [ - term.coefficient.real - * (-1) ** [state[qubit_map.index(q)] for q in qubits].count("1") - for state in keys - ] - ) - expvals = self.backend.cast(expvals, dtype=counts.dtype).reshape( - len(self.terms), len(freq) - ) - return self.backend.np.sum(expvals @ counts.T) + self.constant.real - - def _compose(self, o, operator): - form = self._form - symbol_map = self.symbol_map - - if isinstance(o, self.__class__): - if self.nqubits != o.nqubits: - raise_error( - RuntimeError, - "Only hamiltonians with the same number of qubits can be composed.", - ) - - if o._form is not None: - symbol_map.update(o.symbol_map) - 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 composition to {type(o)} not implemented.", - ) - - return self.__class__( - form=form, symbol_map=symbol_map, nqubits=self.nqubits, backend=self.backend - ) - - def __add__(self, o): - return self._compose(o, lambda x, y: x + y) - - def __sub__(self, o): - return self._compose(o, lambda x, y: x - y) - - def __rsub__(self, o): - return self._compose(o, lambda x, y: y - x) - - def __mul__(self, o): - # o = complex(o) - return self._compose(o, lambda x, y: y * x) - - def apply_gates(self, state, density_matrix=False): - """Applies gates corresponding to the Hamiltonian terms. - - Gates are applied to the given state. - - Helper method for :meth:`qibo.hamiltonians.SymbolicHamiltonian.__matmul__`. - """ - total = 0 - for term in self.terms: - total += term( - self.backend, - self.backend.cast(state, copy=True), - self.nqubits, - density_matrix=density_matrix, - ) - if self.constant: # pragma: no cover - total += self.constant * state - return total - - def __matmul__(self, o): - """Matrix multiplication with other Hamiltonians or state vectors.""" - if isinstance(o, self.__class__): - return o * self - - if isinstance(o, self.backend.tensor_types): - rank = len(tuple(o.shape)) - if rank not in (1, 2): - raise_error( - NotImplementedError, - f"Cannot multiply Hamiltonian with rank-{rank} tensor.", - ) - state_qubits = int(np.log2(int(o.shape[0]))) - if state_qubits != self.nqubits: - raise_error( - ValueError, - f"Cannot multiply Hamiltonian on {self.nqubits} qubits to " - + f"state of {state_qubits} qubits.", - ) - if rank == 1: # state vector - return self.apply_gates(o) - - return self.apply_gates(o, density_matrix=True) - - raise_error( - NotImplementedError, - f"Hamiltonian matmul to {type(o)} not implemented.", - ) - - def circuit(self, dt, accelerators=None): - """Circuit that implements a Trotter step of this Hamiltonian. - - Args: - dt (float): Time step used for Trotterization. - accelerators (dict, optional): Dictionary with accelerators for distributed circuits. - Defaults to ``None``. - """ - from qibo import Circuit # pylint: disable=import-outside-toplevel - from qibo.hamiltonians.terms import ( # pylint: disable=import-outside-toplevel - TermGroup, - ) - - groups = TermGroup.from_terms(self.terms) - circuit = Circuit(self.nqubits, accelerators=accelerators) - circuit.add( - group.term.expgate(dt / 2.0) for group in chain(groups, groups[::-1]) - ) - - 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/.#hamiltonians.py b/src/qibo/hamiltonians/.#hamiltonians.py deleted file mode 120000 index 977a90663a..0000000000 --- a/src/qibo/hamiltonians/.#hamiltonians.py +++ /dev/null @@ -1 +0,0 @@ -andrea@MacBook-Pro-3.local.11311 \ No newline at end of file diff --git a/src/qibo/hamiltonians/hamiltonians.py b/src/qibo/hamiltonians/hamiltonians.py index 6594a1db8e..cd0ff6afaa 100644 --- a/src/qibo/hamiltonians/hamiltonians.py +++ b/src/qibo/hamiltonians/hamiltonians.py @@ -208,7 +208,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( @@ -561,7 +563,6 @@ def calculate_dense(self) -> Hamiltonian: "Calculating the dense form of a symbolic Hamiltonian. " "This operation is memory inefficient." ) - # if self._terms is None: # calculate dense matrix directly using the form to avoid the # costly ``sympy.expand`` call if len(self.terms) > 40: diff --git a/src/qibo/hamiltonians/models.py b/src/qibo/hamiltonians/models.py index 3826a8d140..dab39dbe7b 100644 --- a/src/qibo/hamiltonians/models.py +++ b/src/qibo/hamiltonians/models.py @@ -4,7 +4,7 @@ import numpy as np from qibo import symbols -from qibo.backends import _check_backend, matrices +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 @@ -102,7 +102,7 @@ def MaxCut( nqubits, dense: bool = True, adj_matrix: Optional[Union[list[list[float]], np.ndarray]] = None, - backend=None, + backend: Optional[Backend] = None, ): """Max Cut Hamiltonian. From ac0d9aa83397c0cb723f89838e34737d50a1d20d Mon Sep 17 00:00:00 2001 From: BrunoLiegiBastonLiegi Date: Fri, 10 Jan 2025 15:19:52 +0100 Subject: [PATCH 13/25] feat: made symbolicterm backend aware --- src/qibo/hamiltonians/models.py | 12 ++++++------ src/qibo/hamiltonians/terms.py | 10 ++++++++-- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/qibo/hamiltonians/models.py b/src/qibo/hamiltonians/models.py index dab39dbe7b..9ab9f0d53c 100644 --- a/src/qibo/hamiltonians/models.py +++ b/src/qibo/hamiltonians/models.py @@ -219,7 +219,7 @@ def Heisenberg( matrix = backend.cast(matrix, dtype=matrix.dtype) for ind, pauli in enumerate(paulis): double_term = _build_spin_model( - nqubits, backend.cast(pauli(0).matrix), condition, backend + 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 @@ -392,13 +392,13 @@ def _build_spin_model(nqubits, matrix, condition, backend): + even + odd ) + eye = backend.matrices.I() + if backend.platform == "cupy": + eye = backend.cast(eye) columns = [ backend.np.reshape( backend.np.concatenate( - [ - matrix if condition(i, j) else backend.matrices.I() - for i in range(nqubits) - ], + [matrix if condition(i, j) else eye for i in range(nqubits)], axis=0, ), (nqubits, 2, 2), @@ -426,7 +426,7 @@ def _OneBodyPauli(nqubits, operator, dense: bool = True, backend=None): if dense: condition = lambda i, j: i == j % nqubits ham = -_build_spin_model( - nqubits, backend.cast(operator(0).matrix), condition, backend + nqubits, operator(0, backend=backend).matrix, condition, backend ) return Hamiltonian(nqubits, ham, backend=backend) diff --git a/src/qibo/hamiltonians/terms.py b/src/qibo/hamiltonians/terms.py index c46557abeb..af858f70a8 100644 --- a/src/qibo/hamiltonians/terms.py +++ b/src/qibo/hamiltonians/terms.py @@ -1,7 +1,10 @@ +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 @@ -135,11 +138,14 @@ class SymbolicTerm(HamiltonianTerm): symbols were not available. """ - def __init__(self, coefficient, factors=1, symbol_map={}): + def __init__( + self, coefficient, factors=1, symbol_map={}, backend: Optional[Backend] = None + ): self.coefficient = complex(coefficient) self._matrix = None self._gate = None self.hamiltonian = None + self.backend = _check_backend(backend) # List of :class:`qibo.symbols.Symbol` that represent the term factors self.factors = [] @@ -175,7 +181,7 @@ def __init__(self, coefficient, factors=1, symbol_map={}): factor = Symbol(q, matrix, name=factor.name) if isinstance(factor, sympy.Symbol): - if isinstance(factor.matrix, np.ndarray): + 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 From 86dc4b77d7e7260d1689776f13249e63c18e7f07 Mon Sep 17 00:00:00 2001 From: BrunoLiegiBastonLiegi Date: Fri, 10 Jan 2025 17:51:10 +0100 Subject: [PATCH 14/25] fix: fix to symb ham terms --- src/qibo/hamiltonians/hamiltonians.py | 2 +- src/qibo/hamiltonians/models.py | 4 ++-- src/qibo/symbols.py | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/qibo/hamiltonians/hamiltonians.py b/src/qibo/hamiltonians/hamiltonians.py index cd0ff6afaa..ae10702ec2 100644 --- a/src/qibo/hamiltonians/hamiltonians.py +++ b/src/qibo/hamiltonians/hamiltonians.py @@ -411,7 +411,7 @@ def terms(self): form = sympy.expand(self.form) terms = [] for f, c in form.as_coefficients_dict().items(): - term = SymbolicTerm(c, f, self.symbol_map) + term = SymbolicTerm(c, f, self.symbol_map, backend=self.backend) if term.target_qubits: terms.append(term) else: diff --git a/src/qibo/hamiltonians/models.py b/src/qibo/hamiltonians/models.py index 9ab9f0d53c..63facfcd61 100644 --- a/src/qibo/hamiltonians/models.py +++ b/src/qibo/hamiltonians/models.py @@ -345,7 +345,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, backend=None): +def _multikron(matrix_list, backend): """Calculates Kronecker product of a list of matrices. Args: @@ -370,7 +370,7 @@ def _multikron(matrix_list, backend=None): h = backend.np.sum(backend.np.reshape(h, (dim, dim)), axis=0) return h """ - return reduce(np.kron, matrix_list) + return reduce(backend.np.kron, matrix_list) def _build_spin_model(nqubits, matrix, condition, backend): diff --git a/src/qibo/symbols.py b/src/qibo/symbols.py index 4dbb7e28f6..928bd7d0ee 100644 --- a/src/qibo/symbols.py +++ b/src/qibo/symbols.py @@ -121,11 +121,11 @@ def full_matrix(self, nqubits): """ from qibo.hamiltonians.models import _multikron - matrix_list = self.target_qubit * [backend.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): From 0f5f66e3d645beb1f0e1003295f3af04a49ec1d2 Mon Sep 17 00:00:00 2001 From: BrunoLiegiBastonLiegi Date: Fri, 10 Jan 2025 18:35:31 +0100 Subject: [PATCH 15/25] feat: replaced loop with einsum in sym term matrix --- src/qibo/hamiltonians/models.py | 8 ++++---- src/qibo/hamiltonians/terms.py | 15 +++++++++------ 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/qibo/hamiltonians/models.py b/src/qibo/hamiltonians/models.py index 63facfcd61..9913ddbcfe 100644 --- a/src/qibo/hamiltonians/models.py +++ b/src/qibo/hamiltonians/models.py @@ -1,4 +1,3 @@ -from functools import reduce from typing import Optional, Union import numpy as np @@ -354,10 +353,10 @@ def _multikron(matrix_list, backend): Returns: ndarray: Kronecker product of all matrices in ``matrix_list``. """ - """ + # this is a better implementation but requires the whole # hamiltonian/symbols modules to be adapted - indices = list(range(2*len(matrix_list))) + indices = list(range(2 * len(matrix_list))) even, odd = indices[::2], indices[1::2] lhs = zip(even, odd) rhs = even + odd @@ -367,10 +366,11 @@ def _multikron(matrix_list, backend): h = backend.np.einsum(*einsum_args, rhs) else: h = np.einsum(*einsum_args, rhs) - h = backend.np.sum(backend.np.reshape(h, (dim, dim)), axis=0) + h = backend.np.sum(backend.np.reshape(h, (-1, dim, dim)), axis=0) return h """ return reduce(backend.np.kron, matrix_list) + """ def _build_spin_model(nqubits, matrix, condition, backend): diff --git a/src/qibo/hamiltonians/terms.py b/src/qibo/hamiltonians/terms.py index af858f70a8..231a4423f4 100644 --- a/src/qibo/hamiltonians/terms.py +++ b/src/qibo/hamiltonians/terms.py @@ -216,6 +216,10 @@ def matrix(self): """ if self._matrix is None: + einsum = ( + self.backend.np.einsum if self.backend.name != "qiboml" else np.einsum + ) + def matrices_product(matrices): """Product of matrices that act on the same tuple of qubits. @@ -223,12 +227,11 @@ def matrices_product(matrices): 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 + nmat = len(matrices) + indices = zip(range(nmat), range(1, nmat + 1)) + lhs = zip(matrices, indices) + lhs = [el for item in lhs for el in item] + return einsum(*lhs, (0, nmat)) self._matrix = self.coefficient for q in self.target_qubits: From 318cae3b116d5d3f592214b6577cbe3269b362ea Mon Sep 17 00:00:00 2001 From: BrunoLiegiBastonLiegi Date: Sun, 12 Jan 2025 12:17:00 +0100 Subject: [PATCH 16/25] feat: removed unnecessary cast --- src/qibo/hamiltonians/hamiltonians.py | 4 +- src/qibo/hamiltonians/terms.py | 56 ++++++++++++++------------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/src/qibo/hamiltonians/hamiltonians.py b/src/qibo/hamiltonians/hamiltonians.py index ae10702ec2..fe413d48b4 100644 --- a/src/qibo/hamiltonians/hamiltonians.py +++ b/src/qibo/hamiltonians/hamiltonians.py @@ -536,9 +536,7 @@ def _calculate_dense_from_terms(self) -> Hamiltonian: # a SymbolicTerm.matrix(backend=None) method that returns the matrix in the # desired backend type and defaults to numpy or GlobalBackend # A similar argument holds for qibo Symbols - tmat = self.backend.np.reshape( - self.backend.cast(term.matrix), 2 * ntargets * (2,) - ) + tmat = self.backend.np.reshape(term.matrix, 2 * ntargets * (2,)) n = self.nqubits - ntargets emat = self.backend.np.reshape( self.backend.np.eye(2**n, dtype=tmat.dtype), 2 * n * (2,) diff --git a/src/qibo/hamiltonians/terms.py b/src/qibo/hamiltonians/terms.py index 231a4423f4..f79cafbaf6 100644 --- a/src/qibo/hamiltonians/terms.py +++ b/src/qibo/hamiltonians/terms.py @@ -1,3 +1,4 @@ +from functools import cached_property from typing import Optional import numpy as np @@ -142,7 +143,6 @@ def __init__( self, coefficient, factors=1, symbol_map={}, backend: Optional[Backend] = None ): self.coefficient = complex(coefficient) - self._matrix = None self._gate = None self.hamiltonian = None self.backend = _check_backend(backend) @@ -178,7 +178,7 @@ def __init__( from qibo.symbols import Symbol q, matrix = symbol_map.get(factor) - factor = Symbol(q, matrix, name=factor.name) + factor = Symbol(q, matrix, name=factor.name, backend=self.backend) if isinstance(factor, sympy.Symbol): if isinstance(factor.matrix, self.backend.tensor_types): @@ -205,7 +205,7 @@ def __init__( self.target_qubits = tuple(sorted(self.matrix_map.keys())) - @property + @cached_property def matrix(self): """Calculates the full matrix corresponding to this term. @@ -214,30 +214,32 @@ def matrix(self): where ``ntargets`` is the number of qubits included in the factors of this term. """ - if self._matrix is None: - - einsum = ( - self.backend.np.einsum if self.backend.name != "qiboml" else np.einsum - ) - - 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``. - """ - nmat = len(matrices) - indices = zip(range(nmat), range(1, nmat + 1)) - lhs = zip(matrices, indices) - lhs = [el for item in lhs for el in item] - return einsum(*lhs, (0, nmat)) - - 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 + einsum = ( + self.backend.np.einsum + if self.backend.platform != "tensorflow" + else np.einsum + ) + + 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``. + """ + nmat = len(matrices) + indices = zip(range(nmat), range(1, nmat + 1)) + lhs = zip(matrices, indices) + lhs = [el for item in lhs for el in item] + return einsum(*lhs, (0, nmat)) + + # if self.backend.platform == "pytorch": + # breakpoint() + matrix = self.coefficient * self.backend.np.ones(1) + for q in self.target_qubits: + prod = matrices_product(self.matrix_map.get(q)) + matrix = self.backend.np.kron(matrix, prod) + return matrix def copy(self): """Creates a shallow copy of the term with the same attributes.""" From e461f96f24c894244684e545e1e1628fbf77ff73 Mon Sep 17 00:00:00 2001 From: BrunoLiegiBastonLiegi Date: Mon, 13 Jan 2025 15:47:03 +0100 Subject: [PATCH 17/25] fix: adding backend here and there --- src/qibo/hamiltonians/hamiltonians.py | 8 +- src/qibo/hamiltonians/models.py | 14 +- src/qibo/hamiltonians/terms.py | 76 ++++--- src/qibo/models/tsp.py | 12 +- src/qibo/symbols.py | 3 +- tests/#test_hamiltonians_from_symbols.py# | 248 ++++++++++++++++++++++ tests/.#test_hamiltonians_from_symbols.py | 1 + tests/test_hamiltonians_from_symbols.py | 130 +++++++----- tests/test_hamiltonians_symbolic.py | 115 +++++++--- tests/test_models_error_mitigation.py | 2 +- tests/test_states.py | 8 +- 11 files changed, 484 insertions(+), 133 deletions(-) create mode 100644 tests/#test_hamiltonians_from_symbols.py# create mode 120000 tests/.#test_hamiltonians_from_symbols.py diff --git a/src/qibo/hamiltonians/hamiltonians.py b/src/qibo/hamiltonians/hamiltonians.py index fe413d48b4..5a5fa638e4 100644 --- a/src/qibo/hamiltonians/hamiltonians.py +++ b/src/qibo/hamiltonians/hamiltonians.py @@ -492,17 +492,19 @@ def _get_symbol_matrix(self, 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) + result = complex(matrix) * self.backend.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) + result = self._qiboSymbol( + q, matrix, backend=self.backend + ).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( diff --git a/src/qibo/hamiltonians/models.py b/src/qibo/hamiltonians/models.py index 9913ddbcfe..3f41a132f7 100644 --- a/src/qibo/hamiltonians/models.py +++ b/src/qibo/hamiltonians/models.py @@ -91,7 +91,9 @@ def TFIM(nqubits, h: float = 0.0, dense: bool = True, backend=None): ) return Hamiltonian(nqubits, ham, backend=backend) - term = lambda q1, q2: symbols.Z(q1) * symbols.Z(q2) + h * symbols.X(q1) + 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 @@ -231,7 +233,7 @@ def Heisenberg( return Hamiltonian(nqubits, matrix, backend=backend) def h(symbol): - return lambda q1, q2: symbol(q1) * symbol(q2) + return lambda q1, q2: symbol(q1, backend=backend) * symbol(q2, backend=backend) def term(q1, q2): return sum( @@ -368,9 +370,6 @@ def _multikron(matrix_list, backend): h = np.einsum(*einsum_args, rhs) h = backend.np.sum(backend.np.reshape(h, (-1, dim, dim)), axis=0) return h - """ - return reduce(backend.np.kron, matrix_list) - """ def _build_spin_model(nqubits, matrix, condition, backend): @@ -430,9 +429,6 @@ def _OneBodyPauli(nqubits, operator, dense: bool = True, backend=None): ) return Hamiltonian(nqubits, ham, backend=backend) - # matrix = -matrix - # terms = [HamiltonianTerm(matrix, i) for i in range(nqubits)] - form = sum([-1 * operator(i) for i in range(nqubits)]) + form = sum([-1 * operator(i, backend=backend) for i in range(nqubits)]) ham = SymbolicHamiltonian(form=form, backend=backend) - # ham.terms = terms return ham diff --git a/src/qibo/hamiltonians/terms.py b/src/qibo/hamiltonians/terms.py index f79cafbaf6..da58e4730c 100644 --- a/src/qibo/hamiltonians/terms.py +++ b/src/qibo/hamiltonians/terms.py @@ -25,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: @@ -82,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: @@ -93,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) @@ -142,15 +149,17 @@ class SymbolicTerm(HamiltonianTerm): def __init__( self, coefficient, factors=1, symbol_map={}, backend: Optional[Backend] = None ): - self.coefficient = complex(coefficient) 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 = [] # Dictionary that maps target qubit ids to a list of matrices that act on each qubit self.matrix_map = {} + # if backend.name == "qiboml": + # breakpoint() if factors != 1: for factor in factors.as_ordered_factors(): # check if factor has some power ``power`` so that the corresponding @@ -214,32 +223,39 @@ def matrix(self): where ``ntargets`` is the number of qubits included in the factors of this term. """ + from qibo.hamiltonians.models import _multikron + einsum = ( self.backend.np.einsum if self.backend.platform != "tensorflow" else np.einsum ) - 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``. - """ - nmat = len(matrices) - indices = zip(range(nmat), range(1, nmat + 1)) - lhs = zip(matrices, indices) - lhs = [el for item in lhs for el in item] - return einsum(*lhs, (0, nmat)) - - # if self.backend.platform == "pytorch": - # breakpoint() - matrix = self.coefficient * self.backend.np.ones(1) - for q in self.target_qubits: - prod = matrices_product(self.matrix_map.get(q)) - matrix = self.backend.np.kron(matrix, prod) - return matrix + # 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) def copy(self): """Creates a shallow copy of the term with the same attributes.""" @@ -253,8 +269,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 928bd7d0ee..ac9b145eee 100644 --- a/src/qibo/symbols.py +++ b/src/qibo/symbols.py @@ -53,10 +53,12 @@ def __init__( 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) + or isinstance(matrix, self.backend.tensor_types) or isinstance( matrix, ( @@ -74,7 +76,6 @@ def __init__( ): raise_error(TypeError, f"Invalid type {type(matrix)} of symbol matrix.") self._matrix = matrix - self.backend = _check_backend(backend) def __getstate__(self): return { diff --git a/tests/#test_hamiltonians_from_symbols.py# b/tests/#test_hamiltonians_from_symbols.py# new file mode 100644 index 0000000000..bf908449d9 --- /dev/null +++ b/tests/#test_hamiltonians_from_symbols.py# @@ -0,0 +1,248 @@ +"""Test dense matrix of Hamiltonians constructed using symbols.""" + +import pickle + +import numpy as np +import pytest +import sympy + +from qibo import hamiltonians, matrices +from qibo.backends import NumpyBackend +from qibo.quantum_info import random_hermitian +from qibo.symbols import I, Symbol, X, Y, Z + + +@pytest.mark.parametrize("symbol", [I, X, Y, Z]) +def test_symbols_pickling(symbol): + symbol = symbol(int(np.random.randint(4))) + dumped_symbol = pickle.dumps(symbol) + new_symbol = pickle.loads(dumped_symbol) + for attr in ("target_qubit", "name", "_gate"): + assert getattr(symbol, attr) == getattr(new_symbol, attr) + np.testing.assert_allclose(symbol.matrix, new_symbol.matrix) + + +@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): + """Check creating TFIM Hamiltonian using sympy.""" + if hamtype == "symbolic": + 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) + 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, backend.matrices.Z) for i, z in enumerate(z_symbols)} + symmap.update({x: (i, backend.matrices.X) for i, x in enumerate(x_symbols)}) + ham = hamiltonians.Hamiltonian.from_symbolic(-symham, symmap, backend=backend) + + if calcterms: + _ = ham.terms + 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): + """Check ``from_symbolic`` when the expression contains powers.""" + npbackend = NumpyBackend() + if hamtype == "symbolic": + 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) + 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 + + final_matrix = ham.matrix + matrix2 = matrix.dot(matrix) + eye = np.eye(2, dtype=matrix.dtype) + target_matrix = np.kron(np.kron(matrix2, eye), eye) + target_matrix -= np.kron(np.kron(eye, matrix2), eye) + target_matrix += 3 * np.kron(np.kron(eye, matrix), eye) + target_matrix -= 2 * np.kron(np.kron(matrix, eye), matrix) + target_matrix += np.eye(8, dtype=matrix.dtype) + 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): + """Check ``from_symbolic`` when the expression contains imaginary unit.""" + if hamtype == "symbolic": + symham = ( + (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) + ) + 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, backend.matrices.X) for i, s in enumerate(x)} + symmap.update({s: (i, backend.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) * 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("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 + ) + # Check that Trotter dense matrix agrees will full Hamiltonian matrix + fham = hamiltonians.Hamiltonian.from_symbolic(symham, symmap, backend=backend) + symham = ( + Z(0, backend=backend) * Z(1, backend=backend) + - 0.5 * Z(0, backend=backend) * Z(2, backend=backend) + + 2 * Z(1, backend=backend) * Z(2, backend=backend) + + 0.35 * Z(1, backend=backend) + + 0.25 * Z(2, backend=backend) * Z(3, backend=backend) + + 0.5 * Z(2, backend=backend) + + Z(3, backend=backend) + - Z(0, backend=backend) + ) + sham = hamiltonians.SymbolicHamiltonian(symham, backend=backend) + if calcterms: + _ = sham.terms + backend.assert_allclose(sham.matrix, fham.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): + """Check creating sum(X) Hamiltonian using sympy.""" + if hamtype == "symbolic": + symham = -sum(X(i, backend=backend) 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, backend. + matrices.X) for i, x in enumerate(x_symbols)} + ham = hamiltonians.Hamiltonian.from_symbolic(symham, symmap, backend=backend) + if calcterms: + _ = ham.terms + 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): + """Check creating Hamiltonian with three-qubit interaction using sympy.""" + if hamtype == "symbolic": + 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) + 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 + final_matrix = ham.matrix + 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 * backend.np.kron( + backend.np.kron(backend.matrices.Y, backend.matrices.Z), backend.np.kron(backend.matrices.I(), backend.matrices.X) + ) + 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 * backend.np.kron( + backend.np.kron(backend.matrices.I(), backend.matrices.X), backend.np.kron(backend.matrices.I(), backend.matrices.Y) + ) + 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 * backend.np.kron( + backend.np.kron(backend.matrices.I(), backend.matrices.Z), backend.np.kron(backend.matrices.I(), backend.matrices.I()) + ) + 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): + """Check creating Hamiltonian from expression which contains the identity symbol.""" + 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 = 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( + backend.np.kron(backend.matrices.Y, backend.matrices.Z), backend.np.kron(backend.matrices.I(), backend.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_from_symbols.py b/tests/.#test_hamiltonians_from_symbols.py new file mode 120000 index 0000000000..977a90663a --- /dev/null +++ b/tests/.#test_hamiltonians_from_symbols.py @@ -0,0 +1 @@ +andrea@MacBook-Pro-3.local.11311 \ No newline at end of file diff --git a/tests/test_hamiltonians_from_symbols.py b/tests/test_hamiltonians_from_symbols.py index 4e45fec904..bbc864fcb5 100644 --- a/tests/test_hamiltonians_from_symbols.py +++ b/tests/test_hamiltonians_from_symbols.py @@ -29,9 +29,12 @@ def test_tfim_hamiltonian_from_symbols(backend, nqubits, hamtype, calcterms): """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)) + 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) else: h = 0.5 @@ -41,8 +44,8 @@ def test_tfim_hamiltonian_from_symbols(backend, nqubits, hamtype, calcterms): 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)}) + symmap = {z: (i, backend.matrices.Z) for i, z in enumerate(z_symbols)} + symmap.update({x: (i, backend.matrices.X) for i, x in enumerate(x_symbols)}) ham = hamiltonians.Hamiltonian.from_symbolic(-symham, symmap, backend=backend) if calcterms: @@ -60,10 +63,12 @@ def test_from_symbolic_with_power(backend, hamtype, calcterms): 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) + 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) @@ -94,10 +99,10 @@ def test_from_symbolic_with_complex_numbers(backend, hamtype, calcterms): """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) + (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) ) ham = hamiltonians.SymbolicHamiltonian(symham, backend=backend) else: @@ -109,17 +114,17 @@ def test_from_symbolic_with_complex_numbers(backend, hamtype, calcterms): - 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)}) + symmap = {s: (i, backend.matrices.X) for i, s in enumerate(x)} + symmap.update({s: (i, backend.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) + 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) @@ -141,14 +146,14 @@ def test_from_symbolic_application_hamiltonian(backend, calcterms): # 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) + Z(0, backend=backend) * Z(1, backend=backend) + - 0.5 * Z(0, backend=backend) * Z(2, backend=backend) + + 2 * Z(1, backend=backend) * Z(2, backend=backend) + + 0.35 * Z(1, backend=backend) + + 0.25 * Z(2, backend=backend) * Z(3, backend=backend) + + 0.5 * Z(2, backend=backend) + + Z(3, backend=backend) + - Z(0, backend=backend) ) sham = hamiltonians.SymbolicHamiltonian(symham, backend=backend) if calcterms: @@ -162,12 +167,12 @@ def test_from_symbolic_application_hamiltonian(backend, calcterms): def test_x_hamiltonian_from_symbols(backend, nqubits, hamtype, calcterms): """Check creating sum(X) Hamiltonian using sympy.""" if hamtype == "symbolic": - symham = -sum(X(i) for i in range(nqubits)) + symham = -sum(X(i, backend=backend) 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)} + symmap = {x: (i, backend.matrices.X) for i, x in enumerate(x_symbols)} ham = hamiltonians.Hamiltonian.from_symbolic(symham, symmap, backend=backend) if calcterms: _ = ham.terms @@ -181,8 +186,20 @@ def test_x_hamiltonian_from_symbols(backend, nqubits, hamtype, calcterms): def test_three_qubit_term_hamiltonian_from_symbols(backend, hamtype, calcterms): """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) + 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) else: x_symbols = sympy.symbols(" ".join(f"X{i}" for i in range(4))) @@ -204,44 +221,57 @@ def test_three_qubit_term_hamiltonian_from_symbols(backend, hamtype, calcterms): if calcterms: _ = ham.terms 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): """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_symbolic.py b/tests/test_hamiltonians_symbolic.py index 00da0f0b35..f9ecc788a0 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 @@ -43,7 +45,9 @@ def test_symbolic_hamiltonian_errors(backend): @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) + final_ham = SymbolicHamiltonian( + symbolic_tfim(nqubits, backend, h=1), backend=backend + ) target_ham = TFIM(nqubits, h=1, backend=backend) if calcterms: _ = final_ham.terms @@ -53,10 +57,20 @@ def test_symbolictfim_hamiltonian_to_dense(backend, nqubits, calcterms): @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) + 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: @@ -69,7 +83,9 @@ def test_symbolicxxz_hamiltonian_to_dense(backend, nqubits, calcterms): @pytest.mark.parametrize("calcdense", [False, True]) def test_symbolic_hamiltonian_scalar_mul(backend, nqubits, calcterms, calcdense): """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 @@ -78,7 +94,9 @@ def test_symbolic_hamiltonian_scalar_mul(backend, nqubits, calcterms, calcdense) 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) + local_ham = SymbolicHamiltonian( + symbolic_tfim(nqubits, backend, h=1.0), backend=backend + ) if calcterms: _ = local_ham.terms if calcdense: @@ -92,7 +110,9 @@ def test_symbolic_hamiltonian_scalar_mul(backend, nqubits, calcterms, calcdense) @pytest.mark.parametrize("calcdense", [False, True]) def test_symbolic_hamiltonian_scalar_add(backend, nqubits, calcterms, calcdense): """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 @@ -101,7 +121,9 @@ def test_symbolic_hamiltonian_scalar_add(backend, nqubits, calcterms, calcdense) 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) + local_ham = SymbolicHamiltonian( + symbolic_tfim(nqubits, backend, h=1.0), backend=backend + ) if calcterms: _ = local_ham.terms if calcdense: @@ -115,7 +137,9 @@ def test_symbolic_hamiltonian_scalar_add(backend, nqubits, calcterms, calcdense) @pytest.mark.parametrize("calcdense", [False, True]) def test_symbolic_hamiltonian_scalar_sub(backend, nqubits, calcterms, calcdense): """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 @@ -125,7 +149,9 @@ def test_symbolic_hamiltonian_scalar_sub(backend, nqubits, calcterms, calcdense) 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) + local_ham = SymbolicHamiltonian( + symbolic_tfim(nqubits, backend, h=1.0), backend=backend + ) if calcterms: _ = local_ham.terms if calcdense: @@ -141,8 +167,12 @@ def test_symbolic_hamiltonian_operator_add_and_sub( backend, nqubits, calcterms, calcdense ): """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) + local_ham1 = SymbolicHamiltonian( + symbolic_tfim(nqubits, backend, h=1.0), backend=backend + ) + local_ham2 = SymbolicHamiltonian( + symbolic_tfim(nqubits, backend, h=0.5), backend=backend + ) if calcterms: _ = local_ham1.terms _ = local_ham2.terms @@ -156,8 +186,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) + local_ham1 = SymbolicHamiltonian( + symbolic_tfim(nqubits, backend, h=1.0), backend=backend + ) + local_ham2 = SymbolicHamiltonian( + symbolic_tfim(nqubits, backend, h=0.5), backend=backend + ) if calcterms: _ = local_ham1.terms _ = local_ham2.terms @@ -174,13 +208,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 @@ -192,8 +235,12 @@ def test_symbolic_hamiltonian_operator_add_and_sub( @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) + 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: @@ -216,7 +263,9 @@ def test_symbolic_hamiltonian_matmul(backend, nqubits, density_matrix, calcterms 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 @@ -231,7 +280,9 @@ def test_symbolic_hamiltonian_matmul(backend, nqubits, density_matrix, calcterms def test_symbolic_hamiltonian_state_expectation( backend, nqubits, normalize, calcterms, calcdense ): - local_ham = SymbolicHamiltonian(symbolic_tfim(nqubits, h=1.0), backend=backend) + 2 + local_ham = ( + SymbolicHamiltonian(symbolic_tfim(nqubits, backend, h=1.0), backend=backend) + 2 + ) if calcterms: _ = local_ham.terms if calcdense: @@ -255,7 +306,7 @@ def test_symbolic_hamiltonian_state_expectation( def test_symbolic_hamiltonian_state_expectation_different_nqubits( backend, give_nqubits, calcterms, calcdense ): - 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: @@ -317,7 +368,9 @@ def test_symbolic_hamiltonian_abstract_symbol_ev(backend, density_matrix, calcte 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 @@ -334,8 +387,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 +408,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_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) From 80bf3f7b4376fda42b91318e74b98d3ccb235ebe Mon Sep 17 00:00:00 2001 From: BrunoLiegiBastonLiegi Date: Mon, 13 Jan 2025 16:00:00 +0100 Subject: [PATCH 18/25] feat: symbolic term forces the backend of its symbols --- src/qibo/hamiltonians/terms.py | 4 + tests/#test_hamiltonians_from_symbols.py# | 248 ---------------------- tests/.#test_hamiltonians_from_symbols.py | 1 - 3 files changed, 4 insertions(+), 249 deletions(-) delete mode 100644 tests/#test_hamiltonians_from_symbols.py# delete mode 120000 tests/.#test_hamiltonians_from_symbols.py diff --git a/src/qibo/hamiltonians/terms.py b/src/qibo/hamiltonians/terms.py index da58e4730c..b5ef69e477 100644 --- a/src/qibo/hamiltonians/terms.py +++ b/src/qibo/hamiltonians/terms.py @@ -190,6 +190,10 @@ def __init__( factor = Symbol(q, matrix, name=factor.name, backend=self.backend) if isinstance(factor, sympy.Symbol): + # 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 diff --git a/tests/#test_hamiltonians_from_symbols.py# b/tests/#test_hamiltonians_from_symbols.py# deleted file mode 100644 index bf908449d9..0000000000 --- a/tests/#test_hamiltonians_from_symbols.py# +++ /dev/null @@ -1,248 +0,0 @@ -"""Test dense matrix of Hamiltonians constructed using symbols.""" - -import pickle - -import numpy as np -import pytest -import sympy - -from qibo import hamiltonians, matrices -from qibo.backends import NumpyBackend -from qibo.quantum_info import random_hermitian -from qibo.symbols import I, Symbol, X, Y, Z - - -@pytest.mark.parametrize("symbol", [I, X, Y, Z]) -def test_symbols_pickling(symbol): - symbol = symbol(int(np.random.randint(4))) - dumped_symbol = pickle.dumps(symbol) - new_symbol = pickle.loads(dumped_symbol) - for attr in ("target_qubit", "name", "_gate"): - assert getattr(symbol, attr) == getattr(new_symbol, attr) - np.testing.assert_allclose(symbol.matrix, new_symbol.matrix) - - -@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): - """Check creating TFIM Hamiltonian using sympy.""" - if hamtype == "symbolic": - 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) - 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, backend.matrices.Z) for i, z in enumerate(z_symbols)} - symmap.update({x: (i, backend.matrices.X) for i, x in enumerate(x_symbols)}) - ham = hamiltonians.Hamiltonian.from_symbolic(-symham, symmap, backend=backend) - - if calcterms: - _ = ham.terms - 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): - """Check ``from_symbolic`` when the expression contains powers.""" - npbackend = NumpyBackend() - if hamtype == "symbolic": - 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) - 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 - - final_matrix = ham.matrix - matrix2 = matrix.dot(matrix) - eye = np.eye(2, dtype=matrix.dtype) - target_matrix = np.kron(np.kron(matrix2, eye), eye) - target_matrix -= np.kron(np.kron(eye, matrix2), eye) - target_matrix += 3 * np.kron(np.kron(eye, matrix), eye) - target_matrix -= 2 * np.kron(np.kron(matrix, eye), matrix) - target_matrix += np.eye(8, dtype=matrix.dtype) - 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): - """Check ``from_symbolic`` when the expression contains imaginary unit.""" - if hamtype == "symbolic": - symham = ( - (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) - ) - 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, backend.matrices.X) for i, s in enumerate(x)} - symmap.update({s: (i, backend.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) * 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("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 - ) - # Check that Trotter dense matrix agrees will full Hamiltonian matrix - fham = hamiltonians.Hamiltonian.from_symbolic(symham, symmap, backend=backend) - symham = ( - Z(0, backend=backend) * Z(1, backend=backend) - - 0.5 * Z(0, backend=backend) * Z(2, backend=backend) - + 2 * Z(1, backend=backend) * Z(2, backend=backend) - + 0.35 * Z(1, backend=backend) - + 0.25 * Z(2, backend=backend) * Z(3, backend=backend) - + 0.5 * Z(2, backend=backend) - + Z(3, backend=backend) - - Z(0, backend=backend) - ) - sham = hamiltonians.SymbolicHamiltonian(symham, backend=backend) - if calcterms: - _ = sham.terms - backend.assert_allclose(sham.matrix, fham.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): - """Check creating sum(X) Hamiltonian using sympy.""" - if hamtype == "symbolic": - symham = -sum(X(i, backend=backend) 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, backend. - matrices.X) for i, x in enumerate(x_symbols)} - ham = hamiltonians.Hamiltonian.from_symbolic(symham, symmap, backend=backend) - if calcterms: - _ = ham.terms - 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): - """Check creating Hamiltonian with three-qubit interaction using sympy.""" - if hamtype == "symbolic": - 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) - 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 - final_matrix = ham.matrix - 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 * backend.np.kron( - backend.np.kron(backend.matrices.Y, backend.matrices.Z), backend.np.kron(backend.matrices.I(), backend.matrices.X) - ) - 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 * backend.np.kron( - backend.np.kron(backend.matrices.I(), backend.matrices.X), backend.np.kron(backend.matrices.I(), backend.matrices.Y) - ) - 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 * backend.np.kron( - backend.np.kron(backend.matrices.I(), backend.matrices.Z), backend.np.kron(backend.matrices.I(), backend.matrices.I()) - ) - 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): - """Check creating Hamiltonian from expression which contains the identity symbol.""" - 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 = 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( - backend.np.kron(backend.matrices.Y, backend.matrices.Z), backend.np.kron(backend.matrices.I(), backend.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_from_symbols.py b/tests/.#test_hamiltonians_from_symbols.py deleted file mode 120000 index 977a90663a..0000000000 --- a/tests/.#test_hamiltonians_from_symbols.py +++ /dev/null @@ -1 +0,0 @@ -andrea@MacBook-Pro-3.local.11311 \ No newline at end of file From 1b34d33ceac58eb3c5deb02f6bbcfe92d3f59132 Mon Sep 17 00:00:00 2001 From: BrunoLiegiBastonLiegi Date: Mon, 13 Jan 2025 17:20:51 +0100 Subject: [PATCH 19/25] fix: removed calcterms/calcdense tests --- src/qibo/hamiltonians/__init__.py | 6 +- src/qibo/hamiltonians/hamiltonians.py | 100 ++--------- src/qibo/hamiltonians/terms.py | 17 +- tests/test_hamiltonians_from_symbols.py | 211 ++++++------------------ tests/test_hamiltonians_models.py | 8 +- tests/test_hamiltonians_symbolic.py | 113 ++----------- tests/test_hamiltonians_terms.py | 19 +-- tests/test_hamiltonians_trotter.py | 8 - 8 files changed, 84 insertions(+), 398 deletions(-) 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 5a5fa638e4..a023c7ec82 100644 --- a/src/qibo/hamiltonians/hamiltonians.py +++ b/src/qibo/hamiltonians/hamiltonians.py @@ -70,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) @@ -313,14 +285,6 @@ 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``. @@ -330,7 +294,6 @@ def __init__( self, form: sympy.Expr, nqubits: Optional[int] = None, - symbol_map: Optional[dict] = None, backend: Optional[Backend] = None, ): super().__init__() @@ -341,9 +304,6 @@ def __init__( ) self._form = form self.constant = 0 # used only when we perform calculations using ``_terms`` - self.symbol_map = symbol_map if symbol_map is not None else {} - # 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 @@ -372,14 +332,11 @@ def _calculate_nqubits_from_form(self, form): 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 + 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 @@ -411,7 +368,7 @@ def terms(self): form = sympy.expand(self.form) terms = [] for f, c in form.as_coefficients_dict().items(): - term = SymbolicTerm(c, f, self.symbol_map, backend=self.backend) + term = SymbolicTerm(c, f, backend=self.backend) if term.target_qubits: terms.append(term) else: @@ -480,25 +437,10 @@ def _get_symbol_matrix(self, term): 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) * self.backend.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, backend=self.backend - ).full_matrix(self.nqubits) + elif 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) elif term.is_number: # if the term is number we should return in the form of identity @@ -682,7 +624,6 @@ def expectation_from_samples(self, freq: dict, qubit_map: list = None) -> float: def _compose(self, o, operator): form = self._form - symbol_map = self.symbol_map if isinstance(o, self.__class__): if self.nqubits != o.nqubits: @@ -692,7 +633,6 @@ def _compose(self, o, operator): ) if o._form is not None: - symbol_map.update(o.symbol_map) form = operator(form, o._form) if form is not None else o._form elif isinstance(o, (self.backend.numeric_types, self.backend.tensor_types)): @@ -703,9 +643,7 @@ def _compose(self, o, operator): f"SymbolicHamiltonian composition to {type(o)} not implemented.", ) - return self.__class__( - form=form, symbol_map=symbol_map, nqubits=self.nqubits, backend=self.backend - ) + return self.__class__(form=form, nqubits=self.nqubits, backend=self.backend) def __add__(self, o): return self._compose(o, lambda x, y: x + y) @@ -788,19 +726,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/terms.py b/src/qibo/hamiltonians/terms.py index b5ef69e477..9cc2b29c03 100644 --- a/src/qibo/hamiltonians/terms.py +++ b/src/qibo/hamiltonians/terms.py @@ -139,16 +139,9 @@ 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={}, backend: Optional[Backend] = None - ): + def __init__(self, coefficient, factors=1, backend: Optional[Backend] = None): self._gate = None self.hamiltonian = None self.backend = _check_backend(backend) @@ -181,14 +174,6 @@ def __init__( 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, backend=self.backend) - if isinstance(factor, sympy.Symbol): # forces the backend of the factor # this way it is not necessary to explicitely define the diff --git a/tests/test_hamiltonians_from_symbols.py b/tests/test_hamiltonians_from_symbols.py index bbc864fcb5..d3aecd88d0 100644 --- a/tests/test_hamiltonians_from_symbols.py +++ b/tests/test_hamiltonians_from_symbols.py @@ -23,64 +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, 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) - 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, backend.matrices.Z) for i, z in enumerate(z_symbols)} - symmap.update({x: (i, backend.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, 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) - 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) @@ -93,33 +63,17 @@ 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, 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) - ) - 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, backend.matrices.X) for i, s in enumerate(x)} - symmap.update({s: (i, backend.matrices.Y) for i, s in enumerate(y)}) - ham = hamiltonians.Hamiltonian.from_symbolic(symham, symmap, backend=backend) + symham = ( + (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) + ) + ham = hamiltonians.SymbolicHamiltonian(symham, backend=backend) - if calcterms: - _ = ham.terms 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) @@ -128,98 +82,32 @@ def test_from_symbolic_with_complex_numbers(backend, hamtype, calcterms): 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 - ) - # Check that Trotter dense matrix agrees will full Hamiltonian matrix - fham = hamiltonians.Hamiltonian.from_symbolic(symham, symmap, backend=backend) - symham = ( - Z(0, backend=backend) * Z(1, backend=backend) - - 0.5 * Z(0, backend=backend) * Z(2, backend=backend) - + 2 * Z(1, backend=backend) * Z(2, backend=backend) - + 0.35 * Z(1, backend=backend) - + 0.25 * Z(2, backend=backend) * Z(3, backend=backend) - + 0.5 * Z(2, backend=backend) - + Z(3, backend=backend) - - Z(0, backend=backend) - ) - sham = hamiltonians.SymbolicHamiltonian(symham, backend=backend) - if calcterms: - _ = sham.terms - backend.assert_allclose(sham.matrix, fham.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, backend=backend) 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, backend.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, 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) - 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 = backend.np.kron( backend.np.kron(backend.matrices.X, backend.matrices.Y), @@ -249,8 +137,7 @@ def test_three_qubit_term_hamiltonian_from_symbols(backend, hamtype, calcterms): 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, backend=backend) * I(1, backend=backend) * Z(2, backend=backend) @@ -259,8 +146,6 @@ def test_hamiltonian_with_identity_symbol(backend, calcterms): ) ham = hamiltonians.SymbolicHamiltonian(symham, backend=backend) - if calcterms: - _ = ham.terms final_matrix = ham.matrix target_matrix = backend.np.kron( backend.np.kron(backend.matrices.X, backend.matrices.I()), diff --git a/tests/test_hamiltonians_models.py b/tests/test_hamiltonians_models.py index 5d2a2017d2..7e0866acc8 100644 --- a/tests/test_hamiltonians_models.py +++ b/tests/test_hamiltonians_models.py @@ -33,10 +33,8 @@ def test_hamiltonian_models(backend, model, kwargs, 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): +@pytest.mark.parametrize("dense", [True, False]) +def test_maxcut(backend, nqubits, dense): size = 2**nqubits ham = np.zeros(shape=(size, size), dtype=np.complex128) for i in range(nqubits): @@ -51,8 +49,6 @@ def test_maxcut(backend, nqubits, dense, calcterms): 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) diff --git a/tests/test_hamiltonians_symbolic.py b/tests/test_hamiltonians_symbolic.py index f9ecc788a0..0637e9bca7 100644 --- a/tests/test_hamiltonians_symbolic.py +++ b/tests/test_hamiltonians_symbolic.py @@ -29,34 +29,29 @@ 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 @pytest.mark.parametrize("nqubits", [3, 4]) -@pytest.mark.parametrize("calcterms", [False, True]) -def test_symbolictfim_hamiltonian_to_dense(backend, nqubits, calcterms): +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): +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) ) @@ -73,78 +68,50 @@ def test_symbolicxxz_hamiltonian_to_dense(backend, nqubits, calcterms): ) 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, 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, backend, h=1.0), backend=backend ) - if calcterms: - _ = local_ham.terms - if calcdense: - _ = local_ham.dense 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, 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, backend, h=1.0), backend=backend ) - if calcterms: - _ = local_ham.terms - if calcdense: - _ = local_ham.dense 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, 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) @@ -152,20 +119,12 @@ def test_symbolic_hamiltonian_scalar_sub(backend, nqubits, calcterms, calcdense) local_ham = SymbolicHamiltonian( symbolic_tfim(nqubits, backend, h=1.0), backend=backend ) - if calcterms: - _ = local_ham.terms - if calcdense: - _ = local_ham.dense 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, backend, h=1.0), backend=backend @@ -173,12 +132,6 @@ def test_symbolic_hamiltonian_operator_add_and_sub( local_ham2 = SymbolicHamiltonian( symbolic_tfim(nqubits, backend, h=0.5), backend=backend ) - if calcterms: - _ = local_ham1.terms - _ = local_ham2.terms - if calcdense: - _ = local_ham1.dense - _ = local_ham2.dense local_ham = local_ham1 + local_ham2 target_ham = TFIM(nqubits, h=1.0, backend=backend) + TFIM( nqubits, h=0.5, backend=backend @@ -192,12 +145,6 @@ def test_symbolic_hamiltonian_operator_add_and_sub( local_ham2 = SymbolicHamiltonian( symbolic_tfim(nqubits, backend, h=0.5), backend=backend ) - if calcterms: - _ = local_ham1.terms - _ = local_ham2.terms - if calcdense: - _ = local_ham1.dense - _ = local_ham2.dense local_ham = local_ham1 - local_ham2 target_ham = TFIM(nqubits, h=1.0, backend=backend) - TFIM( nqubits, h=0.5, backend=backend @@ -232,9 +179,7 @@ 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): +def test_symbolic_hamiltonian_hamiltonianmatmul(backend, nqubits): local_ham1 = SymbolicHamiltonian( symbolic_tfim(nqubits, backend, h=1.0), backend=backend ) @@ -243,12 +188,6 @@ def test_symbolic_hamiltonian_hamiltonianmatmul(backend, nqubits, calcterms, cal ) 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) @@ -256,8 +195,7 @@ 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 @@ -267,26 +205,16 @@ def test_symbolic_hamiltonian_matmul(backend, nqubits, density_matrix, calcterms 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 -): +def test_symbolic_hamiltonian_state_expectation(backend, nqubits, normalize): local_ham = ( SymbolicHamiltonian(symbolic_tfim(nqubits, backend, h=1.0), backend=backend) + 2 ) - if calcterms: - _ = local_ham.terms - if calcdense: - _ = local_ham.dense dense_ham = TFIM(nqubits, h=1.0, backend=backend) + 2 state = random_statevector(2**nqubits, backend=backend) @@ -301,20 +229,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, 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)) @@ -363,8 +285,7 @@ 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)) @@ -372,8 +293,6 @@ def test_symbolic_hamiltonian_abstract_symbol_ev(backend, density_matrix, calcte 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) 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 b63b3a0814..844d59e10e 100644 --- a/tests/test_hamiltonians_trotter.py +++ b/tests/test_hamiltonians_trotter.py @@ -104,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) From 4d8c9c1211c850a5e740607de943da75fc31f8b2 Mon Sep 17 00:00:00 2001 From: BrunoLiegiBastonLiegi Date: Mon, 13 Jan 2025 17:38:02 +0100 Subject: [PATCH 20/25] fix: minor adjustments --- src/qibo/hamiltonians/models.py | 6 ------ src/qibo/hamiltonians/terms.py | 2 -- src/qibo/symbols.py | 7 +++---- tests/test_models_tsp.py | 4 ++-- 4 files changed, 5 insertions(+), 14 deletions(-) diff --git a/src/qibo/hamiltonians/models.py b/src/qibo/hamiltonians/models.py index 3f41a132f7..4992392063 100644 --- a/src/qibo/hamiltonians/models.py +++ b/src/qibo/hamiltonians/models.py @@ -356,8 +356,6 @@ def _multikron(matrix_list, backend): ndarray: Kronecker product of all matrices in ``matrix_list``. """ - # this is a better implementation but requires the whole - # hamiltonian/symbols modules to be adapted indices = list(range(2 * len(matrix_list))) even, odd = indices[::2], indices[1::2] lhs = zip(even, odd) @@ -411,10 +409,6 @@ def _build_spin_model(nqubits, matrix, condition, backend): else: h = backend.np.einsum(*einsum_args, rhs) h = backend.np.sum(backend.np.reshape(h, (nqubits, dim, dim)), axis=0) - # h = sum( - # _multikron(matrix if condition(i, j) else matrices.I for j in range(nqubits)) - # for i in range(nqubits) - # ) return h diff --git a/src/qibo/hamiltonians/terms.py b/src/qibo/hamiltonians/terms.py index 9cc2b29c03..eeef194e17 100644 --- a/src/qibo/hamiltonians/terms.py +++ b/src/qibo/hamiltonians/terms.py @@ -151,8 +151,6 @@ def __init__(self, coefficient, factors=1, backend: Optional[Backend] = None): self.factors = [] # Dictionary that maps target qubit ids to a list of matrices that act on each qubit self.matrix_map = {} - # if backend.name == "qiboml": - # breakpoint() if factors != 1: for factor in factors.as_ordered_factors(): # check if factor has some power ``power`` so that the corresponding diff --git a/src/qibo/symbols.py b/src/qibo/symbols.py index ac9b145eee..c5b7ef289d 100644 --- a/src/qibo/symbols.py +++ b/src/qibo/symbols.py @@ -39,7 +39,7 @@ 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) @@ -47,7 +47,7 @@ def __new__(cls, q, matrix=None, name="Symbol", commutative=False, **assumptions def __init__( self, q, - matrix=None, + matrix, name="Symbol", commutative=False, backend: Optional[Backend] = None, @@ -56,8 +56,7 @@ def __init__( 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, diff --git a/tests/test_models_tsp.py b/tests/test_models_tsp.py index d0501844ae..5681fc86b6 100644 --- a/tests/test_models_tsp.py +++ b/tests/test_models_tsp.py @@ -38,8 +38,8 @@ def qaoa_function_of_layer(backend, layer): def test_tsp(backend, nlayers): final_state = backend.to_numpy(qaoa_function_of_layer(backend, nlayers)) assert_regression_fixture( - backend, final_state.real, f"tsp_layer{nlayers}_real.out", rtol=1e-3, atol=1e-5 + backend, final_state.real, f"tsp_layer{nlayers}_real.out", rtol=1e-3, atol=1e-2 ) assert_regression_fixture( - backend, final_state.imag, f"tsp_layer{nlayers}_imag.out", rtol=1e-3, atol=1e-5 + backend, final_state.imag, f"tsp_layer{nlayers}_imag.out", rtol=1e-3, atol=1e-2 ) From ff26b760f8ee20ef0ef4571589a3a54b30854f74 Mon Sep 17 00:00:00 2001 From: BrunoLiegiBastonLiegi Date: Tue, 14 Jan 2025 15:04:55 +0100 Subject: [PATCH 21/25] feat: minor optimizations: reintroduced reduce and using cache --- src/qibo/hamiltonians/hamiltonians.py | 34 ++++++++++----------------- src/qibo/hamiltonians/models.py | 15 ++++++++---- src/qibo/hamiltonians/terms.py | 11 +++++++-- tests/test_hamiltonians_models.py | 14 ++++++++--- tests/test_hamiltonians_symbolic.py | 8 +++++++ 5 files changed, 51 insertions(+), 31 deletions(-) diff --git a/src/qibo/hamiltonians/hamiltonians.py b/src/qibo/hamiltonians/hamiltonians.py index a023c7ec82..64ec34e079 100644 --- a/src/qibo/hamiltonians/hamiltonians.py +++ b/src/qibo/hamiltonians/hamiltonians.py @@ -1,6 +1,6 @@ """Module defining Hamiltonian classes.""" -from functools import cached_property +from functools import cache, cached_property, reduce from itertools import chain from math import prod from typing import Optional @@ -396,6 +396,7 @@ def exp(self, a): return self.dense.exp(a) # only useful for dense_from_form, which might not be needed in the end + @cache def _get_symbol_matrix(self, term): """Calculates numerical matrix corresponding to symbolic expression. @@ -424,18 +425,16 @@ 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 + result = self.backend.np.linalg.matrix_power(matrix, exponent) elif isinstance(term, self._qiboSymbol): # if we have a Qibo symbol the matrix construction is @@ -470,16 +469,13 @@ def _calculate_dense_from_terms(self) -> Hamiltonian: """Calculates equivalent Hamiltonian using the term representation.""" matrix = 0 indices = list(range(2 * self.nqubits)) - # most likely the looped einsum could be avoided by preparing all the - # matrices first and performing a single einsum in the end with a suitable - # choice of indices + einsum = ( + np.einsum + if self.backend.platform == "tensorflow" + else self.backend.np.einsum + ) for term in self.terms: ntargets = len(term.target_qubits) - # I have to cast to a backend array because SymbolicTerm does not support - # backend declaration and just works with numpy, might be worth implementing - # a SymbolicTerm.matrix(backend=None) method that returns the matrix in the - # desired backend type and defaults to numpy or GlobalBackend - # A similar argument holds for qibo Symbols tmat = self.backend.np.reshape(term.matrix, 2 * ntargets * (2,)) n = self.nqubits - ntargets emat = self.backend.np.reshape( @@ -488,10 +484,7 @@ def _calculate_dense_from_terms(self) -> Hamiltonian: 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) - if self.backend.platform == "tensorflow": - matrix += np.einsum(tmat, tc, emat, ec, indices) - else: - matrix += self.backend.np.einsum(tmat, tc, emat, ec, indices) + matrix += einsum(tmat, tc, emat, ec, indices) matrix = ( self.backend.np.reshape(matrix, 2 * (2**self.nqubits,)) @@ -655,7 +648,6 @@ def __rsub__(self, o): return self._compose(o, lambda x, y: y - x) def __mul__(self, o): - # o = complex(o) return self._compose(o, lambda x, y: y * x) def apply_gates(self, state, density_matrix=False): diff --git a/src/qibo/hamiltonians/models.py b/src/qibo/hamiltonians/models.py index 4992392063..b2a22d8b2d 100644 --- a/src/qibo/hamiltonians/models.py +++ b/src/qibo/hamiltonians/models.py @@ -1,3 +1,4 @@ +from functools import reduce from typing import Optional, Union import numpy as np @@ -355,7 +356,8 @@ def _multikron(matrix_list, backend): Returns: ndarray: Kronecker product of all matrices in ``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) @@ -368,6 +370,9 @@ def _multikron(matrix_list, backend): 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, backend): @@ -389,13 +394,13 @@ def _build_spin_model(nqubits, matrix, condition, backend): + even + odd ) - eye = backend.matrices.I() - if backend.platform == "cupy": - eye = backend.cast(eye) columns = [ backend.np.reshape( backend.np.concatenate( - [matrix if condition(i, j) else eye for i in range(nqubits)], + [ + matrix if condition(i, j) else backend.matrices.I() + for i in range(nqubits) + ], axis=0, ), (nqubits, 2, 2), diff --git a/src/qibo/hamiltonians/terms.py b/src/qibo/hamiltonians/terms.py index eeef194e17..0a0c79ee13 100644 --- a/src/qibo/hamiltonians/terms.py +++ b/src/qibo/hamiltonians/terms.py @@ -1,4 +1,4 @@ -from functools import cached_property +from functools import cached_property, reduce from typing import Optional import numpy as np @@ -210,8 +210,9 @@ def matrix(self): where ``ntargets`` is the number of qubits included in the factors of this term. """ - from qibo.hamiltonians.models import _multikron + # from qibo.hamiltonians.models import _multikron + """ einsum = ( self.backend.np.einsum if self.backend.platform != "tensorflow" @@ -243,6 +244,12 @@ def matrix(self): 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 self.coefficient * reduce(self.backend.np.kron, matrices) def copy(self): """Creates a shallow copy of the term with the same attributes.""" diff --git a/tests/test_hamiltonians_models.py b/tests/test_hamiltonians_models.py index 7e0866acc8..b547cd3a6d 100644 --- a/tests/test_hamiltonians_models.py +++ b/tests/test_hamiltonians_models.py @@ -32,9 +32,15 @@ def test_hamiltonian_models(backend, model, kwargs, filename): assert_regression_fixture(backend, matrix, filename) -@pytest.mark.parametrize("nqubits", [3, 4]) +@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, dense): +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 + ) + size = 2**nqubits ham = np.zeros(shape=(size, size), dtype=np.complex128) for i in range(nqubits): @@ -48,7 +54,9 @@ def test_maxcut(backend, nqubits, dense): M = np.eye(2**nqubits) - h ham += M target_ham = backend.cast(-ham / 2) - final_ham = hamiltonians.MaxCut(nqubits, dense, backend=backend) + final_ham = hamiltonians.MaxCut( + nqubits, dense, adj_matrix=adj_matrix, backend=backend + ) backend.assert_allclose(final_ham.matrix, target_ham) diff --git a/tests/test_hamiltonians_symbolic.py b/tests/test_hamiltonians_symbolic.py index 0637e9bca7..7d4e7a7cbc 100644 --- a/tests/test_hamiltonians_symbolic.py +++ b/tests/test_hamiltonians_symbolic.py @@ -41,6 +41,14 @@ def test_symbolic_hamiltonian_errors(backend): 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 + + @pytest.mark.parametrize("nqubits", [3, 4]) def test_symbolictfim_hamiltonian_to_dense(backend, nqubits): final_ham = SymbolicHamiltonian( From feb98a18bba64856e4275b00cf8e2a1ea4a7e0da Mon Sep 17 00:00:00 2001 From: BrunoLiegiBastonLiegi Date: Tue, 14 Jan 2025 15:37:46 +0100 Subject: [PATCH 22/25] fix: minor fix --- src/qibo/hamiltonians/terms.py | 2 +- tests/test_hamiltonians_models.py | 36 +++++++++++++++---------------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/qibo/hamiltonians/terms.py b/src/qibo/hamiltonians/terms.py index 0a0c79ee13..a1d9015938 100644 --- a/src/qibo/hamiltonians/terms.py +++ b/src/qibo/hamiltonians/terms.py @@ -249,7 +249,7 @@ def matrix(self): reduce(self.backend.np.matmul, self.matrix_map.get(q)) for q in self.target_qubits ] - return self.coefficient * reduce(self.backend.np.kron, matrices) + return complex(self.coefficient) * reduce(self.backend.np.kron, matrices) def copy(self): """Creates a shallow copy of the term with the same attributes.""" diff --git a/tests/test_hamiltonians_models.py b/tests/test_hamiltonians_models.py index b547cd3a6d..a5b52f5baf 100644 --- a/tests/test_hamiltonians_models.py +++ b/tests/test_hamiltonians_models.py @@ -40,24 +40,24 @@ def test_maxcut(backend, nqubits, adj_matrix, dense): final_ham = hamiltonians.MaxCut( nqubits, dense, adj_matrix=adj_matrix, backend=backend ) - - 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) + 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"]) From 29a010e40af567e6ee9e3fda6c52cfba565b4a29 Mon Sep 17 00:00:00 2001 From: BrunoLiegiBastonLiegi Date: Tue, 14 Jan 2025 15:40:44 +0100 Subject: [PATCH 23/25] fix: restored tol for tsp test --- tests/test_models_tsp.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_models_tsp.py b/tests/test_models_tsp.py index 5681fc86b6..d0501844ae 100644 --- a/tests/test_models_tsp.py +++ b/tests/test_models_tsp.py @@ -38,8 +38,8 @@ def qaoa_function_of_layer(backend, layer): def test_tsp(backend, nlayers): final_state = backend.to_numpy(qaoa_function_of_layer(backend, nlayers)) assert_regression_fixture( - backend, final_state.real, f"tsp_layer{nlayers}_real.out", rtol=1e-3, atol=1e-2 + backend, final_state.real, f"tsp_layer{nlayers}_real.out", rtol=1e-3, atol=1e-5 ) assert_regression_fixture( - backend, final_state.imag, f"tsp_layer{nlayers}_imag.out", rtol=1e-3, atol=1e-2 + backend, final_state.imag, f"tsp_layer{nlayers}_imag.out", rtol=1e-3, atol=1e-5 ) From 5dc31599121f0c6ecfda0df17255aa06022f4c8c Mon Sep 17 00:00:00 2001 From: BrunoLiegiBastonLiegi Date: Tue, 14 Jan 2025 16:27:50 +0100 Subject: [PATCH 24/25] fix: replaced another einsum --- src/qibo/hamiltonians/models.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/qibo/hamiltonians/models.py b/src/qibo/hamiltonians/models.py index b2a22d8b2d..1c25fb731a 100644 --- a/src/qibo/hamiltonians/models.py +++ b/src/qibo/hamiltonians/models.py @@ -377,6 +377,17 @@ def _multikron(matrix_list, backend): def _build_spin_model(nqubits, matrix, condition, backend): """Helper method for building nearest-neighbor spin model Hamiltonians.""" + h = sum( + 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( @@ -412,8 +423,9 @@ def _build_spin_model(nqubits, matrix, condition, backend): if backend.platform == "tensorflow": h = np.einsum(*einsum_args, rhs) else: - h = backend.np.einsum(*einsum_args, rhs) + h = backend.np.einsum(*einsum_args, rhs, optimize=True) h = backend.np.sum(backend.np.reshape(h, (nqubits, dim, dim)), axis=0) + """ return h From 1216b5a148a1c3d3c5ac679fc3d1275e31d3c0e9 Mon Sep 17 00:00:00 2001 From: BrunoLiegiBastonLiegi Date: Tue, 14 Jan 2025 17:25:34 +0100 Subject: [PATCH 25/25] feat: added a more comprehensice test for to dense --- src/qibo/hamiltonians/hamiltonians.py | 22 ++++++++++++++-------- tests/test_hamiltonians_symbolic.py | 9 +++++++++ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/qibo/hamiltonians/hamiltonians.py b/src/qibo/hamiltonians/hamiltonians.py index 64ec34e079..1a87905b8c 100644 --- a/src/qibo/hamiltonians/hamiltonians.py +++ b/src/qibo/hamiltonians/hamiltonians.py @@ -395,7 +395,6 @@ def ground_state(self): def exp(self, a): return self.dense.exp(a) - # only useful for dense_from_form, which might not be needed in the end @cache def _get_symbol_matrix(self, term): """Calculates numerical matrix corresponding to symbolic expression. @@ -434,11 +433,18 @@ def _get_symbol_matrix(self, term): # symbolic op for power base, exponent = term.as_base_exp() matrix = self._get_symbol_matrix(base) - result = self.backend.np.linalg.matrix_power(matrix, exponent) + 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: @@ -455,8 +461,6 @@ def _get_symbol_matrix(self, term): return result - # not sure this is useful, it appears to be significantly slower than - # the from_terms counterpart def _calculate_dense_from_form(self) -> Hamiltonian: """Calculates equivalent Hamiltonian using symbolic form. @@ -465,8 +469,9 @@ 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.""" + "Calculates equivalent Hamiltonian using the term representation." matrix = 0 indices = list(range(2 * self.nqubits)) einsum = ( @@ -492,6 +497,7 @@ def _calculate_dense_from_terms(self) -> Hamiltonian: else self.backend.np.zeros(2 * (2**self.nqubits,)) ) return Hamiltonian(self.nqubits, matrix, backend=self.backend) + self.constant + """ def calculate_dense(self) -> Hamiltonian: log.warning( @@ -500,9 +506,9 @@ def calculate_dense(self) -> Hamiltonian: ) # 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() + # 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) diff --git a/tests/test_hamiltonians_symbolic.py b/tests/test_hamiltonians_symbolic.py index 7d4e7a7cbc..ebb99c1f73 100644 --- a/tests/test_hamiltonians_symbolic.py +++ b/tests/test_hamiltonians_symbolic.py @@ -49,6 +49,15 @@ def test_symbolic_hamiltonian_form_setter(backend): 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]) def test_symbolictfim_hamiltonian_to_dense(backend, nqubits): final_ham = SymbolicHamiltonian(