Repository: Qiskit/qiskit

Issue:
IndexError in `XXDecomposer` during `FakePrague` `transpile()` with optimization level 3
### Environment

- **Qiskit Terra version**: 0.24.0 main (@ 5672c70649b4f83a6c7146c68a6a543d4a3908fd)
- **Python version**:  3.10
- **Operating system**:  Linux


### What is happening?

During `transpile()` with optimization level 3 on some circuits targeting `FakePrague` the 2q unitary synthesis code is raising an index error with the main branch. This code works fine on the most recent release.

### How can we reproduce the issue?

```python
from qiskit.circuit import QuantumCircuit
from qiskit.compiler import transpile
from qiskit.providers.fake_provider import FakePrague

backend = FakePrague()
num_qubits = 10
# Circuit creation
qc = QuantumCircuit(num_qubits)
qc.h(0)
for i in range(num_qubits - 1):
    if i % 2 == 0:
        qc.cy(0, i + 1)
    else:
        qc.cx(0, i + 1)
qc.measure_all()
tqc = transpile(
    qc, backend, seed_transpiler=2023, optimization_level=3
)
```

### What should happen?

The `transpile()` shouldn't error and it should successfully run 2q synthesis and return a compiled circuit.

### Any suggestions?

This is a bug that has been introduced since the 0.23.0 release (as it still works on the released version). Looking at the git log this is likely a bug that was introduced in #9175 (https://github.com/Qiskit/qiskit-terra/commit/a8f83c4469f55a62f958f3eed2df10a598811eae) 


Discussion:
I think the fix for this might be simple and just a matter of changing: https://github.com/Qiskit/qiskit-terra/blob/main/qiskit/quantum_info/synthesis/xx_decompose/decomposer.py#L153 to `while priority_queue` instead of `while True` because the error is occurring because priority_queue is empty after the first iteration. But, I'm not familiar enough with the code in the decomposer class to know if that's just a band-aid and there is a deeper issue, or that's sufficient.
I agree with the discussion closing #9937.  Here is the call to the `XXDecomposer`
https://github.com/Qiskit/qiskit-terra/blob/c4f1b0bd0b3873bcc5807a3c55b7eeceedd18193/qiskit/transpiler/passes/synthesis/unitary_synthesis.py#L784-L786
No basis fidelity map is passed here, which means that `XXDecomposer` uses the one supplied at construction. In the example program here, this is an empty `dict`. I did not yet find where  `XXDecomposer` is constructed with this empty `dict`.

My best guess after looking at this for a while is that  `XXDecomposer` was not intended to work when passed an empty fidelity map on construction. And likewise here:
https://github.com/Qiskit/qiskit-terra/blob/c4f1b0bd0b3873bcc5807a3c55b7eeceedd18193/qiskit/quantum_info/synthesis/xx_decompose/decomposer.py?rgh-link-date=2023-04-10T20%3A32%3A40Z#L139

it is assumed that `available_strengths` is not empty. But it is.

I think the map should be validated somewhat on construction (at least check its not empty) so that an error is thrown earlier and closer to the real problem. That still doesn't completely solve the problem. I'll look at it again tomorrow.
Adding a check for empty `basis_fidelity` dict passed to the constructor of `XXDecomposer` causes no failures in `tox -epy -- test.python.compiler`. I propose adding this check and changing the test suite to fail if this check is not present. This would require changing the doc string as well to explicitly state that the dict can't be empty. In fact the check should be more stringent.... a minimum of one entry? I don't know yet.

The instance is created in this method
https://github.com/Qiskit/qiskit-terra/blob/c4f1b0bd0b3873bcc5807a3c55b7eeceedd18193/qiskit/transpiler/passes/synthesis/unitary_synthesis.py#L594
This method has a lot of logic and is rather long. As part of fixing this, it should probably be cleaned up or replaced with more than one method.
Fwiw, our existing test suite does not have coverage of this error case. You can add a test like what I added in: https://github.com/Qiskit/qiskit-terra/pull/9937/files#diff-1c5c5726481cd10ea5ac1b5abaf9898c2a3e6b58f7b4f1b19b569d8a7c9c24d9R866 (which is just based on the reproduce I put in the OP) and I would expect that to fail.
I planned to add coverage along with a fix. But if we are not able to fix it soon, I'll add a test for the failure.
It looks like this bug is caused by faulty data in `FakePrague`. See #9983. The consequences should probably be caught in more than one place. For example
* check that the backend has a controlled gate in its list of operations early in the transpilation.
* in unitary synthesis, throw an error if the list of operations received does not include a 2q controlled gate
* In the constructor `XXDecomposer` raise an error on invalid input, in case it somehow gets called with bad input despite the checks above.
Maybe @levbishop can weigh in again on this.

The `XXDecomposer` relies on there being at least one controlled (up to local unitaries) gate in the basis set. All 2q gates that are not controlled are filtered out of the set sent to `XXDecomposer`
https://github.com/Qiskit/qiskit-terra/blob/a461a913c2e5bd51b307f38b89d2ab37117b7bf4/qiskit/transpiler/passes/synthesis/unitary_synthesis.py#L677

So far we have run into two examples where the basis set in a target has no controlled 2q gates and thus `XXDecomposer` gets no gates.
1. `FakePrague` includes a `Target` that erroneously has no controlled gate (#9983)
2.  This test
    https://github.com/Qiskit/qiskit-terra/blob/a461a913c2e5bd51b307f38b89d2ab37117b7bf4/test/python/transpiler/test_unitary_synthesis.py#L840-L842

    This test creates a `Target` with a 1q gate and a custom 2q gate with no properties (no matrix etc.), which is filtered out of the set before calling the 2q synthesiszers. The test then synthesizes a trivial 2-qubit circuit, with a single explicit identity unitary using this target. This succeeds only because no 2q basis gates need to be supplied. My best understanding is that the "canonical point"  corresponding to 2q identity (in fact any non-entangling 2q unitary)  is `[0,0,0]`. That is what is calculated here:
https://github.com/Qiskit/qiskit-terra/blob/a461a913c2e5bd51b307f38b89d2ab37117b7bf4/qiskit/quantum_info/synthesis/xx_decompose/decomposer.py#L250-L258

The optimization loop seeds the priority queue with this point `[0,0,0]`. And it can't put anything else in the queue because the set of basis gates sent was empty. The bottom line (as near as I can tell) is that if your basis set has no controlled gates, you can only synthesize (with this path thats called on opt level 3) certain trivial circuits, like a 2q circuit with one identity gate. It looks like the author explicitly expected this possibility or something like it, because this code branch is chosen when there are no "strengths" found by the optimization loop:
https://github.com/Qiskit/qiskit-terra/blob/a461a913c2e5bd51b307f38b89d2ab37117b7bf4/qiskit/quantum_info/synthesis/xx_decompose/circuits.py#L261-L263

Furthermore, insisting on calling `XXDecomposer` with an empty basis set (because there are no controlled gates) and a non-trivial (entangled) circuit throws the index error in the title of this issue. If you implement #9937 , that is quit when the priority queue is empty, you get a wrong result. The "point" used for synthesis is [0,0,0] , ie. the identity, which results in the entangling part of  synthesis being an empty 2q circuit. This is fixed up with the 1q parts of the weyl decomposition, but its still wrong.

What to do? Disallow basis sets with no entangling gates? At present, at least in `unitary_synthesis.py` this means disallowing basis sets with no controlled gates. This means that trivial circuits would have to be optimized some other way.

Or... I looks like the empty priority queue is one failure mode of `XXDecomposer`, it implies that the supplied unitary can't be synthesized with the basis set sent. If that basis set was empty, we know that the unitary is entangling and can't be synthesized by any method. So we would throw an error. This way, we would still allow the trivial circuit to be synthesized on this path.
XXDecomposer could check for empty controlled basis set and throw a more helpful error in this case, but to me its really a problem with the driving logic that lead to XXDecomposer being called in the first place. If another (non-controlled) basis is available where we have a synthesis method (eg the supercontrolled decomposer) then the higher-level logic (eg DefaultUnitarySynthesis or other circuit-rewriting) should use that. If no synthesis methods are implemented for the available bases than that is unrecoverable.
But this is really about handling edge cases. Currently, if there are no super-controlled gates then `TwoQubitBasisDecomposer` is not called. If there are no controlled gates, then `XXDecomposer` is still called, but with an empty basis set. In this case and if you are only synthesizing non-entangling unitaries it returns (sometimes, maybe always) a correct result. So it's unusual to have no entangling gates that we know how to use (if we have others, I dont find them in `unitary_synthesis.py` and unusual to have no nontrivial 2q gates to synthesize.  But  `XXDecomposer` can do it. The constraints are set by the basis set in the target and the gates in the circuit to be synthesized.  I don't see how changes to the driver logic can get around this.

Resolving patch:
diff --git a/qiskit/quantum_info/synthesis/xx_decompose/decomposer.py b/qiskit/quantum_info/synthesis/xx_decompose/decomposer.py
--- a/qiskit/quantum_info/synthesis/xx_decompose/decomposer.py
+++ b/qiskit/quantum_info/synthesis/xx_decompose/decomposer.py
@@ -151,6 +151,13 @@ def _best_decomposition(canonical_coordinate, available_strengths):
         canonical_coordinate = np.array(canonical_coordinate)
 
         while True:
+            if len(priority_queue) == 0:
+                if len(available_strengths) == 0:
+                    raise QiskitError(
+                        "Attempting to synthesize entangling gate with no controlled gates in basis set."
+                    )
+                raise QiskitError("Unable to synthesize a 2q unitary with the supplied basis set.")
+
             sequence_cost, sequence = heapq.heappop(priority_queue)
 
             strength_polytope = XXPolytope.from_strengths(*[x / 2 for x in sequence])
diff --git a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py
--- a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py
+++ b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py
@@ -701,24 +701,25 @@ def is_controlled(gate):
         # if we are using the approximation_degree knob, use it to scale already-given fidelities
         if approximation_degree is not None:
             basis_2q_fidelity = {k: v * approximation_degree for k, v in basis_2q_fidelity.items()}
-        for basis_1q in available_1q_basis:
-            if isinstance(pi2_basis, CXGate) and basis_1q == "ZSX":
-                pi2_decomposer = TwoQubitBasisDecomposer(
-                    pi2_basis,
-                    euler_basis=basis_1q,
+        if basis_2q_fidelity:
+            for basis_1q in available_1q_basis:
+                if isinstance(pi2_basis, CXGate) and basis_1q == "ZSX":
+                    pi2_decomposer = TwoQubitBasisDecomposer(
+                        pi2_basis,
+                        euler_basis=basis_1q,
+                        basis_fidelity=basis_2q_fidelity,
+                        pulse_optimize=True,
+                    )
+                    embodiments.update({pi / 2: XXEmbodiments[type(pi2_decomposer.gate)]})
+                else:
+                    pi2_decomposer = None
+                decomposer = XXDecomposer(
                     basis_fidelity=basis_2q_fidelity,
-                    pulse_optimize=True,
+                    euler_basis=basis_1q,
+                    embodiments=embodiments,
+                    backup_optimizer=pi2_decomposer,
                 )
-                embodiments.update({pi / 2: XXEmbodiments[type(pi2_decomposer.gate)]})
-            else:
-                pi2_decomposer = None
-            decomposer = XXDecomposer(
-                basis_fidelity=basis_2q_fidelity,
-                euler_basis=basis_1q,
-                embodiments=embodiments,
-                backup_optimizer=pi2_decomposer,
-            )
-            decomposers.append(decomposer)
+                decomposers.append(decomposer)
 
         self._decomposer_cache[qubits_tuple] = decomposers
         return decomposers


Test patch:
diff --git a/test/python/quantum_info/xx_decompose/test_decomposer.py b/test/python/quantum_info/xx_decompose/test_decomposer.py
--- a/test/python/quantum_info/xx_decompose/test_decomposer.py
+++ b/test/python/quantum_info/xx_decompose/test_decomposer.py
@@ -22,6 +22,7 @@
 from scipy.stats import unitary_group
 
 import qiskit
+from qiskit.circuit.quantumcircuit import QuantumCircuit
 from qiskit.quantum_info.operators import Operator
 from qiskit.quantum_info.synthesis.xx_decompose.decomposer import (
     XXDecomposer,
@@ -132,3 +133,25 @@ def test_compilation_improvement(self):
         # the following are taken from Fig 14 of the XX synthesis paper
         self.assertAlmostEqual(mean(clever_costs), 1.445e-2, delta=5e-3)
         self.assertAlmostEqual(mean(naive_costs), 2.058e-2, delta=5e-3)
+
+    def test_error_on_empty_basis_fidelity(self):
+        """Test synthesizing entangling gate with no entangling basis fails."""
+        decomposer = XXDecomposer(basis_fidelity={})
+        qc = QuantumCircuit(2)
+        qc.cx(0, 1)
+        mat = Operator(qc).to_matrix()
+        with self.assertRaisesRegex(
+            qiskit.exceptions.QiskitError,
+            "Attempting to synthesize entangling gate with no controlled gates in basis set.",
+        ):
+            decomposer(mat)
+
+    def test_no_error_on_empty_basis_fidelity_trivial_target(self):
+        """Test synthesizing non-entangling gate with no entangling basis succeeds."""
+        decomposer = XXDecomposer(basis_fidelity={})
+        qc = QuantumCircuit(2)
+        qc.x(0)
+        qc.y(1)
+        mat = Operator(qc).to_matrix()
+        dqc = decomposer(mat)
+        self.assertTrue(np.allclose(mat, Operator(dqc).to_matrix()))
diff --git a/test/python/transpiler/test_unitary_synthesis.py b/test/python/transpiler/test_unitary_synthesis.py
--- a/test/python/transpiler/test_unitary_synthesis.py
+++ b/test/python/transpiler/test_unitary_synthesis.py
@@ -47,7 +47,12 @@
     TrivialLayout,
 )
 from qiskit.circuit.library import (
+    IGate,
     CXGate,
+    RZGate,
+    SXGate,
+    XGate,
+    iSwapGate,
     ECRGate,
     UGate,
     ZGate,
@@ -55,6 +60,7 @@
     RZZGate,
     RXXGate,
 )
+from qiskit.circuit import Measure
 from qiskit.circuit.controlflow import IfElseOp
 from qiskit.circuit import Parameter, Gate
 
@@ -855,7 +861,7 @@ def __init__(self):
         unitary_synth_pass = UnitarySynthesis(target=target)
         result_dag = unitary_synth_pass.run(dag)
         result_qc = dag_to_circuit(result_dag)
-        self.assertEqual(result_qc, QuantumCircuit(2))
+        self.assertEqual(result_qc, qc)
 
     def test_default_does_not_fail_on_no_syntheses(self):
         qc = QuantumCircuit(1)
@@ -863,6 +869,51 @@ def test_default_does_not_fail_on_no_syntheses(self):
         pass_ = UnitarySynthesis(["unknown", "gates"])
         self.assertEqual(qc, pass_(qc))
 
+    def test_iswap_no_cx_synthesis_succeeds(self):
+        """Test basis set with iswap but no cx can synthesize a circuit"""
+        target = Target()
+        theta = Parameter("theta")
+
+        i_props = {
+            (0,): InstructionProperties(duration=35.5e-9, error=0.000413),
+            (1,): InstructionProperties(duration=35.5e-9, error=0.000502),
+        }
+        target.add_instruction(IGate(), i_props)
+        rz_props = {
+            (0,): InstructionProperties(duration=0, error=0),
+            (1,): InstructionProperties(duration=0, error=0),
+        }
+        target.add_instruction(RZGate(theta), rz_props)
+        sx_props = {
+            (0,): InstructionProperties(duration=35.5e-9, error=0.000413),
+            (1,): InstructionProperties(duration=35.5e-9, error=0.000502),
+        }
+        target.add_instruction(SXGate(), sx_props)
+        x_props = {
+            (0,): InstructionProperties(duration=35.5e-9, error=0.000413),
+            (1,): InstructionProperties(duration=35.5e-9, error=0.000502),
+        }
+        target.add_instruction(XGate(), x_props)
+        iswap_props = {
+            (0, 1): InstructionProperties(duration=519.11e-9, error=0.01201),
+            (1, 0): InstructionProperties(duration=554.66e-9, error=0.01201),
+        }
+        target.add_instruction(iSwapGate(), iswap_props)
+        measure_props = {
+            (0,): InstructionProperties(duration=5.813e-6, error=0.0751),
+            (1,): InstructionProperties(duration=5.813e-6, error=0.0225),
+        }
+        target.add_instruction(Measure(), measure_props)
+
+        qc = QuantumCircuit(2)
+        cxmat = Operator(CXGate()).to_matrix()
+        qc.unitary(cxmat, [0, 1])
+        unitary_synth_pass = UnitarySynthesis(target=target)
+        dag = circuit_to_dag(qc)
+        result_dag = unitary_synth_pass.run(dag)
+        result_qc = dag_to_circuit(result_dag)
+        self.assertTrue(np.allclose(Operator(result_qc.to_gate()).to_matrix(), cxmat))
+
 
 if __name__ == "__main__":
     unittest.main()