Repository: Qiskit/qiskit

Issue:
Control-flow "parameter" circuits cannot be copied
### Environment

- **Qiskit Terra version**: 0.19.0
- **Python version**: any
- **Operating system**: any


### What is happening?

When creating a control-flow instruction with the new builder interface, the `QuantumCircuit` bodies used in the `params` cannot be copied with `QuantumCircuit.copy`.  This is because the builders interally store the `qargs` and `cargs` as `tuple` (since they _should_ be immutable), but `QuantumCircuit.copy` assumes that they'll be `list`.

### How can we reproduce the issue?

```python
In [1]: from qiskit import QuantumCircuit
   ...: from qiskit.circuit import QuantumRegister, ClassicalRegister
   ...:
   ...: qreg = QuantumRegister(4)
   ...: creg = ClassicalRegister(1)
   ...: circ = QuantumCircuit(qreg, creg)
   ...:
   ...: with circ.if_test((creg, 0)):
   ...:     circ.h(0)
   ...:
   ...: if_else_inst, _, _ = circ._data[0]
   ...: true_body = if_else_inst.params[0]
   ...: true_body.copy()
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-1-4dae35f52209> in <module>
     11 if_else_inst, _, _ = circ._data[0]
     12 true_body = if_else_inst.params[0]
---> 13 true_body.copy()

~/code/qiskit/terra/qiskit/circuit/quantumcircuit.py in copy(self, name)
   2143         )
   2144
-> 2145         cpy._data = [
   2146             (instr_copies[id(inst)], qargs.copy(), cargs.copy())
   2147             for inst, qargs, cargs in self._data

~/code/qiskit/terra/qiskit/circuit/quantumcircuit.py in <listcomp>(.0)
   2144
   2145         cpy._data = [
-> 2146             (instr_copies[id(inst)], qargs.copy(), cargs.copy())
   2147             for inst, qargs, cargs in self._data
   2148         ]

AttributeError: 'tuple' object has no attribute 'copy'
```

### What should happen?

The copy should succeed here.  This is needed in Aer 0.10.

### Any suggestions?

The simplest is just to make the builder interface output lists instead of tuples.  We _could_ change `qargs.copy()` in `QuantumCircuit.copy` to be `tuple(qargs)`, but right now that will have some performance implications, and it's pretty likely that there are other places that assume that the args are lists.

Personally I would prefer us to change the `QuantumCircuit.data` element type to be `(Instruction, Tuple[Qubit], Tuple[Clbit])`, but that's a much bigger change than is necessary right now.  It's currently best to be consistent.


Discussion:

Resolving patch:
diff --git a/qiskit/circuit/controlflow/builder.py b/qiskit/circuit/controlflow/builder.py
--- a/qiskit/circuit/controlflow/builder.py
+++ b/qiskit/circuit/controlflow/builder.py
@@ -368,7 +368,10 @@ def build(
             # We already did the broadcasting and checking when the first call to
             # QuantumCircuit.append happened (which the user wrote), and added the instruction into
             # this scope.  We just need to finish the job now.
-            out._append(operation, qubits, clbits)
+            #
+            # We have to convert the tuples to lists, because some parts of QuantumCircuit still
+            # expect exactly this type.
+            out._append(operation, list(qubits), list(clbits))
 
         return out
 


Test patch:
diff --git a/test/python/circuit/test_control_flow_builders.py b/test/python/circuit/test_control_flow_builders.py
--- a/test/python/circuit/test_control_flow_builders.py
+++ b/test/python/circuit/test_control_flow_builders.py
@@ -12,6 +12,7 @@
 
 """Test operations on the builder interfaces for control flow in dynamic QuantumCircuits."""
 
+import copy
 import math
 
 import ddt
@@ -1802,6 +1803,130 @@ def test_labels_propagated_to_instruction(self):
             self.assertIsInstance(instruction, IfElseOp)
             self.assertEqual(instruction.label, label)
 
+    def test_copy_of_circuits(self):
+        """Test that various methods of copying a circuit made with the builder interface works."""
+        test = QuantumCircuit(5, 5)
+        cond = (test.clbits[2], False)
+        with test.if_test(cond) as else_:
+            test.cx(0, 1)
+        with else_:
+            test.h(2)
+        with test.for_loop(range(5)):
+            with test.if_test(cond):
+                test.x(3)
+        with test.while_loop(cond):
+            test.measure(0, 4)
+        self.assertEqual(test, test.copy())
+        self.assertEqual(test, copy.copy(test))
+        self.assertEqual(test, copy.deepcopy(test))
+
+    def test_copy_of_instructions(self):
+        """Test that various methods of copying the actual instructions created by the builder
+        interface work."""
+        qubits = [Qubit() for _ in [None] * 3]
+        clbits = [Clbit() for _ in [None] * 3]
+        cond = (clbits[1], False)
+
+        with self.subTest("if"):
+            test = QuantumCircuit(qubits, clbits)
+            with test.if_test(cond):
+                test.cx(0, 1)
+                test.measure(2, 2)
+            if_instruction, _, _ = test.data[0]
+            self.assertEqual(if_instruction, if_instruction.copy())
+            self.assertEqual(if_instruction, copy.copy(if_instruction))
+            self.assertEqual(if_instruction, copy.deepcopy(if_instruction))
+
+        with self.subTest("if/else"):
+            test = QuantumCircuit(qubits, clbits)
+            with test.if_test(cond) as else_:
+                test.cx(0, 1)
+                test.measure(2, 2)
+            with else_:
+                test.cx(1, 0)
+                test.measure(2, 2)
+            if_instruction, _, _ = test.data[0]
+            self.assertEqual(if_instruction, if_instruction.copy())
+            self.assertEqual(if_instruction, copy.copy(if_instruction))
+            self.assertEqual(if_instruction, copy.deepcopy(if_instruction))
+
+        with self.subTest("for"):
+            test = QuantumCircuit(qubits, clbits)
+            with test.for_loop(range(4)):
+                test.cx(0, 1)
+                test.measure(2, 2)
+            for_instruction, _, _ = test.data[0]
+            self.assertEqual(for_instruction, for_instruction.copy())
+            self.assertEqual(for_instruction, copy.copy(for_instruction))
+            self.assertEqual(for_instruction, copy.deepcopy(for_instruction))
+
+        with self.subTest("while"):
+            test = QuantumCircuit(qubits, clbits)
+            with test.while_loop(cond):
+                test.cx(0, 1)
+                test.measure(2, 2)
+            while_instruction, _, _ = test.data[0]
+            self.assertEqual(while_instruction, while_instruction.copy())
+            self.assertEqual(while_instruction, copy.copy(while_instruction))
+            self.assertEqual(while_instruction, copy.deepcopy(while_instruction))
+
+    def test_copy_of_instruction_parameters(self):
+        """Test that various methods of copying the parameters inside instructions created by the
+        builder interface work.  Regression test of gh-7367."""
+        qubits = [Qubit() for _ in [None] * 3]
+        clbits = [Clbit() for _ in [None] * 3]
+        cond = (clbits[1], False)
+
+        with self.subTest("if"):
+            test = QuantumCircuit(qubits, clbits)
+            with test.if_test(cond):
+                test.cx(0, 1)
+                test.measure(2, 2)
+            if_instruction, _, _ = test.data[0]
+            (true_body,) = if_instruction.blocks
+            self.assertEqual(true_body, true_body.copy())
+            self.assertEqual(true_body, copy.copy(true_body))
+            self.assertEqual(true_body, copy.deepcopy(true_body))
+
+        with self.subTest("if/else"):
+            test = QuantumCircuit(qubits, clbits)
+            with test.if_test(cond) as else_:
+                test.cx(0, 1)
+                test.measure(2, 2)
+            with else_:
+                test.cx(1, 0)
+                test.measure(2, 2)
+            if_instruction, _, _ = test.data[0]
+            true_body, false_body = if_instruction.blocks
+            self.assertEqual(true_body, true_body.copy())
+            self.assertEqual(true_body, copy.copy(true_body))
+            self.assertEqual(true_body, copy.deepcopy(true_body))
+            self.assertEqual(false_body, false_body.copy())
+            self.assertEqual(false_body, copy.copy(false_body))
+            self.assertEqual(false_body, copy.deepcopy(false_body))
+
+        with self.subTest("for"):
+            test = QuantumCircuit(qubits, clbits)
+            with test.for_loop(range(4)):
+                test.cx(0, 1)
+                test.measure(2, 2)
+            for_instruction, _, _ = test.data[0]
+            (for_body,) = for_instruction.blocks
+            self.assertEqual(for_body, for_body.copy())
+            self.assertEqual(for_body, copy.copy(for_body))
+            self.assertEqual(for_body, copy.deepcopy(for_body))
+
+        with self.subTest("while"):
+            test = QuantumCircuit(qubits, clbits)
+            with test.while_loop(cond):
+                test.cx(0, 1)
+                test.measure(2, 2)
+            while_instruction, _, _ = test.data[0]
+            (while_body,) = while_instruction.blocks
+            self.assertEqual(while_body, while_body.copy())
+            self.assertEqual(while_body, copy.copy(while_body))
+            self.assertEqual(while_body, copy.deepcopy(while_body))
+
 
 @ddt.ddt
 class TestControlFlowBuildersFailurePaths(QiskitTestCase):