Repository: qiboteam/qibo

Issue:
When using .copy() method on circuit the depth changes of original circuit
Copying a circuit with the .copy() method alters the attribute ``.queue.moments`` which results in a different depths.

```python
from qibo import gates, models
nqubits = 3
circuit = models.Circuit(nqubits)
circuit.add(gates.X(0))
circuit.add(gates.X(1))
circuit.add(gates.H(0))
circuit.add(gates.H(0))
print(circuit.depth) # -> 3
print(circuit.queue.moments)
print('----- copy circuit, original circuit was altered -----')
copied_circuit = circuit.copy(deep = False)
print(circuit.depth) # -> 6
print(circuit.queue.moments)
print('----- copied circuit  -----')
print(copied_circuit.depth) # -> 6
print(copied_circuit.queue.moments)
```

Also, when using the ``copy.deepcopy`` python module, the copied circuit has the same problem, but the circuit itself was not altered:

```python
from qibo import gates, models
from copy import deepcopy
nqubits = 3
circuit = models.Circuit(nqubits)
circuit.add(gates.X(0))
circuit.add(gates.X(1))
circuit.add(gates.H(0))
circuit.add(gates.H(0))
print(circuit.depth) # -> 3
print(circuit.queue.moments) 
print('----- copy.deepcopy circuit -----')
deepcopy_circuit = deepcopy(circuit)
print(deepcopy_circuit.queue.moments)
print(deepcopy_circuit.depth) # -> 6
print('--------- no change in original circuit ---------')
print(circuit.depth) # -> 3
print(circuit.queue.moments)
```

Only when setting ``circuit.copy(deep = True)`` the copy process works.


Discussion:

Patch:
diff --git a/src/qibo/models/circuit.py b/src/qibo/models/circuit.py
index 9bb6d05ee..452ad91b1 100644
--- a/src/qibo/models/circuit.py
+++ b/src/qibo/models/circuit.py
@@ -1,4 +1,5 @@
 import collections
+import copy
 from typing import Dict, List, Tuple, Union
 
 import numpy as np
@@ -350,8 +351,6 @@ class Circuit:
         Returns:
             The copied circuit object.
         """
-        import copy
-
         if deep:
             new_circuit = self.__class__(**self.init_kwargs)
             for gate in self.queue:
@@ -361,14 +360,7 @@ class Circuit:
                         NotImplementedError,
                         "Cannot create deep copy of fused circuit.",
                     )
-
-                new_gate = copy.copy(gate)
-                new_circuit.queue.append(new_gate)
-                if isinstance(gate, gates.ParametrizedGate):
-                    new_circuit.parametrized_gates.append(new_gate)
-                    if gate.trainable:
-                        new_circuit.trainable_gates.append(new_gate)
-            new_circuit.measurements = list(self.measurements)
+                new_circuit.add(copy.copy(gate))
         else:
             if self.accelerators:  # pragma: no cover
                 raise_error(
@@ -376,8 +368,9 @@ class Circuit:
                     "Non-deep copy is not allowed for distributed "
                     "circuits because they modify gate objects.",
                 )
-            new_circuit = self._shallow_copy()
-            new_circuit.queue = copy.copy(self.queue)
+            new_circuit = self.__class__(**self.init_kwargs)
+            for gate in self.queue:
+                new_circuit.add(gate)
         return new_circuit
 
     def invert(self):


Test patch:
diff --git a/tests/test_models_circuit.py b/tests/test_models_circuit.py
index ce4e9a4db..fecb553aa 100644
--- a/tests/test_models_circuit.py
+++ b/tests/test_models_circuit.py
@@ -330,9 +330,12 @@ def test_circuit_copy(deep):
     c1 = Circuit(2)
     c1.add([gates.H(0), gates.H(1), gates.CNOT(0, 1)])
     c2 = c1.copy(deep)
-    assert c2.depth == c1.depth
-    assert c2.ngates == c1.ngates
-    assert c2.nqubits == c1.nqubits
+    assert c1.depth == 2
+    assert c2.depth == 2
+    assert c1.ngates == 3
+    assert c2.ngates == 3
+    assert c1.nqubits == 2
+    assert c2.nqubits == 2
     for g1, g2 in zip(c1.queue, c2.queue):
         if deep:
             assert g1.__class__ == g2.__class__
@@ -342,12 +345,19 @@ def test_circuit_copy(deep):
             assert g1 is g2
 
 
-def test_circuit_copy_with_measurements():
+@pytest.mark.parametrize("deep", [False, True])
+def test_circuit_copy_with_measurements(deep):
     c1 = Circuit(4)
     c1.add([gates.H(0), gates.H(3), gates.CNOT(0, 2)])
     c1.add(gates.M(0, 1, register_name="a"))
     c1.add(gates.M(3, register_name="b"))
-    c2 = c1.copy()
+    c2 = c1.copy(deep)
+    assert c1.nqubits == 4
+    assert c2.nqubits == 4
+    assert c1.depth == 3
+    assert c2.depth == 3
+    assert c1.ngates == 5
+    assert c2.ngates == 5
     assert c2.measurement_tuples == {"a": (0, 1), "b": (3,)}