Repository: Qiskit/qiskit

Issue:
Deepcopy of control flow with standalone clbit results in invalid circuit
### Environment

- **Qiskit Terra version**: 0.24.1 (but also on main)
- **Python version**: 3.11
- **Operating system**:  Linux


### What is happening?

When running a deepcopy of a circuit object using a clbit as a standalone bit as the condition for a control flow operation results in the deepcopied circuit being invalid so that qasm3 or qpy serialization, and visualization will fail because the internal state of the circuit is corrupted and not valid in the copy.

### How can we reproduce the issue?

```python
import copy

from qiskit.circuit import QuantumCircuit, Qubit, Clbit, ClassicalRegister, QuantumRegister, Gate, Parameter

bits = [Qubit(), Qubit(), Clbit()]
qc = QuantumCircuit(bits)
with qc.if_test((qc.clbits[0], 1)):
    qc.x(0)

copy_qc = copy.deepcopy(qc)
print(copy_qc)
```

### What should happen?

It should print the visualization of the circuit. This will stack trace though during the circuit to dag conversion. You can also replace `print(copy_qc)` with something like:

```
import io
from qiskit.qpy import dump

with io.BytesIO() as fd:
    dump(copy_qc, fd)
```
this will also fail on a lookup of the clbit.

### Any suggestions?

I think the special `__deepcopy__` handling in the `qiskit.circuit.Instruction` class is not valid for the `ControlFlowOp` when the `.condition` attribute is a clbit. It might just be simplest to revert to the default deepcopy behavior for `ControlFlowOp`.


Discussion:
It's the `Bit` instances that are the problem here, really - a deepcopy of an immutable object should return itself. If we make `Bit.__deepcopy__` and `__copy__` return `self`, it should all automatically fix itself.

The handling would fail for a deep-copied regular instruction with a single clbit condition as well, there's just not really any point in our tests where we'd try and and see the failure.
Yeah that's fair, adding a `Bit.__deepcopy__` to return self would fix this. That's easy enough to add.

Resolving patch:
diff --git a/qiskit/circuit/bit.py b/qiskit/circuit/bit.py
--- a/qiskit/circuit/bit.py
+++ b/qiskit/circuit/bit.py
@@ -13,6 +13,7 @@
 """
 Quantum bit and Classical bit objects.
 """
+import copy
 
 from qiskit.circuit.exceptions import CircuitError
 from qiskit.utils.deprecation import deprecate_func
@@ -120,3 +121,20 @@ def __eq__(self, other):
             return self._repr == other._repr
         except AttributeError:
             return False
+
+    def __copy__(self):
+        # Bits are immutable.
+        return self
+
+    def __deepcopy__(self, memo=None):
+        if (self._register, self._index) == (None, None):
+            return self
+
+        # Old-style bits need special handling for now, since some code seems
+        # to rely on their registers getting deep-copied.
+        bit = type(self).__new__(type(self))
+        bit._register = copy.deepcopy(self._register, memo)
+        bit._index = self._index
+        bit._hash = self._hash
+        bit._repr = self._repr
+        return bit


Test patch:
diff --git a/test/python/circuit/test_bit.py b/test/python/circuit/test_bit.py
--- a/test/python/circuit/test_bit.py
+++ b/test/python/circuit/test_bit.py
@@ -13,7 +13,7 @@
 # pylint: disable=missing-function-docstring
 
 """Test library of quantum circuits."""
-
+import copy
 from unittest import mock
 
 from qiskit.test import QiskitTestCase
@@ -57,6 +57,25 @@ def test_old_style_bit_equality(self):
 
         self.assertNotEqual(bit.Bit(test_reg, 0), bit.Bit(reg_difftype, 0))
 
+    def test_old_style_bit_deepcopy(self):
+        """Verify deep-copies of bits are equal but not the same instance."""
+        test_reg = mock.MagicMock(size=3, name="foo")
+        test_reg.__str__.return_value = "Register(3, 'foo')"
+
+        bit1 = bit.Bit(test_reg, 0)
+        bit2 = copy.deepcopy(bit1)
+
+        self.assertIsNot(bit1, bit2)
+        self.assertIsNot(bit1._register, bit2._register)
+        self.assertEqual(bit1, bit2)
+
+    def test_old_style_bit_copy(self):
+        """Verify copies of bits are the same instance."""
+        bit1 = bit.Bit()
+        bit2 = copy.copy(bit1)
+
+        self.assertIs(bit1, bit2)
+
 
 class TestNewStyleBit(QiskitTestCase):
     """Test behavior of new-style bits."""
@@ -65,7 +84,21 @@ def test_bits_do_not_require_registers(self):
         """Verify we can create a bit outside the context of a register."""
         self.assertIsInstance(bit.Bit(), bit.Bit)
 
-    def test_newstyle_bit_equality(self):
+    def test_new_style_bit_deepcopy(self):
+        """Verify deep-copies of bits are the same instance."""
+        bit1 = bit.Bit()
+        bit2 = copy.deepcopy(bit1)
+
+        self.assertIs(bit1, bit2)
+
+    def test_new_style_bit_copy(self):
+        """Verify copies of bits are the same instance."""
+        bit1 = bit.Bit()
+        bit2 = copy.copy(bit1)
+
+        self.assertIs(bit1, bit2)
+
+    def test_new_style_bit_equality(self):
         """Verify bits instances are equal only to themselves."""
         bit1 = bit.Bit()
         bit2 = bit.Bit()