Repository: Qiskit/qiskit

Issue:
`copy.deepcopy` doesn't work on `QuantumCircuit`
### Environment

- **Qiskit Terra version**:  [a7fa6c5](https://github.com/Qiskit/qiskit/commit/a7fa6c52a6191034e52ef015378b9fc8cee34a26)
- **Python version**: 3.11
- **Operating system**: macOS


### What is happening?

Calling `copy.deepcopy` on `QuantumCircuit` and then calling `measure_all()` on the copy leads to crash.

### How can we reproduce the issue?

```
import copy
from qiskit import QuantumCircuit
qc = QuantumCircuit(2)
qc.h(1)
qc2 = copy.deepcopy(qc)
qc2.measure_all()
```

### What should happen?

The copy of circuit works.

### Any suggestions?

_No response_


Discussion:
Thanks for the report.  For the future, it's helpful if you can include some of the error message (or the full traceback) in the top comment as well, but no worries - here it is:
```
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
Cell In[1], line 6
      4 qc.h(1)
      5 qc2 = copy.deepcopy(qc)
----> 6 qc2.measure_all()

File ~/code/qiskit/terra/qiskit/circuit/quantumcircuit.py:2810, in QuantumCircuit.measure_all(self, inplace, add_bits)
   2808     circ.add_register(new_creg)
   2809     circ.barrier()
-> 2810     circ.measure(circ.qubits, new_creg)
   2811 else:
   2812     if len(circ.clbits) < len(circ.qubits):

File ~/code/qiskit/terra/qiskit/circuit/quantumcircuit.py:2748, in QuantumCircuit.measure(self, qubit, cbit)
   2674 r"""Measure a quantum bit (``qubit``) in the Z basis into a classical bit (``cbit``).
   2675
   2676 When a quantum state is measured, a qubit is projected in the computational (Pauli Z) basis
   (...)
   2744
   2745 """
   2746 from .measure import Measure
-> 2748 return self.append(Measure(), [qubit], [cbit])

File ~/code/qiskit/terra/qiskit/circuit/quantumcircuit.py:1422, in QuantumCircuit.append(self, instruction, qargs, cargs)
   1420     self._check_dups(qarg)
   1421     instruction = CircuitInstruction(operation, qarg, carg)
-> 1422     circuit_scope.append(instruction)
   1423     instructions._add_ref(circuit_scope.instructions, len(circuit_scope.instructions) - 1)
   1424 return instructions

File ~/code/qiskit/terra/qiskit/circuit/quantumcircuit.py:5462, in _OuterCircuitScopeInterface.append(self, instruction)
   5460 def append(self, instruction):
   5461     # QuantumCircuit._append is semi-public, so we just call back to it.
-> 5462     return self.circuit._append(instruction)

File ~/code/qiskit/terra/qiskit/circuit/quantumcircuit.py:1481, in QuantumCircuit._append(self, instruction, qargs, cargs)
   1479 if old_style:
   1480     instruction = CircuitInstruction(instruction, qargs, cargs)
-> 1481 self._data.append(instruction)
   1482 self._track_operation(instruction.operation)
   1483 return instruction.operation if old_style else instruction

KeyError: "Bit Clbit(ClassicalRegister(2, 'meas'), 0) has not been added to this circuit."
```

Just flicking through the various properties of how the Rust-based `CircuitData` is deep-copied, I suspect somewhere the internal Rust-side components are getting out of sync when new registers are added to a circuit after the copy.

Resolving patch:
diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py
--- a/qiskit/circuit/quantumcircuit.py
+++ b/qiskit/circuit/quantumcircuit.py
@@ -562,9 +562,11 @@ def __deepcopy__(self, memo=None):
         #   copy.deepcopy(memo).
         cls = self.__class__
         result = cls.__new__(cls)
-        for k in self.__dict__.keys() - {"_data"}:
+        for k in self.__dict__.keys() - {"_data", "_builder_api"}:
             setattr(result, k, copy.deepcopy(self.__dict__[k], memo))
 
+        result._builder_api = _OuterCircuitScopeInterface(result)
+
         # Avoids pulling self._data into a Python list
         # like we would when pickling.
         result._data = self._data.copy()


Test patch:
diff --git a/test/python/circuit/test_circuit_operations.py b/test/python/circuit/test_circuit_operations.py
--- a/test/python/circuit/test_circuit_operations.py
+++ b/test/python/circuit/test_circuit_operations.py
@@ -12,6 +12,8 @@
 
 
 """Test Qiskit's QuantumCircuit class."""
+import copy
+import pickle
 
 import numpy as np
 from ddt import data, ddt
@@ -638,6 +640,57 @@ def test_measure_all(self):
 
         self.assertEqual(expected, circuit)
 
+    def test_measure_all_after_copy(self):
+        """
+        Test measure_all on a circuit that has been copied.
+        """
+        qc = QuantumCircuit(2)
+        qc.h(1)
+
+        qc2 = qc.copy()
+
+        qc.measure_all()
+        qc2.measure_all()
+
+        expected_cregs = [ClassicalRegister(2, "meas")]
+        self.assertEqual(qc.cregs, expected_cregs)
+        self.assertEqual(qc2.cregs, expected_cregs)
+        self.assertEqual(qc, qc2)
+
+    def test_measure_all_after_deepcopy(self):
+        """
+        Test measure_all on a circuit that has been deep-copied.
+        """
+        qc = QuantumCircuit(2)
+        qc.h(1)
+
+        qc2 = copy.deepcopy(qc)
+
+        qc.measure_all()
+        qc2.measure_all()
+
+        expected_cregs = [ClassicalRegister(2, "meas")]
+        self.assertEqual(qc.cregs, expected_cregs)
+        self.assertEqual(qc2.cregs, expected_cregs)
+        self.assertEqual(qc, qc2)
+
+    def test_measure_all_after_pickle(self):
+        """
+        Test measure_all on a circuit that has been pickled.
+        """
+        qc = QuantumCircuit(2)
+        qc.h(1)
+
+        qc2 = pickle.loads(pickle.dumps(qc))
+
+        qc.measure_all()
+        qc2.measure_all()
+
+        expected_cregs = [ClassicalRegister(2, "meas")]
+        self.assertEqual(qc.cregs, expected_cregs)
+        self.assertEqual(qc2.cregs, expected_cregs)
+        self.assertEqual(qc, qc2)
+
     def test_measure_all_not_add_bits_equal(self):
         """Test measure_all applies measurements to all qubits.
         Does not create a new ClassicalRegister if the existing one is big enough.
@@ -755,9 +808,9 @@ def test_remove_final_measurements_copy_with_parameters(self):
         circuit.rz(theta, qr)
         circuit.measure(qr, cr)
         circuit.remove_final_measurements()
-        copy = circuit.copy()
+        copied = circuit.copy()
 
-        self.assertEqual(copy, circuit)
+        self.assertEqual(copied, circuit)
 
     def test_remove_final_measurements_multiple_measures(self):
         """Test remove_final_measurements only removes measurements at the end of the circuit