Repository: Qiskit/qiskit

Issue:
Port TwoQubitBasisDecomposition to Rust



Discussion:

Resolving patch:
diff --git a/qiskit/synthesis/two_qubit/two_qubit_decompose.py b/qiskit/synthesis/two_qubit/two_qubit_decompose.py
--- a/qiskit/synthesis/two_qubit/two_qubit_decompose.py
+++ b/qiskit/synthesis/two_qubit/two_qubit_decompose.py
@@ -36,7 +36,7 @@
 import numpy as np
 
 from qiskit.circuit import QuantumRegister, QuantumCircuit, Gate
-from qiskit.circuit.library.standard_gates import CXGate, RXGate
+from qiskit.circuit.library.standard_gates import CXGate, U3Gate, U2Gate, U1Gate
 from qiskit.exceptions import QiskitError
 from qiskit.quantum_info.operators import Operator
 from qiskit.synthesis.one_qubit.one_qubit_decompose import (
@@ -494,152 +494,34 @@ def __init__(
         self.gate = gate
         self.basis_fidelity = basis_fidelity
         self.pulse_optimize = pulse_optimize
-
-        basis = self.basis = TwoQubitWeylDecomposition(Operator(gate).data)
-        self._decomposer1q = OneQubitEulerDecomposer(euler_basis)
-
-        # FIXME: find good tolerances
-        self.is_supercontrolled = math.isclose(basis.a, np.pi / 4) and math.isclose(basis.c, 0.0)
-
-        # Create some useful matrices U1, U2, U3 are equivalent to the basis,
-        # expand as Ui = Ki1.Ubasis.Ki2
-        b = basis.b
-        K11l = (
-            1
-            / (1 + 1j)
-            * np.array(
-                [
-                    [-1j * cmath.exp(-1j * b), cmath.exp(-1j * b)],
-                    [-1j * cmath.exp(1j * b), -cmath.exp(1j * b)],
-                ],
-                dtype=complex,
-            )
-        )
-        K11r = (
-            1
-            / math.sqrt(2)
-            * np.array(
-                [
-                    [1j * cmath.exp(-1j * b), -cmath.exp(-1j * b)],
-                    [cmath.exp(1j * b), -1j * cmath.exp(1j * b)],
-                ],
-                dtype=complex,
-            )
-        )
-        K12l = 1 / (1 + 1j) * np.array([[1j, 1j], [-1, 1]], dtype=complex)
-        K12r = 1 / math.sqrt(2) * np.array([[1j, 1], [-1, -1j]], dtype=complex)
-        K32lK21l = (
-            1
-            / math.sqrt(2)
-            * np.array(
-                [
-                    [1 + 1j * np.cos(2 * b), 1j * np.sin(2 * b)],
-                    [1j * np.sin(2 * b), 1 - 1j * np.cos(2 * b)],
-                ],
-                dtype=complex,
-            )
-        )
-        K21r = (
-            1
-            / (1 - 1j)
-            * np.array(
-                [
-                    [-1j * cmath.exp(-2j * b), cmath.exp(-2j * b)],
-                    [1j * cmath.exp(2j * b), cmath.exp(2j * b)],
-                ],
-                dtype=complex,
-            )
-        )
-        K22l = 1 / math.sqrt(2) * np.array([[1, -1], [1, 1]], dtype=complex)
-        K22r = np.array([[0, 1], [-1, 0]], dtype=complex)
-        K31l = (
-            1
-            / math.sqrt(2)
-            * np.array(
-                [[cmath.exp(-1j * b), cmath.exp(-1j * b)], [-cmath.exp(1j * b), cmath.exp(1j * b)]],
-                dtype=complex,
-            )
-        )
-        K31r = 1j * np.array([[cmath.exp(1j * b), 0], [0, -cmath.exp(-1j * b)]], dtype=complex)
-        K32r = (
-            1
-            / (1 - 1j)
-            * np.array(
-                [
-                    [cmath.exp(1j * b), -cmath.exp(-1j * b)],
-                    [-1j * cmath.exp(1j * b), -1j * cmath.exp(-1j * b)],
-                ],
-                dtype=complex,
-            )
+        # Use cx as gate name for pulse optimal decomposition detection
+        # otherwise use USER_GATE as a unique key to support custom gates
+        # including parameterized gates like UnitaryGate.
+        if isinstance(gate, CXGate):
+            gate_name = "cx"
+        else:
+            gate_name = "USER_GATE"
+
+        self._inner_decomposer = two_qubit_decompose.TwoQubitBasisDecomposer(
+            gate_name,
+            Operator(gate).data,
+            basis_fidelity=basis_fidelity,
+            euler_basis=euler_basis,
+            pulse_optimize=pulse_optimize,
         )
-        k1ld = basis.K1l.T.conj()
-        k1rd = basis.K1r.T.conj()
-        k2ld = basis.K2l.T.conj()
-        k2rd = basis.K2r.T.conj()
-
-        # Pre-build the fixed parts of the matrices used in 3-part decomposition
-        self.u0l = K31l.dot(k1ld)
-        self.u0r = K31r.dot(k1rd)
-        self.u1l = k2ld.dot(K32lK21l).dot(k1ld)
-        self.u1ra = k2rd.dot(K32r)
-        self.u1rb = K21r.dot(k1rd)
-        self.u2la = k2ld.dot(K22l)
-        self.u2lb = K11l.dot(k1ld)
-        self.u2ra = k2rd.dot(K22r)
-        self.u2rb = K11r.dot(k1rd)
-        self.u3l = k2ld.dot(K12l)
-        self.u3r = k2rd.dot(K12r)
-
-        # Pre-build the fixed parts of the matrices used in the 2-part decomposition
-        self.q0l = K12l.T.conj().dot(k1ld)
-        self.q0r = K12r.T.conj().dot(_ipz).dot(k1rd)
-        self.q1la = k2ld.dot(K11l.T.conj())
-        self.q1lb = K11l.dot(k1ld)
-        self.q1ra = k2rd.dot(_ipz).dot(K11r.T.conj())
-        self.q1rb = K11r.dot(k1rd)
-        self.q2l = k2ld.dot(K12l)
-        self.q2r = k2rd.dot(K12r)
-
-        # Decomposition into different number of gates
-        # In the future could use different decomposition functions for different basis classes, etc
+        self.is_supercontrolled = self._inner_decomposer.super_controlled
         if not self.is_supercontrolled:
             warnings.warn(
-                "Only know how to decompose properly for supercontrolled basis gate. "
-                "This gate is ~Ud({}, {}, {})".format(basis.a, basis.b, basis.c),
+                "Only know how to decompose properly for a supercontrolled basis gate.",
                 stacklevel=2,
             )
-        self.decomposition_fns = [
-            self.decomp0,
-            self.decomp1,
-            self.decomp2_supercontrolled,
-            self.decomp3_supercontrolled,
-        ]
-        self._rqc = None
 
-    def traces(self, target):
-        r"""
-        Give the expected traces :math:`\Big\vert\text{Tr}(U \cdot U_\text{target}^{\dag})\Big\vert`
-        for a different number of basis gates.
+    def num_basis_gates(self, unitary):
+        """Computes the number of basis gates needed in
+        a decomposition of input unitary
         """
-        # Future gotcha: extending this to non-supercontrolled basis.
-        # Careful: closest distance between a1,b1,c1 and a2,b2,c2 may be between reflections.
-        # This doesn't come up if either c1==0 or c2==0 but otherwise be careful.
-        ta, tb, tc = target.a, target.b, target.c
-        bb = self.basis.b
-        return [
-            4
-            * complex(
-                math.cos(ta) * math.cos(tb) * math.cos(tc),
-                math.sin(ta) * math.sin(tb) * math.sin(tc),
-            ),
-            4
-            * complex(
-                math.cos(math.pi / 4 - ta) * math.cos(bb - tb) * math.cos(tc),
-                math.sin(math.pi / 4 - ta) * math.sin(bb - tb) * math.sin(tc),
-            ),
-            4 * math.cos(tc),
-            4,
-        ]
+        unitary = np.asarray(unitary, dtype=complex)
+        return self._inner_decomposer.num_basis_gates(unitary)
 
     @staticmethod
     def decomp0(target):
@@ -655,9 +537,7 @@ def decomp0(target):
         which is optimal for all targets and bases
         """
 
-        U0l = target.K1l.dot(target.K2l)
-        U0r = target.K1r.dot(target.K2r)
-        return U0r, U0l
+        return two_qubit_decompose.TwoQubitBasisDecomposer.decomp0(target)
 
     def decomp1(self, target):
         r"""Decompose target :math:`\sim U_d(x, y, z)` with :math:`1` use of the basis gate
@@ -671,13 +551,7 @@ def decomp1(self, target):
 
         which is optimal for all targets and bases with ``z==0`` or ``c==0``.
         """
-        # FIXME: fix for z!=0 and c!=0 using closest reflection (not always in the Weyl chamber)
-        U0l = target.K1l.dot(self.basis.K1l.T.conj())
-        U0r = target.K1r.dot(self.basis.K1r.T.conj())
-        U1l = self.basis.K2l.T.conj().dot(target.K2l)
-        U1r = self.basis.K2r.T.conj().dot(target.K2r)
-
-        return U1r, U1l, U0r, U0l
+        return self._inner_decomposer.decomp1(target)
 
     def decomp2_supercontrolled(self, target):
         r"""
@@ -696,15 +570,7 @@ def decomp2_supercontrolled(self, target):
         This is an exact decomposition for supercontrolled basis and target :math:`\sim U_d(x, y, 0)`.
         No guarantees for non-supercontrolled basis.
         """
-
-        U0l = target.K1l.dot(self.q0l)
-        U0r = target.K1r.dot(self.q0r)
-        U1l = self.q1la.dot(rz_array(-2 * target.a)).dot(self.q1lb)
-        U1r = self.q1ra.dot(rz_array(2 * target.b)).dot(self.q1rb)
-        U2l = self.q2l.dot(target.K2l)
-        U2r = self.q2r.dot(target.K2r)
-
-        return U2r, U2l, U1r, U1l, U0r, U0l
+        return self._inner_decomposer.decomp2_supercontrolled(target)
 
     def decomp3_supercontrolled(self, target):
         r"""
@@ -712,17 +578,7 @@ def decomp3_supercontrolled(self, target):
         This is an exact decomposition for supercontrolled basis :math:`\sim U_d(\pi/4, b, 0)`, all b,
         and any target. No guarantees for non-supercontrolled basis.
         """
-
-        U0l = target.K1l.dot(self.u0l)
-        U0r = target.K1r.dot(self.u0r)
-        U1l = self.u1l
-        U1r = self.u1ra.dot(rz_array(-2 * target.c)).dot(self.u1rb)
-        U2l = self.u2la.dot(rz_array(-2 * target.a)).dot(self.u2lb)
-        U2r = self.u2ra.dot(rz_array(2 * target.b)).dot(self.u2rb)
-        U3l = self.u3l.dot(target.K2l)
-        U3r = self.u3r.dot(target.K2r)
-
-        return U3r, U3l, U2r, U2l, U1r, U1l, U0r, U0l
+        return self._inner_decomposer.decomp3_supercontrolled(target)
 
     def __call__(
         self,
@@ -748,313 +604,41 @@ def __call__(
         Raises:
             QiskitError: if ``pulse_optimize`` is True but we don't know how to do it.
         """
-        basis_fidelity = basis_fidelity or self.basis_fidelity
-        if approximate is False:
-            basis_fidelity = 1.0
-        unitary = np.asarray(unitary, dtype=complex)
 
-        target_decomposed = TwoQubitWeylDecomposition(unitary)
-        traces = self.traces(target_decomposed)
-        expected_fidelities = [trace_to_fid(traces[i]) * basis_fidelity**i for i in range(4)]
-
-        best_nbasis = int(np.argmax(expected_fidelities))
-        if _num_basis_uses is not None:
-            best_nbasis = _num_basis_uses
-        decomposition = self.decomposition_fns[best_nbasis](target_decomposed)
-
-        # attempt pulse optimal decomposition
-        try:
-            if self.pulse_optimize in {None, True}:
-                return_circuit = self._pulse_optimal_chooser(
-                    best_nbasis, decomposition, target_decomposed
-                )
-                if return_circuit:
-                    return return_circuit
-        except QiskitError:
-            if self.pulse_optimize:
-                raise
-
-        # do default decomposition
+        sequence = self._inner_decomposer(
+            np.asarray(unitary, dtype=complex),
+            basis_fidelity,
+            approximate,
+            _num_basis_uses=_num_basis_uses,
+        )
         q = QuantumRegister(2)
-        decomposition_euler = [self._decomposer1q._decompose(x) for x in decomposition]
-        return_circuit = QuantumCircuit(q)
-        return_circuit.global_phase = target_decomposed.global_phase
-        return_circuit.global_phase -= best_nbasis * self.basis.global_phase
-        if best_nbasis == 2:
-            return_circuit.global_phase += np.pi
-        for i in range(best_nbasis):
-            return_circuit.compose(decomposition_euler[2 * i], [q[0]], inplace=True)
-            return_circuit.compose(decomposition_euler[2 * i + 1], [q[1]], inplace=True)
-            return_circuit.append(self.gate, [q[0], q[1]])
-        return_circuit.compose(decomposition_euler[2 * best_nbasis], [q[0]], inplace=True)
-        return_circuit.compose(decomposition_euler[2 * best_nbasis + 1], [q[1]], inplace=True)
-        return return_circuit
-
-    def _pulse_optimal_chooser(
-        self, best_nbasis, decomposition, target_decomposed
-    ) -> QuantumCircuit:
-        """Determine method to find pulse optimal circuit. This method may be
-        removed once a more general approach is used.
-
-        Returns:
-            QuantumCircuit: pulse optimal quantum circuit.
-            None: Probably ``nbasis==1`` and original circuit is fine.
-
-        Raises:
-            QiskitError: Decomposition for selected basis not implemented.
-        """
-        circuit = None
-        if self.pulse_optimize and best_nbasis in {0, 1}:
-            # already pulse optimal
-            return None
-        elif self.pulse_optimize and best_nbasis > 3:
-            raise QiskitError(
-                f"Unexpected number of entangling gates ({best_nbasis}) in decomposition."
-            )
-        if self._decomposer1q.basis in {"ZSX", "ZSXX"}:
-            if isinstance(self.gate, CXGate):
-                if best_nbasis == 3:
-                    circuit = self._get_sx_vz_3cx_efficient_euler(decomposition, target_decomposed)
-                elif best_nbasis == 2:
-                    circuit = self._get_sx_vz_2cx_efficient_euler(decomposition, target_decomposed)
-            else:
-                raise QiskitError("pulse_optimizer currently only works with CNOT entangling gate")
-        else:
-            raise QiskitError(
-                '"pulse_optimize" currently only works with ZSX basis '
-                f"({self._decomposer1q.basis} used)"
-            )
-        return circuit
-
-    def _get_sx_vz_2cx_efficient_euler(self, decomposition, target_decomposed):
-        """
-        Decomposition of SU(4) gate for device with SX, virtual RZ, and CNOT gates assuming
-        two CNOT gates are needed.
-
-        This first decomposes each unitary from the KAK decomposition into ZXZ on the source
-        qubit of the CNOTs and XZX on the targets in order to commute operators to beginning and
-        end of decomposition. The beginning and ending single qubit gates are then
-        collapsed and re-decomposed with the single qubit decomposer. This last step could be avoided
-        if performance is a concern.
-        """
-        best_nbasis = 2  # by assumption
-        num_1q_uni = len(decomposition)
-        # list of euler angle decompositions on qubits 0 and 1
-        euler_q0 = np.empty((num_1q_uni // 2, 3), dtype=float)
-        euler_q1 = np.empty((num_1q_uni // 2, 3), dtype=float)
-        global_phase = 0.0
-
-        # decompose source unitaries to zxz
-        zxz_decomposer = OneQubitEulerDecomposer("ZXZ")
-        for iqubit, decomp in enumerate(decomposition[0::2]):
-            euler_angles = zxz_decomposer.angles_and_phase(decomp)
-            euler_q0[iqubit, [1, 2, 0]] = euler_angles[:3]
-            global_phase += euler_angles[3]
-        # decompose target unitaries to xzx
-        xzx_decomposer = OneQubitEulerDecomposer("XZX")
-        for iqubit, decomp in enumerate(decomposition[1::2]):
-            euler_angles = xzx_decomposer.angles_and_phase(decomp)
-            euler_q1[iqubit, [1, 2, 0]] = euler_angles[:3]
-            global_phase += euler_angles[3]
-        qc = QuantumCircuit(2)
-        qc.global_phase = target_decomposed.global_phase
-        qc.global_phase -= best_nbasis * self.basis.global_phase
-        qc.global_phase += global_phase
-
-        # TODO: make this more effecient to avoid double decomposition
-        # prepare beginning 0th qubit local unitary
-        circ = QuantumCircuit(1)
-        circ.rz(euler_q0[0][0], 0)
-        circ.rx(euler_q0[0][1], 0)
-        circ.rz(euler_q0[0][2] + euler_q0[1][0] + math.pi / 2, 0)
-        # re-decompose to basis of 1q decomposer
-        qceuler = self._decomposer1q(Operator(circ).data)
-        qc.compose(qceuler, [0], inplace=True)
-
-        # prepare beginning 1st qubit local unitary
-        circ = QuantumCircuit(1)
-        circ.rx(euler_q1[0][0], 0)
-        circ.rz(euler_q1[0][1], 0)
-        circ.rx(euler_q1[0][2] + euler_q1[1][0], 0)
-        qceuler = self._decomposer1q(Operator(circ).data)
-        qc.compose(qceuler, [1], inplace=True)
-
-        qc.cx(0, 1)
-        # the central decompositions are dependent on the specific form of the
-        # unitaries coming out of the two qubit decomposer which have some flexibility
-        # of choice.
-        qc.sx(0)
-        qc.rz(euler_q0[1][1] - math.pi, 0)
-        qc.sx(0)
-        qc.rz(euler_q1[1][1], 1)
-        qc.global_phase += math.pi / 2
-
-        qc.cx(0, 1)
-
-        circ = QuantumCircuit(1)
-        circ.rz(euler_q0[1][2] + euler_q0[2][0] + math.pi / 2, 0)
-        circ.rx(euler_q0[2][1], 0)
-        circ.rz(euler_q0[2][2], 0)
-        qceuler = self._decomposer1q(Operator(circ).data)
-        qc.compose(qceuler, [0], inplace=True)
-        circ = QuantumCircuit(1)
-        circ.rx(euler_q1[1][2] + euler_q1[2][0], 0)
-        circ.rz(euler_q1[2][1], 0)
-        circ.rx(euler_q1[2][2], 0)
-        qceuler = self._decomposer1q(Operator(circ).data)
-        qc.compose(qceuler, [1], inplace=True)
-
-        return qc
-
-    def _get_sx_vz_3cx_efficient_euler(self, decomposition, target_decomposed):
-        """
-        Decomposition of SU(4) gate for device with SX, virtual RZ, and CNOT gates assuming
-        three CNOT gates are needed.
-
-        This first decomposes each unitary from the KAK decomposition into ZXZ on the source
-        qubit of the CNOTs and XZX on the targets in order commute operators to beginning and
-        end of decomposition. Inserting Hadamards reverses the direction of the CNOTs and transforms
-        a variable Rx -> variable virtual Rz. The beginning and ending single qubit gates are then
-        collapsed and re-decomposed with the single qubit decomposer. This last step could be avoided
-        if performance is a concern.
-        """
-        best_nbasis = 3  # by assumption
-        num_1q_uni = len(decomposition)
-        # create structure to hold euler angles: 1st index represents unitary "group" wrt cx
-        # 2nd index represents index of euler triple.
-        euler_q0 = np.empty((num_1q_uni // 2, 3), dtype=float)
-        euler_q1 = np.empty((num_1q_uni // 2, 3), dtype=float)
-        global_phase = 0.0
-        atol = 1e-10  # absolute tolerance for floats
-
-        # decompose source unitaries to zxz
-        zxz_decomposer = OneQubitEulerDecomposer("ZXZ")
-        for iqubit, decomp in enumerate(decomposition[0::2]):
-            euler_angles = zxz_decomposer.angles_and_phase(decomp)
-            euler_q0[iqubit, [1, 2, 0]] = euler_angles[:3]
-            global_phase += euler_angles[3]
-        # decompose target unitaries to xzx
-        xzx_decomposer = OneQubitEulerDecomposer("XZX")
-        for iqubit, decomp in enumerate(decomposition[1::2]):
-            euler_angles = xzx_decomposer.angles_and_phase(decomp)
-            euler_q1[iqubit, [1, 2, 0]] = euler_angles[:3]
-            global_phase += euler_angles[3]
-
-        qc = QuantumCircuit(2)
-        qc.global_phase = target_decomposed.global_phase
-        qc.global_phase -= best_nbasis * self.basis.global_phase
-        qc.global_phase += global_phase
-
-        x12 = euler_q0[1][2] + euler_q0[2][0]
-        x12_isNonZero = not math.isclose(x12, 0, abs_tol=atol)
-        x12_isOddMult = None
-        x12_isPiMult = math.isclose(math.sin(x12), 0, abs_tol=atol)
-        if x12_isPiMult:
-            x12_isOddMult = math.isclose(math.cos(x12), -1, abs_tol=atol)
-            x12_phase = math.pi * math.cos(x12)
-        x02_add = x12 - euler_q0[1][0]
-        x12_isHalfPi = math.isclose(x12, math.pi / 2, abs_tol=atol)
-
-        # TODO: make this more effecient to avoid double decomposition
-        circ = QuantumCircuit(1)
-        circ.rz(euler_q0[0][0], 0)
-        circ.rx(euler_q0[0][1], 0)
-        if x12_isNonZero and x12_isPiMult:
-            circ.rz(euler_q0[0][2] - x02_add, 0)
-        else:
-            circ.rz(euler_q0[0][2] + euler_q0[1][0], 0)
-        circ.h(0)
-        qceuler = self._decomposer1q(Operator(circ).data)
-        qc.compose(qceuler, [0], inplace=True)
-
-        circ = QuantumCircuit(1)
-        circ.rx(euler_q1[0][0], 0)
-        circ.rz(euler_q1[0][1], 0)
-        circ.rx(euler_q1[0][2] + euler_q1[1][0], 0)
-        circ.h(0)
-        qceuler = self._decomposer1q(Operator(circ).data)
-        qc.compose(qceuler, [1], inplace=True)
-
-        qc.cx(1, 0)
-
-        if x12_isPiMult:
-            # even or odd multiple
-            if x12_isNonZero:
-                qc.global_phase += x12_phase
-            if x12_isNonZero and x12_isOddMult:
-                qc.rz(-euler_q0[1][1], 0)
-            else:
-                qc.rz(euler_q0[1][1], 0)
-                qc.global_phase += math.pi
-        if x12_isHalfPi:
-            qc.sx(0)
-            qc.global_phase -= math.pi / 4
-        elif x12_isNonZero and not x12_isPiMult:
-            # this is non-optimal but doesn't seem to occur currently
-            if self.pulse_optimize is None:
-                qc.compose(self._decomposer1q(Operator(RXGate(x12)).data), [0], inplace=True)
-            else:
-                raise QiskitError("possible non-pulse-optimal decomposition encountered")
-        if math.isclose(euler_q1[1][1], math.pi / 2, abs_tol=atol):
-            qc.sx(1)
-            qc.global_phase -= math.pi / 4
-        else:
-            # this is non-optimal but doesn't seem to occur currently
-            if self.pulse_optimize is None:
-                qc.compose(
-                    self._decomposer1q(Operator(RXGate(euler_q1[1][1])).data), [1], inplace=True
-                )
-            else:
-                raise QiskitError("possible non-pulse-optimal decomposition encountered")
-        qc.rz(euler_q1[1][2] + euler_q1[2][0], 1)
-
-        qc.cx(1, 0)
+        circ = QuantumCircuit(q, global_phase=sequence.global_phase)
+        for name, params, qubits in sequence:
+            try:
+                getattr(circ, name)(*params, *qubits)
+            except AttributeError as exc:
+                if name == "USER_GATE":
+                    circ.append(self.gate, qubits)
+                elif name == "u3":
+                    gate = U3Gate(*params)
+                    circ.append(gate, qubits)
+                elif name == "u2":
+                    gate = U2Gate(*params)
+                    circ.append(gate, qubits)
+                elif name == "u1":
+                    gate = U1Gate(*params)
+                    circ.append(gate, qubits)
+                else:
+                    raise QiskitError(f"Unknown gate {name}") from exc
 
-        qc.rz(euler_q0[2][1], 0)
-        if math.isclose(euler_q1[2][1], math.pi / 2, abs_tol=atol):
-            qc.sx(1)
-            qc.global_phase -= math.pi / 4
-        else:
-            # this is non-optimal but doesn't seem to occur currently
-            if self.pulse_optimize is None:
-                qc.compose(
-                    self._decomposer1q(Operator(RXGate(euler_q1[2][1])).data), [1], inplace=True
-                )
-            else:
-                raise QiskitError("possible non-pulse-optimal decomposition encountered")
-
-        qc.cx(1, 0)
-
-        circ = QuantumCircuit(1)
-        circ.h(0)
-        circ.rz(euler_q0[2][2] + euler_q0[3][0], 0)
-        circ.rx(euler_q0[3][1], 0)
-        circ.rz(euler_q0[3][2], 0)
-        qceuler = self._decomposer1q(Operator(circ).data)
-        qc.compose(qceuler, [0], inplace=True)
-
-        circ = QuantumCircuit(1)
-        circ.h(0)
-        circ.rx(euler_q1[2][2] + euler_q1[3][0], 0)
-        circ.rz(euler_q1[3][1], 0)
-        circ.rx(euler_q1[3][2], 0)
-        qceuler = self._decomposer1q(Operator(circ).data)
-        qc.compose(qceuler, [1], inplace=True)
-
-        # TODO: fix the sign problem to avoid correction here
-        if cmath.isclose(
-            target_decomposed.unitary_matrix[0, 0], -(Operator(qc).data[0, 0]), abs_tol=atol
-        ):
-            qc.global_phase += math.pi
-        return qc
+        return circ
 
-    def num_basis_gates(self, unitary):
-        """Computes the number of basis gates needed in
-        a decomposition of input unitary
+    def traces(self, target):
+        r"""
+        Give the expected traces :math:`\Big\vert\text{Tr}(U \cdot U_\text{target}^{\dag})\Big\vert`
+        for a different number of basis gates.
         """
-        return two_qubit_decompose._num_basis_gates(
-            self.basis.b, self.basis_fidelity, np.asarray(unitary, dtype=complex)
-        )
+        return self._inner_decomposer.traces(target._inner_decomposition)
 
 
 class TwoQubitDecomposeUpToDiagonal:
diff --git a/qiskit/transpiler/passes/optimization/consolidate_blocks.py b/qiskit/transpiler/passes/optimization/consolidate_blocks.py
--- a/qiskit/transpiler/passes/optimization/consolidate_blocks.py
+++ b/qiskit/transpiler/passes/optimization/consolidate_blocks.py
@@ -131,7 +131,7 @@ def run(self, dag):
                 if (  # pylint: disable=too-many-boolean-expressions
                     self.force_consolidate
                     or unitary.num_qubits > 2
-                    or self.decomposer.num_basis_gates(unitary) < basis_count
+                    or self.decomposer.num_basis_gates(matrix) < basis_count
                     or len(block) > max_2q_depth
                     or ((self.basis_gates is not None) and outside_basis)
                     or ((self.target is not None) and outside_basis)
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
@@ -808,10 +808,23 @@ def is_controlled(gate):
         if basis_2q_fidelity:
             for basis_1q in available_1q_basis:
                 if isinstance(pi2_basis, CXGate) and basis_1q == "ZSX":
+                    # If we're going to use the pulse optimal decomposition
+                    # in TwoQubitBasisDecomposer we need to compute the basis
+                    # fidelity to use for the decomposition. Either use the
+                    # cx error rate if approximation degree is None, or
+                    # the approximation degree value if it's a float
+                    if approximation_degree is None:
+                        props = target["cx"].get(qubits_tuple)
+                        if props is not None:
+                            fidelity = 1.0 - getattr(props, "error", 0.0)
+                        else:
+                            fidelity = 1.0
+                    else:
+                        fidelity = approximation_degree
                     pi2_decomposer = TwoQubitBasisDecomposer(
                         pi2_basis,
                         euler_basis=basis_1q,
-                        basis_fidelity=basis_2q_fidelity,
+                        basis_fidelity=fidelity,
                         pulse_optimize=True,
                     )
                     embodiments.update({pi / 2: XXEmbodiments[pi2_decomposer.gate.base_class]})


Test patch: