Repository: Qiskit/qiskit — pull request #10008 (python)
Title: Filter possible 2q decomposers more carefully in unitary synthesis

This PR fixes a few bugs in transpilation.\r\n\r\nCloses #9996\r\nCloses #9935\r\n\r\nThis PR supersedes #9994 , #9984, #9986, #9982 . These PRs will be closed\r\n\r\n* Skips running some decomposers when we know they will fail. In particular, before this PR, `XXDecomposer` was used unconditionally when a `Target` is supplied to `UnitarySynthesis`. Now it is skipped when there are no controlled basis gates to send to it, except in the case that the unitary to be synthesized is a product of 1q gates.\r\n* Makes `XXDecomposer`  throw more informative errors\r\n\r\n#### Behavior of previous bugs and tests\r\n\r\n* Adds a test that this bug  #9996 is fixed. Synthesis with a basis set whose only engtangling gate is iSwap no longer throws an error, but rather runs to completion. Resynthesizing with a CX-gate in the basis does not change the circuit. But the corresponding numeric unitary is correct.\r\n\r\n* `FakePrague` and other fake backends erroneously have no entangling gate ( #9983 #10004)  Prior to this PR this caused an error to be thrown when synthesizing some circuits #9935  With this PR an error is no longer thrown. All decomposers are filtered out in `DefaultUnitarySynthesis` so that none are run (Calling `transpile` with optimization level 3) A circuit is returned by `transpile`. But it is incorrect. An issue should be filed.\r\n\r\n* Synthesizing a non-entangling unitary with a basis containing no entangling gates still works. There was an existing test for this.\r\n\r\n* I am pretty sure that an error thrown by any decomposer will cause synthesis of the entire circuit to fail. There were existing tests for some cases in the test suite. These are still in place and still have the same behavior. \r\n\r\n#### Details\r\n\r\nPreviously all analysis of the unitary to be decomposed happened in the decomposers themselves. Now we do a bit of analysis on the unitary to be decomposed in order to choose which decomposers to run. This allows filtering some that would fail. To do this we call `TwoQubitWeylDecomposition` on the unitary. Since the 2q decomposers perform this same Weyl decomposition as a first step, we allow the decomposers to take this weyl decomposition as an argument. The argument is optional to avoid breaking code using the old API.

Resolved issues:
title: IndexError in `XXDecomposer` during `FakePrague` `transpile()` with optimization level 3
body: ### Environment\n\n- **Qiskit Terra version**: 0.24.0 main (@ 5672c70649b4f83a6c7146c68a6a543d4a3908fd)\r\n- **Python version**:  3.10\r\n- **Operating system**:  Linux\r\n\n\n### What is happening?\n\nDuring `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.\n\n### How can we reproduce the issue?\n\n```python\r\nfrom qiskit.circuit import QuantumCircuit\r\nfrom qiskit.compiler import transpile\r\nfrom qiskit.providers.fake_provider import FakePrague\r\n\r\nbackend = FakePrague()\r\nnum_qubits = 10\r\n# Circuit creation\r\nqc = QuantumCircuit(num_qubits)\r\nqc.h(0)\r\nfor i in range(num_qubits - 1):\r\n    if i % 2 == 0:\r\n        qc.cy(0, i + 1)\r\n    else:\r\n        qc.cx(0, i + 1)\r\nqc.measure_all()\r\ntqc = transpile(\r\n    qc, backend, seed_transpiler=2023, optimization_level=3\r\n)\r\n```\n\n### What should happen?\n\nThe `transpile()` shouldn't error and it should successfully run 2q synthesis and return a compiled circuit.\n\n### Any suggestions?\n\nThis 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) 

Hints:

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

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