# This code is part of Qiskit.
#
# (C) Copyright IBM 2017, 2024.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

# pylint: disable=missing-function-docstring

"""
Tests for the default UnitarySynthesis transpiler pass.
"""

import unittest
import math
import numpy as np
import scipy
from ddt import ddt, data

from qiskit import transpile, generate_preset_pass_manager
from qiskit.providers.fake_provider import GenericBackendV2
from qiskit.circuit import QuantumCircuit, QuantumRegister, ClassicalRegister
from qiskit.circuit.library import quantum_volume
from qiskit.circuit.parameterexpression import ParameterValueType
from qiskit.converters import circuit_to_dag, dag_to_circuit
from qiskit.transpiler.passes import UnitarySynthesis
from qiskit.quantum_info.operators import Operator
from qiskit.quantum_info.random import random_unitary
from qiskit.transpiler import PassManager, CouplingMap, Target, InstructionProperties
from qiskit.exceptions import QiskitError
from qiskit.transpiler.passes import (
    Collect2qBlocks,
    ConsolidateBlocks,
    Optimize1qGates,
    SabreLayout,
    Unroll3qOrMore,
    CheckMap,
    BarrierBeforeFinalMeasurements,
    SabreSwap,
    TrivialLayout,
)
from qiskit.circuit.library import (
    IGate,
    CXGate,
    RZGate,
    RXGate,
    SXGate,
    XGate,
    iSwapGate,
    ECRGate,
    UGate,
    ZGate,
    RYYGate,
    RZZGate,
    RXXGate,
    PauliEvolutionGate,
    CPhaseGate,
)
from qiskit.quantum_info import SparsePauliOp
from qiskit.circuit import Measure
from qiskit.circuit.controlflow import IfElseOp
from qiskit.circuit import Parameter, Gate
from qiskit.synthesis.unitary.qsd import qs_decomposition

from test import combine  # pylint: disable=wrong-import-order
from test import QiskitTestCase  # pylint: disable=wrong-import-order
from test.python.providers.fake_mumbai_v2 import (  # pylint: disable=wrong-import-order
    FakeMumbaiFractionalCX,
)
from ..legacy_cmaps import YORKTOWN_CMAP


class FakeBackend2QV2(GenericBackendV2):
    """A 2-qubit fake backend"""

    def __init__(self):
        super().__init__(num_qubits=2, basis_gates=["rx", "u"], seed=42)
        cx_props = {
            (0, 1): InstructionProperties(duration=5.23e-7, error=0.00098115),
        }
        self._target.add_instruction(CXGate(), cx_props)
        ecr_props = {
            (1, 0): InstructionProperties(duration=4.52e-9, error=0.0000132115),
        }
        self._target.add_instruction(ECRGate(), ecr_props)


class FakeBackend5QV2(GenericBackendV2):
    """A 5-qubit fake backend"""

    def __init__(self, bidirectional=True):
        super().__init__(num_qubits=5, basis_gates=["u"], seed=42)
        cx_props = {
            (0, 1): InstructionProperties(duration=5.23e-7, error=0.00098115),
            (3, 4): InstructionProperties(duration=5.23e-7, error=0.00098115),
        }
        if bidirectional:
            cx_props[(1, 0)] = InstructionProperties(duration=6.23e-7, error=0.00099115)
            cx_props[(4, 3)] = InstructionProperties(duration=7.23e-7, error=0.00099115)
        self._target.add_instruction(CXGate(), cx_props)
        ecr_props = {
            (1, 2): InstructionProperties(duration=4.52e-9, error=0.0000132115),
            (2, 3): InstructionProperties(duration=4.52e-9, error=0.0000132115),
        }
        if bidirectional:
            ecr_props[(2, 1)] = InstructionProperties(duration=5.52e-9, error=0.0000232115)
            ecr_props[(3, 2)] = InstructionProperties(duration=5.52e-9, error=0.0000232115)


@ddt
class TestUnitarySynthesisBasisGates(QiskitTestCase):
    """Test UnitarySynthesis pass with basis gates."""

    def test_empty_basis_gates(self):
        """Verify when basis_gates is None, we do not synthesize unitaries."""
        qc = QuantumCircuit(3)
        op_1q = random_unitary(2, seed=0)
        op_2q = random_unitary(4, seed=0)
        op_3q = random_unitary(8, seed=0)
        qc.unitary(op_1q.data, [0])
        qc.unitary(op_2q.data, [0, 1])
        qc.unitary(op_3q.data, [0, 1, 2])
        out = UnitarySynthesis(basis_gates=None, min_qubits=2)(qc)
        self.assertEqual(out.count_ops(), {"unitary": 3})

    @data(
        ["u3", "cx"],
        ["u1", "u2", "u3", "cx"],
        ["ry", "rz", "rxx"],
        ["rx", "rz", "rzz"],
        ["rx", "rz", "iswap"],
        ["u3", "rx", "rz", "cz", "iswap"],
    )
    def test_two_qubit_synthesis_to_basis(self, basis_gates):
        """Verify two qubit unitaries are synthesized to match basis gates."""
        bell = QuantumCircuit(2)
        bell.h(0)
        bell.cx(0, 1)
        bell_op = Operator(bell)

        qc = QuantumCircuit(2)
        qc.unitary(bell_op, [0, 1])
        dag = circuit_to_dag(qc)

        out = UnitarySynthesis(basis_gates).run(dag)
        self.assertTrue(set(out.count_ops()).issubset(basis_gates))

    @data(True, False, None)
    def test_two_qubit_synthesis_to_directional_cx_from_coupling_map(self, natural_direction):
        """Verify natural cx direction is used when specified in coupling map."""

        qr = QuantumRegister(2)
        coupling_map = CouplingMap([[0, 1], [1, 2], [1, 3], [3, 4]])
        triv_layout_pass = TrivialLayout(coupling_map)
        qc = QuantumCircuit(qr)
        qc.unitary(random_unitary(4, seed=12), [0, 1])
        unisynth_pass = UnitarySynthesis(
            basis_gates=["id", "rz", "sx", "x", "cx", "reset"],
            coupling_map=coupling_map,
            pulse_optimize=True,
            natural_direction=natural_direction,
        )
        pm = PassManager([triv_layout_pass, unisynth_pass])
        qc_out = pm.run(qc)

        if natural_direction is False:
            self.assertTrue(
                all(((qr[1], qr[0]) == instr.qubits for instr in qc_out.get_instructions("cx")))
            )
        else:
            # the decomposer defaults to the [1, 0] direction but the coupling
            # map specifies a [0, 1] direction. Check that this is respected.
            self.assertTrue(
                all(((qr[0], qr[1]) == instr.qubits for instr in qc_out.get_instructions("cx")))
            )
        self.assertEqual(Operator(qc), Operator(qc_out))

    def test_two_qubit_synthesis_not_pulse_optimal(self):
        """Verify not attempting pulse optimal decomposition when pulse_optimize==False."""

        qr = QuantumRegister(2)
        qc = QuantumCircuit(qr)
        qc.unitary(random_unitary(4, seed=12), [0, 1])
        coupling_map = CouplingMap([[0, 1]])
        pm_nonoptimal = PassManager(
            [
                TrivialLayout(coupling_map),
                UnitarySynthesis(
                    basis_gates=["id", "rz", "sx", "x", "cx", "reset"],
                    coupling_map=coupling_map,
                    pulse_optimize=False,
                    natural_direction=True,
                ),
            ]
        )
        pm_optimal = PassManager(
            [
                TrivialLayout(coupling_map),
                UnitarySynthesis(
                    basis_gates=["id", "rz", "sx", "x", "cx", "reset"],
                    coupling_map=coupling_map,
                    pulse_optimize=True,
                    natural_direction=True,
                ),
            ]
        )
        qc_nonoptimal = pm_nonoptimal.run(qc)
        qc_optimal = pm_optimal.run(qc)
        self.assertGreater(qc_nonoptimal.count_ops()["sx"], qc_optimal.count_ops()["sx"])

    def test_two_qubit_pulse_optimal_true_raises(self):
        """Verify raises if pulse optimal==True but cx is not in the basis."""
        basis_gates = ["id", "rz", "sx", "x", "cx", "reset"]
        # this assumes iswap pulse optimal decomposition doesn't exist
        basis_gates = [gate if gate != "cx" else "iswap" for gate in basis_gates]
        qr = QuantumRegister(2)
        coupling_map = CouplingMap([[0, 1], [1, 2], [1, 3], [3, 4]])
        triv_layout_pass = TrivialLayout(coupling_map)
        qc = QuantumCircuit(qr)
        qc.unitary(random_unitary(4, seed=12), [0, 1])
        unisynth_pass = UnitarySynthesis(
            basis_gates=basis_gates,
            coupling_map=coupling_map,
            pulse_optimize=True,
            natural_direction=True,
        )
        pm = PassManager([triv_layout_pass, unisynth_pass])
        with self.assertRaises(QiskitError):
            pm.run(qc)

    def test_two_qubit_natural_direction_true_gate_length_raises(self):
        """Verify that error is raised if preferred direction cannot be inferred
        from gate lenghts/errors.
        """
        qr = QuantumRegister(2)
        coupling_map = CouplingMap([[0, 1], [1, 0], [1, 2], [1, 3], [3, 4]])
        triv_layout_pass = TrivialLayout(coupling_map)
        qc = QuantumCircuit(qr)
        qc.unitary(random_unitary(4, seed=12), [0, 1])
        unisynth_pass = UnitarySynthesis(
            basis_gates=["id", "rz", "sx", "x", "cx", "reset"],
            pulse_optimize=True,
            natural_direction=True,
        )
        pm = PassManager([triv_layout_pass, unisynth_pass])
        with self.assertRaises(QiskitError):
            pm.run(qc)

    def test_two_qubit_pulse_optimal_none_optimal(self):
        """Verify pulse optimal decomposition when pulse_optimize==None."""
        qr = QuantumRegister(2)
        coupling_map = CouplingMap([[0, 1], [1, 2], [1, 3], [3, 4]])
        triv_layout_pass = TrivialLayout(coupling_map)
        qc = QuantumCircuit(qr)
        qc.unitary(random_unitary(4, seed=12), [0, 1])
        unisynth_pass = UnitarySynthesis(
            basis_gates=["id", "rz", "sx", "x", "cx", "reset"],
            coupling_map=coupling_map,
            pulse_optimize=None,
            natural_direction=True,
        )
        pm = PassManager([triv_layout_pass, unisynth_pass])
        qc_out = pm.run(qc)
        if isinstance(qc_out, QuantumCircuit):
            num_ops = qc_out.count_ops()
        else:
            num_ops = qc_out[0].count_ops()
        self.assertIn("sx", num_ops)
        self.assertLessEqual(num_ops["sx"], 12)

    def test_two_qubit_pulse_optimal_none_no_raise(self):
        """Verify pulse optimal decomposition when pulse_optimize==None doesn't
        raise when pulse optimal decomposition unknown."""
        basis_gates = ["id", "rz", "sx", "x", "cx", "reset"]
        # this assumes iswap pulse optimal decomposition doesn't exist
        basis_gates = [gate if gate != "cx" else "iswap" for gate in basis_gates]
        qr = QuantumRegister(2)
        coupling_map = CouplingMap([[0, 1], [1, 2], [1, 3], [3, 4]])
        triv_layout_pass = TrivialLayout(coupling_map)
        qc = QuantumCircuit(qr)
        qc.unitary(random_unitary(4, seed=12), [0, 1])
        unisynth_pass = UnitarySynthesis(
            basis_gates=basis_gates,
            coupling_map=coupling_map,
            pulse_optimize=None,
            natural_direction=True,
        )
        pm = PassManager([triv_layout_pass, unisynth_pass])
        try:
            qc_out = pm.run(qc)
        except QiskitError:
            self.fail("pulse_optimize=None raised exception unexpectedly")
        if isinstance(qc_out, QuantumCircuit):
            num_ops = qc_out.count_ops()
        else:
            num_ops = qc_out[0].count_ops()
        self.assertIn("sx", num_ops)
        self.assertLessEqual(num_ops["sx"], 14)

    def test_qv_natural(self):
        """Check that quantum volume circuit compiles for natural direction"""
        qv64 = quantum_volume(5, seed=15)

        def construct_passmanager(basis_gates, coupling_map, synthesis_fidelity, pulse_optimize):
            seed = 2
            _map = [SabreLayout(coupling_map, max_iterations=2, seed=seed)]
            _unroll3q = Unroll3qOrMore()
            _swap_check = CheckMap(coupling_map)
            _swap = [
                BarrierBeforeFinalMeasurements(),
                SabreSwap(coupling_map, heuristic="lookahead", seed=seed),
            ]
            _optimize = [
                Collect2qBlocks(),
                ConsolidateBlocks(basis_gates=basis_gates),
                UnitarySynthesis(
                    basis_gates,
                    synthesis_fidelity,
                    coupling_map,
                    pulse_optimize=pulse_optimize,
                    natural_direction=True,
                ),
                Optimize1qGates(basis_gates),
            ]

            pm = PassManager()
            pm.append(_map)  # map to hardware by inserting swaps
            pm.append(_unroll3q)
            pm.append(_swap_check)
            pm.append(_swap)
            pm.append(_optimize)
            return pm

        coupling_map = CouplingMap([[0, 1], [1, 2], [3, 2], [3, 4], [5, 4]])
        basis_gates = ["rz", "sx", "cx"]

        pm1 = construct_passmanager(
            basis_gates=basis_gates,
            coupling_map=coupling_map,
            synthesis_fidelity=0.99,
            pulse_optimize=True,
        )
        pm2 = construct_passmanager(
            basis_gates=basis_gates,
            coupling_map=coupling_map,
            synthesis_fidelity=0.99,
            pulse_optimize=False,
        )

        qv64_1 = pm1.run(qv64.decompose())
        qv64_2 = pm2.run(qv64.decompose())
        edges = [list(edge) for edge in coupling_map.get_edges()]
        self.assertTrue(
            all(
                [qv64_1.qubits.index(qubit) for qubit in instr.qubits] in edges
                for instr in qv64_1.get_instructions("cx")
            )
        )
        self.assertEqual(Operator(qv64_1), Operator(qv64_2))

    @data(1, 2, 3)
    def test_coupling_map_transpile(self, opt):
        """test natural_direction works with transpile"""
        qr = QuantumRegister(2)
        circ = QuantumCircuit(qr)
        circ.append(random_unitary(4, seed=1), [0, 1])
        circ_01 = transpile(
            circ, basis_gates=["rz", "sx", "cx"], optimization_level=opt, coupling_map=[[0, 1]]
        )
        circ_10 = transpile(
            circ, basis_gates=["rz", "sx", "cx"], optimization_level=opt, coupling_map=[[1, 0]]
        )
        circ_01_index = {qubit: index for index, qubit in enumerate(circ_01.qubits)}
        circ_10_index = {qubit: index for index, qubit in enumerate(circ_10.qubits)}

        self.assertTrue(
            all(
                (
                    (1, 0) == (circ_10_index[instr.qubits[0]], circ_10_index[instr.qubits[1]])
                    for instr in circ_10.get_instructions("cx")
                )
            )
        )
        self.assertTrue(
            all(
                (
                    (0, 1) == (circ_01_index[instr.qubits[0]], circ_01_index[instr.qubits[1]])
                    for instr in circ_01.get_instructions("cx")
                )
            )
        )

    def test_if_simple(self):
        """Test a simple if statement."""
        basis_gates = {"u", "cx"}
        qr = QuantumRegister(2)
        cr = ClassicalRegister(2)

        qc_uni = QuantumCircuit(2)
        qc_uni.h(0)
        qc_uni.cx(0, 1)
        qc_uni_mat = Operator(qc_uni)

        qc_true_body = QuantumCircuit(2)
        qc_true_body.unitary(qc_uni_mat, [0, 1])

        qc = QuantumCircuit(qr, cr)
        qc.if_test((cr, 1), qc_true_body, [0, 1], [])
        dag = circuit_to_dag(qc)
        cdag = UnitarySynthesis(basis_gates=basis_gates).run(dag)
        cqc = dag_to_circuit(cdag)
        cbody = cqc.data[0].operation.params[0]
        self.assertEqual(cbody.count_ops().keys(), basis_gates)
        self.assertEqual(qc_uni_mat, Operator(cbody))

    def test_nested_control_flow(self):
        """Test unrolling nested control flow blocks."""
        qr = QuantumRegister(2)
        cr = ClassicalRegister(1)
        qc_uni1 = QuantumCircuit(2)
        qc_uni1.swap(0, 1)
        qc_uni1_mat = Operator(qc_uni1)

        qc = QuantumCircuit(qr, cr)
        with qc.for_loop(range(3)):
            with qc.while_loop((cr, 0)):
                qc.unitary(qc_uni1_mat, [0, 1])
        dag = circuit_to_dag(qc)
        cdag = UnitarySynthesis(basis_gates=["u", "cx"]).run(dag)
        cqc = dag_to_circuit(cdag)
        cbody = cqc.data[0].operation.params[2].data[0].operation.params[0]
        self.assertEqual(cbody.count_ops().keys(), {"u", "cx"})
        self.assertEqual(qc_uni1_mat, Operator(cbody))

    def test_default_does_not_fail_on_no_syntheses(self):
        qc = QuantumCircuit(1)
        qc.unitary(np.eye(2), [0])
        pass_ = UnitarySynthesis(["unknown", "gates"])
        self.assertEqual(qc, pass_(qc))


@ddt
class TestUnitarySynthesisTarget(QiskitTestCase):
    """Test UnitarySynthesis pass with target/BackendV2."""

    @combine(
        opt_level=[0, 1, 2, 3],
        bidirectional=[True, False],
        dsc=(
            "test natural_direction works with transpile using opt_level {opt_level} on"
            " target with multiple 2q gates with bidirectional={bidirectional}"
        ),
        name="opt_level_{opt_level}_bidirectional_{bidirectional}",
    )
    def test_coupling_map_transpile_with_backendv2(self, opt_level, bidirectional):
        backend = FakeBackend5QV2(bidirectional)
        qr = QuantumRegister(2)
        circ = QuantumCircuit(qr)
        circ.append(random_unitary(4, seed=1), [0, 1])
        circ_01 = transpile(
            circ, backend=backend, optimization_level=opt_level, layout_method="trivial"
        )
        circ_01_index = {qubit: index for index, qubit in enumerate(circ_01.qubits)}
        self.assertGreaterEqual(len(circ_01.get_instructions("cx")), 1)
        for instr in circ_01.get_instructions("cx"):
            self.assertEqual(
                (0, 1), (circ_01_index[instr.qubits[0]], circ_01_index[instr.qubits[1]])
            )

    @combine(
        opt_level=[0, 1, 2, 3],
        bidirectional=[True, False],
        dsc=(
            "Test direction with transpile using opt_level {opt_level} on"
            " target with multiple 2q gates with bidirectional={bidirectional}"
            "direction [0, 1] is lower error and should be picked."
        ),
        name="opt_level_{opt_level}_bidirectional_{bidirectional}",
    )
    def test_coupling_unequal_duration_with_backendv2(self, opt_level, bidirectional):
        qr = QuantumRegister(2)
        circ = QuantumCircuit(qr)
        circ.append(random_unitary(4, seed=1), [1, 0])
        backend = FakeBackend5QV2(bidirectional)
        tqc = transpile(
            circ,
            backend=backend,
            optimization_level=opt_level,
            translation_method="synthesis",
            layout_method="trivial",
        )
        tqc_index = {qubit: index for index, qubit in enumerate(tqc.qubits)}
        self.assertGreaterEqual(len(tqc.get_instructions("cx")), 1)
        for instr in tqc.get_instructions("cx"):
            self.assertEqual((0, 1), (tqc_index[instr.qubits[0]], tqc_index[instr.qubits[1]]))

    @combine(
        opt_level=[0, 1, 2, 3],
        dsc=(
            "Test direction with transpile using opt_level {opt_level} on"
            " target with multiple 2q gates"
        ),
        name="opt_level_{opt_level}",
    )
    def test_non_overlapping_kak_gates_with_backendv2(self, opt_level):
        qr = QuantumRegister(2)
        circ = QuantumCircuit(qr)
        circ.append(random_unitary(4, seed=1), [1, 0])
        backend = FakeBackend2QV2()
        tqc = transpile(
            circ,
            backend=backend,
            optimization_level=opt_level,
            translation_method="synthesis",
            layout_method="trivial",
        )
        tqc_index = {qubit: index for index, qubit in enumerate(tqc.qubits)}
        self.assertGreaterEqual(len(tqc.get_instructions("ecr")), 1)
        for instr in tqc.get_instructions("ecr"):
            self.assertEqual((1, 0), (tqc_index[instr.qubits[0]], tqc_index[instr.qubits[1]]))

    def test_fractional_cx_with_backendv2(self):
        """Test fractional CX gets used if present in target."""
        qr = QuantumRegister(2)
        circ = QuantumCircuit(qr)
        circ.append(random_unitary(4, seed=1), [0, 1])
        backend = FakeMumbaiFractionalCX()
        synth_pass = UnitarySynthesis(target=backend.target)
        tqc = synth_pass(circ)
        tqc_index = {qubit: index for index, qubit in enumerate(tqc.qubits)}
        self.assertGreaterEqual(len(tqc.get_instructions("rzx")), 1)
        for instr in tqc.get_instructions("rzx"):
            self.assertEqual((0, 1), (tqc_index[instr.qubits[0]], tqc_index[instr.qubits[1]]))

    @combine(
        opt_level=[0, 1, 2, 3],
        dsc=(
            "Test direction with transpile using opt_level {opt_level} on"
            "target with multiple 2q gates available in reverse direction"
        ),
        name="opt_level_{opt_level}",
    )
    def test_reverse_direction(self, opt_level):
        target = Target(2)
        target.add_instruction(CXGate(), {(0, 1): InstructionProperties(error=1.2e-6)})
        target.add_instruction(ECRGate(), {(0, 1): InstructionProperties(error=1.2e-7)})
        target.add_instruction(
            UGate(Parameter("theta"), Parameter("phi"), Parameter("lam")), {(0,): None, (1,): None}
        )
        qr = QuantumRegister(2)
        circ = QuantumCircuit(qr)
        circ.append(random_unitary(4, seed=1), [1, 0])
        tqc = transpile(
            circ,
            target=target,
            optimization_level=opt_level,
            translation_method="synthesis",
            layout_method="trivial",
        )
        tqc_index = {qubit: index for index, qubit in enumerate(tqc.qubits)}
        self.assertGreaterEqual(len(tqc.get_instructions("ecr")), 1)
        for instr in tqc.get_instructions("ecr"):
            self.assertEqual((0, 1), (tqc_index[instr.qubits[0]], tqc_index[instr.qubits[1]]))

    @combine(
        opt_level=[0, 1, 2, 3],
        dsc=("Test controlled but not supercontrolled basis"),
        name="opt_level_{opt_level}",
    )
    def test_controlled_basis(self, opt_level):
        target = Target(2)
        target.add_instruction(RYYGate(np.pi / 8), {(0, 1): InstructionProperties(error=1.2e-6)})
        target.add_instruction(
            UGate(Parameter("theta"), Parameter("phi"), Parameter("lam")), {(0,): None, (1,): None}
        )
        qr = QuantumRegister(2)
        circ = QuantumCircuit(qr)
        circ.append(random_unitary(4, seed=1), [1, 0])
        tqc = transpile(
            circ,
            target=target,
            optimization_level=opt_level,
            translation_method="synthesis",
            layout_method="trivial",
        )
        self.assertGreaterEqual(len(tqc.get_instructions("ryy")), 1)
        self.assertEqual(Operator(tqc), Operator(circ))

    def test_approximation_controlled(self):
        target = Target(2)
        target.add_instruction(RZZGate(np.pi / 10), {(0, 1): InstructionProperties(error=0.006)})
        target.add_instruction(RXXGate(np.pi / 3), {(0, 1): InstructionProperties(error=0.01)})
        target.add_instruction(
            UGate(Parameter("theta"), Parameter("phi"), Parameter("lam")),
            {(0,): InstructionProperties(error=0.001), (1,): InstructionProperties(error=0.002)},
        )
        circ = QuantumCircuit(2)
        circ.append(random_unitary(4, seed=7), [1, 0])

        dag = circuit_to_dag(circ)
        dag_100 = UnitarySynthesis(target=target, approximation_degree=1.0).run(dag)
        dag_99 = UnitarySynthesis(target=target, approximation_degree=0.99).run(dag)
        self.assertGreaterEqual(dag_100.depth(), dag_99.depth())
        self.assertEqual(Operator(dag_to_circuit(dag_100)), Operator(circ))

    def test_mapping_control_flow(self):
        """Test that inner dags use proper qubit mapping."""
        qr = QuantumRegister(3, "q")
        qc = QuantumCircuit(qr)

        # Create target that supports CX only between 0 and 2.
        fake_target = Target()
        fake_target.add_instruction(CXGate(), {(0, 2): None})
        fake_target.add_instruction(
            UGate(Parameter("t"), Parameter("p"), Parameter("l")),
            {
                (0,): None,
                (1,): None,
                (2,): None,
            },
        )

        qc_uni1 = QuantumCircuit(2)
        qc_uni1.swap(0, 1)
        qc_uni1_mat = Operator(qc_uni1)

        loop_body = QuantumCircuit(2)
        loop_body.unitary(qc_uni1_mat, [0, 1])

        # Loop body uses qubits 0 and 2, mapped to 0 and 1 in the block.
        # If synthesis doesn't handle recursive mapping, it'll incorrectly
        # look for a CX on (0, 1) instead of on (0, 2).
        qc.for_loop((0,), None, loop_body, [0, 2], [])

        dag = circuit_to_dag(qc)
        UnitarySynthesis(basis_gates=["u", "cx"], target=fake_target).run(dag)

    def test_single_qubit_with_target(self):
        """Test input circuit with only 1q works with target."""
        qc = QuantumCircuit(1)
        qc.append(ZGate(), [qc.qubits[0]])
        dag = circuit_to_dag(qc)
        backend = GenericBackendV2(num_qubits=5, seed=42)
        unitary_synth_pass = UnitarySynthesis(target=backend.target)
        result_dag = unitary_synth_pass.run(dag)
        result_qc = dag_to_circuit(result_dag)
        self.assertEqual(qc, result_qc)

    def test_single_qubit_identity_with_target(self):
        """Test input single qubit identity works with target."""
        qc = QuantumCircuit(1)
        qc.unitary([[1.0, 0.0], [0.0, 1.0]], 0)
        dag = circuit_to_dag(qc)
        backend = GenericBackendV2(num_qubits=5)
        unitary_synth_pass = UnitarySynthesis(target=backend.target)
        result_dag = unitary_synth_pass.run(dag)
        result_qc = dag_to_circuit(result_dag)
        self.assertEqual(result_qc, QuantumCircuit(1))

    def test_unitary_synthesis_with_ideal_and_variable_width_ops(self):
        """Test unitary synthesis works with a target that contains ideal and variadic ops."""
        qc = QuantumCircuit(2)
        qc.unitary(np.eye(4), [0, 1])
        dag = circuit_to_dag(qc)
        target = GenericBackendV2(num_qubits=5).target
        target.add_instruction(IfElseOp, name="if_else")
        target.add_instruction(ZGate())
        target.add_instruction(ECRGate())
        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))

    def test_unitary_synthesis_custom_gate_target(self):
        qc = QuantumCircuit(2)
        qc.unitary(np.eye(4), [0, 1])
        dag = circuit_to_dag(qc)

        class CustomGate(Gate):
            """Custom Opaque Gate"""

            def __init__(self):
                super().__init__("custom", 2, [])

        target = Target(num_qubits=2)
        target.add_instruction(
            UGate(Parameter("t"), Parameter("p"), Parameter("l")), {(0,): None, (1,): None}
        )
        target.add_instruction(CustomGate(), {(0, 1): None, (1, 0): None})
        unitary_synth_pass = UnitarySynthesis(target=target)
        result_dag = unitary_synth_pass.run(dag)
        result_qc = dag_to_circuit(result_dag)
        self.assertEqual(result_qc, 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))

    @combine(is_random=[True, False], param_gate=[RXXGate, RZZGate, CPhaseGate])
    def test_parameterized_basis_gate_in_target(self, is_random, param_gate):
        """Test synthesis with parameterized RZZ/RXX gate."""
        theta = Parameter("θ")
        lam = Parameter("λ")
        phi = Parameter("ϕ")
        target = Target(num_qubits=2)
        target.add_instruction(RZGate(lam))
        target.add_instruction(RXGate(phi))
        target.add_instruction(param_gate(theta))
        qc = QuantumCircuit(2)
        if is_random:
            qc.unitary(random_unitary(4, seed=1234), [0, 1])
        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", param_gate(theta).name}))
        self.assertTrue(np.allclose(Operator(qc_transpiled), Operator(qc)))

    def test_custom_parameterized_gate_in_target(self):
        """Test synthesis with custom parameterized gate in target."""

        class CustomXXGate(RXXGate):
            """Custom RXXGate subclass that's not a standard gate"""

            _standard_gate = None

            def __init__(self, theta, label=None):
                super().__init__(theta, label)
                self.name = "MyCustomXXGate"

        theta = Parameter("θ")
        lam = Parameter("λ")
        phi = Parameter("ϕ")

        target = Target(num_qubits=2)
        target.add_instruction(RZGate(lam))
        target.add_instruction(RXGate(phi))
        target.add_instruction(CustomXXGate(theta))

        qc = QuantumCircuit(2)
        qc.unitary(random_unitary(4, seed=1234), [0, 1])
        qc_transpiled = UnitarySynthesis(target=target)(qc)
        opcount = qc_transpiled.count_ops()
        self.assertTrue(set(opcount).issubset({"rz", "rx", "MyCustomXXGate"}))

        self.assertTrue(np.allclose(Operator(qc_transpiled), Operator(qc)))

    def test_custom_parameterized_gate_in_target_skips(self):
        """Test that synthesis is skipped with custom parameterized
        gate in target that is not RXX equivalent."""

        class CustomXYGate(Gate):
            """Custom Gate subclass that's not a standard gate and not RXX equivalent"""

            _standard_gate = None

            def __init__(self, theta: ParameterValueType, label=None):
                """Create new custom rotstion XY gate."""
                super().__init__("MyCustomXYGate", 2, [theta])

            def __array__(self, dtype=None):
                """Return a Numpy.array for the custom gate."""
                theta = self.params[0]
                cos = math.cos(theta)
                isin = 1j * math.sin(theta)
                return np.array(
                    [[1, 0, 0, 0], [0, cos, -isin, 0], [0, -isin, cos, 0], [0, 0, 0, 1]],
                    dtype=dtype,
                )

            def inverse(self, annotated: bool = False):
                return CustomXYGate(-self.params[0])

        theta = Parameter("θ")
        lam = Parameter("λ")
        phi = Parameter("ϕ")

        target = Target(num_qubits=2)
        target.add_instruction(RZGate(lam))
        target.add_instruction(RXGate(phi))
        target.add_instruction(CustomXYGate(theta))

        qc = QuantumCircuit(2)
        qc.unitary(random_unitary(4, seed=1234), [0, 1])
        qc_transpiled = UnitarySynthesis(target=target)(qc)
        opcount = qc_transpiled.count_ops()
        self.assertTrue(set(opcount).issubset({"unitary"}))
        self.assertTrue(np.allclose(Operator(qc_transpiled), Operator(qc)))

    @data(
        ["rx", "ry", "rxx"],
        ["rx", "rz", "rzz"],
    )
    def test_parameterized_backend(self, basis_gates):
        """Test synthesis with parameterized backend."""
        backend = GenericBackendV2(3, basis_gates=basis_gates, seed=0)
        qc = QuantumCircuit(3)
        qc.unitary(random_unitary(4, seed=1234), [0, 1])
        qc.unitary(random_unitary(4, seed=4321), [0, 2])
        qc.cp(np.pi / 2, 0, 1)
        qc_transpiled = transpile(qc, backend, optimization_level=3, seed_transpiler=42)
        opcount = qc_transpiled.count_ops()
        self.assertTrue(set(opcount).issubset(basis_gates))
        self.assertTrue(np.allclose(Operator(qc_transpiled), Operator(qc)))

    @data(1, 2, 3)
    def test_qsd(self, opt):
        """Test that the unitary synthesis pass runs qsd successfully with a target."""
        num_qubits = 3
        target = Target(num_qubits=num_qubits)
        target.add_instruction(UGate(Parameter("theta"), Parameter("phi"), Parameter("lam")))
        target.add_instruction(CXGate())
        mat = scipy.stats.ortho_group.rvs(2**num_qubits)
        qc = qs_decomposition(mat, opt_a1=True, opt_a2=False)
        qc_transpiled = transpile(qc, target=target, optimization_level=opt)
        self.assertTrue(np.allclose(mat, Operator(qc_transpiled).data))

    def test_3q_with_measure(self):
        """Test 3-qubit synthesis with measurements."""
        backend = FakeBackend5QV2()

        qc = QuantumCircuit(3, 1)
        qc.unitary(np.eye(2**3), range(3))
        qc.measure(0, 0)

        qc_transpiled = transpile(qc, backend)
        self.assertTrue(qc_transpiled.size, 1)
        self.assertTrue(qc_transpiled.count_ops().get("measure", 0), 1)

    def test_3q_series(self):
        """Test a series of 3-qubit blocks."""
        backend = GenericBackendV2(5, basis_gates=["u", "cx"], seed=1)

        x = QuantumCircuit(3)
        x.x(2)
        x_mat = Operator(x)

        qc = QuantumCircuit(3)
        qc.unitary(x_mat, range(3))
        qc.unitary(np.eye(2**3), range(3))

        tqc = transpile(qc, backend, optimization_level=0, initial_layout=[0, 1, 2])

        expected = np.kron(np.eye(2**2), x_mat)
        self.assertEqual(Operator(tqc), Operator(expected))

    def test_3q_measure_all(self):
        """Regression test of #13586."""
        hamiltonian = SparsePauliOp.from_list(
            [("IXX", 1), ("IYY", 1), ("IZZ", 1), ("XXI", 1), ("YYI", 1), ("ZZI", 1)]
        )

        qc = QuantumCircuit(3)
        qc.x([1, 2])
        op = PauliEvolutionGate(hamiltonian, time=1)
        qc.append(op.power(8), [0, 1, 2])
        qc.measure_all()

        backend = GenericBackendV2(5, basis_gates=["u", "cx"], seed=1)
        tqc = transpile(qc, backend)

        ops = tqc.count_ops()
        self.assertIn("u", ops)
        self.assertIn("cx", ops)
        self.assertIn("measure", ops)

    def test_target_with_global_gates(self):
        """Test that 2q decomposition can handle a target with global gates."""
        basis_gates = ["h", "p", "cp", "rz", "cx", "ccx", "swap"]
        target = Target.from_configuration(basis_gates=basis_gates)

        bell = QuantumCircuit(2)
        bell.h(0)
        bell.cx(0, 1)
        bell_op = Operator(bell)
        qc = QuantumCircuit(2)
        qc.unitary(bell_op, [0, 1])

        tqc = transpile(qc, target=target)
        self.assertTrue(set(tqc.count_ops()).issubset(basis_gates))

    def test_determinism(self):
        """Test that the decomposition is deterministic."""
        gate_counts = {"rx": 6, "rz": 12, "iswap": 2}
        basis_gates = ["rx", "rz", "iswap"]
        target = Target.from_configuration(basis_gates=basis_gates)
        pm = generate_preset_pass_manager(target=target, optimization_level=2, seed_transpiler=42)

        qc = QuantumCircuit(2)
        qc.h(0)
        qc.cx(0, 1)

        for _ in range(10):
            out = pm.run(qc)
            self.assertTrue(Operator(out).equiv(qc))
            self.assertTrue(set(out.count_ops()).issubset(basis_gates))
            for basis_gate in basis_gates:
                self.assertLessEqual(out.count_ops()[basis_gate], gate_counts[basis_gate])

    @combine(gate=["unitary", "swap"], natural_direction=[True, False])
    def test_two_qubit_synthesis_to_directional_cx_target(self, gate, natural_direction):
        """Verify two qubit unitaries are synthesized to match basis gates."""
        # TODO: should make check more explicit e.g. explicitly set gate
        # direction in test instead of using specific fake backend
        backend = GenericBackendV2(
            num_qubits=5,
            basis_gates=["id", "rz", "sx", "x", "cx", "reset"],
            coupling_map=YORKTOWN_CMAP,
            seed=1,
        )
        coupling_map = CouplingMap(backend.coupling_map)
        triv_layout_pass = TrivialLayout(coupling_map)

        qr = QuantumRegister(2)
        qc = QuantumCircuit(qr)
        if gate == "unitary":
            qc.unitary(random_unitary(4, seed=12), [0, 1])
        elif gate == "swap":
            qc.swap(qr[0], qr[1])

        unisynth_pass = UnitarySynthesis(
            target=backend.target,
            pulse_optimize=True,
            natural_direction=natural_direction,
        )
        pm = PassManager([triv_layout_pass, unisynth_pass])
        qc_out = pm.run(qc)
        self.assertEqual(Operator(qc), Operator(qc_out))

    @data(True, False)
    def test_two_qubit_synthesis_to_directional_cx_multiple_registers_target(
        self, natural_direction
    ):
        """Verify two qubit unitaries are synthesized to match basis gates
        across multiple registers."""
        # TODO: should make check more explicit e.g. explicitly set gate
        # direction in test instead of using specific fake backend
        backend = GenericBackendV2(
            num_qubits=5,
            basis_gates=["id", "rz", "sx", "x", "cx", "reset"],
            coupling_map=YORKTOWN_CMAP,
            seed=1,
        )
        qr0 = QuantumRegister(1)
        qr1 = QuantumRegister(1)
        coupling_map = CouplingMap(backend.coupling_map)
        triv_layout_pass = TrivialLayout(coupling_map)
        qc = QuantumCircuit(qr0, qr1)
        qc.unitary(random_unitary(4, seed=12), [qr0[0], qr1[0]])
        unisynth_pass = UnitarySynthesis(
            target=backend.target,
            pulse_optimize=True,
            natural_direction=natural_direction,
        )
        pm = PassManager([triv_layout_pass, unisynth_pass])
        qc_out = pm.run(qc)
        self.assertEqual(Operator(qc), Operator(qc_out))

    def test_two_qubit_natural_direction_true_duration_fallback_target(self):
        """Verify fallback path when pulse_optimize==True."""
        basis_gates = ["id", "rz", "sx", "x", "cx", "reset"]
        qr = QuantumRegister(2)
        coupling_map = CouplingMap([[0, 1], [1, 0], [1, 2], [1, 3], [3, 4]])
        backend = GenericBackendV2(
            num_qubits=5, basis_gates=basis_gates, coupling_map=coupling_map, seed=1
        )

        triv_layout_pass = TrivialLayout(coupling_map)
        qc = QuantumCircuit(qr)
        qc.unitary(random_unitary(4, seed=12), [0, 1])
        unisynth_pass = UnitarySynthesis(
            target=backend.target,
            pulse_optimize=True,
            natural_direction=True,
        )
        pm = PassManager([triv_layout_pass, unisynth_pass])
        qc_out = pm.run(qc)
        self.assertTrue(
            all(((qr[0], qr[1]) == instr.qubits for instr in qc_out.get_instructions("cx")))
        )


if __name__ == "__main__":
    unittest.main()
