Repository: Qiskit/qiskit

Issue:
Performance regression in transpile with optimization_level <=2
<!-- ⚠️ 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 (since https://github.com/Qiskit/qiskit-terra/commit/35db627fbd1c9762d9f394c71a1df129c24797f4 )
- **Python version**: Any
- **Operating system**: Any

### What is the current behavior?

The benchmarking site has flagged a regression on transpile of circuits with optimization <= 2 after https://github.com/Qiskit/qiskit-terra/commit/35db627fbd1c9762d9f394c71a1df129c24797f4 merged. This is a bit unexpected since the primary change was on block collection and consolidation which is only run on level 3 (which got faster).

https://qiskit.github.io/qiskit/#transpiler_levels.TranspilerLevelBenchmarks.time_transpile_qv_14_x_14?machine=qiskit-benchmarking&os=Ubuntu%2018.04&ram=16%20GB&p-transpiler%20optimization%20level=0&p-transpiler%20optimization%20level=1&p-transpiler%20optimization%20level=2&p-transpiler%20optimization%20level=3&commits=35db627f




Discussion:
My working idea here is that this was caused by the addition of `inline=True` https://github.com/Qiskit/qiskit-terra/commit/35db627f#diff-d00f0de07831e0961cb92a4af5f40e5eR480 which makes the decomposition of unitaries from the input QV circuits slower.

Resolving patch:
diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py
--- a/qiskit/circuit/quantumcircuit.py
+++ b/qiskit/circuit/quantumcircuit.py
@@ -576,31 +576,73 @@ def compose(self, other, qubits=None, clbits=None, front=False, inplace=False):
                 lcr_1: 0 ═══════════                           lcr_1: 0 ═══════════════════════
 
         """
-        if front:
-            raise CircuitError("Front composition of QuantumCircuit not supported yet.")
-
-        if isinstance(other, QuantumCircuit):
-            from qiskit.converters.circuit_to_dag import circuit_to_dag
-            from qiskit.converters.dag_to_circuit import dag_to_circuit
-
-            dag_self = circuit_to_dag(self)
-            dag_other = circuit_to_dag(other)
-            dag_self.compose(dag_other, qubits=qubits, clbits=clbits, front=front)
-            composed_circuit = dag_to_circuit(dag_self)
-            if inplace:  # FIXME: this is just a hack for inplace to work. Still copies.
-                self.__dict__.update(composed_circuit.__dict__)
-                return None
+
+        if inplace:
+            dest = self
+        else:
+            dest = self.copy()
+
+        if not isinstance(other, QuantumCircuit):
+            if front:
+                dest.data.insert(0, (other, qubits, clbits))
             else:
-                return composed_circuit
+                dest.append(other, qargs=qubits, cargs=clbits)
 
-        else:  # fall back to append which accepts Instruction and BaseOperator
             if inplace:
-                self.append(other, qargs=qubits, cargs=clbits)
                 return None
-            else:
-                new_circuit = self.copy()
-                new_circuit.append(other, qargs=qubits, cargs=clbits)
-                return new_circuit
+            return dest
+
+        instrs = other.data
+
+        if other.num_qubits > self.num_qubits or \
+           other.num_clbits > self.num_clbits:
+            raise CircuitError("Trying to compose with another QuantumCircuit "
+                               "which has more 'in' edges.")
+
+        # number of qubits and clbits must match number in circuit or None
+        identity_qubit_map = dict(zip(other.qubits, self.qubits))
+        identity_clbit_map = dict(zip(other.clbits, self.clbits))
+
+        if qubits is None:
+            qubit_map = identity_qubit_map
+        elif len(qubits) != len(other.qubits):
+            raise CircuitError("Number of items in qubits parameter does not"
+                               " match number of qubits in the circuit.")
+        else:
+            qubit_map = {other.qubits[i]: (self.qubits[q] if isinstance(q, int) else q)
+                         for i, q in enumerate(qubits)}
+        if clbits is None:
+            clbit_map = identity_clbit_map
+        elif len(clbits) != len(other.clbits):
+            raise CircuitError("Number of items in clbits parameter does not"
+                               " match number of clbits in the circuit.")
+        else:
+            clbit_map = {other.clbits[i]: (self.clbits[c] if isinstance(c, int) else c)
+                         for i, c in enumerate(clbits)}
+
+        edge_map = {**qubit_map, **clbit_map} or {**identity_qubit_map, **identity_clbit_map}
+
+        mapped_instrs = []
+        for instr, qargs, cargs in instrs:
+            n_qargs = [edge_map[qarg] for qarg in qargs]
+            n_cargs = [edge_map[carg] for carg in cargs]
+            n_instr = instr.copy()
+
+            if instr.condition is not None:
+                from qiskit.dagcircuit import DAGCircuit  # pylint: disable=cyclic-import
+                n_instr.condition = DAGCircuit._map_condition(edge_map, instr.condition)
+
+            mapped_instrs.append((n_instr, n_qargs, n_cargs))
+
+        if front:
+            dest._data = mapped_instrs + dest._data
+        else:
+            dest._data += mapped_instrs
+
+        if inplace:
+            return None
+
+        return dest
 
     @property
     def qubits(self):
diff --git a/qiskit/dagcircuit/dagcircuit.py b/qiskit/dagcircuit/dagcircuit.py
--- a/qiskit/dagcircuit/dagcircuit.py
+++ b/qiskit/dagcircuit/dagcircuit.py
@@ -444,7 +444,8 @@ def _check_wiremap_validity(self, wire_map, keymap, valmap):
                 raise DAGCircuitError("inconsistent wire_map at (%s,%s)" %
                                       (kname, vname))
 
-    def _map_condition(self, wire_map, condition):
+    @staticmethod
+    def _map_condition(wire_map, condition):
         """Use the wire_map dict to change the condition tuple's creg name.
 
         Args:


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
@@ -451,6 +451,45 @@ def test_compose_one_liner(self):
 
         self.assertEqual(circ, expected)
 
+    def test_compose_front_circuit(self):
+        """Test composing a circuit at the front of a circuit.
+        """
+
+        qc = QuantumCircuit(2)
+        qc.h(0)
+        qc.cx(0, 1)
+
+        other = QuantumCircuit(2)
+        other.cz(1, 0)
+        other.z(1)
+
+        output = qc.compose(other, front=True)
+
+        expected = QuantumCircuit(2)
+        expected.cz(1, 0)
+        expected.z(1)
+        expected.h(0)
+        expected.cx(0, 1)
+
+        self.assertEqual(output, expected)
+
+    def test_compose_front_gate(self):
+        """Test composing a gate at the front of a circuit.
+        """
+
+        qc = QuantumCircuit(2)
+        qc.h(0)
+        qc.cx(0, 1)
+
+        output = qc.compose(CXGate(), [1, 0], front=True)
+
+        expected = QuantumCircuit(2)
+        expected.cx(1, 0)
+        expected.h(0)
+        expected.cx(0, 1)
+
+        self.assertEqual(output, expected)
+
 
 if __name__ == '__main__':
     unittest.main()