# !/usr/bin/env python3
# Copyright (c) 2022 Institute for Quantum Computing, Baidu Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

r"""
The source file of the Circuit class.
"""

import warnings
import paddle
from .container import Sequential
from .layer import Layer, SuperpositionLayer, WeakSuperpositionLayer, LinearEntangledLayer
from .layer import RealBlockLayer, RealEntangledLayer, ComplexBlockLayer, ComplexEntangledLayer, QAOALayer
from ..gate import Gate, H, S, Sdg, T, Tdg, X, Y, Z, P, RX, RY, RZ, U3
from ..gate import CNOT, CX, CY, CZ, SWAP
from ..gate import CP, CRX, CRY, CRZ, CU, RXX, RYY, RZZ
from ..gate import MS, CSWAP, CCX
from ..gate import UniversalTwoQubits, UniversalThreeQubits
from ..gate import Oracle, ControlOracle
from ..channel import BitFlip, PhaseFlip, BitPhaseFlip, AmplitudeDamping, GeneralizedAmplitudeDamping, PhaseDamping
from ..channel import Depolarizing, GeneralizedDepolarizing, PauliChannel, ResetChannel, ThermalRelaxation
from ..channel import MixedUnitaryChannel, Channel, ChoiRepr, KrausRepr, StinespringRepr
from ..intrinsic import _get_float_dtype, _format_qubits_idx, _cnot_idx_fetch
from ..state import zero_state
from ..operator import Collapse
from typing import Union, Iterable, Optional, Dict, List, Tuple
import paddle_quantum as pq
from paddle_quantum import State, get_backend, get_dtype, Backend
from math import pi
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
from ..gate.functional.visual import _circuit_plot


class Circuit(Sequential):
    r"""Quantum circuit.

    Args:
        num_qubits: Number of qubits. Defaults to None.
    """

    def __init__(self, num_qubits: Optional[int] = None):
        super().__init__()
        self.__num_qubits = num_qubits

        # whether the circuit is a dynamic quantum circuit
        self.__isdynamic = num_qubits is None

        # alias
        self.toffoli = self.ccx
        self.cx = self.cnot

        # preparing cnot index in 'cycle' case
        if num_qubits is not None and num_qubits > 1:
            cnot_qubits_idx = _format_qubits_idx("cycle", num_qubits, num_acted_qubits=2)
            self.__cnot_cycle_idx = _cnot_idx_fetch(num_qubits=num_qubits, qubits_idx=cnot_qubits_idx)


    @property
    def num_qubits(self) -> int:
        r"""Number of qubits.
        """
        return self.__num_qubits

    @property
    def isdynamic(self) -> bool:
        r"""Whether the circuit is dynamic
        """
        return self.__dynamic

    @num_qubits.setter
    def num_qubits(self, value: int) -> None:
        assert isinstance(value, int)
        self.__num_qubits = value

    @property
    def param(self) -> paddle.Tensor:
        r"""Flattened parameters in the circuit.
        """
        if len(self.parameters()) == 0:
            return []
        return paddle.concat([paddle.flatten(param) for param in self.parameters()])

    @property
    def grad(self) -> np.ndarray:
        r"""Gradients with respect to the flattened parameters.
        """
        grad_list = []
        for param in self.parameters():
            assert param.grad is not None, (
                'The gradient is None, run the backward first before calling this property, '
                'otherwise check where the gradient chain is broken.')
            grad_list.append(param.grad.numpy().flatten())
        return np.concatenate(grad_list)

    def update_param(self, theta: Union[paddle.Tensor, np.ndarray, float], idx: int = None) -> None:
        r"""Replace parameters of all/one layer(s) by ``theta``.

        Args:
            theta: New parameters
            idx: Index of replacement. Defaults to None, referring to all layers.
        """
        if not isinstance(theta, paddle.Tensor):
            theta = paddle.to_tensor(theta, dtype='float32')
        theta = paddle.flatten(theta)

        backend_dtype = _get_float_dtype(get_dtype())
        if backend_dtype != 'float32':
            warnings.warn(
                f"\ndtype of parameters will be float32 instead of {backend_dtype}", UserWarning)

        if idx is None:
            assert self.param.shape == theta.shape, \
                f"the shape of input parameters is not correct: expect {self.param.shape}, received {theta.shape}"
            for layer in self.sublayers():
                for name, _ in layer.named_parameters():
                    param = getattr(layer, name)
                    num_param = int(paddle.numel(param))

                    param = paddle.create_parameter(
                        shape=param.shape,
                        dtype='float32',
                        default_initializer=paddle.nn.initializer.Assign(
                            theta[:num_param].reshape(param.shape)),
                    )

                    setattr(layer, 'theta', param)
                    if num_param == theta.shape[0]:
                        return
                    theta = theta[num_param:]
        elif isinstance(idx, int):
            assert idx < len(self.sublayers(
            )), f"the index is out of range, expect below {len(self.sublayers())}"

            layer = self.sublayers()[idx]
            assert theta.shape == paddle.concat([paddle.flatten(param) for param in layer.parameters()]).shape, (
                "The shape of input parameters is not correct.")

            for name, _ in layer.named_parameters():
                param = getattr(layer, name)
                num_param = int(paddle.numel(param))

                param = paddle.create_parameter(
                    shape=param.shape,
                    dtype='float32',
                    default_initializer=paddle.nn.initializer.Assign(
                        theta[:num_param].reshape(param.shape)),
                )

                setattr(layer, 'theta', param)
                if num_param == theta.shape[0]:
                    return
                theta = theta[num_param:]
        else:
            raise ValueError("idx must be an integer or None")

    def transfer_static(self) -> None:
        r"""
        set ``stop_gradient`` of all parameters of the circuit as ``True``
        """
        for layer in self.sublayers():
            for name, _ in layer.named_parameters():
                param = getattr(layer, name)
                param.stop_gradient = True
                setattr(layer, 'theta', param)

    def randomize_param(self, arg0: float = 0, arg1: float = 2 * pi, initializer_type: str= 'Uniform') -> None:
        r"""Randomize parameters of the circuit based on the initializer.  Current we only support Uniform and Normal initializer. 

        A detail introduction can be found in https://www.paddlepaddle.org.cn/documentation/docs/zh/api/paddle/nn/initializer/Uniform_en.html

        Args:
            arg0: first argument of the initializer. Defaults to 0.
            arg1: first argument of the initializer. Defaults to 2 * pi.
            initializer_type: The type of the initializer. Defaults to 'Uniform'.
        """
        assert initializer_type in ["Uniform", "Normal"], (
            "The initializer should be Uniform or Normal."
        )

        for layer in self.sublayers():
            for name, _ in layer.named_parameters():
                param = getattr(layer, name)

                if initializer_type == 'Uniform':
                    param = paddle.create_parameter(
                        shape=param.shape,
                        dtype=param.dtype,
                        default_initializer=paddle.nn.initializer.Uniform(
                            low=arg0, high=arg1),
                    )
                    setattr(layer, 'theta', param)      
                elif initializer_type == 'Normal':
                    param = paddle.create_parameter(
                        shape=param.shape,
                        dtype=param.dtype,
                        default_initializer=paddle.nn.initializer.Normal(
                            mean=arg0, std=arg1),
                    )
                    setattr(layer, 'theta', param)

    def __num_qubits_update(self, qubits_idx: Union[Iterable[int], int, str]) -> None:
        r"""Update ``self.num_qubits`` according to ``qubits_idx``, or report error.

        Args:
            qubits_idx: Input qubit indices of a quantum gate.
        """
        num_qubits = self.__num_qubits
        if isinstance(qubits_idx, str) or qubits_idx is None:
            assert num_qubits is not None, \
                f"The qubit idx cannot be a string or None when the number of qubits is unknown: received {qubits_idx}"
            return

        if isinstance(qubits_idx, Iterable):
            max_idx = np.max(qubits_idx)
        else:
            max_idx = qubits_idx

        if num_qubits is None:
            self.__num_qubits = max_idx + 1
            return

        assert max_idx + 1 <= num_qubits or self.__isdynamic, (
            "The circuit is not a dynamic quantum circuit. "
            f"Invalid input qubit idx: {max_idx} num_qubit: {self.__num_qubits}")
        self.__num_qubits = int(max(max_idx + 1, num_qubits))

    def h(
            self, qubits_idx: Union[Iterable[int], int, str] = 'full', depth: int = 1
    ) -> None:
        r"""Add single-qubit Hadamard gates.

        The matrix form of such a gate is:

        .. math::

            H = \frac{1}{\sqrt{2}}
                \begin{bmatrix}
                    1&1\\
                    1&-1
                \end{bmatrix}

        Args:
            qubits_idx: Indices of the qubits on which the gates are applied. Defaults to 'full'.
            depth: Number of layers. Defaults to 1.
        """
        self.__num_qubits_update(qubits_idx)
        self.append(H(qubits_idx, self.num_qubits, depth))

    def s(
            self, qubits_idx: Union[Iterable[int], int, str] = 'full', depth: int = 1
    ) -> None:
        r"""Add single-qubit S gates.

        The matrix form of such a gate is:

        .. math::

            S =
                \begin{bmatrix}
                    1&0\\
                    0&i
                \end{bmatrix}

        Args:
            qubits_idx: Indices of the qubits on which the gates are applied. Defaults to 'full'.
            depth: Number of layers. Defaults to 1.
        """
        self.__num_qubits_update(qubits_idx)
        self.append(S(qubits_idx, self.num_qubits, depth))

    def sdg(
            self, qubits_idx: Union[Iterable[int], int, str] = 'full', depth: int = 1
    ) -> None:
        r"""Add single-qubit S dagger (S inverse) gates.

        The matrix form of such a gate is:

        .. math::

            S^\dagger =
                \begin{bmatrix}
                    1&0\\
                    0&-i
                \end{bmatrix}

        Args:
            qubits_idx: Indices of the qubits on which the gates are applied. Defaults to ``'full'``.
            depth: Number of layers. Defaults to ``1``.
        """
        self.__num_qubits_update(qubits_idx)
        self.append(Sdg(qubits_idx, self.num_qubits, depth))

    def t(
            self, qubits_idx: Union[Iterable[int], int, str] = 'full', depth: int = 1
    ) -> None:
        r"""Add single-qubit T gates.

        The matrix form of such a gate is:

        .. math::

            T = \begin{bmatrix}
                    1&0\\
                    0&e^\frac{i\pi}{4}
                \end{bmatrix}

        Args:
            qubits_idx: Indices of the qubits on which the gates are applied. Defaults to 'full'.
            depth: Number of layers. Defaults to 1.
        """
        self.__num_qubits_update(qubits_idx)
        self.append(T(qubits_idx, self.num_qubits, depth))

    def tdg(
            self, qubits_idx: Union[Iterable[int], int, str] = 'full', depth: int = 1
    ) -> None:
        r"""Add single-qubit T dagger (T inverse) gates.

        The matrix form of such a gate is:

        .. math::

            T^\dagger =
                \begin{bmatrix}
                    1&0\\
                    0&e^{-\frac{i\pi}{4}}
                \end{bmatrix}

        Args:
            qubits_idx: Indices of the qubits on which the gates are applied. Defaults to ``'full'``.
            depth: Number of layers. Defaults to 1.
        """
        self.__num_qubits_update(qubits_idx)
        self.append(Tdg(qubits_idx, self.num_qubits, depth))

    def x(
            self, qubits_idx: Union[Iterable[int], int, str] = 'full', depth: int = 1
    ) -> None:
        r"""Add single-qubit X gates.

        The matrix form of such a gate is:

        .. math::
           X = \begin{bmatrix}
                0 & 1 \\
                1 & 0
            \end{bmatrix}

        Args:
            qubits_idx: Indices of the qubits on which the gates are applied. Defaults to 'full'.
            depth: Number of layers. Defaults to 1.
        """
        self.__num_qubits_update(qubits_idx)
        self.append(X(qubits_idx, self.num_qubits, depth))

    def y(
            self, qubits_idx: Union[Iterable[int], int, str] = 'full', depth: int = 1
    ) -> None:
        r"""Add single-qubit Y gates.

        The matrix form of such a gate is:

        .. math::

            Y = \begin{bmatrix}
                0 & -i \\
                i & 0
            \end{bmatrix}

        Args:
            qubits_idx: Indices of the qubits on which the gates are applied. Defaults to 'full'.
            depth: Number of layers. Defaults to 1.
        """
        self.__num_qubits_update(qubits_idx)
        self.append(Y(qubits_idx, self.num_qubits, depth))

    def z(
            self, qubits_idx: Union[Iterable[int], int, str] = 'full', depth: int = 1
    ) -> None:
        r"""Add single-qubit Z gates.

        The matrix form of such a gate is:

        .. math::

            Z = \begin{bmatrix}
                1 & 0 \\
                0 & -1
            \end{bmatrix}

        Args:
            qubits_idx: Indices of the qubits on which the gates are applied. Defaults to 'full'.
            depth: Number of layers. Defaults to 1.
        """
        self.__num_qubits_update(qubits_idx)
        self.append(Z(qubits_idx, self.num_qubits, depth))

    def p(
            self, qubits_idx: Union[Iterable[int], int, str] = 'full', depth: int = 1,
            param: Union[paddle.Tensor, float] = None, param_sharing: bool = False
    ) -> None:
        r"""Add single-qubit P gates.

        The matrix form of such a gate is:

        .. math::

            P(\theta) = \begin{bmatrix}
                1 & 0 \\
                0 & e^{i\theta}
            \end{bmatrix}

        Args:
            qubits_idx: Indices of the qubits on which the gates are applied. Defaults to 'full'.
            depth: Number of layers. Defaults to 1.
            param: Parameters of the gates. Defaults to None.
            param_sharing: Whether gates in the same layer share a parameter. Defaults to False.
        """
        self.__num_qubits_update(qubits_idx)
        self.append(P(qubits_idx, self.num_qubits, depth, param, param_sharing))

    def rx(
            self, qubits_idx: Union[Iterable[int], int, str] = 'full', depth: int = 1,
            param: Union[paddle.Tensor, float] = None, param_sharing: bool = False
    ) -> None:
        r"""Add single-qubit rotation gates about the x-axis.

        The matrix form of such a gate is:

        .. math::

            R_X(\theta) = \begin{bmatrix}
                \cos\frac{\theta}{2} & -i\sin\frac{\theta}{2} \\
                -i\sin\frac{\theta}{2} & \cos\frac{\theta}{2}
            \end{bmatrix}

        Args:
            qubits_idx: Indices of the qubits on which the gates are applied. Defaults to 'full'.
            depth: Number of layers. Defaults to 1.
            param: Parameters of the gates. Defaults to None.
            param_sharing: Whether gates in the same layer share a parameter. Defaults to False.
        """
        self.__num_qubits_update(qubits_idx)
        self.append(RX(qubits_idx, self.num_qubits, depth, param, param_sharing))

    def ry(
            self, qubits_idx: Union[Iterable[int], int, str] = 'full', depth: int = 1,
            param: Union[paddle.Tensor, float] = None, param_sharing: bool = False
    ) -> None:
        r"""Add single-qubit rotation gates about the y-axis.

        The matrix form of such a gate is:

        .. math::

            R_Y(\theta) = \begin{bmatrix}
                \cos\frac{\theta}{2} & -\sin\frac{\theta}{2} \\
                \sin\frac{\theta}{2} & \cos\frac{\theta}{2}
            \end{bmatrix}

        Args:
            qubits_idx: Indices of the qubits on which the gates are applied. Defaults to 'full'.
            depth: Number of layers. Defaults to 1.
            param: Parameters of the gates. Defaults to None.
            param_sharing: Whether gates in the same layer share a parameter. Defaults to False.
        """
        self.__num_qubits_update(qubits_idx)
        self.append(RY(qubits_idx, self.num_qubits, depth, param, param_sharing))

    def rz(
            self, qubits_idx: Union[Iterable[int], int, str] = 'full', depth: int = 1,
            param: Union[paddle.Tensor, float] = None, param_sharing: bool = False
    ) -> None:
        r"""Add single-qubit rotation gates about the z-axis.

        The matrix form of such a gate is:

        .. math::

            R_Z(\theta) = \begin{bmatrix}
                e^{-i\frac{\theta}{2}} & 0 \\
                0 & e^{i\frac{\theta}{2}}
            \end{bmatrix}

        Args:
            qubits_idx: Indices of the qubits on which the gates are applied. Defaults to 'full'.
            depth: Number of layers. Defaults to 1.
            param: Parameters of the gates. Defaults to None.
            param_sharing: Whether gates in the same layer share a parameter. Defaults to False.
        """
        self.__num_qubits_update(qubits_idx)
        self.append(RZ(qubits_idx, self.num_qubits, depth, param, param_sharing))

    def u3(
            self, qubits_idx: Union[Iterable[int], int, str] = 'full', depth: int = 1,
            param: Union[paddle.Tensor, Iterable[float]] = None, param_sharing: bool = False
    ) -> None:
        r"""Add single-qubit rotation gates.

        The matrix form of such a gate is:

        .. math::

            \begin{align}
                U_3(\theta, \phi, \lambda) =
                    \begin{bmatrix}
                        \cos\frac\theta2&-e^{i\lambda}\sin\frac\theta2\\
                        e^{i\phi}\sin\frac\theta2&e^{i(\phi+\lambda)}\cos\frac\theta2
                    \end{bmatrix}
            \end{align}

        Args:
            qubits_idx: Indices of the qubits on which the gates are applied. Defaults to 'full'.
            depth: Number of layers. Defaults to 1.
            param: Parameters of the gates. Defaults to None.
            param_sharing: Whether gates in the same layer share a parameter. Defaults to False.
        """
        self.__num_qubits_update(qubits_idx)
        self.append(U3(qubits_idx, self.num_qubits, depth, param, param_sharing))

    def cnot(
            self, qubits_idx: Union[Iterable[int], str] = 'cycle', depth: int = 1
    ) -> None:
        r"""Add CNOT gates.

        For a 2-qubit quantum circuit, when `qubits_idx` is `[0, 1]`, the matrix form of such a gate is:

        .. math::

            \begin{align}
                \mathit{CNOT} &=|0\rangle \langle 0|\otimes I + |1 \rangle \langle 1|\otimes X\\
                &=
                \begin{bmatrix}
                    1 & 0 & 0 & 0 \\
                    0 & 1 & 0 & 0 \\
                    0 & 0 & 0 & 1 \\
                    0 & 0 & 1 & 0
                \end{bmatrix}
            \end{align}

        Args:
            qubits_idx: Indices of the qubits on which the gates are applied. Defaults to 'cycle'.
            depth: Number of layers. Defaults to 1.
        """
        self.__num_qubits_update(qubits_idx)
        if qubits_idx == "cycle" and not self.__isdynamic:
            self.append(CNOT(qubits_idx, self.num_qubits, depth, self.__cnot_cycle_idx))
        else:
            self.append(CNOT(qubits_idx, self.num_qubits, depth))

    def cy(
            self, qubits_idx: Union[Iterable[int], str] = 'cycle', depth: int = 1
    ) -> None:
        r"""Add controlled Y gates.

        For a 2-qubit quantum circuit, when `qubits_idx` is `[0, 1]`, the matrix form of such a gate is:

        .. math::

            \begin{align}
                \mathit{CY} &=|0\rangle \langle 0|\otimes I + |1 \rangle \langle 1|\otimes Y\\
                &=
                \begin{bmatrix}
                    1 & 0 & 0 & 0 \\
                    0 & 1 & 0 & 0 \\
                    0 & 0 & 0 & -1j \\
                    0 & 0 & 1j & 0
                \end{bmatrix}
            \end{align}

        Args:
            qubits_idx: Indices of the qubits on which the gates are applied. Defaults to 'cycle'.
            depth: Number of layers. Defaults to 1.
        """
        self.__num_qubits_update(qubits_idx)
        self.append(CY(qubits_idx, self.num_qubits, depth))

    def cz(
            self, qubits_idx: Union[Iterable[int], str] = 'linear', depth: int = 1
    ) -> None:
        r"""Add controlled Z gates.

        For a 2-qubit quantum circuit, when `qubits_idx` is `[0, 1]`, the matrix form of such a gate is:

        .. math::

            \begin{align}
                \mathit{CZ} &=|0\rangle \langle 0|\otimes I + |1 \rangle \langle 1|\otimes Z\\
                &=
                \begin{bmatrix}
                    1 & 0 & 0 & 0 \\
                    0 & 1 & 0 & 0 \\
                    0 & 0 & 1 & 0 \\
                    0 & 0 & 0 & -1
                \end{bmatrix}
            \end{align}

        Args:
            qubits_idx: Indices of the qubits on which the gates are applied. Defaults to 'linear'.
            depth: Number of layers. Defaults to 1.
        """
        self.__num_qubits_update(qubits_idx)
        self.append(CZ(qubits_idx, self.num_qubits, depth))

    def swap(
            self, qubits_idx: Union[Iterable[int], str] = 'linear', depth: int = 1
    ) -> None:
        r"""Add SWAP gates.

        The matrix form of such a gate is:

        .. math::

            \begin{align}
                \mathit{SWAP} =
                \begin{bmatrix}
                    1 & 0 & 0 & 0 \\
                    0 & 0 & 1 & 0 \\
                    0 & 1 & 0 & 0 \\
                    0 & 0 & 0 & 1
                \end{bmatrix}
            \end{align}

        Args:
            qubits_idx: Indices of the qubits on which the gates are applied. Defaults to 'linear'.
            depth: Number of layers. Defaults to 1.
        """
        self.__num_qubits_update(qubits_idx)
        self.append(SWAP(qubits_idx, self.num_qubits, depth))

    def cp(
            self, qubits_idx: Union[Iterable[int], str] = 'cycle', depth: int = 1,
            param: Union[paddle.Tensor, float] = None, param_sharing: bool = False
    ) -> None:
        r"""Add controlled P gates.

        For a 2-qubit quantum circuit, when `qubits_idx` is `[0, 1]`, the matrix form of such a gate is:

        .. math::

            \begin{align}
                \mathit{CP}(\theta) =
                \begin{bmatrix}
                    1 & 0 & 0 & 0 \\
                    0 & 1 & 0 & 0 \\
                    0 & 0 & 1 & 0 \\
                    0 & 0 & 0 & e^{i\theta}
                \end{bmatrix}
            \end{align}

        Args:
            qubits_idx: Indices of the qubits on which the gates are applied. Defaults to 'cycle'.
            depth: Number of layers. Defaults to 1.
            param: Parameters of the gates. Defaults to None.
            param_sharing: Whether gates in the same layer share a parameter. Defaults to False.
        """
        self.__num_qubits_update(qubits_idx)
        self.append(CP(qubits_idx, self.num_qubits, depth, param, param_sharing))

    def crx(
            self, qubits_idx: Union[Iterable[int], str] = 'cycle', depth: int = 1,
            param: Union[paddle.Tensor, float] = None, param_sharing: bool = False
    ) -> None:
        r"""Add controlled rotation gates about the x-axis.

        For a 2-qubit quantum circuit, when `qubits_idx` is `[0, 1]`, the matrix form of such a gate is:

        .. math::

            \begin{align}
                \mathit{CR_X} &=|0\rangle \langle 0|\otimes I + |1 \rangle \langle 1|\otimes R_X\\
                &=
                \begin{bmatrix}
                    1 & 0 & 0 & 0 \\
                    0 & 1 & 0 & 0 \\
                    0 & 0 & \cos\frac{\theta}{2} & -i\sin\frac{\theta}{2} \\
                    0 & 0 & -i\sin\frac{\theta}{2} & \cos\frac{\theta}{2}
                \end{bmatrix}
            \end{align}

        Args:
            qubits_idx: Indices of the qubits on which the gates are applied. Defaults to 'cycle'.
            depth: Number of layers. Defaults to 1.
            param: Parameters of the gates. Defaults to None.
            param_sharing: Whether gates in the same layer share a parameter. Defaults to False.
        """
        self.__num_qubits_update(qubits_idx)
        self.append(CRX(qubits_idx, self.num_qubits, depth, param, param_sharing))

    def cry(
            self, qubits_idx: Union[Iterable[int], str] = 'cycle', depth: int = 1,
            param: Union[paddle.Tensor, float] = None, param_sharing: bool = False
    ) -> None:
        r"""Add controlled rotation gates about the y-axis.

        For a 2-qubit quantum circuit, when `qubits_idx` is `[0, 1]`, the matrix form of such a gate is:

        .. math::

            \begin{align}
                \mathit{CR_Y} &=|0\rangle \langle 0|\otimes I + |1 \rangle \langle 1|\otimes R_Y\\
                &=
                \begin{bmatrix}
                    1 & 0 & 0 & 0 \\
                    0 & 1 & 0 & 0 \\
                    0 & 0 & \cos\frac{\theta}{2} & -\sin\frac{\theta}{2} \\
                    0 & 0 & \sin\frac{\theta}{2} & \cos\frac{\theta}{2}
                \end{bmatrix}
            \end{align}

        Args:
            qubits_idx: Indices of the qubits on which the gates are applied. Defaults to 'cycle'.
            depth: Number of layers. Defaults to 1.
            param: Parameters of the gates. Defaults to None.
            param_sharing: Whether gates in the same layer share a parameter. Defaults to False.
        """
        self.__num_qubits_update(qubits_idx)
        self.append(CRY(qubits_idx, self.num_qubits, depth, param, param_sharing))

    def crz(
            self, qubits_idx: Union[Iterable[int], str] = 'cycle', depth: int = 1,
            param: Union[paddle.Tensor, float] = None, param_sharing: bool = False
    ) -> None:
        r"""Add controlled rotation gates about the z-axis.

        For a 2-qubit quantum circuit, when `qubits_idx` is `[0, 1]`, the matrix form of such a gate is:

        .. math::

            \begin{align}
                \mathit{CR_Z} &= |0\rangle \langle 0|\otimes I + |1 \rangle \langle 1|\otimes R_Z\\
                &=
                \begin{bmatrix}
                    1 & 0 & 0 & 0 \\
                    0 & 1 & 0 & 0 \\
                    0 & 0 & e^{-i\frac{\theta}{2}} & 0 \\
                    0 & 0 & 0 & e^{i\frac{\theta}{2}}
                \end{bmatrix}
            \end{align}

        Args:
            qubits_idx: Indices of the qubits on which the gates are applied. Defaults to 'cycle'.
            depth: Number of layers. Defaults to 1.
            param: Parameters of the gates. Defaults to None.
            param_sharing: Whether gates in the same layer share a parameter. Defaults to False.
        """
        self.__num_qubits_update(qubits_idx)
        self.append(CRZ(qubits_idx, self.num_qubits, depth, param, param_sharing))

    def cu(
            self, qubits_idx: Union[Iterable[int], str] = 'cycle', depth: int = 1,
            param: Union[paddle.Tensor, float] = None, param_sharing: bool = False
    ) -> None:
        r"""Add controlled single-qubit rotation gates.

        For a 2-qubit quantum circuit, when `qubits_idx` is `[0, 1]`, the matrix form of such a gate is:

        .. math::

            \begin{align}
                \mathit{CU}
                &=
                \begin{bmatrix}
                    1 & 0 & 0 & 0 \\
                    0 & 1 & 0 & 0 \\
                    0 & 0 & \cos\frac\theta2 &-e^{i\lambda}\sin\frac\theta2 \\
                    0 & 0 & e^{i\phi}\sin\frac\theta2&e^{i(\phi+\lambda)}\cos\frac\theta2
                \end{bmatrix}
            \end{align}

        Args:
            qubits_idx: Indices of the qubits on which the gates are applied. Defaults to 'cycle'.
            depth: Number of layers. Defaults to 1.
            param: Parameters of the gates. Defaults to None.
            param_sharing: Whether gates in the same layer share a parameter. Defaults to False.
        """
        self.__num_qubits_update(qubits_idx)
        self.append(CU(qubits_idx, self.num_qubits, depth, param, param_sharing))

    def rxx(
            self, qubits_idx: Union[Iterable[int], str] = 'linear', depth: int = 1,
            param: Union[paddle.Tensor, float] = None, param_sharing: bool = False
    ) -> None:
        r"""Add RXX gates.

        The matrix form of such a gate is:

        .. math::

            \begin{align}
                \mathit{R_{XX}}(\theta) =
                    \begin{bmatrix}
                        \cos\frac{\theta}{2} & 0 & 0 & -i\sin\frac{\theta}{2} \\
                        0 & \cos\frac{\theta}{2} & -i\sin\frac{\theta}{2} & 0 \\
                        0 & -i\sin\frac{\theta}{2} & \cos\frac{\theta}{2} & 0 \\
                        -i\sin\frac{\theta}{2} & 0 & 0 & \cos\frac{\theta}{2}
                    \end{bmatrix}
            \end{align}

        Args:
            qubits_idx: Indices of the qubits on which the gates are applied. Defaults to 'linear'.
            depth: Number of layers. Defaults to 1.
            param: Parameters of the gates. Defaults to None.
            param_sharing: Whether gates in the same layer share a parameter. Defaults to False.
        """
        self.__num_qubits_update(qubits_idx)
        self.append(RXX(qubits_idx, self.num_qubits, depth, param, param_sharing))

    def ryy(
            self, qubits_idx: Union[Iterable[int], str] = 'linear', depth: int = 1,
            param: Union[paddle.Tensor, float] = None, param_sharing: bool = False
    ) -> None:
        r"""Add RYY gates.

        The matrix form of such a gate is:

        .. math::

            \begin{align}
                \mathit{R_{YY}}(\theta) =
                    \begin{bmatrix}
                        \cos\frac{\theta}{2} & 0 & 0 & i\sin\frac{\theta}{2} \\
                        0 & \cos\frac{\theta}{2} & -i\sin\frac{\theta}{2} & 0 \\
                        0 & -i\sin\frac{\theta}{2} & \cos\frac{\theta}{2} & 0 \\
                        i\sin\frac{\theta}{2} & 0 & 0 & cos\frac{\theta}{2}
                    \end{bmatrix}
            \end{align}

        Args:
            qubits_idx: Indices of the qubits on which the gates are applied. Defaults to 'linear'.
            depth: Number of layers. Defaults to 1.
            param: Parameters of the gates. Defaults to None.
            param_sharing: Whether gates in the same layer share a parameter. Defaults to False.
        """
        self.__num_qubits_update(qubits_idx)
        self.append(RYY(qubits_idx, self.num_qubits, depth, param, param_sharing))

    def rzz(
            self, qubits_idx: Union[Iterable[int], str] = 'linear', depth: int = 1,
            param: Union[paddle.Tensor, float] = None, param_sharing: bool = False
    ) -> None:
        r"""Add RZZ gates.

        The matrix form of such a gate is:

        .. math::

            \begin{align}
                \mathit{R_{ZZ}}(\theta) =
                    \begin{bmatrix}
                        e^{-i\frac{\theta}{2}} & 0 & 0 & 0 \\
                        0 & e^{i\frac{\theta}{2}} & 0 & 0 \\
                        0 & 0 & e^{i\frac{\theta}{2}} & 0 \\
                        0 & 0 & 0 & e^{-i\frac{\theta}{2}}
                    \end{bmatrix}
            \end{align}

        Args:
            qubits_idx: Indices of the qubits on which the gates are applied. Defaults to 'linear'.
            depth: Number of layers. Defaults to 1.
            param: Parameters of the gates. Defaults to None.
            param_sharing: Whether gates in the same layer share a parameter. Defaults to False.
        """
        self.__num_qubits_update(qubits_idx)
        self.append(RZZ(qubits_idx, self.num_qubits, depth, param, param_sharing))

    def ms(
            self, qubits_idx: Union[Iterable[int], str] = 'cycle', depth: int = 1
    ) -> None:
        r"""Add Mølmer-Sørensen (MS) gates.

        The matrix form of such a gate is:

        .. math::

            \begin{align}
                \mathit{MS} = \mathit{R_{XX}}(-\frac{\pi}{2}) = \frac{1}{\sqrt{2}}
                    \begin{bmatrix}
                        1 & 0 & 0 & i \\
                        0 & 1 & i & 0 \\
                        0 & i & 1 & 0 \\
                        i & 0 & 0 & 1
                    \end{bmatrix}
            \end{align}

        Args:
            qubits_idx: Indices of the qubits on which the gates are applied. Defaults to 'cycle'.
            depth: Number of layers. Defaults to 1.
        """
        self.__num_qubits_update(qubits_idx)
        self.append(MS(qubits_idx, self.num_qubits, depth))

    def cswap(
            self, qubits_idx: Union[Iterable[int], str] = 'cycle', depth: int = 1
    ) -> None:
        r"""Add CSWAP (Fredkin) gates.

        The matrix form of such a gate is:

        .. math::

            \begin{align}
                \mathit{CSWAP} =
                \begin{bmatrix}
                    1 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\
                    0 & 1 & 0 & 0 & 0 & 0 & 0 & 0 \\
                    0 & 0 & 1 & 0 & 0 & 0 & 0 & 0 \\
                    0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 \\
                    0 & 0 & 0 & 0 & 1 & 0 & 0 & 0 \\
                    0 & 0 & 0 & 0 & 0 & 0 & 1 & 0 \\
                    0 & 0 & 0 & 0 & 0 & 1 & 0 & 0 \\
                    0 & 0 & 0 & 0 & 0 & 0 & 0 & 1
                \end{bmatrix}
            \end{align}

        Args:
            qubits_idx: Indices of the qubits on which the gates are applied. Defaults to 'cycle'.
            depth: Number of layers. Defaults to 1.
        """
        self.__num_qubits_update(qubits_idx)
        self.append(CSWAP(qubits_idx, self.num_qubits, depth))

    def ccx(
            self, qubits_idx: Union[Iterable[int], str] = 'cycle', depth: int = 1
    ) -> None:
        r"""Add CCX (Toffoli) gates.

        The matrix form of such a gate is:

        .. math::

            \begin{align}
                    \mathit{CCX} = \begin{bmatrix}
                        1 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\
                        0 & 1 & 0 & 0 & 0 & 0 & 0 & 0 \\
                        0 & 0 & 1 & 0 & 0 & 0 & 0 & 0 \\
                        0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 \\
                        0 & 0 & 0 & 0 & 1 & 0 & 0 & 0 \\
                        0 & 0 & 0 & 0 & 0 & 1 & 0 & 0 \\
                        0 & 0 & 0 & 0 & 0 & 0 & 0 & 1 \\
                        0 & 0 & 0 & 0 & 0 & 0 & 1 & 0
                    \end{bmatrix}
                \end{align}

        Args:
            qubits_idx: Indices of the qubits on which the gates are applied. Defaults to 'cycle'.
            depth: Number of layers. Defaults to 1.
        """
        self.__num_qubits_update(qubits_idx)
        self.append(CCX(qubits_idx, self.num_qubits, depth))

    def universal_two_qubits(
            self, qubits_idx: Union[Iterable[int], str] = 'cycle', depth: int = 1,
            param: Union[paddle.Tensor, float] = None, param_sharing: bool = False
    ) -> None:
        r"""Add universal two-qubit gates. One of such a gate requires 15 parameters.

        Args:
            qubits_idx: Indices of the qubits on which the gates are applied. Defaults to 'cycle'.
            depth: Number of layers. Defaults to 1.
            param: Parameters of the gates. Defaults to None.
            param_sharing: Whether gates in the same layer share a parameter. Defaults to False.
        """
        self.__num_qubits_update(qubits_idx)
        self.append(UniversalTwoQubits(
            qubits_idx, self.num_qubits, depth, param, param_sharing))

    def universal_three_qubits(
            self, qubits_idx: Union[Iterable[int], str] = 'cycle', depth: int = 1,
            param: Union[paddle.Tensor, float] = None, param_sharing: bool = False
    ) -> None:
        r"""Add universal three-qubit gates. One of such a gate requires 81 parameters.

        Args:
            qubits_idx: Indices of the qubits on which the gates are applied. Defaults to 'cycle'.
            depth: Number of layers. Defaults to 1.
            param: Parameters of the gates. Defaults to None.
            param_sharing: Whether gates in the same layer share a parameter. Defaults to False.

        Raises:
            ValueError: The ``param`` must be paddle.Tensor or float.
        """
        self.__num_qubits_update(qubits_idx)
        self.append(UniversalThreeQubits(
            qubits_idx, self.num_qubits, depth, param, param_sharing))

    def oracle(
            self, oracle: paddle.tensor, qubits_idx: Union[Iterable[Iterable[int]], Iterable[int], int], depth: int = 1,
            gate_name: Optional[str] = 'O', latex_name: Optional[str] = None, plot_width: Optional[float] = None
    ) -> None:
        """Add an oracle gate.

        Args:
            oracle: Unitary oracle to be implemented.
            qubits_idx: Indices of the qubits on which the gates are applied.
            depth: Number of layers. Defaults to 1.
            gate_name: name of this oracle.
            latex_name: latex name of this oracle, default to be the gate name.
            plot_width: width of this gate in circuit plot, default to be proportional with the gate name.
        """
        self.__num_qubits_update(qubits_idx)
        gate_info = {
            'gatename': gate_name,
            'texname': f"${gate_name}$" if latex_name is None else latex_name,
            'plot_width': 0.6 * len(gate_name) if plot_width is None else plot_width}
        self.append(Oracle(
            oracle, qubits_idx, self.num_qubits, depth, gate_info))

    def control_oracle(
            self, oracle: paddle.Tensor, qubits_idx: Union[Iterable[Iterable[int]], Iterable[int]], depth: int = 1,
            gate_name: Optional[str] = 'O', latex_name: Optional[str] = None, plot_width: Optional[float] = None
    ) -> None:
        """Add a controlled oracle gate.

        Args:
            oracle: Unitary oracle to be implemented.
            qubits_idx: Indices of the qubits on which the gates are applied.
            depth: Number of layers. Defaults to 1.
            gate_name: name of this oracle.
            latex_name: latex name of this oracle, default to be the gate name.
            plot_width: width of this gate in circuit plot, default to be proportional with the gate name.
        """
        self.__num_qubits_update(qubits_idx)
        gate_info = {
            'gatename': f"c{gate_name}",
            'texname': f"${gate_name}$" if latex_name is None else latex_name,
            'plot_width': 0.6 * len(gate_name) if plot_width is None else plot_width}
        self.append(ControlOracle(
            oracle, qubits_idx, self.num_qubits, depth, gate_info))

    def collapse(self, qubits_idx: Union[Iterable[int], int, str] = 'full',
                 desired_result: Union[int, str] = None, if_print: bool = False,
                 measure_basis: Union[Iterable[paddle.Tensor], str] = 'z') -> None:
        r"""
        Args:
            qubits_idx: list of qubits to be collapsed. Defaults to ``'full'``.
            desired_result: The desired result you want to collapse. Defaults to ``None`` meaning randomly choose one.
            if_print: whether print the information about the collapsed state. Defaults to ``False``.
            measure_basis: The basis of the measurement. The quantum state will collapse to the corresponding eigenstate.

        Raises:
            NotImplementedError: If the basis of measurement is not z. Other bases will be implemented in future.
            TypeError: cannot get probability of state when the backend is unitary_matrix.

        Note:
            When desired_result is `None`, Collapse does not support gradient calculation
        """
        self.__num_qubits_update(qubits_idx)
        self.append(Collapse(
            qubits_idx, self.num_qubits, desired_result, if_print, measure_basis))

    def superposition_layer(
            self, qubits_idx: Iterable[int] = None, depth: int = 1
    ) -> None:
        r"""Add layers of Hadamard gates.

        Args:
            qubits_idx: Indices of the qubits on which the gates are applied. Defaults to ``None``.
            depth: Number of layers. Defaults to 1.
        """
        self.__num_qubits_update(qubits_idx)
        self.extend(
            SuperpositionLayer(qubits_idx, self.num_qubits, depth))

    def weak_superposition_layer(
            self, qubits_idx: Iterable[int] = None, depth: int = 1
    ) -> None:
        r"""Add layers of Ry gates with a rotation angle :math:`\pi/4`.

        Args:
            qubits_idx: Indices of the qubits on which the gates are applied. Defaults to ``None``.
            depth: Number of layers. Defaults to 1.
        """
        self.__num_qubits_update(qubits_idx)
        self.extend(
            WeakSuperpositionLayer(qubits_idx, self.num_qubits, depth))

    def linear_entangled_layer(
            self, qubits_idx: Iterable[int] = None, depth: int = 1
    ) -> None:
        r"""Add linear entangled layers consisting of Ry gates, Rz gates, and CNOT gates.

        Args:
            qubits_idx: Indices of the qubits on which the gates are applied. Defaults to ``None``.
            depth: Number of layers. Defaults to 1.
        """
        self.__num_qubits_update(qubits_idx)
        self.extend(
            LinearEntangledLayer(qubits_idx, self.num_qubits, depth))

    def real_entangled_layer(
            self, qubits_idx: Iterable[int] = None, depth: int = 1
    ) -> None:
        r"""Add strongly entangled layers consisting of Ry gates and CNOT gates.

        Args:
            qubits_idx: Indices of the qubits on which the gates are applied. Defaults to ``None``.
            depth: Number of layers. Defaults to 1.
        """
        self.__num_qubits_update(qubits_idx)
        self.extend(
            RealEntangledLayer(qubits_idx, self.num_qubits, depth))

    def complex_entangled_layer(
            self, qubits_idx: Iterable[int] = None, depth: int = 1
    ) -> None:
        r"""Add strongly entangled layers consisting of single-qubit rotation gates and CNOT gates.

        Args:
            qubits_idx: Indices of the qubits on which the gates are applied. Defaults to ``None``.
            depth: Number of layers. Defaults to 1.
        """
        self.__num_qubits_update(qubits_idx)
        self.extend(
            ComplexEntangledLayer(qubits_idx, self.num_qubits, depth))

    def real_block_layer(
            self, qubits_idx: Iterable[int] = None, depth: int = 1
    ) -> None:
        r"""Add weakly entangled layers consisting of Ry gates and CNOT gates.

        Args:
            qubits_idx: Indices of the qubits on which the gates are applied. Defaults to ``None``.
            depth: Number of layers. Defaults to 1.
        """
        self.__num_qubits_update(qubits_idx)
        self.extend(
            RealBlockLayer(qubits_idx, self.num_qubits, depth))

    def complex_block_layer(
            self, qubits_idx: Iterable[int] = None, depth: int = 1
    ) -> None:
        r"""Add weakly entangled layers consisting of single-qubit rotation gates and CNOT gates.

        Args:
            qubits_idx: Indices of the qubits on which the gates are applied. Defaults to ``None``.
            depth: Number of layers. Defaults to 1.
        """
        self.__num_qubits_update(qubits_idx)
        self.extend(
            ComplexBlockLayer(qubits_idx, self.num_qubits, depth))

    def qaoa_layer(self, edges: Iterable, nodes: Iterable, depth: Optional[int] = 1) -> None:
        # TODO: see qaoa layer in layer.py
        self.__num_qubits_update(edges)
        self.__num_qubits_update(nodes)
        self.extend(QAOALayer(edges, nodes, depth))

    def bit_flip(
            self, prob: Union[paddle.Tensor, float],
            qubits_idx: Union[Iterable[int], int, str] = 'full'
    ) -> None:
        r"""Add bit flip channels.

        Args:
            prob: Probability of a bit flip.
            qubits_idx: Indices of the qubits on which the channels are applied. Defaults to 'full'.
        """
        self.__num_qubits_update(qubits_idx)
        self.append(BitFlip(prob, qubits_idx, self.num_qubits))

    def phase_flip(
            self, prob: Union[paddle.Tensor, float], qubits_idx: Union[Iterable[int], int, str] = 'full'
    ) -> None:
        r"""Add phase flip channels.

        Args:
            prob: Probability of a phase flip.
            qubits_idx: Indices of the qubits on which the channels are applied. Defaults to 'full'.
        """
        self.__num_qubits_update(qubits_idx)
        self.append(PhaseFlip(prob, qubits_idx,
                    self.num_qubits))

    def bit_phase_flip(
            self, prob: Union[paddle.Tensor, float], qubits_idx: Union[Iterable[int], int, str] = 'full'
    ) -> None:
        r"""Add bit phase flip channels.

        Args:
            prob: Probability of a bit phase flip.
            qubits_idx: Indices of the qubits on which the channels are applied. Defaults to 'full'.
        """
        self.__num_qubits_update(qubits_idx)
        self.append(BitPhaseFlip(prob, qubits_idx, self.num_qubits))

    def amplitude_damping(
            self, gamma: Union[paddle.Tensor, float],
            qubits_idx: Union[Iterable[int], int, str] = 'full'
    ) -> None:
        r"""Add amplitude damping channels.

        Args:
            gamma: Damping probability.
            qubits_idx: Indices of the qubits on which the channels are applied. Defaults to 'full'.
        """
        self.__num_qubits_update(qubits_idx)
        self.append(AmplitudeDamping(gamma, qubits_idx, self.num_qubits))

    def generalized_amplitude_damping(
            self, gamma: Union[paddle.Tensor, float], prob: Union[paddle.Tensor, float],
            qubits_idx: Union[Iterable[int], int, str] = 'full'
    ) -> None:
        r"""Add generalized amplitude damping channels.

        Args:
            gamma: Damping probability. Its value should be in the range :math:`[0, 1]`.
            prob: Excitation probability. Its value should be in the range :math:`[0, 1]`.
            qubits_idx: Indices of the qubits on which the channels are applied. Defaults to 'full'.
        """
        self.__num_qubits_update(qubits_idx)
        self.append(GeneralizedAmplitudeDamping(
            gamma, prob, qubits_idx, self.num_qubits))

    def phase_damping(
            self, gamma: Union[paddle.Tensor, float], qubits_idx: Union[Iterable[int], int, str] = 'full'
    ) -> None:
        r"""Add phase damping channels.

        Args:
            gamma: Parameter of the phase damping channel.
            qubits_idx: Indices of the qubits on which the channels are applied. Defaults to 'full'.
        """
        self.__num_qubits_update(qubits_idx)
        self.append(PhaseDamping(gamma, qubits_idx, self.num_qubits))

    def depolarizing(
            self, prob: Union[paddle.Tensor, float], qubits_idx: Union[Iterable[int], int, str] = 'full'
    ) -> None:
        r"""Add depolarizing channels.

        Args:
            prob: Parameter of the depolarizing channel.
            qubits_idx: Indices of the qubits on which the channels are applied. Defaults to 'full'.
        """
        self.__num_qubits_update(qubits_idx)
        self.append(Depolarizing(prob, qubits_idx, self.num_qubits))

    def generalized_depolarizing(
            self, prob: Union[paddle.Tensor, float], qubits_idx: Union[Iterable[int], int, str]
    ) -> None:
        r"""Add a general depolarizing channel.

        Args:
            prob: Probabilities corresponding to the Pauli basis.
            qubits_idx: Indices of the qubits on which the channel is applied.
        """
        self.__num_qubits_update(qubits_idx)
        self.append(GeneralizedDepolarizing(prob, qubits_idx, self.num_qubits))

    def pauli_channel(
            self, prob: Union[paddle.Tensor, float], qubits_idx: Union[Iterable[int], int, str] = 'full'
    ) -> None:
        r"""Add Pauli channels.

        Args:
            prob: Probabilities corresponding to the Pauli X, Y, and Z operators.
            qubits_idx: Indices of the qubits on which the channels are applied. Defaults to 'full'.
        """
        self.__num_qubits_update(qubits_idx)
        self.append(PauliChannel(prob, qubits_idx, self.num_qubits))

    def reset_channel(
            self, prob: Union[paddle.Tensor, float], qubits_idx: Union[Iterable[int], int, str] = 'full'
    ) -> None:
        r"""Add reset channels.

        Args:
            prob: Probabilities of resetting to :math:`|0\rangle` and to :math:`|1\rangle`.
            qubits_idx: Indices of the qubits on which the channels are applied. Defaults to 'full'.
        """
        self.__num_qubits_update(qubits_idx)
        self.append(ResetChannel(prob, qubits_idx, self.num_qubits))

    def thermal_relaxation(
            self, const_t: Union[paddle.Tensor, Iterable[float]], exec_time: Union[paddle.Tensor, float],
            qubits_idx: Union[Iterable[int], int, str] = 'full'
    ) -> None:
        r"""Add thermal relaxation channels.

        Args:
            const_t: :math:`T_1` and :math:`T_2` relaxation time in microseconds.
            exec_time: Quantum gate execution time in the process of relaxation in nanoseconds.
            qubits_idx: Indices of the qubits on which the channels are applied. Defaults to 'full'.
        """
        self.__num_qubits_update(qubits_idx)
        self.append(
            ThermalRelaxation(const_t, exec_time, qubits_idx, self.num_qubits))

    def mixed_unitary_channel(
            self, num_unitary: Union[paddle.Tensor, int],
            qubits_idx: Union[Iterable[int], int, str] = 'full'
    ) -> None:
        r"""Add mixed random unitary channels

        Args:
            num_unitary: The amount of random unitaries to be generated.
            qubits_idx: Indices of the qubits on which the channels act. Defaults to ``'full'``.
        """
        self.__num_qubits_update(qubits_idx)
        self.append(MixedUnitaryChannel(num_unitary, qubits_idx, self.num_qubits))

    def choi_channel(
            self, choi_repr: Iterable[paddle.Tensor],
            qubits_idx: Union[Iterable[Iterable[int]], Iterable[int], int]
    ) -> None:
        r"""Add custom channels in the Choi representation.

        Args:
            choi_repr: Choi representation of this channel.
            qubits_idx: Indices of the qubits on which the channels are applied.
        """
        self.__num_qubits_update(qubits_idx)
        self.append(ChoiRepr(choi_repr, qubits_idx, self.num_qubits))

    def kraus_channel(
            self, kraus_oper: Iterable[paddle.Tensor],
            qubits_idx: Union[Iterable[Iterable[int]], Iterable[int], int]
    ) -> None:
        r"""Add custom channels in the Kraus representation.

        Args:
            kraus_oper: Kraus representation of this channel.
            qubits_idx: Indices of the qubits on which the channels are applied.
        """
        self.__num_qubits_update(qubits_idx)
        self.append(KrausRepr(kraus_oper, qubits_idx, self.num_qubits))

    def stinespring_channel(
            self, stinespring_repr: Iterable[paddle.Tensor],
            qubits_idx: Union[Iterable[Iterable[int]], Iterable[int], int]
    ) -> None:
        r"""Add custom channels in the Stinespring representation.

        Args:
            stinespring_repr: Stinespring representation of this channel.
            qubits_idx: Indices of the qubits on which the channels are applied.
        """
        self.__num_qubits_update(qubits_idx)
        self.append(
            StinespringRepr(stinespring_repr, qubits_idx, self.num_qubits))

    def unitary_matrix(self) -> paddle.Tensor:
        r"""Get the unitary matrix form of the circuit.

        Returns:
            Unitary matrix form of the circuit.
        """
        backend = get_backend()
        self.to(backend=Backend.UnitaryMatrix)
        unitary = State(paddle.eye(
            2 ** self.num_qubits).cast(get_dtype()), backend=Backend.UnitaryMatrix)
        for layer in self._sub_layers.values():
            unitary = layer(unitary)
        self.to(backend=backend)
        return unitary.data

    @property
    def gate_history(self) -> List[Dict[str, Union[str, List[int], paddle.Tensor]]]:
        r"""List of gates information of circuit

        Returns:
            history of quantum gates of circuit

        """
        gate_history = []
        for op in self.sublayers():
            if isinstance(op, Layer):
                gate_history.extend(op.gate_history)
            else:
                if op.gate_info['gatename'] is None:
                    raise NotImplementedError(
                        f"{type(op)} has no gate name and hence cannot be recorded into history.")
                op.gate_history_generation()
                gate_history.extend(op.gate_history)
        return gate_history

    @property
    def depth(self) -> int:
        r"""Depth of gate sequences.
        
        Returns:
            depth of this circuit
        
        Note:
            The measurement is omitted, and all gates are assumed to have depth 1. 
            See Niel's answer in the [StackExchange](https://quantumcomputing.stackexchange.com/a/5772).
        
        """
        qubit_depth = np.array([0] * self.num_qubits)
        for gate_info in self.gate_history:
            qubits_idx = gate_info['which_qubits']
            if isinstance(qubits_idx, int):
                qubit_depth[qubits_idx] += 1
            else:
                qubit_depth[qubits_idx] = np.max(qubit_depth[qubits_idx]) + 1
        return int(np.max(qubit_depth))

    def __count_history(self, history):
        # Record length of each section
        length = [5]
        n = self.__num_qubits
        # Record current section number for every qubit
        qubit = [0] * n
        # Number of sections
        qubit_max = max(qubit)
        # Record section number for each gate
        gate = []
        for current_gate in history:
            # Single-qubit gates with no params to print
            if current_gate['gate'] in {'h', 's', 't', 'x', 'y', 'z', 'u', 'sdg', 'tdg'}:
                curr_qubit = current_gate['which_qubits']
                gate.append(qubit[curr_qubit])
                qubit[curr_qubit] = qubit[curr_qubit] + 1
                # A new section is added
                if qubit[curr_qubit] > qubit_max:
                    length.append(5)
                    qubit_max = qubit[curr_qubit]
            elif current_gate['gate'] in {'p', 'rx', 'ry', 'rz'}:
                curr_qubit = current_gate['which_qubits']
                gate.append(qubit[curr_qubit])
                if length[qubit[curr_qubit]] == 5:
                    length[qubit[curr_qubit]] = 13
                qubit[curr_qubit] = qubit[curr_qubit] + 1
                if qubit[curr_qubit] > qubit_max:
                    length.append(5)
                    qubit_max = qubit[curr_qubit]
            elif current_gate['gate'] in {
                'cnot', 'swap', 'rxx', 'ryy', 'rzz', 'ms',
                'cy', 'cz', 'cu', 'cp', 'crx', 'cry', 'crz', 'cswap', 'ccx'
            }:
                a = max(current_gate['which_qubits'])
                b = min(current_gate['which_qubits'])
                ind = max(qubit[b: a + 1])
                gate.append(ind)
                if length[ind] < 13 and current_gate['gate'] in {
                    'rxx', 'ryy', 'rzz', 'cp', 'crx', 'cry', 'crz'
                }:
                    length[ind] = 13
                for j in range(b, a + 1):
                    qubit[j] = ind + 1
                if ind + 1 > qubit_max:
                    length.append(5)
                    qubit_max = ind + 1

        return length, gate

    @property
    def qubit_history(self) -> List[List[Tuple[Dict[str, Union[str, List[int], paddle.Tensor]], int]]]:
        r""" gate information on each qubit

        Returns:
            list of gate history on each qubit

        Note:
            The entry ``qubit_history[i][j][0/1]`` returns the gate information / gate index of the j-th gate
            applied on the i-th qubit.
        """
        history_qubit = [[] for _ in range(self.num_qubits)]
        for idx, i in enumerate(self.gate_history):
            qubits = i["which_qubits"]
            if not isinstance(qubits, Iterable):
                history_qubit[qubits].append([i, idx])
            else:
                for j in qubits:
                    history_qubit[j].append([i, idx])
        return history_qubit

    def __str__(self):
        history = self.gate_history
        num_qubits = self.__num_qubits
        length, gate = self.__count_history(history)
        # Ignore the unused section
        total_length = sum(length) - 5
        print_list = [['-' if i % 2 == 0 else ' '] *
                      total_length for i in range(num_qubits * 2)]
        for i, current_gate in enumerate(history):
            if current_gate['gate'] in {'h', 's', 't', 'x', 'y', 'z', 'u'}:
                # Calculate starting position ind of current gate
                sec = gate[i]
                ind = sum(length[:sec])
                print_list[current_gate['which_qubits'] * 2][ind +
                                                             length[sec] // 2] = current_gate['gate'].upper()
            elif current_gate['gate'] in {'sdg'}:
                sec = gate[i]
                ind = sum(length[:sec])
                print_list[current_gate['which_qubits'] * 2][
                    ind + length[sec] // 2 - 1: ind + length[sec] // 2 + 2] = current_gate['gate'].upper()
            elif current_gate['gate'] in {'tdg'}:
                sec = gate[i]
                ind = sum(length[:sec])
                print_list[current_gate['which_qubits'] * 2][
                    ind + length[sec] // 2 - 1: ind + length[sec] // 2 + 2] = current_gate['gate'].upper()
            elif current_gate['gate'] in {'p', 'rx', 'ry', 'rz'}:
                sec = gate[i]
                ind = sum(length[:sec])
                line = current_gate['which_qubits'] * 2
                # param = self.__param[current_gate['theta'][2 if current_gate['gate'] == 'rz' else 0]]
                param = current_gate['theta']
                if current_gate['gate'] == 'p':
                    print_list[line][ind + 2] = 'P'
                    print_list[line][ind + 3] = ' '
                else:
                    print_list[line][ind + 2] = 'R'
                    print_list[line][ind + 3] = current_gate['gate'][1]
                print_list[line][ind + 4] = '('
                print_list[line][ind + 5: ind +
                                 10] = format(float(param.numpy()), '.3f')[:5]
                print_list[line][ind + 10] = ')'
            # Two-qubit gates
            elif current_gate['gate'] in {'cnot', 'swap', 'rxx', 'ryy', 'rzz', 'ms', 'cz', 'cy',
                                          'cu', 'cp', 'crx', 'cry', 'crz'}:
                sec = gate[i]
                ind = sum(length[:sec])
                cqubit = current_gate['which_qubits'][0]
                tqubit = current_gate['which_qubits'][1]
                if current_gate['gate'] in {'cnot', 'swap', 'cy', 'cz', 'cu'}:
                    print_list[cqubit * 2][ind + length[sec] // 2] = \
                        '*' if current_gate['gate'] in {'cnot',
                                                        'cy', 'cz', 'cu'} else 'x'
                    print_list[tqubit * 2][ind + length[sec] // 2] = \
                        'x' if current_gate['gate'] in {
                            'swap', 'cnot'} else current_gate['gate'][1]
                elif current_gate['gate'] == 'ms':
                    for qubit in {cqubit, tqubit}:
                        print_list[qubit * 2][ind + length[sec] // 2 - 1] = 'M'
                        print_list[qubit * 2][ind + length[sec] // 2] = '_'
                        print_list[qubit * 2][ind + length[sec] // 2 + 1] = 'S'
                elif current_gate['gate'] in {'rxx', 'ryy', 'rzz'}:
                    # param = self.__param[current_gate['theta'][0]]
                    param = current_gate['theta']
                    for line in {cqubit * 2, tqubit * 2}:
                        print_list[line][ind + 2] = 'R'
                        print_list[line][ind + 3: ind +
                                         5] = current_gate['gate'][1:3].lower()
                        print_list[line][ind + 5] = '('
                        print_list[line][ind + 6: ind +
                                         10] = format(float(param.numpy()), '.2f')[:4]
                        print_list[line][ind + 10] = ')'
                elif current_gate['gate'] in {'crx', 'cry', 'crz'}:
                    param = current_gate['theta']
                    print_list[cqubit * 2][ind + length[sec] // 2] = '*'
                    print_list[tqubit * 2][ind + 2] = 'R'
                    print_list[tqubit * 2][ind + 3] = current_gate['gate'][2]
                    print_list[tqubit * 2][ind + 4] = '('
                    print_list[tqubit * 2][ind + 5: ind +
                                           10] = format(float(param.numpy()), '.3f')[:5]
                    print_list[tqubit * 2][ind + 10] = ')'
                elif current_gate['gate'] == 'cp':
                    param = current_gate['theta']
                    print_list[cqubit * 2][ind + length[sec] // 2] = '*'
                    print_list[tqubit * 2][ind + 2] = ' '
                    print_list[tqubit * 2][ind + 3] = 'P'
                    print_list[tqubit * 2][ind + 4] = '('
                    print_list[tqubit * 2][ind + 5: ind +
                                           10] = format(float(param.numpy()), '.3f')[:5]
                    print_list[tqubit * 2][ind + 10] = ')'
                    
                start_line = min(cqubit, tqubit)
                end_line = max(cqubit, tqubit)
                for k in range(start_line * 2 + 1, end_line * 2):
                    print_list[k][ind + length[sec] // 2] = '|'
            # Three-qubit gates
            elif current_gate['gate'] in {'cswap'}:
                sec = gate[i]
                ind = sum(length[:sec])
                cqubit = current_gate['which_qubits'][0]
                tqubit1 = current_gate['which_qubits'][1]
                tqubit2 = current_gate['which_qubits'][2]
                start_line = min(current_gate['which_qubits'])
                end_line = max(current_gate['which_qubits'])
                for k in range(start_line * 2 + 1, end_line * 2):
                    print_list[k][ind + length[sec] // 2] = '|'
                if current_gate['gate'] in {'cswap'}:
                    print_list[cqubit * 2][ind + length[sec] // 2] = '*'
                    print_list[tqubit1 * 2][ind + length[sec] // 2] = 'x'
                    print_list[tqubit2 * 2][ind + length[sec] // 2] = 'x'
            elif current_gate['gate'] in {'ccx'}:
                sec = gate[i]
                ind = sum(length[:sec])
                cqubit1 = current_gate['which_qubits'][0]
                cqubit2 = current_gate['which_qubits'][1]
                tqubit = current_gate['which_qubits'][2]
                start_line = min(current_gate['which_qubits'])
                end_line = max(current_gate['which_qubits'])
                for k in range(start_line * 2 + 1, end_line * 2):
                    print_list[k][ind + length[sec] // 2] = '|'
                if current_gate['gate'] in {'ccx'}:
                    print_list[cqubit1 * 2][ind + length[sec] // 2] = '*'
                    print_list[cqubit2 * 2][ind + length[sec] // 2] = '*'
                    print_list[tqubit * 2][ind + length[sec] // 2] = 'X'
            else:
                raise NotImplementedError(
                    f"Not support to print the gate {current_gate['gate']}.")

        print_list = list(map(''.join, print_list))
        return_str = '\n'.join(print_list)

        return return_str

    def plot(
            self,
            save_path: Optional[str] = None,
            dpi: Optional[int] = 100,
            show: Optional[bool] = True,
            output: Optional[bool] = False,
            scale: Optional[float] = 1.0,
            tex: Optional[bool] = False,
    ) -> Union[None, matplotlib.figure.Figure]:
        r'''display the circuit using matplotlib

        Args:
            save_path: the save path of image
            dpi: dots per inches, here is resolution ratio
            show: whether execute ``plt.show()``
            output: whether return the ``matplotlib.figure.Figure`` instance
            scale: scale coefficient of figure, default to 1.0
            tex: a bool flag which controls latex fonts of gate display, default to ``False``.

        Returns:
            a ``matplotlib.figure.Figure`` instance or ``None`` depends on ``output``

        Note:
            Using ``plt.show()`` may cause a distortion, but it will not happen in the figure saved.
            If the depth is too long, there will be some patches unable to display.
            Setting ``tex = True`` requires that you have TeX and the other dependencies properly 
            installed on your system. See 
            https://matplotlib.org/stable/gallery/text_labels_and_annotations/tex_demo.html
            for more details.
        '''
        _fig = _circuit_plot(self, dpi=dpi, scale=scale, tex=tex)
        if save_path:
            plt.savefig(save_path, dpi=dpi, )
        if show:  # whether display in window
            plt.show()
        if output:
            return _fig  # return the ``matplotlib.pyplot.figure`` instance

    def extend(self, cir):
        r""" extend for quantum circuit

        Args:
            cir: a Circuit or a Sequential

        Returns:
            concatenation of two quantum circuits
        """
        if isinstance(cir, Circuit):
            if self.__num_qubits is None:
                self.__num_qubits = cir.num_qubits
            else:
                self.__num_qubits = self.__num_qubits if cir.num_qubits is None else max(self.__num_qubits,
                                                                                         cir.num_qubits)
            super().extend(cir)
        elif isinstance(cir, Sequential):
            super().extend(cir)
        else:
            raise TypeError("the input type must be Circuit or Sequential")

    def forward(self, state: Optional[State] = None) -> State:
        r""" forward the input

        Args:
            state: initial state

        Returns:
            output quantum state
        """
        assert self.__num_qubits is not None, "Information about num_qubits is required before running the circuit"

        if state is None:
            state = zero_state(self.__num_qubits, self.backend, self.dtype)
        else:
            assert self.__num_qubits == state.num_qubits, \
                f"num_qubits does not agree: expected {self.__num_qubits}, received {state.num_qubits}"

        if self.backend == Backend.QuLeaf and state.backend == Backend.QuLeaf:
            state.oper_history = self.oper_history
            return state

        state = state.clone()
        state.is_swap_back = False
        state = super().forward(state)

        # reset state in the original qubit sequence
        state.reset_sequence()
        state.is_swap_back = True
        return state
