Repository: Qiskit/qiskit

Issue:
QPY fails to load circuits whose first register qubit is after a loose bit
### Environment

- **Qiskit Terra version**: 0.22.2
- **Python version**: 3.10
- **Operating system**: macOS


### What is happening?

QPY fails to load a circuit that has registers, but bit index 0 isn't in a register or at least, isn't in the circuit by the time a later register is read.

### How can we reproduce the issue?

```python
import io

from qiskit.circuit import QuantumCircuit, Clbit, ClassicalRegister
from qiskit.qpy import load, dump

qc = QuantumCircuit()
qc.add_bits([Clbit()])
qc.add_register(ClassicalRegister(2))

with io.BytesIO() as f:
    dump(qc, f)
    f.seek(0)
    out, = load(f)
```
fails with
```text
TypeError                                 Traceback (most recent call last)
<ipython-input-1-ddaf374c6759> in <module>
     11     dump(qc, f)
     12     f.seek(0)
---> 13     out, = load(f)
     14

~/code/qiskit/terra/qiskit/qpy/interface.py in load(file_obj, metadata_deserializer)
    269     for _ in range(data.num_programs):
    270         programs.append(
--> 271             loader(file_obj, data.qpy_version, metadata_deserializer=metadata_deserializer)
    272         )
    273     return programs

~/code/qiskit/terra/qiskit/qpy/binary_io/circuits.py in read_circuit(file_obj, version, metadata_deserializer)
    924                         if start > len(circ.qubits):
    925                             bits = [bit_type() for _ in range(start - bit_len)]
--> 926                             circ.add_bits(bit_len)
    927                         if in_circuit:
    928                             circ.add_register(reg)

~/code/qiskit/terra/qiskit/circuit/quantumcircuit.py in add_bits(self, bits)
   1394     def add_bits(self, bits: Iterable[Bit]) -> None:
   1395         """Add Bits to the circuit."""
-> 1396         duplicate_bits = set(self._qubit_indices).union(self._clbit_indices).intersection(bits)
   1397         if duplicate_bits:
   1398             raise CircuitError(f"Attempted to add bits found already in circuit: {duplicate_bits}")

TypeError: 'int' object is not iterable
```

### What should happen?

Correct load from QPY.

### Any suggestions?

`qpy/binary_io/circuits.py:926` is obviously wrong: it's passing an `int` to `QuantumCircuit.add_bits` rather than the `bits` that it has defined right above.  This isn't the only issue, though - the whole register reconstruction has an incorrect model of bit ownership and bit ordering, so a more complete example like
```python
import io
from qiskit.circuit import QuantumCircuit, Qubit, Clbit, QuantumRegister, ClassicalRegister
from qiskit.qpy import dump, load

qc = QuantumCircuit()
qc.add_bits([Qubit(), Clbit()])
qc.add_register(QuantumRegister(2, name="q1"))
qc.add_register(ClassicalRegister(2, name="c1"))
with io.BytesIO() as fptr:
    dump(qc, fptr)
    fptr.seek(0)
    new_circuit = load(fptr)[0]
assert qc == new_circuit
```
fails, because in the output circuit, the clbits go `[('c1', 0), ('c1', 1), Clbit()]`, whereas they _should_ be `[Clbit(), ('c1', 0), ('c1', 1)]`.


Discussion:

Resolving patch:
diff --git a/qiskit/qpy/binary_io/circuits.py b/qiskit/qpy/binary_io/circuits.py
--- a/qiskit/qpy/binary_io/circuits.py
+++ b/qiskit/qpy/binary_io/circuits.py
@@ -32,7 +32,7 @@
 from qiskit.circuit.quantumcircuit import QuantumCircuit
 from qiskit.circuit.quantumregister import QuantumRegister, Qubit
 from qiskit.extensions import quantum_initializer
-from qiskit.qpy import common, formats, exceptions, type_keys
+from qiskit.qpy import common, formats, type_keys
 from qiskit.qpy.binary_io import value, schedules
 from qiskit.quantum_info.operators import SparsePauliOp
 from qiskit.synthesis import evolution as evo_synth
@@ -845,124 +845,29 @@ def read_circuit(file_obj, version, metadata_deserializer=None):
     num_registers = header["num_registers"]
     num_instructions = header["num_instructions"]
     out_registers = {"q": {}, "c": {}}
+    circ = QuantumCircuit(
+        [Qubit() for _ in [None] * num_qubits],
+        [Clbit() for _ in [None] * num_clbits],
+        name=name,
+        global_phase=global_phase,
+        metadata=metadata,
+    )
     if num_registers > 0:
-        circ = QuantumCircuit(name=name, global_phase=global_phase, metadata=metadata)
         if version < 4:
             registers = _read_registers(file_obj, num_registers)
         else:
             registers = _read_registers_v4(file_obj, num_registers)
 
-        for bit_type_label, bit_type, reg_type in [
-            ("q", Qubit, QuantumRegister),
-            ("c", Clbit, ClassicalRegister),
-        ]:
-            register_bits = set()
+        for bit_type_label, reg_type in [("q", QuantumRegister), ("c", ClassicalRegister)]:
             # Add quantum registers and bits
-            for register_name in registers[bit_type_label]:
-                standalone, indices, in_circuit = registers[bit_type_label][register_name]
-                indices_defined = [x for x in indices if x >= 0]
-                # If a register has no bits in the circuit skip it
-                if not indices_defined:
-                    continue
-                if standalone:
-                    start = min(indices_defined)
-                    count = start
-                    out_of_order = False
-                    for index in indices:
-                        if index < 0:
-                            out_of_order = True
-                            continue
-                        if not out_of_order and index != count:
-                            out_of_order = True
-                        count += 1
-                        if index in register_bits:
-                            # If we have a bit in the position already it's been
-                            # added by an earlier register in the circuit
-                            # otherwise it's invalid qpy
-                            if not in_circuit:
-                                continue
-                            raise exceptions.QpyError("Duplicate register bits found")
-
-                        register_bits.add(index)
-
-                    num_reg_bits = len(indices)
-                    # Create a standlone register of the appropriate length (from
-                    # the number of indices in the qpy data) and add it to the circuit
-                    reg = reg_type(num_reg_bits, register_name)
-                    # If any bits from qreg are out of order in the circuit handle
-                    # is case
-                    if out_of_order or not in_circuit:
-                        for index, pos in sorted(
-                            enumerate(x for x in indices if x >= 0), key=lambda x: x[1]
-                        ):
-                            if bit_type_label == "q":
-                                bit_len = len(circ.qubits)
-                            else:
-                                bit_len = len(circ.clbits)
-                            if pos < bit_len:
-                                # If we have a bit in the position already it's been
-                                # added by an earlier register in the circuit
-                                # otherwise it's invalid qpy
-                                if not in_circuit:
-                                    continue
-                                raise exceptions.QpyError("Duplicate register bits found")
-                            # Fill any holes between the current register bit and the
-                            # next one
-                            if pos > bit_len:
-                                bits = [bit_type() for _ in range(pos - bit_len)]
-                                circ.add_bits(bits)
-                            circ.add_bits([reg[index]])
-                        if in_circuit:
-                            circ.add_register(reg)
-                    else:
-                        if bit_type_label == "q":
-                            bit_len = len(circ.qubits)
-                        else:
-                            bit_len = len(circ.clbits)
-                        # If there is a hole between the start of the register and the
-                        # current bits and standalone bits to fill the gap.
-                        if start > len(circ.qubits):
-                            bits = [bit_type() for _ in range(start - bit_len)]
-                            circ.add_bits(bit_len)
-                        if in_circuit:
-                            circ.add_register(reg)
-                        out_registers[bit_type_label][register_name] = reg
-                else:
-                    for index in indices:
-                        if bit_type_label == "q":
-                            bit_len = len(circ.qubits)
-                        else:
-                            bit_len = len(circ.clbits)
-                        # Add any missing bits
-                        bits = [bit_type() for _ in range(index + 1 - bit_len)]
-                        circ.add_bits(bits)
-                        if index in register_bits:
-                            raise exceptions.QpyError("Duplicate register bits found")
-                        register_bits.add(index)
-                    if bit_type_label == "q":
-                        bits = [circ.qubits[i] for i in indices]
-                    else:
-                        bits = [circ.clbits[i] for i in indices]
-                    reg = reg_type(name=register_name, bits=bits)
-                    if in_circuit:
-                        circ.add_register(reg)
-                    out_registers[bit_type_label][register_name] = reg
-        # If we don't have sufficient bits in the circuit after adding
-        # all the registers add more bits to fill the circuit
-        if len(circ.qubits) < num_qubits:
-            qubits = [Qubit() for _ in range(num_qubits - len(circ.qubits))]
-            circ.add_bits(qubits)
-        if len(circ.clbits) < num_clbits:
-            clbits = [Clbit() for _ in range(num_clbits - len(circ.clbits))]
-            circ.add_bits(clbits)
-    else:
-        circ = QuantumCircuit(
-            [Qubit() for _ in [None] * num_qubits],
-            [Clbit() for _ in [None] * num_clbits],
-            name=name,
-            global_phase=global_phase,
-            metadata=metadata,
-        )
+            circuit_bits = {"q": circ.qubits, "c": circ.clbits}[bit_type_label]
+            for register_name, (_, indices, in_circuit) in registers[bit_type_label].items():
+                register = reg_type(
+                    name=register_name, bits=[circuit_bits[x] for x in indices if x >= 0]
+                )
+                if in_circuit:
+                    circ.add_register(register)
+                out_registers[bit_type_label][register_name] = register
     custom_operations = _read_custom_operations(file_obj, version, vectors)
     for _instruction in range(num_instructions):
         _read_instruction(file_obj, circ, out_registers, custom_operations, version, vectors)


Test patch:
diff --git a/test/python/circuit/test_circuit_load_from_qpy.py b/test/python/circuit/test_circuit_load_from_qpy.py
--- a/test/python/circuit/test_circuit_load_from_qpy.py
+++ b/test/python/circuit/test_circuit_load_from_qpy.py
@@ -1103,3 +1103,65 @@ def test_load_with_loose_bits_and_registers(self):
         qpy_file.seek(0)
         new_circuit = load(qpy_file)[0]
         self.assertEqual(qc, new_circuit)
+
+    def test_registers_after_loose_bits(self):
+        """Test that a circuit whose registers appear after some loose bits roundtrips. Regression
+        test of gh-9094."""
+        qc = QuantumCircuit()
+        qc.add_bits([Qubit(), Clbit()])
+        qc.add_register(QuantumRegister(2, name="q1"))
+        qc.add_register(ClassicalRegister(2, name="c1"))
+        with io.BytesIO() as fptr:
+            dump(qc, fptr)
+            fptr.seek(0)
+            new_circuit = load(fptr)[0]
+        self.assertEqual(qc, new_circuit)
+
+    def test_roundtrip_empty_register(self):
+        """Test that empty registers round-trip correctly."""
+        qc = QuantumCircuit(QuantumRegister(0), ClassicalRegister(0))
+        with io.BytesIO() as fptr:
+            dump(qc, fptr)
+            fptr.seek(0)
+            new_circuit = load(fptr)[0]
+        self.assertEqual(qc, new_circuit)
+        self.assertEqual(qc.qregs, new_circuit.qregs)
+        self.assertEqual(qc.cregs, new_circuit.cregs)
+
+    def test_roundtrip_several_empty_registers(self):
+        """Test that several empty registers round-trip correctly."""
+        qc = QuantumCircuit(
+            QuantumRegister(0, "a"),
+            QuantumRegister(0, "b"),
+            ClassicalRegister(0, "c"),
+            ClassicalRegister(0, "d"),
+        )
+        with io.BytesIO() as fptr:
+            dump(qc, fptr)
+            fptr.seek(0)
+            new_circuit = load(fptr)[0]
+        self.assertEqual(qc, new_circuit)
+        self.assertEqual(qc.qregs, new_circuit.qregs)
+        self.assertEqual(qc.cregs, new_circuit.cregs)
+
+    def test_roundtrip_empty_registers_with_loose_bits(self):
+        """Test that empty registers still round-trip correctly in the presence of loose bits."""
+        loose = [Qubit(), Clbit()]
+
+        qc = QuantumCircuit(loose, QuantumRegister(0), ClassicalRegister(0))
+        with io.BytesIO() as fptr:
+            dump(qc, fptr)
+            fptr.seek(0)
+            new_circuit = load(fptr)[0]
+        self.assertEqual(qc, new_circuit)
+        self.assertEqual(qc.qregs, new_circuit.qregs)
+        self.assertEqual(qc.cregs, new_circuit.cregs)
+
+        qc = QuantumCircuit(QuantumRegister(0), ClassicalRegister(0), loose)
+        with io.BytesIO() as fptr:
+            dump(qc, fptr)
+            fptr.seek(0)
+            new_circuit = load(fptr)[0]
+        self.assertEqual(qc, new_circuit)
+        self.assertEqual(qc.qregs, new_circuit.qregs)
+        self.assertEqual(qc.cregs, new_circuit.cregs)