Repository: Qiskit/qiskit

Issue:
Transpilation fails in qiskit-terra 0.24.0 when optimization level is 3 and a backend is passed
### Environment

- **Qiskit Terra version**: 0.24.0
- **Python version**: 3.10.10
- **Operating system**: Linux


### What is happening?

In `qiskit-terra` 0.24.0, `qiskit.transpile` fails to transpile controlled phase gates (and possibly other gates) when the optimization level is 3 and a backend is passed.

The transpiler call fails with:

```
ParameterExpression with unbound parameters ({Parameter(θ)}) cannot be cast to a float.",
```

This behavior is new in `qiskit-terra` 0.24.0. The test below fails in 0.23.2, showing that transpilation works against a backend for all optimization levels as expected.

### How can we reproduce the issue?

Define a minimal backend with a target featuring a complete gate set. The test below passes in `qiskit-terra` 0.24.0, showing that transpilation at level 3 fails against the backend, but not against the backend's target's basis gate set if passed explicitly.

```python
import math
from contextlib import nullcontext
from typing import Any, ContextManager

import pytest
from qiskit import QuantumCircuit, transpile
from qiskit.circuit.library import Measure, RXGate, RXXGate, RZGate
from qiskit.circuit.parameter import Parameter
from qiskit.providers import BackendV2, Options
from qiskit.transpiler import Target


class Backend(BackendV2):
    def __init__(self) -> None:
        super().__init__(name="backend", provider=None)

    @classmethod
    def _default_options(cls) -> Options:
        return Options()

    @property
    def max_circuits(self) -> int:
        return 10

    @property
    def target(self) -> Target:
        theta = Parameter("θ")
        lam = Parameter("λ")

        target = Target(num_qubits=5)
        target.add_instruction(RZGate(lam))
        target.add_instruction(RXGate(theta))
        target.add_instruction(RXXGate(theta))
        target.add_instruction(Measure())

        return target

    def run(self, *args, **kwargs) -> None:
        print(args, kwargs)


@pytest.mark.parametrize(
    ("optimization_level", "context"),
    [
        (0, nullcontext()),
        (1, nullcontext()),
        (2, nullcontext()),
        (
            3,
            pytest.raises(
                TypeError,
                match=r"ParameterExpression with unbound parameters \(\{Parameter\(θ\)\}\) cannot be cast to a float\.",
            ),
        ),
    ],
)
def test_transpilation(
    optimization_level: int, context: ContextManager[Any]
) -> None:
    backend = Backend()

    qc = QuantumCircuit(2)
    qc.cp(math.pi / 2, 0, 1)

    with context:
        # this fails in 0.24.0 if optimization_level=3
       # but works for all optimization levels in 0.23.2
        transpile(qc, backend, optimization_level=optimization_level)

    # this works in both 0.24.0 and 0.23.2 for all optimization levels
    transpile(
        qc,
        basis_gates=list(backend.target.operation_names),
        optimization_level=optimization_level,
    )
```

### What should happen?

The test above should fail in `qiskit-terra` 0.24.0, like it does for `qiskit-terra` 0.23.2.
For this minimal example, transpiling against `backend=backend`  or against `basis_gates=backend.target.operation_names` should be equivalent and succeed in both cases, independently of the optimization level.

### Any suggestions?

_No response_


Discussion:
Thanks for reporting this. It definitely is a bug, it looks like an issue introduced in the changes to unitary synthesis that were done in 0.24.0 to select a decomposition based on the error rates in the Target. It looks like the code does not correctly handle the case where the 2 qubit basis gate takes a parameter and the checking it's doing to see which types of decomposers to try fail because it's trying to check if the gate is controlled, supercontrolled, etc using the unitary representation (which it can't find with a gate in the target with an unbound parameter). The pass will need to be updated to take into account a 2q basis gate that is parameterized.

By default we only run this pass as part of the 2q kak optimization in level 3 which is why the failure is isolated to level 3. It also works with using the gate names because in that case the extra information the `Target` provides isn't available so the transpiler doesn't know for sure that `rxx` is an `RXXGate` it just assumes it is but  doesn't do this extra checking for error rates and available decomposition methods.
Here is more stripped-down code to trigger the bug:
```python
from qiskit.providers import BackendV2, Options
from qiskit.transpiler import Target
from qiskit import QuantumCircuit, transpile
from qiskit.circuit.parameter import Parameter
from qiskit.circuit.library import RXGate, RXXGate, RZGate

class Backend(BackendV2):
    def __init__(self) -> None:
        super().__init__(name="backend", provider=None)

    @classmethod
    def _default_options(cls) -> Options:
        return Options()

    @property
    def max_circuits(self) -> int:
        return 10

    @property
    def target(self) -> Target:
        theta = Parameter("θ")
        lam = Parameter("λ")

        target = Target(num_qubits=5)
        target.add_instruction(RZGate(lam))
        target.add_instruction(RXGate(theta))
        target.add_instruction(RXXGate(theta))

        return target

    def run(self, *args, **kwargs) -> None:
        print(args, kwargs)


# Fails. TypeError: ParameterExpression with unbound parameters
#         ({Parameter(θ)}) cannot be cast to a float.
def test_transpilation():
    backend = Backend()
    qc = QuantumCircuit(2)
    qc.cp(3.14159 / 2, 0, 1)
    transpile(qc, backend, optimization_level=3)
```
If you change this https://github.com/Qiskit/qiskit-terra/blob/0e44c5e38066c26e06d19fe5fcbc947eb1d25472/qiskit/transpiler/passes/synthesis/unitary_synthesis.py#L709-L715

with `except (QiskitError, TypeError)` (and the same for `is_controlled`) transpilation runs to completion.

However the output circuit for the example included in this bug report with optimization level three is apparently worse by any reasonable measure than the one you get with qiskit-terra v0.23.3.

I am guessing that the quality of the result is degraded because this fix is not sufficient, and not because of an unrelated change between 0.23 and 0.24. But I am not yet sure.
So looking at how this was done on 0.23.3, it looks like for `rxx` it checked that `rxx` was in the supported instructions and then used `RXXGate(pi/2)` as the KAK gate. 

https://github.com/Qiskit/qiskit-terra/blob/0.23.3/qiskit/transpiler/passes/synthesis/unitary_synthesis.py#L43-L79

I think for the bugfix/backport we maybe should just do a hard coded fallback check for rxx and rzx by doing: `Target.instruction_supported(operation_class=RXXGate, parameters=[pi/2[)` and use that if it's available.This should method should return `True` for a parameterized `RXXGate` in the target. This will restore the functionality that we previously supported even if it's not the general fix for all parameterized 2q basis gates.

Resolving patch:
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
@@ -31,7 +31,7 @@
     TwoQubitWeylDecomposition,
 )
 from qiskit.quantum_info import Operator
-from qiskit.circuit import ControlFlowOp, Gate
+from qiskit.circuit import ControlFlowOp, Gate, Parameter
 from qiskit.circuit.library.standard_gates import (
     iSwapGate,
     CXGate,
@@ -673,13 +673,24 @@ def _decomposer_2q_from_target(self, target, qubits, approximation_degree):
         # available instructions on this qubit pair, and their associated property.
         available_2q_basis = {}
         available_2q_props = {}
+
+        # 2q gates sent to 2q decomposers must not have any symbolic parameters.  The
+        # gates must be convertable to a numeric matrix. If a basis gate supports an arbitrary
+        # angle, we have to choose one angle (or more.)
+        def _replace_parameterized_gate(op):
+            if isinstance(op, RXXGate) and isinstance(op.params[0], Parameter):
+                op = RXXGate(pi / 2)
+            elif isinstance(op, RZXGate) and isinstance(op.params[0], Parameter):
+                op = RZXGate(pi / 4)
+            return op
+
         try:
             keys = target.operation_names_for_qargs(qubits_tuple)
             for key in keys:
                 op = target.operation_from_name(key)
                 if not isinstance(op, Gate):
                     continue
-                available_2q_basis[key] = op
+                available_2q_basis[key] = _replace_parameterized_gate(op)
                 available_2q_props[key] = target[key][qubits_tuple]
         except KeyError:
             pass
@@ -690,7 +701,7 @@ def _decomposer_2q_from_target(self, target, qubits, approximation_degree):
                     op = target.operation_from_name(key)
                     if not isinstance(op, Gate):
                         continue
-                    available_2q_basis[key] = op
+                    available_2q_basis[key] = _replace_parameterized_gate(op)
                     available_2q_props[key] = target[key][reverse_tuple]
         except KeyError:
             pass


Test patch:
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
@@ -50,6 +50,7 @@
     IGate,
     CXGate,
     RZGate,
+    RXGate,
     SXGate,
     XGate,
     iSwapGate,
@@ -914,6 +915,21 @@ def test_iswap_no_cx_synthesis_succeeds(self):
         result_qc = dag_to_circuit(result_dag)
         self.assertTrue(np.allclose(Operator(result_qc.to_gate()).to_matrix(), cxmat))
 
+    def test_parameterized_basis_gate_in_target(self):
+        """Test synthesis with parameterized RXX gate."""
+        theta = Parameter("θ")
+        lam = Parameter("λ")
+        target = Target(num_qubits=2)
+        target.add_instruction(RZGate(lam))
+        target.add_instruction(RXGate(theta))
+        target.add_instruction(RXXGate(theta))
+        qc = QuantumCircuit(2)
+        qc.cp(np.pi / 2, 0, 1)
+        qc_transpiled = transpile(qc, target=target, optimization_level=3, seed_transpiler=42)
+        opcount = qc_transpiled.count_ops()
+        self.assertTrue(set(opcount).issubset({"rz", "rx", "rxx"}))
+        self.assertTrue(np.allclose(Operator(qc_transpiled), Operator(qc)))
+
 
 if __name__ == "__main__":
     unittest.main()