Repository: qiboteam/qibo

Issue:
Circuit.light_cone() resets phases of parametrized gates
Example:

```python3
from qibo import gates as gt
from qibo.models import Circuit
from numpy import asarray, pi
from numpy.random import rand

qubits = 7
depth = 2

circuit = Circuit(qubits)
for _ in range(depth):
    for qubit in range(qubits):
        circuit.add(gt.U3(qubit, 0, 0, 0))
    cnots_even = asarray([[k, k + 1] for k in range(0, qubits - 1, 2)])
    cnots_odd = asarray([[k, k + 1] for k in range(1, qubits - 1, 2)])
    for row in cnots_even:
        circuit.add(gt.CNOT(*row))
    for row in cnots_odd:
        circuit.add(gt.CNOT(*row))

phases = list()
for _ in range(depth):
    p = rand(qubits, 3)
    p[:, 0] = p[:, 0] * pi
    p[:, 1] = p[:, 1] * 2*pi
    p[:, 1] = p[:, 2] * 2*pi
    phases.append(p)

phases = asarray(phases).flatten().reshape(qubits * depth, 3)
circuit.set_parameters(phases)
print(circuit.get_parameters())

cone = circuit.copy(True)
cone.add((gt.Y(k) for k in [1, 2]))
cone = cone.light_cone(1, 2)[0]
print(cone.get_parameters())
```

I have tried `cone.set_parameters(phases)` after the deep copy, but it doesn't solve the issue. No matter what, phases are set to 0 after `light_cone()`

Discussion:

Patch:
diff --git a/src/qibo/backends/abstract.py b/src/qibo/backends/abstract.py
index 055fdeebf..277ea8200 100644
--- a/src/qibo/backends/abstract.py
+++ b/src/qibo/backends/abstract.py
@@ -349,15 +349,13 @@ class Backend(abc.ABC):
         raise_error(NotImplementedError)
 
     @abc.abstractmethod
-    def calculate_expectation_state(
-        self, hamiltonian, state, normalize
-    ):  # pragma: no cover
+    def calculate_expectation_state(self, matrix, state, normalize):  # pragma: no cover
         """Calculate expectation value of a state vector given the observable matrix."""
         raise_error(NotImplementedError)
 
     @abc.abstractmethod
     def calculate_expectation_density_matrix(
-        self, hamiltonian, state, normalize
+        self, matrix, state, normalize
     ):  # pragma: no cover
         """Calculate expectation value of a density matrix given the observable matrix."""
         raise_error(NotImplementedError)
diff --git a/src/qibo/backends/numpy.py b/src/qibo/backends/numpy.py
index 2cd635a4b..4c86c9990 100644
--- a/src/qibo/backends/numpy.py
+++ b/src/qibo/backends/numpy.py
@@ -643,17 +643,17 @@ class NumpyBackend(Backend):
             ud = self.np.transpose(self.np.conj(eigenvectors))
             return self.np.matmul(eigenvectors, self.np.matmul(expd, ud))
 
-    def calculate_expectation_state(self, hamiltonian, state, normalize):
+    def calculate_expectation_state(self, matrix, state, normalize):
         statec = self.np.conj(state)
-        hstate = hamiltonian @ state
+        hstate = matrix @ state
         ev = self.np.real(self.np.sum(statec * hstate))
         if normalize:
             norm = self.np.sum(self.np.square(self.np.abs(state)))
             ev = ev / norm
         return ev
 
-    def calculate_expectation_density_matrix(self, hamiltonian, state, normalize):
-        ev = self.np.real(self.np.trace(hamiltonian @ state))
+    def calculate_expectation_density_matrix(self, matrix, state, normalize):
+        ev = self.np.real(self.np.trace(matrix @ state))
         if normalize:
             norm = self.np.real(self.np.trace(state))
             ev = ev / norm
diff --git a/src/qibo/gates/abstract.py b/src/qibo/gates/abstract.py
index 00c79562b..831f7a379 100644
--- a/src/qibo/gates/abstract.py
+++ b/src/qibo/gates/abstract.py
@@ -340,6 +340,11 @@ class ParametrizedGate(Gate):
         for gate in self.device_gates:  # pragma: no cover
             gate.parameters = x
 
+    def on_qubits(self, qubit_map):
+        gate = super().on_qubits(qubit_map)
+        gate.parameters = self.parameters
+        return gate
+
     def substitute_symbols(self):
         params = list(self._parameters)
         for i, param in self.symbolic_parameters.items():
diff --git a/src/qibo/gates/gates.py b/src/qibo/gates/gates.py
index e08b185c6..0fb66e0e3 100644
--- a/src/qibo/gates/gates.py
+++ b/src/qibo/gates/gates.py
@@ -1264,6 +1264,7 @@ class Unitary(ParametrizedGate):
         if self.is_controlled_by:
             controls = (qubit_map.get(i) for i in self.control_qubits)
             gate = gate.controlled_by(*controls)
+        gate.parameters = self.parameters
         return gate
 
     def _dagger(self):
diff --git a/src/qibo/hamiltonians/hamiltonians.py b/src/qibo/hamiltonians/hamiltonians.py
index c197a6a33..d9b97a618 100644
--- a/src/qibo/hamiltonians/hamiltonians.py
+++ b/src/qibo/hamiltonians/hamiltonians.py
@@ -114,10 +114,12 @@ class Hamiltonian(AbstractHamiltonian):
         if isinstance(state, self.backend.tensor_types):
             shape = tuple(state.shape)
             if len(shape) == 1:  # state vector
-                return self.backend.calculate_expectation_state(self, state, normalize)
+                return self.backend.calculate_expectation_state(
+                    self.matrix, state, normalize
+                )
             elif len(shape) == 2:  # density matrix
                 return self.backend.calculate_expectation_density_matrix(
-                    self, state, normalize
+                    self.matrix, state, normalize
                 )
             else:
                 raise_error(
@@ -662,7 +664,7 @@ class SymbolicHamiltonian(AbstractHamiltonian):
                 self.backend,
                 self.backend.cast(state, copy=True),
                 self.nqubits,
-                density_matrix=density_matrix,
+                density_matrix,
             )
         if self.constant:  # pragma: no cover
             total += self.constant * state


Test patch:
diff --git a/src/qibo/tests/test_models_circuit_parametrized.py b/src/qibo/tests/test_models_circuit_parametrized.py
index 6fb28f60f..3314c49c7 100644
--- a/src/qibo/tests/test_models_circuit_parametrized.py
+++ b/src/qibo/tests/test_models_circuit_parametrized.py
@@ -190,6 +190,27 @@ def test_set_parameters_with_gate_fusion(backend, trainable):
     backend.assert_circuitclose(fused_c, c)
 
 
+@pytest.mark.parametrize("trainable", [True, False])
+def test_set_parameters_with_light_cone(backend, trainable):
+    """Check updating parameters of light cone circuit."""
+    params = np.random.random(4)
+    c = Circuit(4)
+    c.add(gates.RX(0, theta=params[0], trainable=trainable))
+    c.add(gates.RY(1, theta=params[1]))
+    c.add(gates.CZ(0, 1))
+    c.add(gates.RX(2, theta=params[2]))
+    c.add(gates.RY(3, theta=params[3], trainable=trainable))
+    c.add(gates.CZ(2, 3))
+    if trainable:
+        c.set_parameters(np.random.random(4))
+    else:
+        c.set_parameters(np.random.random(2))
+    target_state = backend.execute_circuit(c)
+    lc, _ = c.light_cone(1, 2)
+    final_state = backend.execute_circuit(lc)
+    backend.assert_allclose(final_state, target_state)
+
+
 def test_variable_theta():
     """Check that parametrized gates accept `tf.Variable` parameters."""
     try: