Repository: Qiskit/qiskit

Issue:
QuantumCircuit.compose does not raise an error with invalid qubits argument
<!-- ⚠️ If you do not respect this template, your issue will be closed -->
<!-- ⚠️ Make sure to browse the opened and closed issues -->

### Information

- **Qiskit Terra version**: master @ b1c9a23
- **Python version**: 3.7.1
- **Operating system**: osx

### What is the current behavior?
`QuantumCircuit.compose` runs normally if `qubits` argument has duplicate values. If you try to compose two circuits after converting to dag, `DAGCircuit.compose` raises an error. Introduced after #4762 .


### Steps to reproduce the problem
```
n = 3
qc = QuantumCircuit(n)
qc.h(range(n))

m = 2
circ = QuantumCircuit(m)
circ.z(range(m))
circ.cx(0, 1)

new_circuit = qc.compose(circ, qubits=[1, 1])
print(new_circuit)


     ┌───┐          
q_0: ┤ H ├──────────
     ├───┤┌───┐┌───┐
q_1: ┤ H ├┤ Z ├┤ Z ├
     ├───┤└───┘└───┘
q_2: ┤ H ├──────────
     └───┘   
```

But 
```
dag_qc   = circuit_to_dag(qc)
dag_circ = circuit_to_dag(circ)

dag = dag_qc.compose(dag_circ, 
                     qubits=[1, 1], inplace=False)


raises DAGCircuitError
```

### What is the expected behavior?
As duplicate values in `qubits` is not a valid input, `QuantumCircuit.compose` should raise an error.


### Suggested solutions
Similar to `DAGCircuit.compose`
```
        # Check the edge_map for duplicate values
        if len(set(edge_map.values())) != len(edge_map):
              raise CircuitError("duplicates in wire_map")
```




Discussion:
I'm a little hesitant to adding checks in the `compose` method because it might slow down the usage and this use-case is obviously a wrong input, so that users won't be surprised if it doesn't work. But if the DAGCircuitError is too cryptic, we could improve it there? This would not give us additional overhead when users use `compose` correctly but would still give a proper error message.

**Edit:** Maybe it would be better to actually do include the check in compose. If we notice it causes performance issues we can also add a flag that deactivates the checks or use a private method like `_append`?
# Update for Santa:elf:- QGoGP
✅ Error reproducible! Has the same 'duplicates in wire map' DAGCircuitError raised as mentioned by OP

```
from qiskit.converters import circuit_to_dag
from qiskit import QuantumCircuit

n = 3
qc = QuantumCircuit(n)
qc.h(range(n))

m = 2
circ = QuantumCircuit(m)
circ.z(range(m))
circ.cx(0, 1)

new_circuit = qc.compose(circ, qubits=[1, 1])
print(new_circuit)
dag_qc   = circuit_to_dag(qc)
dag_circ = circuit_to_dag(circ)

dag = dag_qc.compose(dag_circ, 
                     qubits=[1, 1], inplace=False)
```
#### Screenshot 

![Screenshot_90](https://user-images.githubusercontent.com/61380731/145870680-618973e3-15f4-4783-bf43-f19dd8d8ba46.png)


Will update comment/add more comments if cause is found/fixed!

> Python version 3.9.7
> qiskit-terra version: 0.19.1


Resolving patch:
diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py
--- a/qiskit/circuit/quantumcircuit.py
+++ b/qiskit/circuit/quantumcircuit.py
@@ -953,9 +953,10 @@ def compose(
                 "Cannot emit a new composed circuit while a control-flow context is active."
             )
 
+        # Avoid mutating `dest` until as much of the error checking as possible is complete, to
+        # avoid an in-place composition getting `self` in a partially mutated state for a simple
+        # error that the user might want to correct in an interactive session.
         dest = self if inplace else self.copy()
-        dest.duration = None
-        dest.unit = "dt"
 
         # As a special case, allow composing some clbits onto no clbits - normally the destination
         # has to be strictly larger. This allows composing final measurements onto unitary circuits.
@@ -997,23 +998,9 @@ def compose(
                 "Trying to compose with another QuantumCircuit which has more 'in' edges."
             )
 
-        for gate, cals in other.calibrations.items():
-            dest._calibrations[gate].update(cals)
-
-        dest.global_phase += other.global_phase
-
-        if not other.data:
-            # Nothing left to do. Plus, accessing 'data' here is necessary
-            # to trigger any lazy building since we now access '_data'
-            # directly.
-            return None if inplace else dest
-
-        # The 'qubits' and 'clbits' used for 'dest'.
+        # Maps bits in 'other' to bits in 'dest'.
         mapped_qubits: list[Qubit]
         mapped_clbits: list[Clbit]
-
-        # Maps bits in 'other' to bits in 'dest'. Used only for
-        # adjusting bits in variables (e.g. condition and target).
         edge_map: dict[Qubit | Clbit, Qubit | Clbit] = {}
         if qubits is None:
             mapped_qubits = dest.qubits
@@ -1025,6 +1012,10 @@ def compose(
                     f"Number of items in qubits parameter ({len(mapped_qubits)}) does not"
                     f" match number of qubits in the circuit ({len(other.qubits)})."
                 )
+            if len(set(mapped_qubits)) != len(mapped_qubits):
+                raise CircuitError(
+                    f"Duplicate qubits referenced in 'qubits' parameter: '{mapped_qubits}'"
+                )
             edge_map.update(zip(other.qubits, mapped_qubits))
 
         if clbits is None:
@@ -1037,8 +1028,25 @@ def compose(
                     f"Number of items in clbits parameter ({len(mapped_clbits)}) does not"
                     f" match number of clbits in the circuit ({len(other.clbits)})."
                 )
+            if len(set(mapped_clbits)) != len(mapped_clbits):
+                raise CircuitError(
+                    f"Duplicate clbits referenced in 'clbits' parameter: '{mapped_clbits}'"
+                )
             edge_map.update(zip(other.clbits, dest.cbit_argument_conversion(clbits)))
 
+        for gate, cals in other.calibrations.items():
+            dest._calibrations[gate].update(cals)
+
+        dest.duration = None
+        dest.unit = "dt"
+        dest.global_phase += other.global_phase
+
+        if not other.data:
+            # Nothing left to do. Plus, accessing 'data' here is necessary
+            # to trigger any lazy building since we now access '_data'
+            # directly.
+            return None if inplace else dest
+
         variable_mapper = _classical_resource_map.VariableMapper(
             dest.cregs, edge_map, dest.add_register
         )


Test patch:
diff --git a/test/python/circuit/test_compose.py b/test/python/circuit/test_compose.py
--- a/test/python/circuit/test_compose.py
+++ b/test/python/circuit/test_compose.py
@@ -31,6 +31,7 @@
     Instruction,
     CASE_DEFAULT,
     SwitchCaseOp,
+    CircuitError,
 )
 from qiskit.circuit.library import HGate, RZGate, CXGate, CCXGate, TwoLocal
 from qiskit.circuit.classical import expr
@@ -880,6 +881,16 @@ def test_expr_target_is_mapped(self):
 
         self.assertEqual(dest, expected)
 
+    def test_rejects_duplicate_bits(self):
+        """Test that compose rejects duplicates in either qubits or clbits."""
+        base = QuantumCircuit(5, 5)
+
+        attempt = QuantumCircuit(2, 2)
+        with self.assertRaisesRegex(CircuitError, "Duplicate qubits"):
+            base.compose(attempt, [1, 1], [0, 1])
+        with self.assertRaisesRegex(CircuitError, "Duplicate clbits"):
+            base.compose(attempt, [0, 1], [1, 1])
+
 
 if __name__ == "__main__":
     unittest.main()