Repository: qiboteam/qibo

Issue:
Question about `SymbolicHamiltonian.circuit` 
I assume it's a bug because it has cost me 2x a couple of days of work to trouble shoot the following issue appearing deep in the `dbi` module code:

```py
import numpy as np
from copy import deepcopy

from qibo import symbols, hamiltonians

d = hamiltonians.SymbolicHamiltonian( symbols.Z(0))
 
a = d.circuit(0.1)
b = d.circuit(0.1)
print(np.linalg.norm( d.circuit(0.2).unitary() - (a+b).unitary()))

a = deepcopy(d).circuit(0.1)
b = deepcopy(d).circuit(0.1)
print(np.linalg.norm( d.circuit(0.2).unitary() - (a+b).unitary()))
```
Output:
```
0.28237154359997657
0.0
```

TLDR: If you assign the return value of `SymbolicHamiltonian.circuit` and then want to use the `Circuit` instance e.g. for further addition then noise comes out. Correct result can be obtained by running on deepcopies.

Discussion:

Patch:
diff --git a/src/qibo/hamiltonians/adiabatic.py b/src/qibo/hamiltonians/adiabatic.py
index 4f69be8bf..d436861c2 100644
--- a/src/qibo/hamiltonians/adiabatic.py
+++ b/src/qibo/hamiltonians/adiabatic.py
@@ -1,4 +1,5 @@
 from abc import ABC, abstractmethod
+from itertools import chain
 
 from qibo.config import raise_error
 from qibo.hamiltonians import hamiltonians, terms
@@ -101,26 +102,6 @@ class BaseAdiabaticHamiltonian:
         )
 
 
-class TrotterCircuit(hamiltonians.TrotterCircuit):
-    """Object that caches the Trotterized evolution circuit.
-
-    See :class:`qibo.hamiltonians.hamiltonians.TrotterCircuit` for more details.
-    """
-
-    def set(self, dt, coefficients):
-        """Updates the circuit parameters for different values of ``t`` and ``dt``.
-
-        The update is done using the ``circuit.set_parameters`` method to avoid
-        recreating the circuit and gates.
-        """
-        params = {
-            gate: group.to_term(coefficients).exp(dt / 2.0)
-            for gate, group in self.gates.items()
-        }
-        self.dt = dt
-        self.circuit.set_parameters(params)
-
-
 class SymbolicAdiabaticHamiltonian(BaseAdiabaticHamiltonian):
     """Adiabatic Hamiltonian that is sum of :class:`qibo.hamiltonians.hamiltonians.SymbolicHamiltonian`."""
 
@@ -152,13 +133,21 @@ class SymbolicAdiabaticHamiltonian(BaseAdiabaticHamiltonian):
         Returns:
             A :class:`qibo.models.Circuit` implementing the Trotterized evolution.
         """
-        if self.trotter_circuit is None:
-            self.trotter_circuit = TrotterCircuit(
-                self.groups, dt, self.nqubits, accelerators
-            )
+        from qibo import Circuit  # pylint: disable=import-outside-toplevel
+        from qibo.hamiltonians.terms import (  # pylint: disable=import-outside-toplevel
+            TermGroup,
+        )
+
         # pylint: disable=E1102
         st = self.schedule(t / self.total_time) if t != 0 else 0
         # pylint: enable=E1102
         coefficients = {self.h0: 1 - st, self.h1: st}
-        self.trotter_circuit.set(dt, coefficients)
-        return self.trotter_circuit.circuit
+
+        groups = self.groups
+        circuit = Circuit(self.nqubits, accelerators=accelerators)
+        circuit.add(
+            group.to_term(coefficients).expgate(dt / 2.0)
+            for group in chain(groups, groups[::-1])
+        )
+
+        return circuit
diff --git a/src/qibo/hamiltonians/hamiltonians.py b/src/qibo/hamiltonians/hamiltonians.py
index 13366aa10..b270c5d00 100644
--- a/src/qibo/hamiltonians/hamiltonians.py
+++ b/src/qibo/hamiltonians/hamiltonians.py
@@ -275,43 +275,6 @@ class Hamiltonian(AbstractHamiltonian):
         )
 
 
-class TrotterCircuit:
-    """Object that caches the Trotterized evolution circuit.
-
-    This object holds a reference to the circuit models and updates its
-    parameters if a different time step ``dt`` is given without recreating
-    every gate from scratch.
-
-    Args:
-        groups (list): List of :class:`qibo.core.terms.TermGroup` objects that
-            correspond to the Trotter groups of terms in the time evolution
-            exponential operator.
-        dt (float): Time step for the Trotterization.
-        nqubits (int): Number of qubits in the system that evolves.
-        accelerators (dict): Dictionary with accelerators for distributed
-            circuits.
-    """
-
-    def __init__(self, groups, dt, nqubits, accelerators):
-        from qibo import Circuit  # pylint: disable=import-outside-toplevel
-
-        self.gates = {}
-        self.dt = dt
-        self.circuit = Circuit(nqubits, accelerators=accelerators)
-        for group in chain(groups, groups[::-1]):
-            gate = group.term.expgate(dt / 2.0)
-            self.gates[gate] = group
-            self.circuit.add(gate)
-
-    def set(self, dt):
-        if self.dt != dt:
-            params = {
-                gate: group.term.exp(dt / 2.0) for gate, group in self.gates.items()
-            }
-            self.dt = dt
-            self.circuit.set_parameters(params)
-
-
 class SymbolicHamiltonian(AbstractHamiltonian):
     """Hamiltonian based on a symbolic representation.
 
@@ -350,7 +313,6 @@ class SymbolicHamiltonian(AbstractHamiltonian):
         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``
-        self.trotter_circuit = None
 
         from qibo.symbols import Symbol  # pylint: disable=import-outside-toplevel
 
@@ -779,20 +741,24 @@ class SymbolicHamiltonian(AbstractHamiltonian):
         )
 
     def circuit(self, dt, accelerators=None):
-        """Circuit that implements a Trotter step of this Hamiltonian
-        for a given time step ``dt``.
+        """Circuit that implements a Trotter step of this Hamiltonian.
+
+        Args:
+            dt (float): Time step used for Trotterization.
+            accelerators (dict): Dictionary with accelerators for distributed circuits.
         """
-        if self.trotter_circuit is None:
-            from qibo.hamiltonians.terms import (  # pylint: disable=import-outside-toplevel
-                TermGroup,
-            )
+        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)
-            self.trotter_circuit = TrotterCircuit(
-                groups, dt, self.nqubits, accelerators
-            )
-        self.trotter_circuit.set(dt)
-        return self.trotter_circuit.circuit
+        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:


Test patch:
diff --git a/tests/test_hamiltonians_trotter.py b/tests/test_hamiltonians_trotter.py
index a438a61bd..a69e9c416 100644
--- a/tests/test_hamiltonians_trotter.py
+++ b/tests/test_hamiltonians_trotter.py
@@ -3,7 +3,7 @@
 import numpy as np
 import pytest
 
-from qibo import hamiltonians
+from qibo import hamiltonians, symbols
 from qibo.backends import NumpyBackend
 from qibo.quantum_info import random_hermitian, random_statevector
 
@@ -143,6 +143,16 @@ def test_trotter_hamiltonian_three_qubit_term(backend):
     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))
+    a = ham.circuit(0.1)
+    b = ham.circuit(0.1)
+    matrix1 = ham.circuit(0.2).unitary(backend)
+    matrix2 = (a + b).unitary(backend)
+    np.testing.assert_allclose(matrix1, matrix2)
+
+
 def test_old_trotter_hamiltonian_errors():
     """Check errors when creating the deprecated ``TrotterHamiltonian`` object."""
     with pytest.raises(NotImplementedError):