# Copyright (c) 2021 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.

from functools import reduce
from math import log2
from math import sqrt
import os.path
import copy
import re
import numpy as np
from scipy.linalg import logm, sqrtm
from matplotlib import colors as mplcolors
import matplotlib.pyplot as plt
import paddle
from paddle import add, to_tensor
from paddle import kron as kron
from paddle import matmul
from paddle import transpose
from paddle import concat, ones
from paddle import zeros
from scipy import sparse
import matplotlib as mpl
from paddle_quantum import simulator
import matplotlib.animation as animation

__all__ = [
    "partial_trace",
    "state_fidelity",
    "trace_distance",
    "gate_fidelity",
    "purity",
    "von_neumann_entropy",
    "relative_entropy",
    "NKron",
    "dagger",
    "random_pauli_str_generator",
    "pauli_str_to_matrix",
    "partial_transpose_2",
    "partial_transpose",
    "negativity",
    "logarithmic_negativity",
    "is_ppt",
    "haar_orthogonal",
    "haar_unitary",
    "haar_state_vector",
    "haar_density_operator",
    "Hamiltonian",
    "plot_n_qubit_state_in_bloch_sphere",
    "plot_state_in_bloch_sphere",
    "plot_rotation_in_bloch_sphere",
]


def partial_trace(rho_AB, dim1, dim2, A_or_B):
    r"""计算量子态的偏迹。

    Args:
        rho_AB (Tensor): 输入的量子态
        dim1 (int): 系统A的维数
        dim2 (int): 系统B的维数
        A_or_B (int): 1或者2，1表示计算系统A上的偏迹，2表示计算系统B上的偏迹

    Returns:
        Tensor: 输入的量子态的偏迹

    """
    if A_or_B == 2:
        dim1, dim2 = dim2, dim1

    idty_np = np.identity(dim2).astype("complex128")
    idty_B = to_tensor(idty_np)

    zero_np = np.zeros([dim2, dim2], "complex128")
    res = to_tensor(zero_np)

    for dim_j in range(dim1):
        row_top = zeros([1, dim_j], dtype="float64")
        row_mid = ones([1, 1], dtype="float64")
        row_bot = zeros([1, dim1 - dim_j - 1], dtype="float64")
        bra_j = concat([row_top, row_mid, row_bot], axis=1)
        bra_j = paddle.cast(bra_j, 'complex128')

        if A_or_B == 1:
            row_tmp = kron(bra_j, idty_B)
            row_tmp_conj = paddle.conj(row_tmp)
            res = add(res, matmul(matmul(row_tmp, rho_AB), transpose(row_tmp_conj, perm=[1, 0]), ), )

        if A_or_B == 2:
            row_tmp = kron(idty_B, bra_j)
            row_tmp_conj = paddle.conj(row_tmp)
            res = add(res, matmul(matmul(row_tmp, rho_AB), transpose(row_tmp_conj, perm=[1, 0]), ), )

    return res


def partial_trace_discontiguous(rho, preserve_qubits=None):
    r"""计算量子态的偏迹，可选取任意子系统。

    Args:
        rho (Tensor): 输入的量子态
        preserve_qubits (list): 要保留的量子比特，默认为 None，表示全保留
    """
    if preserve_qubits is None:
        return rho
    else:
        n = int(log2(rho.size) // 2)
        num_preserve = len(preserve_qubits)

        shape = paddle.ones((n + 1,))
        shape = 2 * shape
        shape[n] = 2 ** n
        shape = paddle.cast(shape, "int32")
        identity = paddle.eye(2 ** n)
        identity = paddle.reshape(identity, shape=shape)
        discard = list()
        for idx in range(0, n):
            if idx not in preserve_qubits:
                discard.append(idx)
        addition = [n]
        preserve_qubits.sort()

        preserve_qubits = paddle.to_tensor(preserve_qubits)
        discard = paddle.to_tensor(discard)
        addition = paddle.to_tensor(addition)
        permute = paddle.concat([discard, preserve_qubits, addition])

        identity = paddle.transpose(identity, perm=permute)
        identity = paddle.reshape(identity, (2 ** n, 2 ** n))

        result = np.zeros((2 ** num_preserve, 2 ** num_preserve), dtype="complex64")
        result = paddle.to_tensor(result)

        for i in range(0, 2 ** num_preserve):
            bra = identity[i * 2 ** num_preserve:(i + 1) * 2 ** num_preserve, :]
            result = result + matmul(matmul(bra, rho), transpose(bra, perm=[1, 0]))

        return result


def state_fidelity(rho, sigma):
    r"""计算两个量子态的保真度。

    .. math::
        F(\rho, \sigma) = \text{tr}(\sqrt{\sqrt{\rho}\sigma\sqrt{\rho}})

    Args:
        rho (numpy.ndarray): 量子态的密度矩阵形式
        sigma (numpy.ndarray): 量子态的密度矩阵形式

    Returns:
        float: 输入的量子态之间的保真度
    """
    assert rho.shape == sigma.shape, 'The shape of two quantum states are different'
    fidelity = np.trace(sqrtm(sqrtm(rho) @ sigma @ sqrtm(rho))).real

    return fidelity


def trace_distance(rho, sigma):
    r"""计算两个量子态的迹距离。

    .. math::
        D(\rho, \sigma) = 1 / 2 * \text{tr}|\rho-\sigma|

    Args:
        rho (numpy.ndarray): 量子态的密度矩阵形式
        sigma (numpy.ndarray): 量子态的密度矩阵形式

    Returns:
        float: 输入的量子态之间的迹距离
    """
    assert rho.shape == sigma.shape, 'The shape of two quantum states are different'
    A = rho - sigma
    distance = 1 / 2 * np.sum(np.abs(np.linalg.eigvals(A)))

    return distance


def gate_fidelity(U, V):
    r"""计算两个量子门的保真度。

    .. math::

        F(U, V) = |\text{tr}(UV^\dagger)|/2^n

    :math:`U` 是一个 :math:`2^n\times 2^n` 的 Unitary 矩阵。

    Args:
        U (numpy.ndarray): 量子门 :math:`U` 的酉矩阵形式
        V (numpy.ndarray): 量子门 :math:`V` 的酉矩阵形式

    Returns:
        float: 输入的量子门之间的保真度
    """
    assert U.shape == V.shape, 'The shape of two unitary matrices are different'
    dim = U.shape[0]
    fidelity = np.absolute(np.trace(np.matmul(U, V.conj().T))) / dim

    return fidelity


def purity(rho):
    r"""计算量子态的纯度。

    .. math::

        P = \text{tr}(\rho^2)

    Args:
        rho (numpy.ndarray): 量子态的密度矩阵形式

    Returns:
        float: 输入的量子态的纯度
    """
    gamma = np.trace(np.matmul(rho, rho))

    return gamma.real


def von_neumann_entropy(rho):
    r"""计算量子态的冯诺依曼熵。

    .. math::

        S = -\text{tr}(\rho \log(\rho))

    Args:
        rho(numpy.ndarray): 量子态的密度矩阵形式

    Returns:
        float: 输入的量子态的冯诺依曼熵
    """
    rho_eigenvalue, _ = np.linalg.eig(rho)
    entropy = -np.sum(rho_eigenvalue * np.log(rho_eigenvalue))

    return entropy.real


def relative_entropy(rho, sig):
    r"""计算两个量子态的相对熵。

    .. math::

        S(\rho \| \sigma)=\text{tr} \rho(\log \rho-\log \sigma)

    Args:
        rho (numpy.ndarray): 量子态的密度矩阵形式
        sig (numpy.ndarray): 量子态的密度矩阵形式

    Returns:
        float: 输入的量子态之间的相对熵
    """
    assert rho.shape == sig.shape, 'The shape of two quantum states are different'
    res = np.trace(rho @ logm(rho) - rho @ logm(sig))
    return res.real


def NKron(matrix_A, matrix_B, *args):
    r"""计算两个及以上的矩阵的Kronecker积。

    Args:
        matrix_A (numpy.ndarray): 一个矩阵
        matrix_B (numpy.ndarray): 一个矩阵
        *args (numpy.ndarray): 其余矩阵

    Returns:
        Tensor: 输入矩阵的Kronecker积

    .. code-block:: python
  
        from paddle_quantum.state import density_op_random
        from paddle_quantum.utils import NKron
        A = density_op_random(2)
        B = density_op_random(2)
        C = density_op_random(2)
        result = NKron(A, B, C)

    ``result`` 应为 :math:`A \otimes B \otimes C`
    """
    return reduce(lambda result, index: np.kron(result, index), args, np.kron(matrix_A, matrix_B), )


def dagger(matrix):
    r"""计算矩阵的埃尔米特转置，即Hermitian transpose。

    Args:
        matrix (Tensor): 需要埃尔米特转置的矩阵

    Returns:
        Tensor: 输入矩阵的埃尔米特转置

    代码示例:

    .. code-block:: python
  
        from paddle_quantum.utils import dagger
        import numpy as np
        rho = paddle.to_tensor(np.array([[1+1j, 2+2j], [3+3j, 4+4j]]))
        print(dagger(rho).numpy())

    ::

        [[1.-1.j 3.-3.j]
        [2.-2.j 4.-4.j]]
    """
    matrix_conj = paddle.conj(matrix)
    matrix_dagger = transpose(matrix_conj, perm=[1, 0])

    return matrix_dagger


def random_pauli_str_generator(n, terms=3):
    r"""随机生成一个可观测量（observable）的列表（ ``list`` ）形式。

    一个可观测量 :math:`O=0.3X\otimes I\otimes I+0.5Y\otimes I\otimes Z` 的
    列表形式为 ``[[0.3, 'x0'], [0.5, 'y0,z2']]`` 。这样一个可观测量是由
    调用 ``random_pauli_str_generator(3, terms=2)`` 生成的。

    Args:
        n (int): 量子比特数量
        terms (int, optional): 可观测量的项数

    Returns:
        list: 随机生成的可观测量的列表形式
    """
    pauli_str = []
    for sublen in np.random.randint(1, high=n + 1, size=terms):
        # Tips: -1 <= coeff < 1
        coeff = np.random.rand() * 2 - 1
        ops = np.random.choice(['x', 'y', 'z'], size=sublen)
        pos = np.random.choice(range(n), size=sublen, replace=False)
        op_list = [ops[i] + str(pos[i]) for i in range(sublen)]
        pauli_str.append([coeff, ','.join(op_list)])
    return pauli_str


def pauli_str_to_matrix(pauli_str, n):
    r"""将输入的可观测量（observable）的列表（ ``list`` ）形式转换为其矩阵形式。

    如输入的 ``pauli_str`` 为 ``[[0.7, 'z0,x1'], [0.2, 'z1']]`` 且 ``n=3`` ,
    则此函数返回可观测量 :math:`0.7Z\otimes X\otimes I+0.2I\otimes Z\otimes I` 的
    矩阵形式。

    Args:
        pauli_str (list): 一个可观测量的列表形式
        n (int): 量子比特数量

    Returns:
        numpy.ndarray: 输入列表对应的可观测量的矩阵形式
    """
    pauli_dict = {'i': np.eye(2) + 0j, 'x': np.array([[0, 1], [1, 0]]) + 0j,
                  'y': np.array([[0, -1j], [1j, 0]]), 'z': np.array([[1, 0], [0, -1]]) + 0j}

    # Parse pauli_str; 'x0,z1,y4' to 'xziiy'
    new_pauli_str = []
    for coeff, op_str in pauli_str:
        init = list('i' * n)
        op_list = re.split(r',\s*', op_str.lower())
        for op in op_list:
            if len(op) > 1:
                pos = int(op[1:])
                assert pos < n, 'n is too small'
                init[pos] = op[0]
            elif op.lower() != 'i':
                raise ValueError('Only Pauli operator "I" can be accepted without specifying its position')
        new_pauli_str.append([coeff, ''.join(init)])

    # Convert new_pauli_str to matrix; 'xziiy' to NKron(x, z, i, i, y)
    matrices = []
    for coeff, op_str in new_pauli_str:
        sub_matrices = []
        for op in op_str:
            sub_matrices.append(pauli_dict[op.lower()])
        if len(op_str) == 1:
            matrices.append(coeff * sub_matrices[0])
        else:
            matrices.append(coeff * NKron(sub_matrices[0], sub_matrices[1], *sub_matrices[2:]))

    return sum(matrices)


def partial_transpose_2(density_op, sub_system=None):
    r"""计算输入量子态的 partial transpose :math:`\rho^{T_A}`

    Args:
        density_op (numpy.ndarray): 量子态的密度矩阵形式
        sub_system (int): 1或2，表示关于哪个子系统进行 partial transpose，默认为第二个

    Returns:
        float: 输入的量子态的 partial transpose

    代码示例:

    .. code-block:: python

        from paddle_quantum.utils import partial_transpose_2
        rho_test = np.arange(1,17).reshape(4,4)
        partial_transpose_2(rho_test, sub_system=1)

    ::

       [[ 1,  2,  9, 10],
        [ 5,  6, 13, 14],
        [ 3,  4, 11, 12],
        [ 7,  8, 15, 16]]
    """
    sys_idx = 2 if sub_system is None else 1

    # Copy the density matrix and not corrupt the original one
    transposed_density_op = np.copy(density_op)
    if sys_idx == 2:
        for j in [0, 2]:
            for i in [0, 2]:
                transposed_density_op[i:i + 2, j:j + 2] = density_op[i:i + 2, j:j + 2].transpose()
    else:
        transposed_density_op[2:4, 0:2] = density_op[0:2, 2:4]
        transposed_density_op[0:2, 2:4] = density_op[2:4, 0:2]

    return transposed_density_op


def partial_transpose(density_op, n):
    r"""计算输入量子态的 partial transpose :math:`\rho^{T_A}`。

    Args:
        density_op (numpy.ndarray): 量子态的密度矩阵形式

    Returns:
        float: 输入的量子态的 partial transpose
    """

    # Copy the density matrix and not corrupt the original one
    transposed_density_op = np.copy(density_op)
    for j in range(0, 2 ** n, 2):
        for i in range(0, 2 ** n, 2):
            transposed_density_op[i:i + 2, j:j + 2] = density_op[i:i + 2, j:j + 2].transpose()

    return transposed_density_op


def negativity(density_op):
    r"""计算输入量子态的 Negativity :math:`N = ||\frac{\rho^{T_A}-1}{2}||`。

    Args:
        density_op (numpy.ndarray): 量子态的密度矩阵形式

    Returns:
        float: 输入的量子态的 Negativity

    代码示例:

    .. code-block:: python

        from paddle_quantum.utils import negativity
        from paddle_quantum.state import bell_state
        rho = bell_state(2)
        print("Negativity of the Bell state is:", negativity(rho))

    ::

        Negativity of the Bell state is: 0.5
    """
    # Implement the partial transpose
    density_op_T = partial_transpose_2(density_op)

    # Calculate through the equivalent expression N = sum(abs(\lambda_i)) when \lambda_i<0
    n = 0
    eigen_val, _ = np.linalg.eig(density_op_T)
    for val in eigen_val:
        if val < 0:
            n = n + np.abs(val)
    return n


def logarithmic_negativity(density_op):
    r"""计算输入量子态的 Logarithmic Negativity :math:`E_N = ||\rho^{T_A}||`。

    Args:
        density_op (numpy.ndarray): 量子态的密度矩阵形式

    Returns:
        float: 输入的量子态的 Logarithmic Negativity

    代码示例:

    .. code-block:: python

        from paddle_quantum.utils import logarithmic_negativity
        from paddle_quantum.state import bell_state
        rho = bell_state(2)
        print("Logarithmic negativity of the Bell state is:", logarithmic_negativity(rho))

    ::

        Logarithmic negativity of the Bell state is: 1.0
    """
    # Calculate the negativity
    n = negativity(density_op)

    # Calculate through the equivalent expression
    log2_n = np.log2(2 * n + 1)
    return log2_n


def is_ppt(density_op):
    r"""计算输入量子态是否满足 PPT 条件。

    Args:
        density_op (numpy.ndarray): 量子态的密度矩阵形式

    Returns:
        bool: 输入的量子态是否满足 PPT 条件

    代码示例:

    .. code-block:: python

        from paddle_quantum.utils import is_ppt
        from paddle_quantum.state import bell_state
        rho = bell_state(2)
        print("Whether the Bell state satisfies PPT condition:", is_ppt(rho))

    ::

        Whether the Bell state satisfies PPT condition: False
    """
    # By default the PPT condition is satisfied
    ppt = True

    # Detect negative eigenvalues from the partial transposed density_op
    if negativity(density_op) > 0:
        ppt = False
    return ppt


def haar_orthogonal(n):
    r"""生成一个服从 Haar random 的正交矩阵。采样算法参考文献：arXiv:math-ph/0609050v2

        Args:
            n (int): 正交矩阵对应的量子比特数

        Returns:
            numpy.ndarray: 一个形状为 ``(2**n, 2**n)`` 随机正交矩阵
    """
    # Matrix dimension
    d = 2 ** n
    # Step 1: sample from Ginibre ensemble
    g = (np.random.randn(d, d))
    # Step 2: perform QR decomposition of G
    q, r = np.linalg.qr(g)
    # Step 3: make the decomposition unique
    lam = np.diag(r) / abs(np.diag(r))
    u = q @ np.diag(lam)

    return u


def haar_unitary(n):
    r"""生成一个服从 Haar random 的酉矩阵。采样算法参考文献：arXiv:math-ph/0609050v2

        Args:
            n (int): 酉矩阵对应的量子比特数

        Returns:
            numpy.ndarray: 一个形状为 ``(2**n, 2**n)`` 随机酉矩阵
    """
    # Matrix dimension
    d = 2 ** n
    # Step 1: sample from Ginibre ensemble
    g = (np.random.randn(d, d) + 1j * np.random.randn(d, d)) / np.sqrt(2)
    # Step 2: perform QR decomposition of G
    q, r = np.linalg.qr(g)
    # Step 3: make the decomposition unique
    lam = np.diag(r) / abs(np.diag(r))
    u = q @ np.diag(lam)

    return u


def haar_state_vector(n, real=False):
    r"""生成一个服从 Haar random 的态矢量。

        Args:
            n (int): 量子态的量子比特数
            real (bool): 生成的态矢量是否为实态矢量，默认为 ``False``

        Returns:
            numpy.ndarray: 一个形状为 ``(2**n, 1)`` 随机态矢量
    """
    # Vector dimension
    d = 2 ** n
    if real:
        # Generate a Haar random orthogonal matrix
        o = haar_orthogonal(n)
        # Perform u onto |0>, i.e., the first column of o
        phi = o[:, 0]
    else:
        # Generate a Haar random unitary
        u = haar_unitary(n)
        # Perform u onto |0>, i.e., the first column of u
        phi = u[:, 0]

    return phi


def haar_density_operator(n, k=None, real=False):
    r"""生成一个服从 Haar random 的密度矩阵。

        Args:
            n (int): 量子态的量子比特数
            k (int): 密度矩阵的秩，默认为 ``None``，表示满秩
            real (bool): 生成的密度矩阵是否为实矩阵，默认为 ``False``

        Returns:
            numpy.ndarray: 一个形状为 ``(2**n, 2**n)`` 随机密度矩阵
    """
    d = 2 ** n
    k = k if k is not None else d
    assert 0 < k <= d, 'rank is an invalid number'
    if real:
        ginibre_matrix = np.random.randn(d, k)
        rho = ginibre_matrix @ ginibre_matrix.T
    else:
        ginibre_matrix = np.random.randn(d, k) + 1j * np.random.randn(d, k)
        rho = ginibre_matrix @ ginibre_matrix.conj().T

    return rho / np.trace(rho)


class Hamiltonian:
    r""" Paddle Quantum 中的 Hamiltonian ``class``。

    用户可以通过一个 Pauli string 来实例化该 ``class``。
    """
    def __init__(self, pauli_str, compress=True):
        r""" 创建一个 Hamiltonian 类。

        Args:
            pauli_str (list): 用列表定义的 Hamiltonian，如 ``[(1, 'Z0, Z1'), (2, 'I')]``
            compress (bool): 是否对输入的 list 进行自动合并（例如 ``(1, 'Z0, Z1')`` 和 ``(2, 'Z1, Z0')`` 这两项将被自动合并），默认为 ``True``

        Note:
            如果设置 ``compress=False``，则不会对输入的合法性进行检验。
        """
        self.__coefficients = None
        self.__terms = None
        self.__pauli_words_r = []
        self.__pauli_words = []
        self.__sites = []
        self.__nqubits = None
        # when internally updating the __pauli_str, be sure to set __update_flag to True
        self.__pauli_str = pauli_str
        self.__update_flag = True
        self.__decompose()
        if compress:
            self.__compress()

    def __getitem__(self, indices):
        new_pauli_str = []
        if isinstance(indices, int):
            indices = [indices]
        elif isinstance(indices, slice):
            indices = list(range(self.n_terms)[indices])
        elif isinstance(indices, tuple):
            indices = list(indices)

        for index in indices:
            new_pauli_str.append([self.coefficients[index], ','.join(self.terms[index])])
        return Hamiltonian(new_pauli_str, compress=False)

    def __add__(self, h_2):
        new_pauli_str = self.pauli_str.copy()
        if isinstance(h_2, float) or isinstance(h_2, int):
            new_pauli_str.extend([[float(h_2), 'I']])
        else:
            new_pauli_str.extend(h_2.pauli_str)
        return Hamiltonian(new_pauli_str)

    def __mul__(self, other):
        new_pauli_str = copy.deepcopy(self.pauli_str)
        for i in range(len(new_pauli_str)):
            new_pauli_str[i][0] *= other
        return Hamiltonian(new_pauli_str, compress=False)

    def __sub__(self, other):
        return self.__add__(other.__mul__(-1))

    def __str__(self):
        str_out = ''
        for idx in range(self.n_terms):
            str_out += '{} '.format(self.coefficients[idx])
            for _ in range(len(self.terms[idx])):
                str_out += self.terms[idx][_]
                if _ != len(self.terms[idx]) - 1:
                    str_out += ', '
            if idx != self.n_terms - 1:
                str_out += '\n'
        return str_out

    def __repr__(self):
        return 'paddle_quantum.Hamiltonian object: \n' + self.__str__()

    @property
    def n_terms(self):
        r"""返回该哈密顿量的项数

        Returns:
            int :哈密顿量的项数
        """
        return len(self.__pauli_str)

    @property
    def pauli_str(self):
        r"""返回哈密顿量对应的 Pauli string

        Returns:
            list : 哈密顿量对应的 Pauli string
        """
        return self.__pauli_str

    @property
    def terms(self):
        r"""返回哈密顿量中的每一项构成的列表

        Returns:
            list :哈密顿量中的每一项，i.e. ``[['Z0, Z1'], ['I']]``
        """
        if self.__update_flag:
            self.__decompose()
            return self.__terms
        else:
            return self.__terms

    @property
    def coefficients(self):
        r""" 返回哈密顿量中的每一项对应的系数构成的列表

        Returns:
            list :哈密顿量中每一项的系数，i.e. ``[1.0, 2.0]``
        """
        if self.__update_flag:
            self.__decompose()
            return self.__coefficients
        else:
            return self.__coefficients

    @property
    def pauli_words(self):
        r"""返回哈密顿量中每一项对应的 Pauli word 构成的列表

        Returns:
            list :每一项对应的 Pauli word，i.e. ``['ZIZ', 'IIX']``
        """
        if self.__update_flag:
            self.__decompose()
            return self.__pauli_words
        else:
            return self.__pauli_words

    @property
    def pauli_words_r(self):
        r"""返回哈密顿量中每一项对应的简化（不包含 I） Pauli word 组成的列表

        Returns:
            list :不包含 "I" 的 Pauli word 构成的列表，i.e. ``['ZXZZ', 'Z', 'X']``
        """
        if self.__update_flag:
            self.__decompose()
            return self.__pauli_words_r
        else:
            return self.__pauli_words_r

    @property
    def sites(self):
        r"""返回该哈密顿量中的每一项对应的量子比特编号组成的列表

        Returns:
            list :哈密顿量中每一项所对应的量子比特编号构成的列表，i.e. ``[[1, 2], [0]]``
        """
        if self.__update_flag:
            self.__decompose()
            return self.__sites
        else:
            return self.__sites

    @property
    def n_qubits(self):
        r"""返回该哈密顿量对应的量子比特数

        Returns:
            int :量子比特数
        """
        if self.__update_flag:
            self.__decompose()
            return self.__nqubits
        else:
            return self.__nqubits

    def __decompose(self):
        r"""将哈密顿量分解为不同的形式

        Notes:
            这是一个内部函数，你不需要直接使用它
            这是一个比较基础的函数，它负责将输入的 Pauli string 拆分为不同的形式并存储在内部变量中
        """
        self.__pauli_words = []
        self.__pauli_words_r = []
        self.__sites = []
        self.__terms = []
        self.__coefficients = []
        self.__nqubits = 1
        new_pauli_str = []
        for coefficient, pauli_term in self.__pauli_str:
            pauli_word_r = ''
            site = []
            single_pauli_terms = re.split(r',\s*', pauli_term.upper())
            self.__coefficients.append(float(coefficient))
            self.__terms.append(single_pauli_terms)
            for single_pauli_term in single_pauli_terms:
                match_I = re.match(r'I', single_pauli_term, flags=re.I)
                if match_I:
                    assert single_pauli_term[0].upper() == 'I', \
                        'The offset is defined with a sole letter "I", i.e. (3.0, "I")'
                    pauli_word_r += 'I'
                    site.append('')
                else:
                    match = re.match(r'([XYZ])([0-9]+)', single_pauli_term, flags=re.I)
                    if match:
                        pauli_word_r += match.group(1).upper()
                        assert int(match.group(2)) not in site, 'each Pauli operator should act on different qubit'
                        site.append(int(match.group(2)))
                    else:
                        raise Exception(
                            'Operators should be defined with a string composed of Pauli operators followed' +
                            'by qubit index on which it act, separated with ",". i.e. "Z0, X1"')
                    self.__nqubits = max(self.__nqubits, int(match.group(2)) + 1)
            self.__pauli_words_r.append(pauli_word_r)
            self.__sites.append(site)
            new_pauli_str.append([float(coefficient), pauli_term.upper()])

        for term_index in range(len(self.__pauli_str)):
            pauli_word = ['I' for _ in range(self.__nqubits)]
            site = self.__sites[term_index]
            for site_index in range(len(site)):
                if type(site[site_index]) == int:
                    pauli_word[site[site_index]] = self.__pauli_words_r[term_index][site_index]
            self.__pauli_words.append(''.join(pauli_word))
            self.__pauli_str = new_pauli_str
            self.__update_flag = False

    def __compress(self):
        r""" 对同类项进行合并。

        Notes:
            这是一个内部函数，你不需要直接使用它
        """
        if self.__update_flag:
            self.__decompose()
        else:
            pass
        new_pauli_str = []
        flag_merged = [False for _ in range(self.n_terms)]
        for term_idx_1 in range(self.n_terms):
            if not flag_merged[term_idx_1]:
                for term_idx_2 in range(term_idx_1 + 1, self.n_terms):
                    if not flag_merged[term_idx_2]:
                        if self.pauli_words[term_idx_1] == self.pauli_words[term_idx_2]:
                            self.__coefficients[term_idx_1] += self.__coefficients[term_idx_2]
                            flag_merged[term_idx_2] = True
                    else:
                        pass
                if self.__coefficients[term_idx_1] != 0:
                    new_pauli_str.append([self.__coefficients[term_idx_1], ','.join(self.__terms[term_idx_1])])
        self.__pauli_str = new_pauli_str
        self.__update_flag = True

    def decompose_with_sites(self):
        r"""将 pauli_str 分解为系数、泡利字符串的简化形式以及它们分别作用的量子比特下标。

        Returns:
            tuple: 包含如下元素的 tuple:

                 - coefficients (list): 元素为每一项的系数
                 - pauli_words_r (list): 元素为每一项的泡利字符串的简化形式，例如 'Z0, Z1, X3' 这一项的泡利字符串为 'ZZX'
                 - sites (list): 元素为每一项作用的量子比特下标，例如 'Z0, Z1, X3' 这一项的 site 为 [0, 1, 3]

        """
        if self.__update_flag:
            self.__decompose()
        return self.coefficients, self.__pauli_words_r, self.__sites

    def decompose_pauli_words(self):
        r"""将 pauli_str 分解为系数和泡利字符串。

        Returns:
            tuple: 包含如下元素的 tuple:

                - coefficients(list): 元素为每一项的系数
                - pauli_words(list): 元素为每一项的泡利字符串，例如 'Z0, Z1, X3' 这一项的泡利字符串为 'ZZIX'
        """
        if self.__update_flag:
            self.__decompose()
        else:
            pass
        return self.coefficients, self.__pauli_words

    def construct_h_matrix(self):
        r"""构建 Hamiltonian 在 Z 基底下的矩阵。

        Returns:
            np.ndarray: Z 基底下的哈密顿量矩阵形式
        """
        coefs, pauli_words, sites = self.decompose_with_sites()
        n_qubit = 1
        for site in sites:
            if type(site[0]) is int:
                n_qubit = max(n_qubit, max(site) + 1)
        h_matrix = np.zeros([2 ** n_qubit, 2 ** n_qubit], dtype='complex64')
        spin_ops = SpinOps(n_qubit, use_sparse=True)
        for idx in range(len(coefs)):
            op = coefs[idx] * sparse.eye(2 ** n_qubit, dtype='complex64')
            for site_idx in range(len(sites[idx])):
                if re.match(r'X', pauli_words[idx][site_idx], re.I):
                    op = op.dot(spin_ops.sigx_p[sites[idx][site_idx]])
                elif re.match(r'Y', pauli_words[idx][site_idx], re.I):
                    op = op.dot(spin_ops.sigy_p[sites[idx][site_idx]])
                elif re.match(r'Z', pauli_words[idx][site_idx], re.I):
                    op = op.dot(spin_ops.sigz_p[sites[idx][site_idx]])
            h_matrix += op
        return h_matrix


class SpinOps:
    r"""矩阵表示下的自旋算符，可以用来构建哈密顿量矩阵或者自旋可观测量。

    """
    def __init__(self, size: int, use_sparse=False):
        r"""SpinOps 的构造函数，用于实例化一个 SpinOps 对象。

        Args:
            size (int): 系统的大小（有几个量子比特）
            use_sparse (bool): 是否使用 sparse matrix 计算，默认为 ``False``
        """
        self.size = size
        self.id = sparse.eye(2, dtype='complex128')
        self.__sigz = sparse.bsr.bsr_matrix([[1, 0], [0, -1]], dtype='complex64')
        self.__sigy = sparse.bsr.bsr_matrix([[0, -1j], [1j, 0]], dtype='complex64')
        self.__sigx = sparse.bsr.bsr_matrix([[0, 1], [1, 0]], dtype='complex64')
        self.__sigz_p = []
        self.__sigy_p = []
        self.__sigx_p = []
        self.__sparse = use_sparse
        for i in range(self.size):
            self.__sigz_p.append(self.__direct_prod_op(spin_op=self.__sigz, spin_index=i))
            self.__sigy_p.append(self.__direct_prod_op(spin_op=self.__sigy, spin_index=i))
            self.__sigx_p.append(self.__direct_prod_op(spin_op=self.__sigx, spin_index=i))

    @property
    def sigz_p(self):
        r""" :math:`Z` 基底下的 :math:`S^z_i` 算符。

        Returns:
            list : :math:`S^z_i` 算符组成的列表，其中每一项对应不同的 :math:`i`
        """
        return self.__sigz_p

    @property
    def sigy_p(self):
        r""" :math:`Z` 基底下的 :math:`S^y_i` 算符。

        Returns:
            list : :math:`S^y_i` 算符组成的列表，其中每一项对应不同的 :math:`i`
        """
        return self.__sigy_p

    @property
    def sigx_p(self):
        r""" :math:`Z` 基底下的 :math:`S^x_i` 算符。

        Returns:
            list : :math:`S^x_i` 算符组成的列表，其中每一项对应不同的 :math:`i`
        """
        return self.__sigx_p

    def __direct_prod_op(self, spin_op, spin_index):
        r"""直积，得到第 n 个自旋（量子比特）上的自旋算符

        Args:
            spin_op: 单体自旋算符
            spin_index: 标记第 n 个自旋（量子比特）

        Returns:
            scipy.sparse or np.ndarray: 直积后的自旋算符，其数据类型取决于 self.__use_sparse
        """
        s_p = copy.copy(spin_op)
        for i in range(self.size):
            if i < spin_index:
                s_p = sparse.kron(self.id, s_p)
            elif i > spin_index:
                s_p = sparse.kron(s_p, self.id)
        if self.__sparse:
            return s_p
        else:
            return s_p.toarray()


def __input_args_dtype_check(
        show_arrow,
        save_gif,
        filename,
        view_angle,
        view_dist
):
    r"""
    该函数实现对输入默认参数的数据类型检查，保证输入函数中的参数为所允许的数据类型

    Args:
        show_arrow (bool): 是否展示向量的箭头，默认为 False
        save_gif (bool): 是否存储 gif 动图
        filename (str): 存储的 gif 动图的名字
        view_angle (list or tuple): 视图的角度，
            第一个元素为关于xy平面的夹角[0-360],第二个元素为关于xz平面的夹角[0-360], 默认为 (30, 45)
        view_dist (int): 视图的距离，默认为 7
    """

    if show_arrow is not None:
        assert type(show_arrow) == bool, \
            'the type of "show_arrow" should be "bool".'
    if save_gif is not None:
        assert type(save_gif) == bool, \
            'the type of "save_gif" should be "bool".'
    if save_gif:
        if filename is not None:
            assert type(filename) == str, \
                'the type of "filename" should be "str".'
            other, ext = os.path.splitext(filename)
            assert ext == '.gif', 'The suffix of the file name must be "gif".'
            # If it does not exist, create a folder
            path, file = os.path.split(filename)
            if not os.path.exists(path):
                os.makedirs(path)
    if view_angle is not None:
        assert type(view_angle) == list or type(view_angle) == tuple, \
            'the type of "view_angle" should be "list" or "tuple".'
        for i in range(2):
            assert type(view_angle[i]) == int, \
                'the type of "view_angle[0]" and "view_angle[1]" should be "int".'
    if view_dist is not None:
        assert type(view_dist) == int, \
            'the type of "view_dist" should be "int".'


def __density_matrix_convert_to_bloch_vector(density_matrix):
    r"""该函数将密度矩阵转化为bloch球面上的坐标

    Args:
        density_matrix (numpy.ndarray): 输入的密度矩阵

    Returns:
        bloch_vector (numpy.ndarray): 存储bloch向量的 x,y,z 坐标，向量的模长，向量的颜色
    """

    # Pauli Matrix
    pauli_x = np.array([[0, 1], [1, 0]])
    pauli_y = np.array([[0, -1j], [1j, 0]])
    pauli_z = np.array([[1, 0], [0, -1]])

    # Convert a density matrix to a Bloch vector.
    ax = np.trace(np.dot(density_matrix, pauli_x)).real
    ay = np.trace(np.dot(density_matrix, pauli_y)).real
    az = np.trace(np.dot(density_matrix, pauli_z)).real

    # Calc the length of bloch vector
    length = ax ** 2 + ay ** 2 + az ** 2
    length = sqrt(length)
    if length > 1.0:
        length = 1.0

    # Calc the color of bloch vector, the value of the color is proportional to the length
    color = length

    bloch_vector = [ax, ay, az, length, color]

    # You must use an array, which is followed by slicing and taking a column
    bloch_vector = np.array(bloch_vector)

    return bloch_vector


def __plot_bloch_sphere(
        ax,
        bloch_vectors=None,
        show_arrow=False,
        clear_plt=True,
        rotating_angle_list=None,
        view_angle=None,
        view_dist=None,
        set_color=None
):
    r"""将 Bloch 向量展示在 Bloch 球面上

    Args:
        ax (Axes3D(fig)): 画布的句柄
        bloch_vectors (numpy.ndarray): 存储bloch向量的 x,y,z 坐标，向量的模长，向量的颜色
        show_arrow (bool): 是否展示向量的箭头，默认为 False
        clear_plt (bool): 是否要清空画布，默认为 True，每次画图的时候清空画布再画图
        rotating_angle_list (list): 旋转角度的列表，用于展示旋转轨迹
        view_angle (list): 视图的角度，
            第一个元素为关于xy平面的夹角[0-360],第二个元素为关于xz平面的夹角[0-360], 默认为 (30, 45)
        view_dist (int): 视图的距离，默认为 7
        set_color (str): 设置指定的颜色，请查阅cmap表，默认为 "红-黑-根据向量的模长渐变" 颜色方案
    """
    # Assign a value to an empty variable
    if view_angle is None:
        view_angle = (30, 45)
    if view_dist is None:
        view_dist = 7
    # Define my_color
    if set_color is None:
        color = 'rainbow'
        black_code = '#000000'
        red_code = '#F24A29'
        if bloch_vectors is not None:
            black_to_red = mplcolors.LinearSegmentedColormap.from_list(
                'my_color',
                [(0, black_code), (1, red_code)],
                N=len(bloch_vectors[:, 4])
            )
            map_vir = plt.get_cmap(black_to_red)
            color = map_vir(bloch_vectors[:, 4])
    else:
        color = set_color

    # Set the view angle and view distance
    ax.view_init(view_angle[0], view_angle[1])
    ax.dist = view_dist

    # Draw the general frame
    def draw_general_frame():

        # Do not show the grid and original axes
        ax.grid(False)
        ax.set_axis_off()
        ax.view_init(view_angle[0], view_angle[1])
        ax.dist = view_dist

        # Set the lower limit and upper limit of each axis
        # To make the bloch_ball look less flat, the default is relatively flat
        ax.set_xlim3d(xmin=-1.5, xmax=1.5)
        ax.set_ylim3d(ymin=-1.5, ymax=1.5)
        ax.set_zlim3d(zmin=-1, zmax=1.3)

        # Draw a new axes
        coordinate_start_x, coordinate_start_y, coordinate_start_z = \
            np.array([[-1.5, 0, 0], [0, -1.5, 0], [0, 0, -1.5]])
        coordinate_end_x, coordinate_end_y, coordinate_end_z = \
            np.array([[3, 0, 0], [0, 3, 0], [0, 0, 3]])
        ax.quiver(
            coordinate_start_x, coordinate_start_y, coordinate_start_z,
            coordinate_end_x, coordinate_end_y, coordinate_end_z,
            arrow_length_ratio=0.03, color="black", linewidth=0.5
        )
        ax.text(0, 0, 1.7, r"|0⟩", color="black", fontsize=16)
        ax.text(0, 0, -1.9, r"|1⟩", color="black", fontsize=16)
        ax.text(1.9, 0, 0, r"|+⟩", color="black", fontsize=16)
        ax.text(-1.7, 0, 0, r"|–⟩", color="black", fontsize=16)
        ax.text(0, 1.7, 0, r"|i+⟩", color="black", fontsize=16)
        ax.text(0, -1.9, 0, r"|i–⟩", color="black", fontsize=16)

        # Draw a surface
        horizontal_angle = np.linspace(0, 2 * np.pi, 80)
        vertical_angle = np.linspace(0, np.pi, 80)
        surface_point_x = np.outer(np.cos(horizontal_angle), np.sin(vertical_angle))
        surface_point_y = np.outer(np.sin(horizontal_angle), np.sin(vertical_angle))
        surface_point_z = np.outer(np.ones(np.size(horizontal_angle)), np.cos(vertical_angle))
        ax.plot_surface(
            surface_point_x, surface_point_y, surface_point_z, rstride=1, cstride=1,
            color="black", linewidth=0.05, alpha=0.03
        )

        # Draw circle
        def draw_circle(circle_horizon_angle, circle_vertical_angle, linewidth=0.5, alpha=0.2):
            r = 1
            circle_point_x = r * np.cos(circle_vertical_angle) * np.cos(circle_horizon_angle)
            circle_point_y = r * np.cos(circle_vertical_angle) * np.sin(circle_horizon_angle)
            circle_point_z = r * np.sin(circle_vertical_angle)
            ax.plot(
                circle_point_x, circle_point_y, circle_point_z,
                color="black", linewidth=linewidth, alpha=alpha
            )

        # draw longitude and latitude
        def draw_longitude_and_latitude():
            # Draw longitude
            num = 3
            theta = np.linspace(0, 0, 100)
            psi = np.linspace(0, 2 * np.pi, 100)
            for i in range(num):
                theta = theta + np.pi / num
                draw_circle(theta, psi)

            # Draw latitude
            num = 6
            theta = np.linspace(0, 2 * np.pi, 100)
            psi = np.linspace(-np.pi / 2, -np.pi / 2, 100)
            for i in range(num):
                psi = psi + np.pi / num
                draw_circle(theta, psi)

            # Draw equator
            theta = np.linspace(0, 2 * np.pi, 100)
            psi = np.linspace(0, 0, 100)
            draw_circle(theta, psi, linewidth=0.5, alpha=0.2)

            # Draw prime meridian
            theta = np.linspace(0, 0, 100)
            psi = np.linspace(0, 2 * np.pi, 100)
            draw_circle(theta, psi, linewidth=0.5, alpha=0.2)

        # If the number of data points exceeds 20, no longitude and latitude lines will be drawn.
        if bloch_vectors is not None and len(bloch_vectors) < 52:
            draw_longitude_and_latitude()
        elif bloch_vectors is None:
            draw_longitude_and_latitude()

        # Draw three invisible points
        invisible_points = np.array([[0.03440399, 0.30279721, 0.95243384],
                                     [0.70776026, 0.57712403, 0.40743499],
                                     [0.46991358, -0.63717908, 0.61088792]])
        ax.scatter(
            invisible_points[:, 0], invisible_points[:, 1], invisible_points[:, 2],
            c='w', alpha=0.01
        )

    # clean plt
    if clear_plt:
        ax.cla()
        draw_general_frame()

    # Draw the data points
    if bloch_vectors is not None:
        ax.scatter(
            bloch_vectors[:, 0], bloch_vectors[:, 1], bloch_vectors[:, 2], c=color, alpha=1.0
        )

    # if show the rotating angle
    if rotating_angle_list is not None:
        bloch_num = len(bloch_vectors)
        rotating_angle_theta, rotating_angle_phi, rotating_angle_lam = rotating_angle_list[bloch_num - 1]
        rotating_angle_theta = round(rotating_angle_theta, 6)
        rotating_angle_phi = round(rotating_angle_phi, 6)
        rotating_angle_lam = round(rotating_angle_lam, 6)

        # Shown at the top right of the perspective
        display_text_angle = [-(view_angle[0] - 10), (view_angle[1] + 10)]
        text_point_x = 2 * np.cos(display_text_angle[0]) * np.cos(display_text_angle[1])
        text_point_y = 2 * np.cos(display_text_angle[0]) * np.sin(-display_text_angle[1])
        text_point_z = 2 * np.sin(-display_text_angle[0])
        ax.text(text_point_x, text_point_y, text_point_z, r'$\theta=' + str(rotating_angle_theta) + r'$',
                color="black", fontsize=14)
        ax.text(text_point_x, text_point_y, text_point_z - 0.1, r'$\phi=' + str(rotating_angle_phi) + r'$',
                color="black", fontsize=14)
        ax.text(text_point_x, text_point_y, text_point_z - 0.2, r'$\lambda=' + str(rotating_angle_lam) + r'$',
                color="black", fontsize=14)

    # If show the bloch_vector
    if show_arrow:
        ax.quiver(
            0, 0, 0, bloch_vectors[:, 0], bloch_vectors[:, 1], bloch_vectors[:, 2],
            arrow_length_ratio=0.05, color=color, alpha=1.0
        )
        

def plot_n_qubit_state_in_bloch_sphere(
        state,
        which_qubits=None,
        show_arrow=False,
        save_gif=False,
        filename=None,
        view_angle=None,
        view_dist=None,
        set_color='#0000FF'
):
    r"""将输入的多量子比特的量子态展示在 Bloch 球面上

    Args:
        state (numpy.ndarray or paddle.Tensor): 输入的量子态，可以支持态矢量和密度矩阵,
        该函数下，列表内每一个量子态对应一张单独的图片
        which_qubits(list or None):若为多量子比特，则给出要展示的量子比特，默认为 None，表示全展示
        show_arrow (bool): 是否展示向量的箭头，默认为 ``False``
        save_gif (bool): 是否存储 gif 动图，默认为 ``False``
        filename (str): 存储的 gif 动图的名字
        view_angle (list or tuple): 视图的角度，
            第一个元素为关于 xy 平面的夹角 [0-360]，第二个元素为关于 xz 平面的夹角 [0-360], 默认为 ``(30, 45)``
        view_dist (int): 视图的距离，默认为 7
        set_color (str): 若要设置指定的颜色，请查阅 ``cmap`` 表。默认为蓝色
    """
    # Check input data
    __input_args_dtype_check(show_arrow, save_gif, filename, view_angle, view_dist)
  
    assert type(state) == paddle.Tensor or type(state) == np.ndarray, \
        'the type of "state" must be "paddle.Tensor" or "np.ndarray".'
    assert type(set_color) == str, \
            'the type of "set_color" should be "str".'
    
    n_qubits = int(np.log2(state.shape[0]))

    if which_qubits is None:
        which_qubits = list(range(n_qubits))
    else:
        assert type(show_qubits)==list,'the type of which_qubits should be None or list'
        assert len(show_qubits)==state_len,'展示的量子数量需要小于n_qubits'
        for i in range(len(which_qubits)):
            assert 0<which_qubits[i]<n_qubits, '0<which_qubits[i]<n_qubits'
            
    # Assign a value to an empty variable
    if filename is None:
        filename = 'state_in_bloch_sphere.gif'
    if view_angle is None:
        view_angle = (30, 45)
    if view_dist is None:
        view_dist = 7

    # Convert Tensor to numpy
    if type(state) == paddle.Tensor:
        state = state.numpy()

    #state_vector to density matrix
    if state.shape[0]>=2 and state.size==2*state.shape[0]:
        state_vector = state
        state = np.outer(state_vector, np.conj(state_vector))
    
    #多量子态分解
    if state.shape[0]>2:
        rho = paddle.to_tensor(state)
        print(rho)
        tmp_s = []
        for q in which_qubits:
            tmp_s.append(partial_trace_discontiguous(rho,[q]))
        state = tmp_s
    else:
        state = [state]
    state_len = len(state)
    
    # Calc the bloch_vectors
    bloch_vector_list = []
    for i in range(state_len):
        bloch_vector_tmp = __density_matrix_convert_to_bloch_vector(state[i])
        bloch_vector_list.append(bloch_vector_tmp)

    # List must be converted to array for slicing.
    bloch_vectors = np.array(bloch_vector_list)

    # A update function for animation class
    def update(frame):
        view_rotating_angle = 5
        new_view_angle = [view_angle[0], view_angle[1] + view_rotating_angle * frame]
        __plot_bloch_sphere(
            ax, bloch_vectors, show_arrow, clear_plt=True,
            view_angle=new_view_angle, view_dist=view_dist, set_color=set_color
        )

    # Dynamic update and save
    if save_gif:
        # Helper function to plot vectors on a sphere.
        fig = plt.figure(figsize=(8, 8), dpi=100)
        fig.subplots_adjust(left=0, right=1, bottom=0, top=1)
        ax = fig.add_subplot(111, projection='3d')

        frames_num = 7
        anim = animation.FuncAnimation(fig, update, frames=frames_num, interval=600, repeat=False)
        anim.save(filename, dpi=100, writer='pillow')
        # close the plt
        plt.close(fig)

    # Helper function to plot vectors on a sphere.
    fig = plt.figure(figsize=(8, 8), dpi=100)
    fig.subplots_adjust(left=0, right=1, bottom=0, top=1)
    dim = np.ceil(sqrt(n_qubits))
    for i in range(1,n_qubits+1):
        ax = fig.add_subplot(dim,dim,i,projection='3d')
        bloch_vector=np.array([bloch_vectors[i-1]])
        __plot_bloch_sphere(
        ax, bloch_vector, show_arrow, clear_plt=True,
        view_angle=view_angle, view_dist=view_dist, set_color=set_color
        )
    plt.show()

def plot_state_in_bloch_sphere(
        state,
        show_arrow=False,
        save_gif=False,
        filename=None,
        view_angle=None,
        view_dist=None,
        set_color=None
):
    r"""将输入的量子态展示在 Bloch 球面上

    Args:
        state (list(numpy.ndarray or paddle.Tensor)): 输入的量子态列表，可以支持态矢量和密度矩阵
        show_arrow (bool): 是否展示向量的箭头，默认为 ``False``
        save_gif (bool): 是否存储 gif 动图，默认为 ``False``
        filename (str): 存储的 gif 动图的名字
        view_angle (list or tuple): 视图的角度，
            第一个元素为关于 xy 平面的夹角 [0-360]，第二个元素为关于 xz 平面的夹角 [0-360], 默认为 ``(30, 45)``
        view_dist (int): 视图的距离，默认为 7
        set_color (str): 若要设置指定的颜色，请查阅 ``cmap`` 表。默认为红色到黑色的渐变颜色
    """
    # Check input data
    __input_args_dtype_check(show_arrow, save_gif, filename, view_angle, view_dist)

    assert type(state) == list or type(state) == paddle.Tensor or type(state) == np.ndarray, \
        'the type of "state" must be "list" or "paddle.Tensor" or "np.ndarray".'
    if type(state) == paddle.Tensor or type(state) == np.ndarray:
        state = [state]
    state_len = len(state)
    assert state_len >= 1, '"state" is NULL.'
    for i in range(state_len):
        assert type(state[i]) == paddle.Tensor or type(state[i]) == np.ndarray, \
            'the type of "state[i]" should be "paddle.Tensor" or "numpy.ndarray".'
    if set_color is not None:
        assert type(set_color) == str, \
            'the type of "set_color" should be "str".'

    # Assign a value to an empty variable
    if filename is None:
        filename = 'state_in_bloch_sphere.gif'
    if view_angle is None:
        view_angle = (30, 45)
    if view_dist is None:
        view_dist = 7

    # Convert Tensor to numpy
    for i in range(state_len):
        if type(state[i]) == paddle.Tensor:
            state[i] = state[i].numpy()

    # Convert state_vector to density_matrix
    for i in range(state_len):
        if state[i].size == 2:
            state_vector = state[i]
            state[i] = np.outer(state_vector, np.conj(state_vector))

    # Calc the bloch_vectors
    bloch_vector_list = []
    for i in range(state_len):
        bloch_vector_tmp = __density_matrix_convert_to_bloch_vector(state[i])
        bloch_vector_list.append(bloch_vector_tmp)

    # List must be converted to array for slicing.
    bloch_vectors = np.array(bloch_vector_list)

    # A update function for animation class
    def update(frame):
        view_rotating_angle = 5
        new_view_angle = [view_angle[0], view_angle[1] + view_rotating_angle * frame]
        __plot_bloch_sphere(
            ax, bloch_vectors, show_arrow, clear_plt=True,
            view_angle=new_view_angle, view_dist=view_dist, set_color=set_color
        )

    # Dynamic update and save
    if save_gif:
        # Helper function to plot vectors on a sphere.
        fig = plt.figure(figsize=(8, 8), dpi=100)
        fig.subplots_adjust(left=0, right=1, bottom=0, top=1)
        ax = fig.add_subplot(111, projection='3d')

        frames_num = 7
        anim = animation.FuncAnimation(fig, update, frames=frames_num, interval=600, repeat=False)
        anim.save(filename, dpi=100, writer='pillow')
        # close the plt
        plt.close(fig)

    # Helper function to plot vectors on a sphere.
    fig = plt.figure(figsize=(8, 8), dpi=100)
    fig.subplots_adjust(left=0, right=1, bottom=0, top=1)
    

    ax = fig.add_subplot(111, projection='3d')
    __plot_bloch_sphere(
    ax, bloch_vectors, show_arrow, clear_plt=True,
    view_angle=view_angle, view_dist=view_dist, set_color=set_color
    )

    plt.show()


def plot_rotation_in_bloch_sphere(
        init_state,
        rotating_angle,
        show_arrow=False,
        save_gif=False,
        filename=None,
        view_angle=None,
        view_dist=None,
        color_scheme=None,
):
    r"""在 Bloch 球面上刻画从初始量子态开始的旋转轨迹

    Args:
        init_state (numpy.ndarray or paddle.Tensor): 输入的初始量子态，可以支持态矢量和密度矩阵
        rotating_angle (list(float)): 旋转角度 ``[theta, phi, lam]``
        show_arrow (bool): 是否展示向量的箭头，默认为 ``False``
        save_gif (bool): 是否存储 gif 动图，默认为 ``False``
        filename (str): 存储的 gif 动图的名字
        view_angle (list or tuple): 视图的角度，
            第一个元素为关于 xy 平面的夹角 [0-360]，第二个元素为关于 xz 平面的夹角 [0-360], 默认为 ``(30, 45)``
        view_dist (int): 视图的距离，默认为 7
        color_scheme (list(str,str,str)): 分别是初始颜色，轨迹颜色，结束颜色。若要设置指定的颜色，请查阅 ``cmap`` 表。默认为红色
    """
    # Check input data
    __input_args_dtype_check(show_arrow, save_gif, filename, view_angle, view_dist)

    assert type(init_state) == paddle.Tensor or type(init_state) == np.ndarray, \
        'the type of input data should be "paddle.Tensor" or "numpy.ndarray".'
    assert type(rotating_angle) == tuple or type(rotating_angle) == list, \
        'the type of rotating_angle should be "tuple" or "list".'
    assert len(rotating_angle) == 3, \
        'the rotating_angle must include [theta=paddle.Tensor, phi=paddle.Tensor, lam=paddle.Tensor].'
    for i in range(3):
        assert type(rotating_angle[i]) == paddle.Tensor or type(rotating_angle[i]) == float, \
            'the rotating_angle must include [theta=paddle.Tensor, phi=paddle.Tensor, lam=paddle.Tensor].'
    if color_scheme is not None:
        assert type(color_scheme) == list and len(color_scheme) <= 3, \
            'the type of "color_scheme" should be "list" and ' \
            'the length of "color_scheme" should be less than or equal to "3".'
        for i in range(len(color_scheme)):
            assert type(color_scheme[i]) == str, \
                'the type of "color_scheme[i] should be "str".'

    # Assign a value to an empty variable
    if filename is None:
        filename = 'rotation_in_bloch_sphere.gif'

    # Assign colors to bloch vectors
    color_list = ['orangered', 'lightsalmon', 'darkred']
    if color_scheme is not None:
        for i in range(len(color_scheme)):
            color_list[i] = color_scheme[i]
    set_init_color, set_trac_color, set_end_color = color_list

    theta, phi, lam = rotating_angle

    # Convert Tensor to numpy
    if type(init_state) == paddle.Tensor:
        init_state = init_state.numpy()

    # Convert state_vector to density_matrix
    if init_state.size == 2:
        state_vector = init_state
        init_state = np.outer(state_vector, np.conj(state_vector))

    # Rotating angle
    def rotating_operation(rotating_angle_each):
        gate_matrix = simulator.u_gate_matrix(rotating_angle_each)
        return np.matmul(np.matmul(gate_matrix, init_state), gate_matrix.conj().T)

    # Rotating angle division
    rotating_frame = 50
    rotating_angle_list = []
    state = []
    for i in range(rotating_frame + 1):
        angle_each = [theta / rotating_frame * i, phi / rotating_frame * i, lam / rotating_frame * i]
        rotating_angle_list.append(angle_each)
        state.append(rotating_operation(angle_each))

    state_len = len(state)
    # Calc the bloch_vectors
    bloch_vector_list = []
    for i in range(state_len):
        bloch_vector_tmp = __density_matrix_convert_to_bloch_vector(state[i])
        bloch_vector_list.append(bloch_vector_tmp)

    # List must be converted to array for slicing.
    bloch_vectors = np.array(bloch_vector_list)

    # A update function for animation class
    def update(frame):
        frame = frame + 2
        if frame <= len(bloch_vectors) - 1:
            __plot_bloch_sphere(
                ax, bloch_vectors[1:frame], show_arrow=show_arrow, clear_plt=True,
                rotating_angle_list=rotating_angle_list,
                view_angle=view_angle, view_dist=view_dist, set_color=set_trac_color
            )

            # The starting and ending bloch vector has to be shown
            # show starting vector
            __plot_bloch_sphere(
                ax, bloch_vectors[:1],  show_arrow=True, clear_plt=False,
                view_angle=view_angle, view_dist=view_dist, set_color=set_init_color
            )

        # Show ending vector
        if frame == len(bloch_vectors):
            __plot_bloch_sphere(
                ax, bloch_vectors[frame - 1:frame], show_arrow=True, clear_plt=False,
                view_angle=view_angle, view_dist=view_dist, set_color=set_end_color
            )

    if save_gif:
        # Helper function to plot vectors on a sphere.
        fig = plt.figure(figsize=(8, 8), dpi=100)
        fig.subplots_adjust(left=0, right=1, bottom=0, top=1)
        ax = fig.add_subplot(111, projection='3d')

        # Dynamic update and save
        stop_frames = 15
        frames_num = len(bloch_vectors) - 2 + stop_frames
        anim = animation.FuncAnimation(fig, update, frames=frames_num, interval=100, repeat=False)
        anim.save(filename, dpi=100, writer='pillow')
        # close the plt
        plt.close(fig)

    # Helper function to plot vectors on a sphere.
    fig = plt.figure(figsize=(8, 8), dpi=100)
    fig.subplots_adjust(left=0, right=1, bottom=0, top=1)
    ax = fig.add_subplot(111, projection='3d')

    # Draw the penultimate bloch vector
    update(len(bloch_vectors) - 3)
    # Draw the last bloch vector
    update(len(bloch_vectors) - 2)

    plt.show()


def pauli_basis(n):
    r"""生成 n 量子比特的泡利基空间
    Args:
        n (int): 量子比特的数量

    Return:
        tuple:
            basis_str: 泡利基空间的一组基底表示（array形式）
            label_str: 泡利基空间对应的一组基底表示（标签形式），形如``[ 'X', 'Y', 'Z', 'I']``
    """
    sigma_x = np.array([[0, 1],  [1, 0]], dtype=np.complex128)
    sigma_y = np.array([[0, -1j], [1j, 0]], dtype=np.complex128)
    sigma_z = np.array([[1, 0],  [0, -1]], dtype=np.complex128)
    sigma_id = np.array([[1, 0],  [0, 1]], dtype=np.complex128)
    pauli = [sigma_x, sigma_y, sigma_z, sigma_id]
    labels = ['X', 'Y', 'Z', 'I']

    num_qubits = n
    num = 1
    if num_qubits > 0:
        basis_str = pauli[:]
        label_str = labels[:]
        pauli_basis = pauli[:]
        palui_label = labels[:]
        while num < num_qubits:
            length = len(basis_str)
            for i in range(4):
                for j in range(length):
                    basis_str.append(np.kron(basis_str[j], pauli_basis[i]))
                    label_str.append(label_str[j] + palui_label[i])
            basis_str = basis_str[-1:-4**(num+1)-1:-1]
            label_str = label_str[-1:-4**(num+1)-1:-1]
            num += 1
        return basis_str, label_str


def decompose(matrix):
    r"""生成 n 量子比特的泡利基空间
    Args:
        matrix (numpy.ndarray): 要分解的矩阵

    Return:
        pauli_form (list): 返回矩阵分解后的哈密顿量，形如 ``[[1, 'Z0, Z1'], [2, 'I']]``
    """
    if np.log2(len(matrix)) % 1 != 0:
        print("Please input correct matrix!")
        return -1
    basis_space, label_str = pauli_basis(np.log2(len(matrix)))
    coefficients = []  # 对应的系数
    pauli_word = []  # 对应的label
    pauli_form = []  # 输出pauli_str list形式：[[1, 'Z0, Z1'], [2, 'I']]
    for i in range(len(basis_space)):
        # 求系数
        a_ij = 1/len(matrix) * np.trace(matrix@basis_space[i])
        if a_ij != 0:
            if a_ij.imag != 0:
                coefficients.append(a_ij)
            else:
                coefficients.append(a_ij.real)
            pauli_word.append(label_str[i])
    for i in range(len(coefficients)):
        pauli_site = []  # 临时存放一个基
        pauli_site.append(coefficients[i])
        word = ''
        for j in range(len(pauli_word[0])):
            if pauli_word[i] == 'I'*int(np.log2(len(matrix))):
                word = 'I'  # 和Hamiltonian类似，若全是I就用一个I指代
                break
            if pauli_word[i][j] == 'I':
                continue   # 如果是I就不加数字下标
            if j != 0 and len(word) != 0:
                word += ','
            word += pauli_word[i][j]
            word += str(j)  # 对每一个label加标签，说明是作用在哪个比特
        pauli_site.append(word)  # 添加上对应作用的门
        pauli_form.append(pauli_site)

    return pauli_form
