# This code is part of Qiskit.
#
# (C) Copyright IBM 2017, 2020.
#
# 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.

"""Test library of n-local circuits."""

import unittest
from test import combine

import numpy as np

from ddt import ddt, data, unpack

from qiskit import transpile
from qiskit.circuit import QuantumCircuit, Parameter, ParameterVector, ParameterExpression, Gate
from qiskit.circuit.library import (
    n_local,
    efficient_su2,
    real_amplitudes,
    excitation_preserving,
    pauli_two_design,
    NLocal,
    TwoLocal,
    RealAmplitudes,
    ExcitationPreserving,
    HGate,
    XGate,
    CRXGate,
    CCXGate,
    SwapGate,
    RXGate,
    RYGate,
    EfficientSU2,
    RZGate,
    RXXGate,
    RYYGate,
    CXGate,
    SXGate,
)
from qiskit.circuit.random.utils import random_circuit
from qiskit.converters.circuit_to_dag import circuit_to_dag
from qiskit.quantum_info import Operator
from qiskit.exceptions import QiskitError

from qiskit._accelerate.circuit_library import get_entangler_map as fast_entangler_map
from qiskit._accelerate.circuit_library import Block

from test import QiskitTestCase  # pylint: disable=wrong-import-order


class Gato(Gate):
    """A custom gate."""

    def __init__(self, x, y):
        super().__init__("meow", 1, [x, y])

    def _define(self):
        x, y = self.params
        self.definition = QuantumCircuit(1)
        self.definition.p(x + y, 0)


@ddt
class TestNLocal(QiskitTestCase):
    """Test the n-local circuit class."""

    def test_if_reps_is_negative(self):
        """Test to check if error is raised for negative value of reps"""
        with self.assertRaises(ValueError):
            _ = NLocal(reps=-1)

    def test_if_reps_is_str(self):
        """Test to check if proper error is raised for str value of reps"""
        with self.assertRaises(TypeError):
            _ = NLocal(reps="3")

    def test_if_reps_is_float(self):
        """Test to check if proper error is raised for float value of reps"""
        with self.assertRaises(TypeError):
            _ = NLocal(reps=5.6)

    def test_if_reps_is_npint32(self):
        """Equality test for reps with int value and np.int32 value"""
        self.assertEqual(NLocal(reps=3), NLocal(reps=np.int32(3)))

    def test_if_reps_is_npint64(self):
        """Equality test for reps with int value and np.int64 value"""
        self.assertEqual(NLocal(reps=3), NLocal(reps=np.int64(3)))

    def test_reps_setter_when_negative(self):
        """Test to check if setter raises error for reps < 0"""
        nlocal = NLocal(reps=1)
        with self.assertRaises(ValueError):
            nlocal.reps = -1

    def assertCircuitEqual(self, qc1, qc2, visual=False, transpiled=True):
        """An equality test specialized to circuits."""
        if transpiled:
            basis_gates = ["id", "u1", "u3", "cx"]
            qc1_transpiled = transpile(qc1, basis_gates=basis_gates, optimization_level=0)
            qc2_transpiled = transpile(qc2, basis_gates=basis_gates, optimization_level=0)
            qc1, qc2 = qc1_transpiled, qc2_transpiled

        if visual:
            self.assertEqual(qc1.draw(), qc2.draw())
        else:
            self.assertEqual(qc1, qc2)

    def test_empty_nlocal(self):
        """Test the creation of an empty NLocal."""
        nlocal = NLocal()
        self.assertEqual(nlocal.num_qubits, 0)
        self.assertEqual(nlocal.num_parameters_settable, 0)
        self.assertEqual(nlocal.reps, 1)

        self.assertEqual(nlocal, QuantumCircuit())

        for attribute in [nlocal.rotation_blocks, nlocal.entanglement_blocks]:
            self.assertEqual(len(attribute), 0)

    @data(
        (XGate(), [[0], [2], [1]]),
        (XGate(), [[0]]),
        (CRXGate(-0.2), [[2, 0], [1, 3]]),
    )
    @unpack
    def test_add_layer_to_empty_nlocal(self, block, entangler_map):
        """Test appending gates to an empty nlocal."""
        nlocal = NLocal()
        nlocal.add_layer(block, entangler_map)

        max_num_qubits = max(max(indices) for indices in entangler_map)
        reference = QuantumCircuit(max_num_qubits + 1)
        for indices in entangler_map:
            reference.append(block, indices)

        self.assertCircuitEqual(nlocal, reference)

    @data([5, 3], [1, 5], [1, 1], [1, 2, 3, 10])
    def test_append_circuit(self, num_qubits):
        """Test appending circuits to an nlocal works normally."""
        # fixed depth of 3 gates per circuit
        depth = 3

        # keep track of a reference circuit
        reference = QuantumCircuit(max(num_qubits))

        # construct the NLocal from the first circuit
        first_circuit = random_circuit(num_qubits[0], depth, seed=4200)
        # TODO Terra bug: if this is to_gate it fails, since the QC adds an instruction not gate
        nlocal = NLocal(max(num_qubits), entanglement_blocks=first_circuit.to_instruction(), reps=1)
        reference.append(first_circuit, list(range(num_qubits[0])))

        # append the rest
        for num in num_qubits[1:]:
            circuit = random_circuit(num, depth, seed=4200)
            nlocal.append(circuit, list(range(num)))
            reference.append(circuit, list(range(num)))

        self.assertCircuitEqual(nlocal, reference)

    @data([5, 3], [1, 5], [1, 1], [1, 2, 3, 10])
    def test_add_nlocal(self, num_qubits):
        """Test adding an nlocal to an nlocal (using add_layer)."""
        # fixed depth of 3 gates per circuit
        depth = 3

        # keep track of a reference circuit
        reference = QuantumCircuit(max(num_qubits))

        # construct the NLocal from the first circuit
        first_circuit = random_circuit(num_qubits[0], depth, seed=4220)
        # TODO Terra bug: if this is to_gate it fails, since the QC adds an instruction not gate
        nlocal = NLocal(max(num_qubits), entanglement_blocks=first_circuit.to_instruction(), reps=1)
        nlocal2 = nlocal.copy()
        _ = nlocal2.data
        reference.append(first_circuit, list(range(num_qubits[0])))

        # append the rest
        for num in num_qubits[1:]:
            circuit = random_circuit(num, depth, seed=4220)
            layer = NLocal(num, entanglement_blocks=circuit, reps=1)
            nlocal.add_layer(layer)
            nlocal2.add_layer(layer)
            reference.append(circuit, list(range(num)))

        self.assertCircuitEqual(nlocal, reference)
        self.assertCircuitEqual(nlocal2, reference)

    @unittest.skip("Feature missing")
    def test_iadd_overload(self):
        """Test the overloaded + operator."""
        num_qubits, depth = 2, 2

        # construct two circuits for adding
        first_circuit = random_circuit(num_qubits, depth, seed=4242)
        circuit = random_circuit(num_qubits, depth, seed=4242)

        # get a reference
        reference = first_circuit + circuit

        # convert the object to be appended to different types
        others = [circuit, circuit.to_instruction(), circuit.to_gate(), NLocal(circuit)]

        # try adding each type
        for other in others:
            nlocal = NLocal(num_qubits, entanglement_blocks=first_circuit, reps=1)
            nlocal += other
            with self.subTest(msg=f"type: {type(other)}"):
                self.assertCircuitEqual(nlocal, reference)

    def test_parameter_getter_from_automatic_repetition(self):
        """Test getting and setting of the nlocal parameters."""
        circuit = QuantumCircuit(2)
        circuit.ry(Parameter("a"), 0)
        circuit.crx(Parameter("b"), 0, 1)

        # repeat circuit and check that parameters are duplicated
        reps = 3
        nlocal = NLocal(2, entanglement_blocks=circuit, reps=reps)
        self.assertTrue(nlocal.num_parameters, 6)
        self.assertTrue(len(nlocal.parameters), 6)

    @data(list(range(6)), ParameterVector("θ", length=6), [0, 1, Parameter("theta"), 3, 4, 5])
    def test_parameter_setter_from_automatic_repetition(self, params):
        """Test getting and setting of the nlocal parameters."""
        circuit = QuantumCircuit(2)
        circuit.ry(Parameter("a"), 0)
        circuit.crx(Parameter("b"), 0, 1)

        # repeat circuit and check that parameters are duplicated
        reps = 3
        nlocal = NLocal(2, entanglement_blocks=circuit, reps=reps)
        nlocal.assign_parameters(params, inplace=True)

        param_set = {p for p in params if isinstance(p, ParameterExpression)}
        with self.subTest(msg="Test the parameters of the non-transpiled circuit"):
            # check the parameters of the final circuit
            self.assertEqual(nlocal.parameters, param_set)

        with self.subTest(msg="Test the parameters of the transpiled circuit"):
            basis_gates = ["id", "u1", "u2", "u3", "cx"]
            transpiled_circuit = transpile(nlocal, basis_gates=basis_gates)
            self.assertEqual(transpiled_circuit.parameters, param_set)

    @data(list(range(6)), ParameterVector("θ", length=6), [0, 1, Parameter("theta"), 3, 4, 5])
    def test_parameters_setter(self, params):
        """Test setting the parameters via list."""
        # construct circuit with some parameters
        initial_params = ParameterVector("p", length=6)
        circuit = QuantumCircuit(1)
        for i, initial_param in enumerate(initial_params):
            circuit.ry(i * initial_param, 0)

        # create an NLocal from the circuit and set the new parameters
        nlocal = NLocal(1, entanglement_blocks=circuit, reps=1)
        nlocal.assign_parameters(params, inplace=True)

        param_set = {p for p in params if isinstance(p, ParameterExpression)}
        with self.subTest(msg="Test the parameters of the non-transpiled circuit"):
            # check the parameters of the final circuit
            self.assertEqual(nlocal.parameters, param_set)

        with self.subTest(msg="Test the parameters of the transpiled circuit"):
            basis_gates = ["id", "u1", "u2", "u3", "cx"]
            transpiled_circuit = transpile(nlocal, basis_gates=basis_gates)
            self.assertEqual(transpiled_circuit.parameters, param_set)

    def test_repetetive_parameter_setting(self):
        """Test alternate setting of parameters and circuit construction."""
        x = Parameter("x")
        circuit = QuantumCircuit(1)
        circuit.rx(x, 0)

        nlocal = NLocal(1, entanglement_blocks=circuit, reps=3, insert_barriers=True)
        with self.subTest(msg="immediately after initialization"):
            self.assertEqual(len(nlocal.parameters), 3)

        with self.subTest(msg="after circuit construction"):
            self.assertEqual(len(nlocal.parameters), 3)

        q = Parameter("q")
        nlocal.assign_parameters([x, q, q], inplace=True)
        with self.subTest(msg="setting parameter to Parameter objects"):
            self.assertEqual(nlocal.parameters, set({x, q}))

        nlocal.assign_parameters([0, -1], inplace=True)
        with self.subTest(msg="setting parameter to numbers"):
            self.assertEqual(nlocal.parameters, set())

    def test_skip_unentangled_qubits(self):
        """Test skipping the unentangled qubits."""
        num_qubits = 6
        entanglement_1 = [[0, 1, 3], [1, 3, 5], [0, 1, 5]]
        skipped_1 = [2, 4]

        entanglement_2 = [entanglement_1, [[0, 1, 2], [2, 3, 5]]]
        skipped_2 = [4]

        for entanglement, skipped in zip([entanglement_1, entanglement_2], [skipped_1, skipped_2]):
            with self.subTest(entanglement=entanglement, skipped=skipped):
                nlocal = NLocal(
                    num_qubits,
                    rotation_blocks=XGate(),
                    entanglement_blocks=CCXGate(),
                    entanglement=entanglement,
                    reps=3,
                    skip_unentangled_qubits=True,
                )
                decomposed = nlocal.decompose()

                skipped_set = {decomposed.qubits[i] for i in skipped}
                dag = circuit_to_dag(decomposed)
                idle = set(dag.idle_wires())
                self.assertEqual(skipped_set, idle)

    @data(
        "linear",
        "full",
        "circular",
        "sca",
        "reverse_linear",
        ["linear", "full"],
        ["reverse_linear", "full"],
        ["circular", "linear", "sca"],
    )
    def test_entanglement_by_str(self, entanglement):
        """Test setting the entanglement of the layers by str."""
        reps = 3
        nlocal = NLocal(
            5,
            rotation_blocks=XGate(),
            entanglement_blocks=CCXGate(),
            entanglement=entanglement,
            reps=reps,
        )

        def get_expected_entangler_map(rep_num, mode):
            if mode == "linear":
                return [(0, 1, 2), (1, 2, 3), (2, 3, 4)]
            elif mode == "reverse_linear":
                return [(2, 3, 4), (1, 2, 3), (0, 1, 2)]
            elif mode == "full":
                return [
                    (0, 1, 2),
                    (0, 1, 3),
                    (0, 1, 4),
                    (0, 2, 3),
                    (0, 2, 4),
                    (0, 3, 4),
                    (1, 2, 3),
                    (1, 2, 4),
                    (1, 3, 4),
                    (2, 3, 4),
                ]
            else:
                circular = [(3, 4, 0), (4, 0, 1), (0, 1, 2), (1, 2, 3), (2, 3, 4)]
                if mode == "circular":
                    return circular
                sca = circular[-rep_num:] + circular[:-rep_num]
                if rep_num % 2 == 1:
                    sca = [tuple(reversed(indices)) for indices in sca]
                return sca

        for rep_num in range(reps):
            entangler_map = nlocal.get_entangler_map(rep_num, 0, 3)
            if isinstance(entanglement, list):
                mode = entanglement[rep_num % len(entanglement)]
            else:
                mode = entanglement
            expected = get_expected_entangler_map(rep_num, mode)

            with self.subTest(rep_num=rep_num):
                # using a set here since the order does not matter
                self.assertEqual(entangler_map, expected)

    def test_pairwise_entanglement(self):
        """Test pairwise entanglement."""
        nlocal = NLocal(
            5,
            rotation_blocks=XGate(),
            entanglement_blocks=CXGate(),
            entanglement="pairwise",
            reps=1,
        )
        entangler_map = nlocal.get_entangler_map(0, 0, 2)
        pairwise = [(0, 1), (2, 3), (1, 2), (3, 4)]

        self.assertEqual(pairwise, entangler_map)

    def test_pairwise_entanglement_raises(self):
        """Test choosing pairwise entanglement raises an error for too large blocks."""
        nlocal = NLocal(3, XGate(), CCXGate(), entanglement="pairwise", reps=1)

        # pairwise entanglement is only defined if the entangling gate has 2 qubits
        with self.assertRaises(ValueError):
            _ = str(nlocal.draw())

    def test_entanglement_by_list(self):
        """Test setting the entanglement by list.

        This is the circuit we test (times 2, with final X layer)
                ┌───┐                ┌───┐┌───┐                  ┌───┐
        q_0: |0>┤ X ├──■────■───X────┤ X ├┤ X ├──■───X─────── .. ┤ X ├
                ├───┤  │    │   │    ├───┤└─┬─┘  │   │           ├───┤
        q_1: |0>┤ X ├──■────┼───┼──X─┤ X ├──■────┼───X──X──── .. ┤ X ├
                ├───┤┌─┴─┐  │   │  │ ├───┤  │    │      │     x2 ├───┤
        q_2: |0>┤ X ├┤ X ├──■───┼──X─┤ X ├──■────■──────X──X─ .. ┤ X ├
                ├───┤└───┘┌─┴─┐ │    ├───┤     ┌─┴─┐       │     ├───┤
        q_3: |0>┤ X ├─────┤ X ├─X────┤ X ├─────┤ X ├───────X─ .. ┤ X ├
                └───┘     └───┘      └───┘     └───┘             └───┘
        """
        circuit = QuantumCircuit(4)
        for _ in range(2):
            circuit.x([0, 1, 2, 3])
            circuit.barrier()
            circuit.ccx(0, 1, 2)
            circuit.ccx(0, 2, 3)
            circuit.swap(0, 3)
            circuit.swap(1, 2)
            circuit.barrier()
            circuit.x([0, 1, 2, 3])
            circuit.barrier()
            circuit.ccx(2, 1, 0)
            circuit.ccx(0, 2, 3)
            circuit.swap(0, 1)
            circuit.swap(1, 2)
            circuit.swap(2, 3)
            circuit.barrier()
        circuit.x([0, 1, 2, 3])

        layer_1_ccx = [(0, 1, 2), (0, 2, 3)]
        layer_1_swap = [(0, 3), (1, 2)]
        layer_1 = [layer_1_ccx, layer_1_swap]

        layer_2_ccx = [(2, 1, 0), (0, 2, 3)]
        layer_2_swap = [(0, 1), (1, 2), (2, 3)]
        layer_2 = [layer_2_ccx, layer_2_swap]

        entanglement = [layer_1, layer_2]

        nlocal = NLocal(
            4,
            rotation_blocks=XGate(),
            entanglement_blocks=[CCXGate(), SwapGate()],
            reps=4,
            entanglement=entanglement,
            insert_barriers=True,
        )

        self.assertCircuitEqual(nlocal, circuit)

    def test_initial_state_as_circuit_object(self):
        """Test setting `initial_state` to `QuantumCircuit` object"""
        #           ┌───┐          ┌───┐
        # q_0: ──■──┤ X ├───────■──┤ X ├
        #      ┌─┴─┐├───┤┌───┐┌─┴─┐├───┤
        # q_1: ┤ X ├┤ H ├┤ X ├┤ X ├┤ X ├
        #      └───┘└───┘└───┘└───┘└───┘
        ref = QuantumCircuit(2)
        ref.cx(0, 1)
        ref.x(0)
        ref.h(1)
        ref.x(1)
        ref.cx(0, 1)
        ref.x(0)
        ref.x(1)

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

        expected = NLocal(
            num_qubits=2,
            rotation_blocks=XGate(),
            entanglement_blocks=CXGate(),
            initial_state=qc,
            reps=1,
        )

        self.assertCircuitEqual(ref, expected)

    def test_inplace_assignment_with_cache(self):
        """Test parameters are correctly re-bound in the cached gates.

        This test requires building with the Rust feature "cache_pygates" enabled, otherwise
        it does not test what it is supposed to.

        Regression test of #13478.
        """
        qc = EfficientSU2(2, flatten=True)
        binds = [1.25] * qc.num_parameters

        qc.assign_parameters(binds, inplace=True)
        bound_op = qc.data[0].operation
        self.assertAlmostEqual(bound_op.params[0], binds[0])


@ddt
class TestNLocalFunction(QiskitTestCase):
    """Test the n_local circuit library function."""

    def test_empty_blocks(self):
        """Test passing no rotation and entanglement blocks."""
        circuit = n_local(2, rotation_blocks=[], entanglement_blocks=[])
        expected = QuantumCircuit(2)

        self.assertEqual(expected, circuit)

    def test_invalid_custom_block(self):
        """Test constructing a block from callable but not with a callable."""
        my_block = QuantumCircuit(2)
        with self.assertRaises(QiskitError):
            _ = Block.from_callable(2, 0, my_block)

    def test_str_blocks(self):
        """Test passing blocks as strings."""
        circuit = n_local(2, "h", "ecr", reps=2)
        expected = QuantumCircuit(2)
        for _ in range(2):
            expected.h([0, 1])
            expected.ecr(0, 1)
        expected.h([0, 1])

        self.assertEqual(expected, circuit)

    def test_stdgate_blocks(self):
        """Test passing blocks as standard gates."""
        circuit = n_local(2, HGate(), CRXGate(Parameter("x")), reps=2)

        param_iter = iter(circuit.parameters)
        expected = QuantumCircuit(2)
        for _ in range(2):
            expected.h([0, 1])
            expected.crx(next(param_iter), 0, 1)
        expected.h([0, 1])

        self.assertEqual(expected, circuit)

    def test_invalid_str_blocks(self):
        """Test passing blocks as invalid string raises."""
        with self.assertRaises(ValueError):
            _ = n_local(2, "h", "iamnotanexisting2qgateeventhoughiwanttobe")

    def test_gate_blocks(self):
        """Test passing blocks as gates."""
        x = ParameterVector("x", 2)
        my_gate = Gato(*x)

        circuit = n_local(4, my_gate, "cx", "linear", reps=3)

        expected_cats = 4 * (3 + 1)  # num_qubits * (reps + 1)
        expected_cx = 3 * 3  # gates per block * reps
        expected_num_params = expected_cats * 2

        self.assertEqual(expected_cats, circuit.count_ops().get("meow", 0))
        self.assertEqual(expected_cx, circuit.count_ops().get("cx", 0))
        self.assertEqual(expected_num_params, circuit.num_parameters)

    def test_gate_lists(self):
        """Test passing a list of strings and gates."""
        reps = 2
        circuit = n_local(4, [XGate(), "ry", SXGate()], ["ryy", CCXGate()], "full", reps)
        expected_1q = 4 * (reps + 1)  # num_qubits * (reps + 1)
        expected_2q = 4 * 3 / 2 * reps  # 4 choose 2 * reps
        expected_3q = 4 * reps  # 4 choose 3 * reps

        ops = circuit.count_ops()
        for gate in ["x", "ry", "sx"]:
            with self.subTest(gate=gate):
                self.assertEqual(expected_1q, ops.get(gate, 0))

        with self.subTest(gate="ryy"):
            self.assertEqual(expected_2q, ops.get("ryy", 0))

        with self.subTest(gate="ccx"):
            self.assertEqual(expected_3q, ops.get("ccx", 0))

    def test_reps(self):
        """Test setting the repetitions."""
        all_reps = [0, 1, 2, 10]
        for reps in all_reps:
            circuit = n_local(2, rotation_blocks="rx", entanglement_blocks="cz", reps=reps)
            expected_rx = (reps + 1) * 2
            expected_cz = reps

            with self.subTest(reps=reps):
                self.assertEqual(expected_rx, circuit.count_ops().get("rx", 0))
                self.assertEqual(expected_cz, circuit.count_ops().get("cz", 0))

    def test_negative_reps(self):
        """Test negative reps raises."""
        with self.assertRaises(ValueError):
            _ = n_local(1, [], [], reps=-1)

    def test_barrier(self):
        """Test setting barriers."""
        circuit = n_local(2, "ry", "cx", reps=2, insert_barriers=True)
        values = np.ones(circuit.num_parameters)

        expected = QuantumCircuit(2)
        expected.ry(1, [0, 1])
        expected.barrier()
        expected.cx(0, 1)
        expected.barrier()
        expected.ry(1, [0, 1])
        expected.barrier()
        expected.cx(0, 1)
        expected.barrier()
        expected.ry(1, [0, 1])

        self.assertEqual(expected, circuit.assign_parameters(values))

    def test_parameter_prefix(self):
        """Test setting the parameter prefix."""
        circuit = n_local(2, "h", "crx", parameter_prefix="x")
        prefixes = [p.name[0] for p in circuit.parameters]
        self.assertTrue(all(prefix == "x" for prefix in prefixes))

    @data(True, False)
    def test_overwrite_block_parameters(self, overwrite):
        """Test overwriting the block parameters."""
        x = Parameter("x")
        block = QuantumCircuit(2)
        block.rxx(x, 0, 1)

        reps = 3
        circuit = n_local(
            4, [], [block.to_gate()], "linear", reps, overwrite_block_parameters=overwrite
        )

        expected_num_params = reps * 3 if overwrite else 1
        self.assertEqual(expected_num_params, circuit.num_parameters)

    @data(True, False)
    def test_skip_final_rotation_layer(self, skip):
        """Test skipping the final rotation layer."""
        reps = 5
        num_qubits = 2
        circuit = n_local(num_qubits, "rx", "ch", reps=reps, skip_final_rotation_layer=skip)
        expected_rx = num_qubits * (reps + (0 if skip else 1))

        self.assertEqual(expected_rx, circuit.count_ops().get("rx", 0))

    def test_skip_unentangled_qubits(self):
        """Test skipping the unentangled qubits."""
        num_qubits = 6
        entanglement_1 = [[0, 1, 3], [1, 3, 5], [0, 1, 5]]
        skipped_1 = [2, 4]

        def entanglement_2(layer):
            return entanglement_1 if layer % 2 == 0 else [[0, 1, 2], [2, 3, 5]]

        skipped_2 = [4]

        for entanglement, skipped in zip([entanglement_1, entanglement_2], [skipped_1, skipped_2]):
            with self.subTest(entanglement=entanglement, skipped=skipped):
                nlocal = n_local(
                    num_qubits,
                    rotation_blocks=XGate(),
                    entanglement_blocks=CCXGate(),
                    entanglement=entanglement,
                    reps=3,
                    skip_unentangled_qubits=True,
                )

                skipped_set = {nlocal.qubits[i] for i in skipped}
                dag = circuit_to_dag(nlocal)
                idle = set(dag.idle_wires())
                self.assertEqual(skipped_set, idle)

    def test_empty_entanglement(self):
        """Test passing an empty list as entanglement."""
        circuit = n_local(3, "h", "cx", entanglement=[], reps=1)
        self.assertEqual(6, circuit.count_ops().get("h", 0))
        self.assertEqual(0, circuit.count_ops().get("cx", 0))

    def test_entanglement_list_of_str(self):
        """Test different entanglement strings per entanglement block."""
        circuit = n_local(3, [], ["cx", "cz"], entanglement=["reverse_linear", "full"], reps=1)
        self.assertEqual(2, circuit.count_ops().get("cx", 0))
        self.assertEqual(3, circuit.count_ops().get("cz", 0))

    def test_invalid_entanglement_list(self):
        """Test passing an invalid list."""
        with self.assertRaises(TypeError):
            _ = n_local(3, "h", "cx", entanglement=[0, 1])  # should be [(0, 1)]

    def test_mismatching_entanglement_blocks_str(self):
        """Test an error is raised if the number of entanglements does not match the blocks."""
        entanglement = ["full", "linear", "pairwise"]
        blocks = ["ryy", "iswap"]

        with self.assertRaises(QiskitError):
            _ = n_local(3, [], blocks, entanglement=entanglement)

    def test_mismatching_entanglement_blocks_indices(self):
        """Test an error is raised if the number of entanglements does not match the blocks."""
        ent1 = [(0, 1), (1, 2)]
        ent2 = [(0, 2)]
        blocks = ["ryy", "iswap"]

        with self.assertRaises(QiskitError):
            _ = n_local(3, [], blocks, entanglement=[ent1, ent1, ent2])

    def test_mismatching_entanglement_indices(self):
        """Test an error is raised if the entanglement does not match the blocksize."""
        entanglement = [[0, 1], [2]]

        with self.assertRaises(QiskitError):
            _ = n_local(3, "ry", "cx", entanglement)

    def test_entanglement_by_callable(self):
        """Test setting the entanglement by callable.

        This is the circuit we test (times 2, with final X layer)
                ┌───┐           ┌───┐┌───┐          ┌───┐
        q_0: |0>┤ X ├──■────■───┤ X ├┤ X ├──■─── .. ┤ X ├
                ├───┤  │    │   ├───┤└─┬─┘  │       ├───┤
        q_1: |0>┤ X ├──■────┼───┤ X ├──■────┼─── .. ┤ X ├
                ├───┤┌─┴─┐  │   ├───┤  │    │    x2 ├───┤
        q_2: |0>┤ X ├┤ X ├──■───┤ X ├──■────■─── .. ┤ X ├
                ├───┤└───┘┌─┴─┐ ├───┤     ┌─┴─┐     ├───┤
        q_3: |0>┤ X ├─────┤ X ├─┤ X ├─────┤ X ├─ .. ┤ X ├
                └───┘     └───┘ └───┘     └───┘     └───┘
        """
        circuit = QuantumCircuit(4)
        for _ in range(2):
            circuit.x([0, 1, 2, 3])
            circuit.barrier()
            circuit.ccx(0, 1, 2)
            circuit.ccx(0, 2, 3)
            circuit.barrier()
            circuit.x([0, 1, 2, 3])
            circuit.barrier()
            circuit.ccx(2, 1, 0)
            circuit.ccx(0, 2, 3)
            circuit.barrier()
        circuit.x([0, 1, 2, 3])

        layer_1 = [(0, 1, 2), (0, 2, 3)]
        layer_2 = [(2, 1, 0), (0, 2, 3)]

        entanglement = lambda offset: layer_1 if offset % 2 == 0 else layer_2

        nlocal = QuantumCircuit(4)
        nlocal.compose(
            n_local(
                4,
                rotation_blocks=XGate(),
                entanglement_blocks=CCXGate(),
                reps=4,
                entanglement=entanglement,
                insert_barriers=True,
            ),
            inplace=True,
        )

        self.assertEqual(nlocal, circuit)

    def test_nice_error_if_circuit_passed(self):
        """Check the transition-helper error."""
        block = QuantumCircuit(1)

        with self.assertRaisesRegex(ValueError, "but you passed a QuantumCircuit"):
            _ = n_local(3, block, "cz")


@ddt
class TestNLocalFamily(QiskitTestCase):
    """Test the derived circuit functions."""

    def test_real_amplitudes(self):
        """Test the real amplitudes circuit."""
        circuit = real_amplitudes(4)
        expected = n_local(4, "ry", "cx", "reverse_linear", reps=3)
        self.assertEqual(expected.assign_parameters(circuit.parameters), circuit)

    def test_real_amplitudes_numqubits_equal1(self):
        """Test the real amplitudes circuit for a single qubit."""
        circuit = real_amplitudes(1)
        expected = n_local(1, "ry", [])
        self.assertEqual(expected.assign_parameters(circuit.parameters), circuit)

    def test_efficient_su2(self):
        """Test the efficient SU(2) circuit."""
        circuit = efficient_su2(4)
        expected = n_local(4, ["ry", "rz"], "cx", "reverse_linear", reps=3)
        self.assertEqual(expected.assign_parameters(circuit.parameters), circuit)

    def test_efficient_su2_numqubits_equal1(self):
        """Test the efficient SU(2) circuit for a single qubit."""
        circuit = efficient_su2(1)
        expected = n_local(1, ["ry", "rz"], [])
        self.assertEqual(expected.assign_parameters(circuit.parameters), circuit)

    @data("fsim", "iswap")
    def test_excitation_preserving(self, mode):
        """Test the excitation preserving circuit."""
        circuit = excitation_preserving(4, mode=mode)

        x = Parameter("x")
        block = QuantumCircuit(2)
        block.rxx(x, 0, 1)
        block.ryy(x, 0, 1)
        if mode == "fsim":
            y = Parameter("y")
            block.cp(y, 0, 1)

        expected = n_local(4, "rz", block.to_gate(), "full", reps=3)
        self.assertEqual(
            expected.assign_parameters(circuit.parameters).decompose(), circuit.decompose()
        )

    @data("fsim", "iswap")
    def test_excitation_preserving_numqubits_equal1(self, mode):
        """Test the excitation preserving circuit for a single qubit."""
        circuit = excitation_preserving(1, mode=mode)
        expected = n_local(1, "rz", [])
        self.assertEqual(
            expected.assign_parameters(circuit.parameters).decompose(), circuit.decompose()
        )

    def test_excitation_preserving_invalid_mode(self):
        """Test an error is raised for an invalid mode."""
        with self.assertRaises(ValueError):
            _ = excitation_preserving(2, mode="Fsim")

        with self.assertRaises(ValueError):
            _ = excitation_preserving(2, mode="swaip")

    def test_two_design(self):
        """Test the Pauli 2-design circuit."""
        circuit = pauli_two_design(3)
        expected_ops = {"rx", "ry", "rz", "cz"}
        circuit_ops = set(circuit.count_ops().keys())

        self.assertTrue(circuit_ops.issubset(expected_ops))

    def test_two_design_numqubits_equal1(self):
        """Test the Pauli 2-design circuit for a single qubit."""
        circuit = pauli_two_design(1)
        expected_ops = {"rx", "ry", "rz", "id"}
        circuit_ops = set(circuit.count_ops().keys())

        self.assertTrue(circuit_ops.issubset(expected_ops))

    def test_two_design_seed(self):
        """Test the seed"""
        seed1 = 123
        seed2 = 321

        with self.subTest(msg="same circuit with same seed"):
            first = pauli_two_design(3, seed=seed1)
            second = pauli_two_design(3, seed=seed1)

            self.assertEqual(first.assign_parameters(second.parameters), second)

        with self.subTest(msg="different circuit with different seed"):
            first = pauli_two_design(3, seed=seed1)
            second = pauli_two_design(3, seed=seed2)

            self.assertNotEqual(first.assign_parameters(second.parameters), second)


@ddt
class TestTwoLocal(QiskitTestCase):
    """Tests for the TwoLocal circuit."""

    def assertCircuitEqual(self, qc1, qc2, visual=False, transpiled=True):
        """An equality test specialized to circuits."""
        if transpiled:
            basis_gates = ["id", "u1", "u3", "cx"]
            qc1_transpiled = transpile(qc1, basis_gates=basis_gates, optimization_level=0)
            qc2_transpiled = transpile(qc2, basis_gates=basis_gates, optimization_level=0)
            qc1, qc2 = qc1_transpiled, qc2_transpiled

        if visual:
            self.assertEqual(qc1.draw(), qc2.draw())
        else:
            self.assertEqual(qc1, qc2)

    def test_skip_final_rotation_layer(self):
        """Test skipping the final rotation layer works."""
        two = TwoLocal(3, ["ry", "h"], ["cz", "cx"], reps=2, skip_final_rotation_layer=True)
        self.assertEqual(two.num_parameters, 6)  # would be 9 with a final rotation layer

    @data(
        (5, "rx", "cx", "full", 2, 15),
        (3, "x", "z", "linear", 1, 0),
        (3, "rx", "cz", "linear", 0, 3),
        (3, ["rx", "ry"], ["cry", "cx"], "circular", 2, 24),
    )
    @unpack
    def test_num_parameters(self, num_qubits, rot, ent, ent_mode, reps, expected):
        """Test the number of parameters."""
        two = TwoLocal(
            num_qubits,
            rotation_blocks=rot,
            entanglement_blocks=ent,
            entanglement=ent_mode,
            reps=reps,
        )

        with self.subTest(msg="num_parameters_settable"):
            self.assertEqual(two.num_parameters_settable, expected)

        with self.subTest(msg="num_parameters"):
            self.assertEqual(two.num_parameters, expected)

    def test_empty_two_local(self):
        """Test the setup of an empty two-local circuit."""
        two = TwoLocal()

        with self.subTest(msg="0 qubits"):
            self.assertEqual(two.num_qubits, 0)

        with self.subTest(msg="no blocks are set"):
            self.assertListEqual(two.rotation_blocks, [])
            self.assertListEqual(two.entanglement_blocks, [])

        with self.subTest(msg="equal to empty circuit"):
            self.assertEqual(two, QuantumCircuit())

    @data("rx", RXGate(Parameter("p")), RXGate, "circuit")
    def test_various_block_types(self, rot):
        """Test setting the rotation blocks to various type and assert the output type is RX."""
        if rot == "circuit":
            rot = QuantumCircuit(1)
            rot.rx(Parameter("angle"), 0)

        two = TwoLocal(3, rot, reps=0)
        self.assertEqual(len(two.rotation_blocks), 1)
        rotation = two.rotation_blocks[0]

        # decompose
        self.assertIsInstance(rotation.data[0].operation, RXGate)

    def test_parameter_setters(self):
        """Test different possibilities to set parameters."""
        two = TwoLocal(3, rotation_blocks="rx", entanglement="cz", reps=2)
        params = [0, 1, 2, Parameter("x"), Parameter("y"), Parameter("z"), 6, 7, 0]
        params_set = {param for param in params if isinstance(param, Parameter)}

        with self.subTest(msg="dict assign and copy"):
            ordered = two.ordered_parameters
            bound = two.assign_parameters(dict(zip(ordered, params)), inplace=False)
            self.assertEqual(bound.parameters, params_set)
            self.assertEqual(two.num_parameters, 9)

        with self.subTest(msg="list assign and copy"):
            ordered = two.ordered_parameters
            bound = two.assign_parameters(params, inplace=False)
            self.assertEqual(bound.parameters, params_set)
            self.assertEqual(two.num_parameters, 9)

        with self.subTest(msg="list assign inplace"):
            ordered = two.ordered_parameters
            two.assign_parameters(params, inplace=True)
            self.assertEqual(two.parameters, params_set)
            self.assertEqual(two.num_parameters, 3)
            self.assertEqual(two.num_parameters_settable, 9)

    def test_parameters_settable_is_constant(self):
        """Test the attribute num_parameters_settable does not change on parameter change."""
        two = TwoLocal(3, rotation_blocks="rx", entanglement="cz", reps=2)
        ordered_params = two.ordered_parameters

        x = Parameter("x")
        two.assign_parameters(dict(zip(ordered_params, [x] * two.num_parameters)), inplace=True)

        with self.subTest(msg="num_parameters collapsed to 1"):
            self.assertEqual(two.num_parameters, 1)

        with self.subTest(msg="num_parameters_settable remained constant"):
            self.assertEqual(two.num_parameters_settable, len(ordered_params))

    def test_compose_inplace_to_circuit(self):
        """Test adding a two-local to an existing circuit."""
        two = TwoLocal(3, ["ry", "rz"], "cz", "full", reps=1, insert_barriers=True)
        circuit = QuantumCircuit(3)
        circuit.compose(two, inplace=True)

        #      ┌──────────┐┌──────────┐ ░           ░ ┌──────────┐ ┌──────────┐
        # q_0: ┤ Ry(θ[0]) ├┤ Rz(θ[3]) ├─░──■──■─────░─┤ Ry(θ[6]) ├─┤ Rz(θ[9]) ├
        #      ├──────────┤├──────────┤ ░  │  │     ░ ├──────────┤┌┴──────────┤
        # q_1: ┤ Ry(θ[1]) ├┤ Rz(θ[4]) ├─░──■──┼──■──░─┤ Ry(θ[7]) ├┤ Rz(θ[10]) ├
        #      ├──────────┤├──────────┤ ░     │  │  ░ ├──────────┤├───────────┤
        # q_2: ┤ Ry(θ[2]) ├┤ Rz(θ[5]) ├─░─────■──■──░─┤ Ry(θ[8]) ├┤ Rz(θ[11]) ├
        #      └──────────┘└──────────┘ ░           ░ └──────────┘└───────────┘
        reference = QuantumCircuit(3)
        param_iter = iter(two.ordered_parameters)
        for i in range(3):
            reference.ry(next(param_iter), i)
        for i in range(3):
            reference.rz(next(param_iter), i)
        reference.barrier()
        reference.cz(0, 1)
        reference.cz(0, 2)
        reference.cz(1, 2)
        reference.barrier()
        for i in range(3):
            reference.ry(next(param_iter), i)
        for i in range(3):
            reference.rz(next(param_iter), i)

        self.assertCircuitEqual(circuit.decompose(), reference)

    def test_composing_two(self):
        """Test adding two two-local circuits."""
        entangler_map = [[0, 3], [0, 2]]
        two = TwoLocal(4, [], "cry", entangler_map, reps=1)
        circuit = two.compose(two)

        reference = QuantumCircuit(4)
        params = two.ordered_parameters
        for _ in range(2):
            reference.cry(params[0], 0, 3)
            reference.cry(params[1], 0, 2)

        self.assertCircuitEqual(reference, circuit)

    def test_ry_blocks(self):
        """Test that the RealAmplitudes circuit is instantiated correctly."""
        two = RealAmplitudes(4)
        with self.subTest(msg="test rotation gate"):
            self.assertEqual(len(two.rotation_blocks), 1)
            self.assertIsInstance(two.rotation_blocks[0].data[0].operation, RYGate)

        with self.subTest(msg="test parameter bounds"):
            expected = [(-np.pi, np.pi)] * two.num_parameters
            np.testing.assert_almost_equal(two.parameter_bounds, expected)

    def test_ry_circuit_reverse_linear(self):
        """Test a RealAmplitudes circuit with entanglement = "reverse_linear"."""
        num_qubits = 3
        reps = 2
        entanglement = "reverse_linear"
        parameters = ParameterVector("theta", num_qubits * (reps + 1))
        param_iter = iter(parameters)

        expected = QuantumCircuit(3)
        for _ in range(reps):
            for i in range(num_qubits):
                expected.ry(next(param_iter), i)
            expected.cx(1, 2)
            expected.cx(0, 1)
        for i in range(num_qubits):
            expected.ry(next(param_iter), i)

        library = RealAmplitudes(
            num_qubits, reps=reps, entanglement=entanglement
        ).assign_parameters(parameters)
        self.assertCircuitEqual(library, expected)

    def test_ry_circuit_full(self):
        """Test a RealAmplitudes circuit with entanglement = "full"."""
        num_qubits = 3
        reps = 2
        entanglement = "full"
        parameters = ParameterVector("theta", num_qubits * (reps + 1))
        param_iter = iter(parameters)

        #      ┌──────────┐          ┌──────────┐                      ┌──────────┐
        # q_0: ┤ Ry(θ[0]) ├──■────■──┤ Ry(θ[3]) ├──────────────■────■──┤ Ry(θ[6]) ├────────────
        #      ├──────────┤┌─┴─┐  │  └──────────┘┌──────────┐┌─┴─┐  │  └──────────┘┌──────────┐
        # q_1: ┤ Ry(θ[1]) ├┤ X ├──┼─────────■────┤ Ry(θ[4]) ├┤ X ├──┼─────────■────┤ Ry(θ[7]) ├
        #      ├──────────┤└───┘┌─┴─┐     ┌─┴─┐  ├──────────┤└───┘┌─┴─┐     ┌─┴─┐  ├──────────┤
        # q_2: ┤ Ry(θ[2]) ├─────┤ X ├─────┤ X ├──┤ Ry(θ[5]) ├─────┤ X ├─────┤ X ├──┤ Ry(θ[8]) ├
        #      └──────────┘     └───┘     └───┘  └──────────┘     └───┘     └───┘  └──────────┘
        expected = QuantumCircuit(3)
        for _ in range(reps):
            for i in range(num_qubits):
                expected.ry(next(param_iter), i)
            expected.cx(0, 1)
            expected.cx(0, 2)
            expected.cx(1, 2)
        for i in range(num_qubits):
            expected.ry(next(param_iter), i)

        library = RealAmplitudes(
            num_qubits, reps=reps, entanglement=entanglement
        ).assign_parameters(parameters)
        self.assertCircuitEqual(library, expected)

    def test_ryrz_blocks(self):
        """Test that the EfficientSU2 circuit is instantiated correctly."""
        two = EfficientSU2(3)
        with self.subTest(msg="test rotation gate"):
            self.assertEqual(len(two.rotation_blocks), 2)
            self.assertIsInstance(two.rotation_blocks[0].data[0].operation, RYGate)
            self.assertIsInstance(two.rotation_blocks[1].data[0].operation, RZGate)

        with self.subTest(msg="test parameter bounds"):
            expected = [(-np.pi, np.pi)] * two.num_parameters
            np.testing.assert_almost_equal(two.parameter_bounds, expected)

    def test_rzsx_blocks(self):
        """Test that the EfficientSU2 circuit is instantiated correctly."""
        two = EfficientSU2(3, ["rz", "sx", "rz", "sx", "rz"])
        expected = [[RZGate], [SXGate], [RZGate], [SXGate], [RZGate]]
        actual = [
            [instruction.operation.base_class for instruction in block]
            for block in two.rotation_blocks
        ]
        self.assertEqual(actual, expected)

    def test_ryrz_circuit(self):
        """Test an EfficientSU2 circuit."""
        num_qubits = 3
        reps = 2
        entanglement = "circular"
        parameters = ParameterVector("theta", 2 * num_qubits * (reps + 1))
        param_iter = iter(parameters)

        #      ┌──────────┐┌──────────┐┌───┐     ┌──────────┐┌──────────┐             »
        # q_0: ┤ Ry(θ[0]) ├┤ Rz(θ[3]) ├┤ X ├──■──┤ Ry(θ[6]) ├┤ Rz(θ[9]) ├─────────────»
        #      ├──────────┤├──────────┤└─┬─┘┌─┴─┐└──────────┘├──────────┤┌───────────┐»
        # q_1: ┤ Ry(θ[1]) ├┤ Rz(θ[4]) ├──┼──┤ X ├─────■──────┤ Ry(θ[7]) ├┤ Rz(θ[10]) ├»
        #      ├──────────┤├──────────┤  │  └───┘   ┌─┴─┐    ├──────────┤├───────────┤»
        # q_2: ┤ Ry(θ[2]) ├┤ Rz(θ[5]) ├──■──────────┤ X ├────┤ Ry(θ[8]) ├┤ Rz(θ[11]) ├»
        #      └──────────┘└──────────┘             └───┘    └──────────┘└───────────┘»
        # «     ┌───┐     ┌───────────┐┌───────────┐
        # «q_0: ┤ X ├──■──┤ Ry(θ[12]) ├┤ Rz(θ[15]) ├─────────────
        # «     └─┬─┘┌─┴─┐└───────────┘├───────────┤┌───────────┐
        # «q_1: ──┼──┤ X ├──────■──────┤ Ry(θ[13]) ├┤ Rz(θ[16]) ├
        # «       │  └───┘    ┌─┴─┐    ├───────────┤├───────────┤
        # «q_2: ──■───────────┤ X ├────┤ Ry(θ[14]) ├┤ Rz(θ[17]) ├
        # «                   └───┘    └───────────┘└───────────┘
        expected = QuantumCircuit(3)
        for _ in range(reps):
            for i in range(num_qubits):
                expected.ry(next(param_iter), i)
            for i in range(num_qubits):
                expected.rz(next(param_iter), i)
            expected.cx(2, 0)
            expected.cx(0, 1)
            expected.cx(1, 2)
        for i in range(num_qubits):
            expected.ry(next(param_iter), i)
        for i in range(num_qubits):
            expected.rz(next(param_iter), i)

        library = EfficientSU2(num_qubits, reps=reps, entanglement=entanglement).assign_parameters(
            parameters
        )

        self.assertCircuitEqual(library, expected)

    def test_swaprz_blocks(self):
        """Test that the ExcitationPreserving circuit is instantiated correctly."""
        two = ExcitationPreserving(5)
        with self.subTest(msg="test rotation gate"):
            self.assertEqual(len(two.rotation_blocks), 1)
            self.assertIsInstance(two.rotation_blocks[0].data[0].operation, RZGate)

        with self.subTest(msg="test entanglement gate"):
            self.assertEqual(len(two.entanglement_blocks), 1)
            block = two.entanglement_blocks[0]
            self.assertEqual(len(block.data), 2)
            self.assertIsInstance(block.data[0].operation, RXXGate)
            self.assertIsInstance(block.data[1].operation, RYYGate)

        with self.subTest(msg="test parameter bounds"):
            expected = [(-np.pi, np.pi)] * two.num_parameters
            np.testing.assert_almost_equal(two.parameter_bounds, expected)

    def test_swaprz_circuit(self):
        """Test a ExcitationPreserving circuit in iswap mode."""
        num_qubits = 3
        reps = 2
        entanglement = "linear"
        parameters = ParameterVector("theta", num_qubits * (reps + 1) + reps * (num_qubits - 1))
        param_iter = iter(parameters)

        #      ┌──────────┐┌────────────┐┌────────────┐ ┌──────────┐               »
        # q_0: ┤ Rz(θ[0]) ├┤0           ├┤0           ├─┤ Rz(θ[5]) ├───────────────»
        #      ├──────────┤│  Rxx(θ[3]) ││  Ryy(θ[3]) │┌┴──────────┴┐┌────────────┐»
        # q_1: ┤ Rz(θ[1]) ├┤1           ├┤1           ├┤0           ├┤0           ├»
        #      ├──────────┤└────────────┘└────────────┘│  Rxx(θ[4]) ││  Ryy(θ[4]) │»
        # q_2: ┤ Rz(θ[2]) ├────────────────────────────┤1           ├┤1           ├»
        #      └──────────┘                            └────────────┘└────────────┘»
        # «                 ┌────────────┐┌────────────┐┌───────────┐               »
        # «q_0: ────────────┤0           ├┤0           ├┤ Rz(θ[10]) ├───────────────»
        # «     ┌──────────┐│  Rxx(θ[8]) ││  Ryy(θ[8]) │├───────────┴┐┌────────────┐»
        # «q_1: ┤ Rz(θ[6]) ├┤1           ├┤1           ├┤0           ├┤0           ├»
        # «     ├──────────┤└────────────┘└────────────┘│  Rxx(θ[9]) ││  Ryy(θ[9]) │»
        # «q_2: ┤ Rz(θ[7]) ├────────────────────────────┤1           ├┤1           ├»
        # «     └──────────┘                            └────────────┘└────────────┘»
        # «
        # «q_0: ─────────────
        # «     ┌───────────┐
        # «q_1: ┤ Rz(θ[11]) ├
        # «     ├───────────┤
        # «q_2: ┤ Rz(θ[12]) ├
        # «     └───────────┘
        expected = QuantumCircuit(3)
        for _ in range(reps):
            for i in range(num_qubits):
                expected.rz(next(param_iter), i)
            shared_param = next(param_iter)
            expected.rxx(shared_param, 0, 1)
            expected.ryy(shared_param, 0, 1)
            shared_param = next(param_iter)
            expected.rxx(shared_param, 1, 2)
            expected.ryy(shared_param, 1, 2)
        for i in range(num_qubits):
            expected.rz(next(param_iter), i)

        library = ExcitationPreserving(
            num_qubits, reps=reps, entanglement=entanglement
        ).assign_parameters(parameters)

        self.assertCircuitEqual(library, expected)

    def test_fsim_circuit(self):
        """Test a ExcitationPreserving circuit in fsim mode."""
        num_qubits = 3
        reps = 2
        entanglement = "linear"
        # need the parameters in the entanglement blocks to be the same because the order
        # can get mixed up in ExcitationPreserving (since parameters are not ordered in circuits)
        parameters = [1] * (num_qubits * (reps + 1) + reps * (1 + num_qubits))
        param_iter = iter(parameters)

        #      ┌───────┐┌─────────┐┌─────────┐        ┌───────┐                   »
        # q_0: ┤ Rz(1) ├┤0        ├┤0        ├─■──────┤ Rz(1) ├───────────────────»
        #      ├───────┤│  Rxx(1) ││  Ryy(1) │ │P(1) ┌┴───────┴┐┌─────────┐       »
        # q_1: ┤ Rz(1) ├┤1        ├┤1        ├─■─────┤0        ├┤0        ├─■─────»
        #      ├───────┤└─────────┘└─────────┘       │  Rxx(1) ││  Ryy(1) │ │P(1) »
        # q_2: ┤ Rz(1) ├─────────────────────────────┤1        ├┤1        ├─■─────»
        #      └───────┘                             └─────────┘└─────────┘       »
        # «              ┌─────────┐┌─────────┐        ┌───────┐                   »
        # «q_0: ─────────┤0        ├┤0        ├─■──────┤ Rz(1) ├───────────────────»
        # «     ┌───────┐│  Rxx(1) ││  Ryy(1) │ │P(1) ┌┴───────┴┐┌─────────┐       »
        # «q_1: ┤ Rz(1) ├┤1        ├┤1        ├─■─────┤0        ├┤0        ├─■─────»
        # «     ├───────┤└─────────┘└─────────┘       │  Rxx(1) ││  Ryy(1) │ │P(1) »
        # «q_2: ┤ Rz(1) ├─────────────────────────────┤1        ├┤1        ├─■─────»
        # «     └───────┘                             └─────────┘└─────────┘       »
        # «
        # «q_0: ─────────
        # «     ┌───────┐
        # «q_1: ┤ Rz(1) ├
        # «     ├───────┤
        # «q_2: ┤ Rz(1) ├
        # «     └───────┘
        expected = QuantumCircuit(3)
        for _ in range(reps):
            for i in range(num_qubits):
                expected.rz(next(param_iter), i)
            shared_param = next(param_iter)
            expected.rxx(shared_param, 0, 1)
            expected.ryy(shared_param, 0, 1)
            expected.cp(next(param_iter), 0, 1)
            shared_param = next(param_iter)
            expected.rxx(shared_param, 1, 2)
            expected.ryy(shared_param, 1, 2)
            expected.cp(next(param_iter), 1, 2)
        for i in range(num_qubits):
            expected.rz(next(param_iter), i)

        library = ExcitationPreserving(
            num_qubits, reps=reps, mode="fsim", entanglement=entanglement
        ).assign_parameters(parameters)

        self.assertCircuitEqual(library, expected)

    def test_excitation_preserving_invalid_mode(self):
        """Test an error is raised for an invalid mode."""
        with self.assertRaises(ValueError):
            _ = ExcitationPreserving(2, mode="Fsim")

        with self.assertRaises(ValueError):
            _ = ExcitationPreserving(2, mode="swaip")

    def test_circular_on_same_block_and_circuit_size(self):
        """Test circular entanglement works correctly if the circuit and block sizes match."""

        two = TwoLocal(2, "ry", "cx", entanglement="circular", reps=1)
        parameters = np.arange(two.num_parameters)

        #      ┌───────┐     ┌───────┐
        # q_0: ┤ Ry(0) ├──■──┤ Ry(2) ├
        #      ├───────┤┌─┴─┐├───────┤
        # q_1: ┤ Ry(1) ├┤ X ├┤ Ry(3) ├
        #      └───────┘└───┘└───────┘
        ref = QuantumCircuit(2)
        ref.ry(parameters[0], 0)
        ref.ry(parameters[1], 1)
        ref.cx(0, 1)
        ref.ry(parameters[2], 0)
        ref.ry(parameters[3], 1)

        self.assertCircuitEqual(two.assign_parameters(parameters), ref)

    def test_circuit_with_numpy_integers(self):
        """Test if TwoLocal can be made from numpy integers"""
        num_qubits = 6
        reps = 3
        expected_np32 = [
            (i, j)
            for i in np.arange(num_qubits, dtype=np.int32)
            for j in np.arange(num_qubits, dtype=np.int32)
            if i < j
        ]
        expected_np64 = [
            (i, j)
            for i in np.arange(num_qubits, dtype=np.int64)
            for j in np.arange(num_qubits, dtype=np.int64)
            if i < j
        ]

        two_np32 = TwoLocal(num_qubits, "ry", "cx", entanglement=expected_np32, reps=reps)
        two_np64 = TwoLocal(num_qubits, "ry", "cx", entanglement=expected_np64, reps=reps)

        expected_cx = reps * num_qubits * (num_qubits - 1) / 2

        self.assertEqual(two_np32.decompose().count_ops()["cx"], expected_cx)
        self.assertEqual(two_np64.decompose().count_ops()["cx"], expected_cx)

    @combine(num_qubits=[4, 5])
    def test_full_vs_reverse_linear(self, num_qubits):
        """Test that 'full' and 'reverse_linear' provide the same unitary element."""
        reps = 2
        full = RealAmplitudes(num_qubits=num_qubits, entanglement="full", reps=reps)
        num_params = (reps + 1) * num_qubits
        np.random.seed(num_qubits)
        params = np.random.rand(num_params)
        reverse = RealAmplitudes(num_qubits=num_qubits, entanglement="reverse_linear", reps=reps)
        full.assign_parameters(params, inplace=True)
        reverse.assign_parameters(params, inplace=True)

        self.assertEqual(Operator(full), Operator(reverse))


@ddt
class TestEntanglement(QiskitTestCase):
    """Test getting the entanglement structure."""

    @data(
        ("linear", [(0, 1), (1, 2), (2, 3)]),
        ("reverse_linear", [(2, 3), (1, 2), (0, 1)]),
        ("pairwise", [(0, 1), (2, 3), (1, 2)]),
        ("circular", [(3, 0), (0, 1), (1, 2), (2, 3)]),
        ("full", [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]),
    )
    @unpack
    def test_2q_str(self, strategy, expected):
        """Test getting by string."""
        entanglement = fast_entangler_map(
            num_qubits=4, block_size=2, entanglement=strategy, offset=0
        )
        self.assertEqual(expected, entanglement)

    @data(
        ("linear", [(0, 1, 2), (1, 2, 3)]),
        ("reverse_linear", [(1, 2, 3), (0, 1, 2)]),
        ("circular", [(2, 3, 0), (3, 0, 1), (0, 1, 2), (1, 2, 3)]),
        ("full", [(0, 1, 2), (0, 1, 3), (0, 2, 3), (1, 2, 3)]),
    )
    @unpack
    def test_3q_str(self, strategy, expected):
        """Test getting by string."""
        entanglement = fast_entangler_map(
            num_qubits=4, block_size=3, entanglement=strategy, offset=0
        )
        self.assertEqual(expected, entanglement)

    def test_2q_sca(self):
        """Test shift, circular, alternating on 2-qubit blocks."""
        expected = {  # offset: result
            0: [(3, 0), (0, 1), (1, 2), (2, 3)],
            1: [(3, 2), (0, 3), (1, 0), (2, 1)],
            2: [(1, 2), (2, 3), (3, 0), (0, 1)],
            3: [(1, 0), (2, 1), (3, 2), (0, 3)],
        }
        for offset in range(8):
            with self.subTest(offset=offset):
                entanglement = fast_entangler_map(
                    num_qubits=4, block_size=2, entanglement="sca", offset=offset
                )
                self.assertEqual(expected[offset % 4], entanglement)

    def test_3q_sca(self):
        """Test shift, circular, alternating on 3-qubit blocks."""
        circular = [(2, 3, 0), (3, 0, 1), (0, 1, 2), (1, 2, 3)]
        for offset in range(8):
            expected = circular[-(offset % 4) :] + circular[: -(offset % 4)]
            if offset % 2 == 1:
                expected = [tuple(reversed(indices)) for indices in expected]
            with self.subTest(offset=offset):
                entanglement = fast_entangler_map(
                    num_qubits=4, block_size=3, entanglement="sca", offset=offset
                )
                self.assertEqual(expected, entanglement)

    @data("full", "reverse_linear", "linear", "circular", "sca", "pairwise")
    def test_0q(self, entanglement):
        """Test the corner case of a single qubit block."""
        entanglement = fast_entangler_map(
            num_qubits=3, block_size=0, entanglement=entanglement, offset=0
        )
        expect = []
        self.assertEqual(entanglement, expect)

    @data("full", "reverse_linear", "linear", "circular", "sca", "pairwise")
    def test_1q(self, entanglement):
        """Test the corner case of a single qubit block."""
        entanglement = fast_entangler_map(
            num_qubits=3, block_size=1, entanglement=entanglement, offset=0
        )
        expect = [(i,) for i in range(3)]

        self.assertEqual(set(entanglement), set(expect))  # order does not matter for 1 qubit

    @data("full", "reverse_linear", "linear", "circular", "sca")
    def test_full_block(self, entanglement):
        """Test the corner case of the block size equal the number of qubits."""
        entanglement = fast_entangler_map(
            num_qubits=5, block_size=5, entanglement=entanglement, offset=0
        )
        expect = [tuple(range(5))]

        self.assertEqual(entanglement, expect)

    def test_pairwise_limit(self):
        """Test pairwise raises an error above 2 qubits."""
        _ = fast_entangler_map(num_qubits=4, block_size=1, entanglement="pairwise", offset=0)
        _ = fast_entangler_map(num_qubits=4, block_size=2, entanglement="pairwise", offset=0)
        with self.assertRaises(QiskitError):
            _ = fast_entangler_map(num_qubits=4, block_size=3, entanglement="pairwise", offset=0)

    def test_invalid_blocksize(self):
        """Test the block size being too large."""
        with self.assertRaises(QiskitError):
            _ = fast_entangler_map(num_qubits=2, block_size=3, entanglement="linear", offset=0)

    def test_invalid_entanglement_str(self):
        """Test invalid entanglement string."""
        with self.assertRaises(QiskitError):
            _ = fast_entangler_map(num_qubits=4, block_size=2, entanglement="lniaer", offset=0)

    def test_as_list(self):
        """Test passing a list just returns the list."""
        expected = [(0, 1), (1, 10), (2, 10)]
        out = fast_entangler_map(num_qubits=20, block_size=2, entanglement=expected, offset=0)
        self.assertEqual(expected, out)

    def test_invalid_list(self):
        """Test passing a list that does not match the block size."""

        # TODO this test fails, somehow the error is not propagated correctly!
        expected = [(0, 1), (1, 2, 10)]
        with self.assertRaises(QiskitError):
            _ = fast_entangler_map(num_qubits=20, block_size=2, entanglement=expected, offset=0)

    def test_callable_list(self):
        """Test using a callable."""

        def my_entanglement(offset):
            return [(0, 1)] if offset % 2 == 0 else [(1, 2)]

        for offset in range(3):
            with self.subTest(offset=offset):
                expect = my_entanglement(offset)
                result = fast_entangler_map(
                    num_qubits=3, block_size=2, entanglement=my_entanglement, offset=offset
                )
                self.assertEqual(expect, result)

    def test_callable_str(self):
        """Test using a callable."""

        def my_entanglement(offset):
            return "linear" if offset % 2 == 0 else "pairwise"

        expected = {"linear": [(0, 1), (1, 2), (2, 3)], "pairwise": [(0, 1), (2, 3), (1, 2)]}

        for offset in range(3):
            with self.subTest(offset=offset):
                result = fast_entangler_map(
                    num_qubits=4, block_size=2, entanglement=my_entanglement, offset=offset
                )
                self.assertEqual(expected["linear" if offset % 2 == 0 else "pairwise"], result)


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