Repository: Qiskit/qiskit

Issue:
Transpiler slow to raise when attempting to target an unreachable basis
<!-- ⚠️ If you do not respect this template, your issue will be closed -->
<!-- ⚠️ Make sure to browse the opened and closed issues to confirm this idea does not exist. -->

### What is the expected enhancement?
When attempting to transpile to a set of `basis_gates` that the transpiler can't reach, the `BasisTranslator` will eventually fail with an error like
```
qiskit.transpiler.exceptions.TranspilerError: "Unable to map source basis {('cx', 2)} to target basis {'delay', 'reset', 'snapshot', 'measure', 'barrier', 'u'} over library <qiskit.circuit.equivalence.EquivalenceLibrary object at 0x124755ba8>."
```
but it is very slow to do so.

```
>>> import qiskit as qk
>>> qc = qk.QuantumCircuit(2)
>>> qc.cx(0,1)
>>> from time import time
>>> def t(basis):
...   start = time()
...   try:
...     qk.transpile(qc, basis_gates=basis)
...   except Exception:
...     pass
...   end = time()
...   return end-start
...
>>> for b in [['u'], ['u3', 'u2', 'u1'], ['sx', 'p'], ['rx', 'ry']]:
...   print(b, t(b))
...
['u'] 17.1777400970459
['u3', 'u2', 'u1'] 16.48400378227234
['sx', 'p'] 14.011835813522339
['rx', 'ry'] 12.08644723892212
```

This is a best case example, but for e.g. a Toffoli where decompositions will contain single qubit gates (and so its possible to make some progress):

```
>>> qc = qk.QuantumCircuit(3)
>>> qc.ccx(0,1,2)
>>> for b in [['u'], ['u3', 'u2', 'u1'], ['sx', 'p'], ['rx', 'ry']]:
...   print(b, t(b))
...
['u'] 69.04226517677307
['u3', 'u2', 'u1'] 67.77550601959229
['sx', 'p'] 65.70645785331726
['rx', 'ry'] 59.11228585243225
```

In general, determining whether or not a given basis transformation is possible will probably be as hard as finding it, but it may be possible to identify e.g. non-universal basis sets and raise a warning in these cases.
Odd RZX decompositions
<!-- ⚠️ 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**: 0.18.3
- **Python version**:
- **Operating system**:

### What is the current behavior?
I am trying to understand how to go to RZX gates and calibrations when targeting IBM Quantum systems.  However, I am already lost at the basis translation step.  Namely decomposing a CX gate gives the expected transformation:

```python

qc = QuantumCircuit(2)
#qc.h(0)
qc.cx(0,1)

qc = transpile(qc, basis_gates=['sx', 'rz', 'x', 'rzx'])
qc.draw()
```

```
global phase: 0
     ┌───┐ ┌──────────┐┌───────────┐┌───┐┌────────────┐
q_0: ┤ X ├─┤ Rz(-π/2) ├┤0          ├┤ X ├┤0           ├
     ├───┴┐└──────────┘│  Rzx(π/4) │└───┘│  Rzx(-π/4) │
q_1: ┤ √X ├────────────┤1          ├─────┤1           ├
     └────┘            └───────────┘     └────────────┘
```
but if I add a H gate then the transformation adds multiple RZX gates even though the H gate is a single-qubit gate:

```python
qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0,1)

qc = transpile(qc, basis_gates=['sx', 'rz', 'x', 'rzx'])
qc.draw()
```

```
global phase: 0
     ┌────┐            ┌───────────┐┌───┐┌────────────┐┌────┐            »
q_0: ┤ √X ├────────────┤0          ├┤ X ├┤0           ├┤ √X ├────────────»
     ├───┬┘┌──────────┐│  Rzx(π/4) │└───┘│  Rzx(-π/4) │├───┬┘┌──────────┐»
q_1: ┤ X ├─┤ Rz(-π/2) ├┤1          ├─────┤1           ├┤ X ├─┤ Rz(-π/2) ├»
     └───┘ └──────────┘└───────────┘     └────────────┘└───┘ └──────────┘»
«     ┌───────────┐     ┌────────────┐ ┌────────┐ ┌────┐┌────────┐┌───────────┐»
«q_0: ┤1          ├─────┤1           ├─┤ Rz(-π) ├─┤ √X ├┤ Rz(-π) ├┤0          ├»
«     │  Rzx(π/4) │┌───┐│  Rzx(-π/4) │┌┴────────┴┐├────┤└────────┘│  Rzx(π/4) │»
«q_1: ┤0          ├┤ X ├┤0           ├┤ Rz(-π/2) ├┤ √X ├──────────┤1          ├»
«     └───────────┘└───┘└────────────┘└──────────┘└────┘          └───────────┘»
«     ┌───┐┌────────────┐┌────┐            ┌───────────┐     ┌────────────┐»
«q_0: ┤ X ├┤0           ├┤ √X ├────────────┤1          ├─────┤1           ├»
«     └───┘│  Rzx(-π/4) │├───┬┘┌──────────┐│  Rzx(π/4) │┌───┐│  Rzx(-π/4) │»
«q_1: ─────┤1           ├┤ X ├─┤ Rz(-π/2) ├┤0          ├┤ X ├┤0           ├»
«          └────────────┘└───┘ └──────────┘└───────────┘└───┘└────────────┘»
«     ┌─────────┐  ┌────┐  ┌────────┐
«q_0: ┤ Rz(π/2) ├──┤ √X ├──┤ Rz(-π) ├
«     └──┬────┬─┘┌─┴────┴─┐└────────┘
«q_1: ───┤ √X ├──┤ Rz(-π) ├──────────
«        └────┘  └────────┘          
```
I am pretty sure this is not the way this should be decomposed here.

### Steps to reproduce the problem



### What is the expected behavior?



### Suggested solutions





Discussion:
Besides checking for non-universal basis sets, we can actually rework the logic of `BasisTranslator._basis_search` and make it faster. The idea is to start with the target basis and check for new gates we can generate using our basis and one rule of the equivalence libary. We slowly expand this set and only if the final set contains all the gates in the original circuit, the given basis transformation is possible.

In more detail:

Right now, `BasisTranslator` performs an A* search on an implicitly defined directed graph `G = (V, E)` (of exponential size in the number of gates in the equivalence library) where `V` is the power set of gates in the library and two nodes `Na, Nb` are connected if we can use an equivalence rule of the library `g -> C_g` for `g in Na` such that `Nb = (Na + C_g) - g`. It also makes use of a heuristic cost function to guide the search and try to reach the target basis as quickly as possible.

We can instead perform a much faster modified BFS search in a smaller directed graph `G = (V, E)`  where `V` is the set of gates in the library (*not* the power set) and we add an edge `(ga, gb)` if there is an equivalence rule in the library `R = gb -> C_gb` and `ga in C_gb` (in the edge we also attach the rule `R`).Then, the starting points of the BFS will be the gates in the target basis and the single modification of the standard bfs is that we are allowed to use an edge `(ga, gb, R)` only if we have previously visited all the gates appearing in the right side of `R`. If this procedure reaches all gates  in the original circuit, the transformation is possible. Otherwise, we can't map the given circuit in this basis.

This is a great idea @georgios-ts ! The original "search over bases" approach stemmed I think from earlier designs where we were considering using template substitutions (and so may not necessarily have one-to-one transformation rules for every gate), so I agree a gate-wise BFS will be sufficient to cover the search over the equivalence library, and should be significantly faster (especially to fail when the target basis is unreachable).


Resolving patch:
diff --git a/qiskit/circuit/library/standard_gates/equivalence_library.py b/qiskit/circuit/library/standard_gates/equivalence_library.py
--- a/qiskit/circuit/library/standard_gates/equivalence_library.py
+++ b/qiskit/circuit/library/standard_gates/equivalence_library.py
@@ -584,13 +584,13 @@
 # q_1: ┤ Sx ├     q_1: ─────┤1          ├┤ sx^0.5 ├─────
 #      └────┘               └───────────┘└────────┘
 q = QuantumRegister(2, "q")
-csx_to_zx45 = QuantumCircuit(q, global_phase=pi / 8)
+csx_to_zx45 = QuantumCircuit(q, global_phase=pi / 4)
 for inst, qargs, cargs in [
     (XGate(), [q[0]], []),
     (RZXGate(pi / 4), [q[0], q[1]], []),
     (TdgGate(), [q[0]], []),
     (XGate(), [q[0]], []),
-    (SXGate().power(0.5), [q[1]], []),
+    (RXGate(pi / 4), [q[1]], []),
 ]:
     csx_to_zx45.append(inst, qargs, cargs)
 _sel.add_equivalence(CSXGate(), csx_to_zx45)
diff --git a/qiskit/transpiler/passes/basis/basis_translator.py b/qiskit/transpiler/passes/basis/basis_translator.py
--- a/qiskit/transpiler/passes/basis/basis_translator.py
+++ b/qiskit/transpiler/passes/basis/basis_translator.py
@@ -15,14 +15,13 @@
 import time
 import logging
 
-from heapq import heappush, heappop
 from itertools import zip_longest
-from itertools import count as iter_count
 from collections import defaultdict
 
-import numpy as np
+import retworkx
 
 from qiskit.circuit import Gate, ParameterVector, QuantumRegister
+from qiskit.circuit.equivalence import Key
 from qiskit.dagcircuit import DAGCircuit
 from qiskit.transpiler.basepasses import TransformationPass
 from qiskit.transpiler.exceptions import TranspilerError
@@ -38,11 +37,10 @@ class BasisTranslator(TransformationPass):
     This pass operates in several steps:
 
     * Determine the source basis from the input circuit.
-    * Perform an A* search over basis sets, starting from the source basis and
-      targeting the device's target_basis, with edges discovered from the
-      provided EquivalenceLibrary. The heuristic used by the A* search is the
-      number of distinct circuit basis gates not in the target_basis, plus the
-      number of distinct device basis gates not used in the current basis.
+    * Perform a Dijkstra search over basis sets, starting from the device's
+      target_basis new gates are being generated using the rules from the provided
+      EquivalenceLibrary and the search stops if all gates in the source basis have
+      been generated.
     * The found path, as a set of rules from the EquivalenceLibrary, is composed
       into a set of gate replacement rules.
     * The composed replacement rules are applied in-place to each op node which
@@ -143,7 +141,7 @@ def run(self, dag):
                 # do an extra non-local search for this op to ensure we include any
                 # single qubit operation for (1,) as valid. This pattern also holds
                 # true for > 2q ops too (so for 4q operations we need to check for 3q, 2q,
-                # and 1q opertaions in the same manner)
+                # and 1q operations in the same manner)
                 if qargs in self._qargs_with_non_global_operation or any(
                     frozenset(qargs).issuperset(incomplete_qargs)
                     for incomplete_qargs in self._qargs_with_non_global_operation
@@ -162,13 +160,11 @@ def run(self, dag):
 
         # Search for a path from source to target basis.
         search_start_time = time.time()
-        basis_transforms = _basis_search(
-            self._equiv_lib, source_basis, target_basis, _basis_heuristic
-        )
+        basis_transforms = _basis_search(self._equiv_lib, source_basis, target_basis)
 
         qarg_local_basis_transforms = {}
         for qarg, local_source_basis in qargs_local_source_basis.items():
-            expanded_target = target_basis | self._qargs_with_non_global_operation[qarg]
+            expanded_target = set(target_basis)
             # For any multiqubit operation that contains a subset of qubits that
             # has a non-local operation, include that non-local operation in the
             # search. This matches with the check we did above to include those
@@ -177,6 +173,8 @@ def run(self, dag):
                 for non_local_qarg, local_basis in self._qargs_with_non_global_operation.items():
                     if qarg.issuperset(non_local_qarg):
                         expanded_target |= local_basis
+            else:
+                expanded_target |= self._qargs_with_non_global_operation[tuple(qarg)]
 
             logger.info(
                 "Performing BasisTranslator search from source basis %s to target "
@@ -185,10 +183,20 @@ def run(self, dag):
                 expanded_target,
                 qarg,
             )
-            qarg_local_basis_transforms[qarg] = _basis_search(
-                self._equiv_lib, local_source_basis, expanded_target, _basis_heuristic
+            local_basis_transforms = _basis_search(
+                self._equiv_lib, local_source_basis, expanded_target
             )
 
+            if local_basis_transforms is None:
+                raise TranspilerError(
+                    "Unable to map source basis {} to target basis {} on qarg {} "
+                    "over library {}.".format(
+                        local_source_basis, expanded_target, qarg, self._equiv_lib
+                    )
+                )
+
+            qarg_local_basis_transforms[qarg] = local_basis_transforms
+
         search_end_time = time.time()
         logger.info(
             "Basis translation path search completed in %.3fs.", search_end_time - search_start_time
@@ -288,22 +296,100 @@ def replace_node(node, instr_map):
         return dag
 
 
-def _basis_heuristic(basis, target):
-    """Simple metric to gauge distance between two bases as the number of
-    elements in the symmetric difference of the circuit basis and the device
-    basis.
-    """
-    return len({gate_name for gate_name, gate_num_qubits in basis} ^ target)
+class StopIfBasisRewritable(Exception):
+    """Custom exception that signals `retworkx.dijkstra_search` to stop."""
+
+
+class BasisSearchVisitor(retworkx.visit.DijkstraVisitor):
+    """Handles events emitted during `retworkx.dijkstra_search`."""
+
+    def __init__(self, graph, source_basis, target_basis, num_gates_for_rule):
+        self.graph = graph
+        self.target_basis = set(target_basis)
+        self._source_gates_remain = set(source_basis)
+        self._num_gates_remain_for_rule = dict(num_gates_for_rule)
+        self._basis_transforms = []
+        self._predecessors = dict()
+        self._opt_cost_map = dict()
+
+    def discover_vertex(self, v, score):
+        gate = self.graph[v]
+        self._source_gates_remain.discard(gate)
+        self._opt_cost_map[gate] = score
+        rule = self._predecessors.get(gate, None)
+        if rule is not None:
+            logger.debug(
+                "Gate %s generated using rule \n%s\n with total cost of %s.",
+                gate.name,
+                rule.circuit,
+                score,
+            )
+            self._basis_transforms.append((gate.name, gate.num_qubits, rule.params, rule.circuit))
+        # we can stop the search if we have found all gates in the original ciruit.
+        if not self._source_gates_remain:
+            # if we start from source gates and apply `basis_transforms` in reverse order, we'll end
+            # up with gates in the target basis. Note though that `basis_transforms` may include
+            # additional transformations that are not required to map our source gates to the given
+            # target basis.
+            self._basis_transforms.reverse()
+            raise StopIfBasisRewritable
+
+    def examine_edge(self, edge):
+        _, target, edata = edge
+        if edata is None:
+            return
+
+        index = edata["index"]
+        self._num_gates_remain_for_rule[index] -= 1
+
+        target = self.graph[target]
+        # if there are gates in this `rule` that we have not yet generated, we can't apply
+        # this `rule`. if `target` is already in basis, it's not beneficial to use this rule.
+        if self._num_gates_remain_for_rule[index] > 0 or target in self.target_basis:
+            raise retworkx.visit.PruneSearch
+
+    def edge_relaxed(self, edge):
+        _, target, edata = edge
+        if edata is not None:
+            gate = self.graph[target]
+            self._predecessors[gate] = edata["rule"]
+
+    def edge_cost(self, edge):
+        """Returns the cost of an edge.
+
+        This function computes the cost of this edge rule by summing
+        the costs of all gates in the rule equivalence circuit. In the
+        end, we need to subtract the cost of the source since `dijkstra`
+        will later add it.
+        """
+
+        if edge is None:
+            # the target of the edge is a gate in the target basis,
+            # so we return a default value of 1.
+            return 1
 
+        cost_tot = 0
+        rule = edge["rule"]
+        for gate, qargs, _ in rule.circuit:
+            key = Key(name=gate.name, num_qubits=len(qargs))
+            cost_tot += self._opt_cost_map[key]
 
-def _basis_search(equiv_lib, source_basis, target_basis, heuristic):
+        source = edge["source"]
+        return cost_tot - self._opt_cost_map[source]
+
+    @property
+    def basis_transforms(self):
+        """Returns the gate basis transforms."""
+        return self._basis_transforms
+
+
+def _basis_search(equiv_lib, source_basis, target_basis):
     """Search for a set of transformations from source_basis to target_basis.
 
     Args:
         equiv_lib (EquivalenceLibrary): Source of valid translations
         source_basis (Set[Tuple[gate_name: str, gate_num_qubits: int]]): Starting basis.
         target_basis (Set[gate_name: str]): Target basis.
-        heuristic (Callable[[source_basis, target_basis], int]): distance heuristic.
 
     Returns:
         Optional[List[Tuple[gate, equiv_params, equiv_circuit]]]: List of (gate,
@@ -312,100 +398,75 @@ def _basis_search(equiv_lib, source_basis, target_basis, heuristic):
             was found.
     """
 
-    source_basis = frozenset(source_basis)
-    target_basis = frozenset(target_basis)
-
-    open_set = set()  # Bases found but not yet inspected.
-    closed_set = set()  # Bases found and inspected.
-
-    # Priority queue for inspection order of open_set. Contains Tuple[priority, count, basis]
-    open_heap = []
-
-    # Map from bases in closed_set to predecessor with lowest cost_from_source.
-    # Values are Tuple[prev_basis, gate_name, params, circuit].
-    came_from = {}
-
-    basis_count = iter_count()  # Used to break ties in priority.
-
-    open_set.add(source_basis)
-    heappush(open_heap, (0, next(basis_count), source_basis))
-
-    # Map from basis to lowest found cost from source.
-    cost_from_source = defaultdict(lambda: np.inf)
-    cost_from_source[source_basis] = 0
-
-    # Map from basis to cost_from_source + heuristic.
-    est_total_cost = defaultdict(lambda: np.inf)
-    est_total_cost[source_basis] = heuristic(source_basis, target_basis)
-
     logger.debug("Begining basis search from %s to %s.", source_basis, target_basis)
 
-    while open_set:
-        _, _, current_basis = heappop(open_heap)
-
-        if current_basis in closed_set:
-            # When we close a node, we don't remove it from the heap,
-            # so skip here.
-            continue
-
-        if {gate_name for gate_name, gate_num_qubits in current_basis}.issubset(target_basis):
-            # Found target basis. Construct transform path.
-            rtn = []
-            last_basis = current_basis
-            while last_basis != source_basis:
-                prev_basis, gate_name, gate_num_qubits, params, equiv = came_from[last_basis]
-
-                rtn.append((gate_name, gate_num_qubits, params, equiv))
-                last_basis = prev_basis
-            rtn.reverse()
-
-            logger.debug("Transformation path:")
-            for gate_name, gate_num_qubits, params, equiv in rtn:
-                logger.debug("%s/%s => %s\n%s", gate_name, gate_num_qubits, params, equiv)
-            return rtn
-
-        logger.debug("Inspecting basis %s.", current_basis)
-        open_set.remove(current_basis)
-        closed_set.add(current_basis)
-
-        for gate_name, gate_num_qubits in current_basis:
-            if gate_name in target_basis:
-                continue
-
-            equivs = equiv_lib._get_equivalences((gate_name, gate_num_qubits))
-
-            basis_remain = current_basis - {(gate_name, gate_num_qubits)}
-            neighbors = [
+    source_basis = {
+        (gate_name, gate_num_qubits)
+        for gate_name, gate_num_qubits in source_basis
+        if gate_name not in target_basis
+    }
+
+    # if source basis is empty, no work to be done.
+    if not source_basis:
+        return []
+
+    all_gates_in_lib = set()
+
+    graph = retworkx.PyDiGraph()
+    nodes_to_indices = dict()
+    num_gates_for_rule = dict()
+
+    def lazy_setdefault(key):
+        if key not in nodes_to_indices:
+            nodes_to_indices[key] = graph.add_node(key)
+        return nodes_to_indices[key]
+
+    rcounter = 0  # running sum of the number of equivalence rules in the library.
+    for key in equiv_lib._get_all_keys():
+        target = lazy_setdefault(key)
+        all_gates_in_lib.add(key)
+        for equiv in equiv_lib._get_equivalences(key):
+            sources = {
+                Key(name=gate.name, num_qubits=len(qargs)) for gate, qargs, _ in equiv.circuit
+            }
+            all_gates_in_lib |= sources
+            edges = [
                 (
-                    frozenset(
-                        basis_remain
-                        | {(inst.name, inst.num_qubits) for inst, qargs, cargs in equiv.data}
-                    ),
-                    params,
-                    equiv,
+                    lazy_setdefault(source),
+                    target,
+                    {"index": rcounter, "rule": equiv, "source": source},
                 )
-                for params, equiv in equivs
+                for source in sources
             ]
 
-            # Weight total path length of transformation weakly.
-            tentative_cost_from_source = cost_from_source[current_basis] + 1e-3
-
-            for neighbor, params, equiv in neighbors:
-                if neighbor in closed_set:
-                    continue
-
-                if tentative_cost_from_source >= cost_from_source[neighbor]:
-                    continue
-
-                open_set.add(neighbor)
-                came_from[neighbor] = (current_basis, gate_name, gate_num_qubits, params, equiv)
-                cost_from_source[neighbor] = tentative_cost_from_source
-                est_total_cost[neighbor] = tentative_cost_from_source + heuristic(
-                    neighbor, target_basis
-                )
-                heappush(open_heap, (est_total_cost[neighbor], next(basis_count), neighbor))
-
-    return None
+            num_gates_for_rule[rcounter] = len(sources)
+            graph.add_edges_from(edges)
+            rcounter += 1
+
+    # This is only neccessary since gates in target basis are currently reported by
+    # their names and we need to have in addition the number of qubits they act on.
+    target_basis_keys = [
+        key
+        for gate in target_basis
+        for key in filter(lambda key, name=gate: key.name == name, all_gates_in_lib)
+    ]
+
+    vis = BasisSearchVisitor(graph, source_basis, target_basis_keys, num_gates_for_rule)
+    # we add a dummy node and connect it with gates in the target basis.
+    # we'll start the search from this dummy node.
+    dummy = graph.add_node("dummy starting node")
+    graph.add_edges_from_no_data([(dummy, nodes_to_indices[key]) for key in target_basis_keys])
+    rtn = None
+    try:
+        retworkx.digraph_dijkstra_search(graph, [dummy], vis.edge_cost, vis)
+    except StopIfBasisRewritable:
+        rtn = vis.basis_transforms
+
+        logger.debug("Transformation path:")
+        for gate_name, gate_num_qubits, params, equiv in rtn:
+            logger.debug("%s/%s => %s\n%s", gate_name, gate_num_qubits, params, equiv)
+
+    return rtn
 
 
 def _compose_transforms(basis_transforms, source_basis, source_dag):


Test patch:
diff --git a/test/python/transpiler/test_basis_translator.py b/test/python/transpiler/test_basis_translator.py
--- a/test/python/transpiler/test_basis_translator.py
+++ b/test/python/transpiler/test_basis_translator.py
@@ -946,12 +946,20 @@ def test_2q_with_non_global_1q(self):
 
         bt_pass = BasisTranslator(std_eqlib, target_basis=None, target=self.target)
         output = bt_pass(qc)
-        expected = QuantumCircuit(2, global_phase=pi / 2)
-        expected.rz(pi / 2, 1)
+        # We need a second run of BasisTranslator to correct gates outside of
+        # the target basis. This is a known isssue, see:
+        #  https://qiskit.org/documentation/release_notes.html#release-notes-0-19-0-known-issues
+        output = bt_pass(output)
+        expected = QuantumCircuit(2)
+        expected.rz(pi, 1)
+        expected.sx(1)
+        expected.rz(3 * pi / 2, 1)
         expected.sx(1)
-        expected.rz(pi / 2, 1)
+        expected.rz(3 * pi, 1)
         expected.cx(0, 1)
-        expected.rz(pi / 2, 1)
+        expected.rz(pi, 1)
+        expected.sx(1)
+        expected.rz(3 * pi / 2, 1)
         expected.sx(1)
-        expected.rz(pi / 2, 1)
+        expected.rz(3 * pi, 1)
         self.assertEqual(output, expected)