Repository: Qiskit/qiskit

Issue:
transpile unrolls even when basis_gates=None
The intention in the transpile() function was that if `basis_gates` is not specified, all gates are fair game and no explicit unrolling will be done (see docstring)

This behavior broke in #2357 when u3 and cx were added to all basis. This means that some explicit unrollings could happen.

This can be fixed in 2 ways:
- Set the basis gates as the full set of gates in the original circuit. If any pass happens to add to this set, enlarge the basis gates (so basis_gates has to be stored in property set and Unroller read from there)

- Modify the unroller to not do anything when basis_gates=None. This will be faster, as the Unroller won't even pass through the circuit once. 


Discussion:
> Modify the unroller to not do anything when basis_gates=None. This will be faster, as the Unroller won't even pass through the circuit once.

This can also be fixed at preset pass manager level, by avoiding adding the unroller to the scheduler if `basis_gates` is `None`.

Resolving patch:
diff --git a/qiskit/compiler/transpile.py b/qiskit/compiler/transpile.py
--- a/qiskit/compiler/transpile.py
+++ b/qiskit/compiler/transpile.py
@@ -273,17 +273,18 @@ def _transpile_circuit(circuit_config_tuple: Tuple[QuantumCircuit, Dict]) -> Qua
 
     pass_manager_config = transpile_config['pass_manager_config']
 
-    # Workaround for ion trap support: If basis gates includes
-    # Mølmer-Sørensen (rxx) and the circuit includes gates outside the basis,
-    # first unroll to u3, cx, then run MSBasisDecomposer to target basis.
-    basic_insts = ['measure', 'reset', 'barrier', 'snapshot']
-    device_insts = set(pass_manager_config.basis_gates).union(basic_insts)
     ms_basis_swap = None
-    if 'rxx' in pass_manager_config.basis_gates and \
-            not device_insts >= circuit.count_ops().keys():
-        ms_basis_swap = pass_manager_config.basis_gates
-        pass_manager_config.basis_gates = list(
-            set(['u3', 'cx']).union(pass_manager_config.basis_gates))
+    if pass_manager_config.basis_gates is not None:
+        # Workaround for ion trap support: If basis gates includes
+        # Mølmer-Sørensen (rxx) and the circuit includes gates outside the basis,
+        # first unroll to u3, cx, then run MSBasisDecomposer to target basis.
+        basic_insts = ['measure', 'reset', 'barrier', 'snapshot']
+        device_insts = set(pass_manager_config.basis_gates).union(basic_insts)
+        if 'rxx' in pass_manager_config.basis_gates and \
+                not device_insts >= circuit.count_ops().keys():
+            ms_basis_swap = pass_manager_config.basis_gates
+            pass_manager_config.basis_gates = list(
+                set(['u3', 'cx']).union(pass_manager_config.basis_gates))
 
     # we choose an appropriate one based on desired optimization level
     level = transpile_config['optimization_level']
@@ -370,13 +371,6 @@ def _parse_basis_gates(basis_gates, backend, circuits):
                                all(isinstance(i, str) for i in basis_gates)):
         basis_gates = [basis_gates] * len(circuits)
 
-    # no basis means don't unroll (all circuit gates are valid basis)
-    for index, circuit in enumerate(circuits):
-        basis = basis_gates[index]
-        if basis is None:
-            gates_in_circuit = {inst.name for inst, _, _ in circuit.data}
-            # Other passes might add new gates that need to be supported
-            basis_gates[index] = list(gates_in_circuit.union(['u3', 'cx']))
     return basis_gates
 
 
diff --git a/qiskit/transpiler/passes/basis/unroller.py b/qiskit/transpiler/passes/basis/unroller.py
--- a/qiskit/transpiler/passes/basis/unroller.py
+++ b/qiskit/transpiler/passes/basis/unroller.py
@@ -30,7 +30,8 @@ def __init__(self, basis):
         """Unroller initializer.
 
         Args:
-            basis (list[str]): Target basis names to unroll to, e.g. `['u3', 'cx']` .
+            basis (list[str] or None): Target basis names to unroll to, e.g. `['u3', 'cx']` . If
+                None, does not unroll any gate.
         """
         super().__init__()
         self.basis = basis
@@ -48,6 +49,9 @@ def run(self, dag):
         Returns:
             DAGCircuit: output unrolled dag
         """
+        if self.basis is None:
+            return dag
+
         # Walk through the DAG and expand each non-basis node
         for node in dag.op_nodes():
             basic_insts = ['measure', 'reset', 'barrier', 'snapshot']


Test patch:
diff --git a/test/python/transpiler/test_preset_passmanagers.py b/test/python/transpiler/test_preset_passmanagers.py
--- a/test/python/transpiler/test_preset_passmanagers.py
+++ b/test/python/transpiler/test_preset_passmanagers.py
@@ -42,17 +42,25 @@ def circuit_2532():
 class TestPresetPassManager(QiskitTestCase):
     """Test preset passmanagers work as expected."""
 
-    @combine(level=[0, 1, 2, 3],
-             dsc='Test that coupling_map can be None (level={level})',
-             name='coupling_map_none_level{level}')
+    @combine(level=[0, 1, 2, 3], name='level{level}')
     def test_no_coupling_map(self, level):
-        """Test that coupling_map can be None"""
+        """Test that coupling_map can be None (level={level})"""
         q = QuantumRegister(2, name='q')
         circuit = QuantumCircuit(q)
         circuit.cz(q[0], q[1])
         result = transpile(circuit, basis_gates=['u1', 'u2', 'u3', 'cx'], optimization_level=level)
         self.assertIsInstance(result, QuantumCircuit)
 
+    @combine(level=[0, 1, 2, 3], name='level{level}')
+    def test_no_basis_gates(self, level):
+        """Test that basis_gates can be None (level={level})"""
+        q = QuantumRegister(2, name='q')
+        circuit = QuantumCircuit(q)
+        circuit.h(q[0])
+        circuit.cz(q[0], q[1])
+        result = transpile(circuit, basis_gates=None, optimization_level=level)
+        self.assertEqual(result, circuit)
+
 
 @ddt
 class TestTranspileLevels(QiskitTestCase):