Repository: Quantum-TII/qibo (python)

Issue:
Imaginary values in symbolic hamiltonians
An error appears when defining the Hamiltonian with imaginary values in the symbolic representation.

Code to reproduce the behavior:
```
import numpy as np
from qibo import gates, hamiltonians, models, matrices, callbacks
import sympy


def Z_hamiltonian(N, L, t, r=1):
    nqubits = N*L
    
    # Define Hamiltonian using these symbols
    Z = sympy.symbols(" ".join((f"Z{i}" for i in range(nqubits))))
    X = sympy.symbols(" ".join((f"X{i}" for i in range(nqubits))))
    Y = sympy.symbols(" ".join((f"Y{i}" for i in range(nqubits))))
    sigma_plus = [0.5*X[i] + 0.5j*Y[i] for i in range(nqubits)]
    sigma_minus = [0.5*X[i] - 0.5j*Y[i] for i in range(nqubits)]
    
    # Define a map from symbols to actual matrices
    symbol_map = {s: (i, matrices.X) for i, s in enumerate(X)}
    symbol_map.update({s: (i, matrices.Z) for i, s in enumerate(Z)})
    symbol_map.update({s: (i, matrices.Y) for i, s in enumerate(Y)})
    
    # Define a dense Hamiltonian
    symbolic_ham = 0
    for s in range(N):
        for i in range(L):
            for rr in range(1,r+1):
                temp1 = (sigma_plus[i+s*L]*sigma_minus[(i+rr)%L+s*L] + sigma_minus[i+s*L]*sigma_plus[(i+rr)%L+s*L])
                for j in range(min(i, (i+rr)%L)+1, max(i, (i+rr)%L)):
                    temp1 *= Z[j+s*L]
                symbolic_ham -= t*temp1
    
    print(sympy.simplify(symbolic_ham))    
    return hamiltonians.Hamiltonian.from_symbolic(symbolic_ham, symbol_map)

N = 2
L = 3
t = 1j
hamiltonian_jw = Z_hamiltonian(N=N, L=L, t=t)
```
If you put t=1 it works perfectly, but when I define it as a complex number I get an error.
In case it is a bug, can be fixed asap? Thank you!


Pull request description:
Complex numbers in sympy Hamiltonians
Fixes #408 by adding support for complex coefficients in symbolic (sympy) Hamiltonians.

Patch:
diff --git a/src/qibo/core/hamiltonians.py b/src/qibo/core/hamiltonians.py
index 685da9718..b48ebb3ac 100644
--- a/src/qibo/core/hamiltonians.py
+++ b/src/qibo/core/hamiltonians.py
@@ -247,6 +247,8 @@ class SymbolicHamiltonian:
                     for _ in range(int(pow) - 1):
                         matrix = matrix.dot(matrix)
                     matrices.append(matrix)
+                elif factor == self.sympy.I: # imaginary unit
+                    matrices[0] *= 1j
                 else:
                     raise_error(ValueError, f"Cannot parse factor {factor}.")
             target_ids |= set(targets)


Test patch:
diff --git a/src/qibo/tests/test_hamiltonians.py b/src/qibo/tests/test_hamiltonians.py
index 543fc2729..11121b2d3 100644
--- a/src/qibo/tests/test_hamiltonians.py
+++ b/src/qibo/tests/test_hamiltonians.py
@@ -606,6 +606,29 @@ def test_from_symbolic_with_power(trotter):
     np.testing.assert_allclose(final_matrix, target_matrix)
 
 
+@pytest.mark.parametrize("trotter", [False, True])
+def test_from_symbolic_with_complex_numbers(trotter):
+    """Check ``from_symbolic`` when the expression contains imaginary unit."""
+    import sympy
+    x = sympy.symbols(" ".join((f"X{i}" for i in range(2))))
+    y = sympy.symbols(" ".join((f"Y{i}" for i in range(2))))
+    symham = (1 + 2j) * x[0] * x[1] + 2 * y[0] * y[1] - 3j * x[0] * y[1] + 1j * y[0] * x[1]
+    symmap = {s: (i, matrices.X) for i, s in enumerate(x)}
+    symmap.update({s: (i, matrices.Y) for i, s in enumerate(y)})
+    if trotter:
+        ham = TrotterHamiltonian.from_symbolic(symham, symmap)
+        final_matrix = ham.dense.matrix
+    else:
+        ham = Hamiltonian.from_symbolic(symham, symmap)
+        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)
+    np.testing.assert_allclose(final_matrix, target_matrix)
+
+
 def test_from_symbolic_application_hamiltonian():
     """Check ``from_symbolic`` for a specific four-qubit Hamiltonian."""
     import sympy