简体   繁体   English

进一步优化ISING model

[英]Further optimizing the ISING model

I've implemented the 2D ISING model in Python, using NumPy and Numba's JIT:我已经在 Python 中实现了 2D ISING model,使用 NumPy 和 Numba 的 JIT:


from timeit import default_timer as timer
import matplotlib.pyplot as plt
import numba as nb
import numpy as np

# TODO for Dict optimization.
# from numba import types
# from numba.typed import Dict

@nb.njit(nogil=True)
def initialstate(N):   
    ''' 
    Generates a random spin configuration for initial condition
    '''
    state = np.empty((N,N),dtype=np.int8)
    for i in range(N):
        for j in range(N):
            state[i,j] = 2*np.random.randint(2)-1
    return state



@nb.njit(nogil=True)
def mcmove(lattice, beta, N):
    '''
    Monte Carlo move using Metropolis algorithm 
    '''
    
    # # TODO* Dict optimization 
    # dict_param = Dict.empty(
    # key_type=types.int64,
    # value_type=types.float64,
    # )
    # dict_param = {cost : np.exp(-cost*beta) for cost in [-8, -4, 0, 4, 8] }

    for _ in range(N):
        for __ in range(N):
                a = np.random.randint(0, N)
                b = np.random.randint(0, N)
                s =  lattice[a, b]
                dE = lattice[(a+1)%N,b] + lattice[a,(b+1)%N] + lattice[(a-1)%N,b] + lattice[a,(b-1)%N]
                cost = 2*s*dE

                if cost < 0:
                    s *= -1
                
                #TODO* elif np.random.rand() < dict_param[cost]:
                elif np.random.rand() < np.exp(-cost*beta):
                    s *= -1
                lattice[a, b] = s

    return lattice


@nb.njit(nogil=True)
def calcEnergy(lattice, N):
    '''
    Energy of a given configuration
    '''
    energy = 0 
    
    for i in range(len(lattice)):
        for j in range(len(lattice)):
            S = lattice[i,j]
            nb = lattice[(i+1)%N, j] + lattice[i,(j+1)%N] + lattice[(i-1)%N, j] + lattice[i,(j-1)%N]
            energy += -nb*S
    return energy/2


@nb.njit(nogil=True)
def calcMag(lattice):
    '''
    Magnetization of a given configuration
    '''
    mag = np.sum(lattice, dtype=np.int32)
    return mag

@nb.njit(nogil=True)
def ISING_model(nT, N, burnin, mcSteps):

    """ 
    nT      :         Number of temperature points.
    N       :         Size of the lattice, N x N.
    burnin  :         Number of MC sweeps for equilibration (Burn-in).
    mcSteps :         Number of MC sweeps for calculation.

    """


    T       = np.linspace(1.2, 3.8, nT); 
    E,M,C,X = np.zeros(nT), np.zeros(nT), np.zeros(nT), np.zeros(nT)
    n1, n2  = 1.0/(mcSteps*N*N), 1.0/(mcSteps*mcSteps*N*N) 


    for temperature in range(nT):
        lattice = initialstate(N)         # initialise

        E1 = M1 = E2 = M2 = 0
        iT = 1/T[temperature]
        iT2= iT*iT
        
        for _ in range(burnin):           # equilibrate
            mcmove(lattice, iT, N)        # Monte Carlo moves

        for _ in range(mcSteps):
            mcmove(lattice, iT, N)           
            Ene = calcEnergy(lattice, N)  # calculate the Energy
            Mag = calcMag(lattice,)       # calculate the Magnetisation

            E1 += Ene
            M1 += Mag
            M2 += Mag*Mag 
            E2 += Ene*Ene

        E[temperature] = n1*E1
        M[temperature] = n1*M1
        C[temperature] = (n1*E2 - n2*E1*E1)*iT2
        X[temperature] = (n1*M2 - n2*M1*M1)*iT

    return T,E,M,C,X


def main():
    
    N = 32
    start_time = timer()
    T,E,M,C,X = ISING_model(nT = 64, N = N, burnin = 8 * 10**4, mcSteps = 8 * 10**4)
    end_time = timer()

    print("Elapsed time: %g seconds" % (end_time - start_time))

    f = plt.figure(figsize=(18, 10)); #  

    # figure title
    f.suptitle(f"Ising Model: 2D Lattice\nSize: {N}x{N}", fontsize=20)

    _ =  f.add_subplot(2, 2, 1 )
    plt.plot(T, E, '-o', color='Blue') 
    plt.xlabel("Temperature (T)", fontsize=20)
    plt.ylabel("Energy ", fontsize=20)
    plt.axis('tight')


    _ =  f.add_subplot(2, 2, 2 )
    plt.plot(T, abs(M), '-o', color='Red')
    plt.xlabel("Temperature (T)", fontsize=20)
    plt.ylabel("Magnetization ", fontsize=20)
    plt.axis('tight')


    _ =  f.add_subplot(2, 2, 3 )
    plt.plot(T, C, '-o', color='Green')
    plt.xlabel("Temperature (T)", fontsize=20)
    plt.ylabel("Specific Heat ", fontsize=20)
    plt.axis('tight')


    _ =  f.add_subplot(2, 2, 4 )
    plt.plot(T, X, '-o', color='Black')
    plt.xlabel("Temperature (T)", fontsize=20)
    plt.ylabel("Susceptibility", fontsize=20)
    plt.axis('tight')

    plt.show()

if __name__ == '__main__':
    main()

Which of course, works:当然,哪个有效:

在此处输入图像描述

I have two main questions:我有两个主要问题:

  1. Is there anything left to optimize?还有什么可以优化的吗? I knew ISING model is hard to simulate, but looking at the following table, it seems like I'm missing something...我知道 ISING model 很难模拟,但是看看下表,我好像漏掉了什么...
    lattice size : 32x32    
    burnin = 8 * 10**4
    mcSteps = 8 * 10**4
    Simulation time = 365.98 seconds

    lattice size : 64x64    
    burnin = 10**5
    mcSteps = 10**5
    Simulation time = 1869.58 seconds
  1. I tried implementing another optimization based on not calculating the exponential over and over again using a dictionary, yet on my tests, it seems like its slower.我尝试基于不使用字典一遍又一遍地计算指数来实现另一种优化,但在我的测试中,它似乎更慢。 What am I doing wrong?我究竟做错了什么?

The computation of the exponential is not really an issue.指数的计算并不是真正的问题。 The main issue is that generating random numbers is expensive and a huge number of random values are generated.主要问题是生成随机数的成本很高,并且会生成大量随机值。 Another issue is that the current computation is intrinsically sequential.另一个问题是当前的计算本质上是顺序的。

Indeed, for N=32 , mcmove tends to generate about 3000 random values, and this function is called 2 * 80_000 times per iteration.事实上,对于N=32mcmove往往会生成大约3000个随机值,而这个 function 每次迭代被调用 2 * 80_000 次。 This means, 2 * 80_000 * 3000 = 480_000_000 random number generated per iteration.这意味着,每次迭代生成2 * 80_000 * 3000 = 480_000_000随机数。 Assuming generating a random number takes about 5 nanoseconds (ie. only 20 cycles on a 4 GHz CPU), then each iteration will take about 2.5 seconds only to generate all the random numbers.假设生成一个随机数大约需要 5 纳秒(即在 4 GHz CPU 上只需要 20 个周期),那么每次迭代将只需要大约 2.5 秒来生成所有随机数。 On my 4.5 GHz i5-9600KF CPU, each iteration takes about 2.5-3.0 seconds.在我的 4.5 GHz i5-9600KF CPU 上,每次迭代大约需要 2.5-3.0 秒。

The first thing to do is to try to generate random number using a faster method.首先要做的是尝试使用更快的方法生成随机数。 The bad news is that this is hard to do in Numba and more generally any-Python-based code.坏消息是这在 Numba 中很难做到,更普遍的是在任何基于 Python 的代码中。 Micro-optimizations using a lower-level language like C or C++ can significantly help to speed up this computation.使用 C 或 C++ 等低级语言的微优化可以显着帮助加快此计算。 Such low-level micro-optimizations are not possible in high-level languages/tools like Python, including Numba.这种低级微优化在高级语言/工具中是不可能的,例如 Python,包括 Numba。 Still, one can implement a random-number generator (RNG) specifically designed so to produce the random values you need .尽管如此,仍然可以实施专门设计的随机数生成器 (RNG) 来生成您需要的随机值 xoshiro256** can be used to generate random numbers quickly though it may not be as random as what Numpy/Numba can produce (there is no free lunch). xoshiro256**可用于快速生成随机数,尽管它可能不如 Numpy/Numba 生成的随机数(天下没有免费的午餐)。 The idea is to generate 64-bit integers and extract range of bits so to produce 2 16-bit integers and a 32-bit floating point value.这个想法是生成 64 位整数并提取位范围,以便生成 2 个 16 位整数和一个 32 位浮点值。 This RNG should be able to generate 3 values in only about 10 cycles on a modern CPU!在现代 CPU 上,这个 RNG 应该能够在大约 10 个周期内生成 3 个值!

Once this optimization has been applied, the computation of the exponential becomes the new bottleneck.一旦应用了这种优化,指数的计算就会成为新的瓶颈。 It can be improved using a lookup table (LUT) like you did.可以像您一样使用查找表(LUT) 对其进行改进。 However, using a dictionary is slow.但是,使用字典很慢。 You can use a basic array for that.您可以为此使用基本数组 This is much faster.这要快得多。 Note the index need to be positive and small.请注意,索引需要为正且小。 Thus, the minimum cost needs to be added.因此,需要添加最低成本。

Once the previous optimization has been implemented, the new bottleneck is the conditionals if cost < 0 and elif c <... .一旦实施了先前的优化,新的瓶颈就是条件if cost < 0 and elif c <... The conditionals are slow because they are unpredictable (due to the result being random).条件语句很慢,因为它们是不可预测的(由于结果是随机的)。 Indeed, modern CPUs try to predict the outcomes of conditionals so to avoid expensive stalls in the CPU pipeline.事实上,现代 CPU 试图预测条件的结果,以避免 CPU 管道中代价高昂的停顿。 This is a complex topic.这是一个复杂的话题。 If you want to know more about this, then please read this great post .如果您想了解更多信息,请阅读这篇精彩的文章 In practice, such a problem can be avoided using a branchless computation.实际上,使用无分支计算可以避免这样的问题。 This means you need to use binary operators and integer sticks so for the sign of s to change regarding the value of the condition.这意味着您需要使用二元运算符和 integer 条,以便根据条件值更改s的符号。 For example: s *= 1 - ((cost < 0) | (c < lut[cost])) * 2 .例如: s *= 1 - ((cost < 0) | (c < lut[cost])) * 2

Note that modulus are generally expensive unless the compiler know the value at compile time .请注意,模数通常很昂贵,除非编译器在编译时知道其值 They are even faster when the value is a power of two because the compiler can use bit tricks so to compute the modulus (more specifically a logical and by a pre-compiled constant).当值是 2 的幂时它们甚至更快,因为编译器可以使用位技巧来计算模数(更具体地说是逻辑和预编译常量)。 For calcEnergy , a solution is to compute the border separately so to completely avoid the modulus.对于calcEnergy ,一个解决方案是单独计算边界以完全避免模数。 Furthermore, loops can be faster when the compiler know the number of iterations at compile time (it can unroll the loops and better vectorize them).此外,当编译器在编译时知道迭代次数时,循环会更快(它可以展开循环并更好地对其进行矢量化)。 Moreover, when N is not a power of two, the RNG can be significantly slower and more complex to implement without any bias, so I assume N is a power of two.此外,当N不是 2 的幂时,RNG 在没有任何偏差的情况下实施起来会明显更慢且更复杂,因此我假设N是 2 的幂。

Here is the final code:这是最终代码:

# [...] Same as in the initial code

@nb.njit(inline="always")
def rol64(x, k):
    return (x << k) | (x >> (64 - k))

@nb.njit(inline="always")
def xoshiro256ss_init():
    state = np.empty(4, dtype=np.uint64)
    maxi = (np.uint64(1) << np.uint64(63)) - np.uint64(1)
    for i in range(4):
        state[i] = np.random.randint(0, maxi)
    return state

@nb.njit(inline="always")
def xoshiro256ss(state):
    result = rol64(state[1] * np.uint64(5), np.uint64(7)) * np.uint64(9)
    t = state[1] << np.uint64(17)
    state[2] ^= state[0]
    state[3] ^= state[1]
    state[1] ^= state[2]
    state[0] ^= state[3]
    state[2] ^= t
    state[3] = rol64(state[3], np.uint64(45))
    return result

@nb.njit(inline="always")
def xoshiro_gen_values(N, state):
    '''
    Produce 2 integers between 0 and N and a simple-precision floating-point number.
    N must be a power of two less than 65536. Otherwise results will be biased (ie. not random).
    N should be known at compile time so for this to be fast
    '''
    rand_bits = xoshiro256ss(state)
    a = (rand_bits >> np.uint64(32)) % N
    b = (rand_bits >> np.uint64(48)) % N
    c = np.uint32(rand_bits) * np.float32(2.3283064370807974e-10)
    return (a, b, c)

@nb.njit(nogil=True)
def mcmove_generic(lattice, beta, N):
    '''
    Monte Carlo move using Metropolis algorithm.
    N must be a small power of two and known at compile time
    '''

    state = xoshiro256ss_init()

    lut = np.full(16, np.nan)
    for cost in (0, 4, 8, 12, 16):
        lut[cost] = np.exp(-cost*beta)

    for _ in range(N):
        for __ in range(N):
            a, b, c = xoshiro_gen_values(N, state)
            s =  lattice[a, b]
            dE = lattice[(a+1)%N,b] + lattice[a,(b+1)%N] + lattice[(a-1)%N,b] + lattice[a,(b-1)%N]
            cost = 2*s*dE

            # Branchless computation of s
            tmp = (cost < 0) | (c < lut[cost])
            s *= 1 - tmp * 2

            lattice[a, b] = s

    return lattice

@nb.njit(nogil=True)
def mcmove(lattice, beta, N):
    assert N in [16, 32, 64, 128]
    if N == 16: return mcmove_generic(lattice, beta, 16)
    elif N == 32: return mcmove_generic(lattice, beta, 32)
    elif N == 64: return mcmove_generic(lattice, beta, 64)
    elif N == 128: return mcmove_generic(lattice, beta, 128)
    else: raise Exception('Not implemented')

@nb.njit(nogil=True)
def calcEnergy(lattice, N):
    '''
    Energy of a given configuration
    '''
    energy = 0 
    # Center
    for i in range(1, len(lattice)-1):
        for j in range(1, len(lattice)-1):
            S = lattice[i,j]
            nb = lattice[i+1, j] + lattice[i,j+1] + lattice[i-1, j] + lattice[i,j-1]
            energy -= nb*S
    # Border
    for i in (0, len(lattice)-1):
        for j in range(1, len(lattice)-1):
            S = lattice[i,j]
            nb = lattice[(i+1)%N, j] + lattice[i,(j+1)%N] + lattice[(i-1)%N, j] + lattice[i,(j-1)%N]
            energy -= nb*S
    for i in range(1, len(lattice)-1):
        for j in (0, len(lattice)-1):
            S = lattice[i,j]
            nb = lattice[(i+1)%N, j] + lattice[i,(j+1)%N] + lattice[(i-1)%N, j] + lattice[i,(j-1)%N]
            energy -= nb*S
    return energy/2

@nb.njit(nogil=True)
def calcMag(lattice):
    '''
    Magnetization of a given configuration
    '''
    mag = np.sum(lattice, dtype=np.int32)
    return mag

# [...] Same as in the initial code

I hope there is no error in the code.希望代码没有错误。 It is hard to check results with a different RNG.很难用不同的 RNG 检查结果。

The resulting code is significantly faster on my machine: it compute 4 iterations in 5.3 seconds with N=32 as opposed to 24.1 seconds.生成的代码在我的机器上明显更快:它在N=32的情况下在 5.3 秒内计算 4 次迭代,而不是 24.1 秒。 The computation is thus 4.5 times faster !因此计算速度提高了4.5 倍

It is very hard to optimize the code further using Numba in Python. The computation cannot be efficiently parallelized due to the long dependency chain in mcmove .在 Python 中使用 Numba 进一步优化代码非常困难。由于mcmove中的长依赖链,计算无法有效并行化。

Based on the Mr. Richard's excellent answer, I found another optimization.基于理查德先生的出色回答,我找到了另一个优化。 In the ISING_model function, the code can be parallelized because we are doing the same operations independently for every temperature.在 ISING_model function 中,代码可以并行化,因为我们对每个温度独立执行相同的操作。 To achieve this, I simply used parallel = True in the ISING_model nb.jit decorator, and used nb.prange for the temperature loop inside the function, ie, for temperature in nb.prange(nT) .为此,我简单地在 ISING_model nb.jit 装饰器中使用了parallel = True ,并在 function 中使用了nb.prange作为温度循环,即, for temperature in nb.prange(nT)

The resulting code is even faster... On my machine, with the setting of ISING_model(nT = 64, N = N, burnin = 8 * 10**4, mcSteps = 8 * 10**4) with N=32 , without parallelization, it computes in 93.1621 seconds and with parallelization, it computes in 29.9872 seconds .生成的代码甚至更快......在我的机器上,设置ISING_model(nT = 64, N = N, burnin = 8 * 10**4, mcSteps = 8 * 10**4)N=32 ,在没有并行化的情况下,它在93.1621 秒内进行计算,在进行并行化时,它在29.9872 秒内进行计算。 Another 3 times faster optimization!另一个 3 倍更快的优化! Which is really cool.这真的很酷。

I put the final code here for everyone to use.我把最终的代码放在这里供大家使用。


from timeit import default_timer as timer
import matplotlib.pyplot as plt
import numba as nb
import numpy as np

@nb.njit(nogil=True)
def initialstate(N):   
    ''' 
    Generates a random spin configuration for initial condition in compliance with the Numba JIT compiler.
    '''
    state = np.empty((N,N),dtype=np.int8)
    for i in range(N):
        for j in range(N):
            state[i,j] = 2*np.random.randint(2)-1
    return state

@nb.njit(inline="always")
def rol64(x, k):
    return (x << k) | (x >> (64 - k))

@nb.njit(inline="always")
def xoshiro256ss_init():
    state = np.empty(4, dtype=np.uint64)
    maxi = (np.uint64(1) << np.uint64(63)) - np.uint64(1)
    for i in range(4):
        state[i] = np.random.randint(0, maxi)
    return state

@nb.njit(inline="always")
def xoshiro256ss(state):
    result = rol64(state[1] * np.uint64(5), np.uint64(7)) * np.uint64(9)
    t = state[1] << np.uint64(17)
    state[2] ^= state[0]
    state[3] ^= state[1]
    state[1] ^= state[2]
    state[0] ^= state[3]
    state[2] ^= t
    state[3] = rol64(state[3], np.uint64(45))
    return result

@nb.njit(inline="always")
def xoshiro_gen_values(N, state):
    '''
    Produce 2 integers between 0 and N and a simple-precision floating-point number.
    N must be a power of two less than 65536. Otherwise results will be biased (ie. not random).
    N should be known at compile time so for this to be fast
    '''
    rand_bits = xoshiro256ss(state)
    a = (rand_bits >> np.uint64(32)) % N
    b = (rand_bits >> np.uint64(48)) % N
    c = np.uint32(rand_bits) * np.float32(2.3283064370807974e-10)
    return (a, b, c)

@nb.njit(nogil=True)
def mcmove_generic(lattice, beta, N):
    '''
    Monte Carlo move using Metropolis algorithm.
    N must be a small power of two and known at compile time
    '''

    state = xoshiro256ss_init()

    lut = np.full(16, np.nan)
    for cost in (0, 4, 8, 12, 16):
        lut[cost] = np.exp(-cost*beta)

    for _ in range(N):
        for __ in range(N):
            a, b, c = xoshiro_gen_values(N, state)
            s =  lattice[a, b]
            dE = lattice[(a+1)%N,b] + lattice[a,(b+1)%N] + lattice[(a-1)%N,b] + lattice[a,(b-1)%N]
            cost = 2*s*dE

            # Branchless computation of s
            tmp = (cost < 0) | (c < lut[cost])
            s *= 1 - tmp * 2

            lattice[a, b] = s

    return lattice

@nb.njit(nogil=True)
def mcmove(lattice, beta, N):
    assert N in [16, 32, 64, 128]
    if N == 16: return mcmove_generic(lattice, beta, 16)
    elif N == 32: return mcmove_generic(lattice, beta, 32)
    elif N == 64: return mcmove_generic(lattice, beta, 64)
    elif N == 128: return mcmove_generic(lattice, beta, 128)
    else: raise Exception('Not implemented')

@nb.njit(nogil=True)
def calcEnergy(lattice, N):
    '''
    Energy of a given configuration
    '''
    energy = 0 
    # Center
    for i in range(1, len(lattice)-1):
        for j in range(1, len(lattice)-1):
            S = lattice[i,j]
            nb = lattice[i+1, j] + lattice[i,j+1] + lattice[i-1, j] + lattice[i,j-1]
            energy -= nb*S
    # Border
    for i in (0, len(lattice)-1):
        for j in range(1, len(lattice)-1):
            S = lattice[i,j]
            nb = lattice[(i+1)%N, j] + lattice[i,(j+1)%N] + lattice[(i-1)%N, j] + lattice[i,(j-1)%N]
            energy -= nb*S
    for i in range(1, len(lattice)-1):
        for j in (0, len(lattice)-1):
            S = lattice[i,j]
            nb = lattice[(i+1)%N, j] + lattice[i,(j+1)%N] + lattice[(i-1)%N, j] + lattice[i,(j-1)%N]
            energy -= nb*S
    return energy/2

@nb.njit(nogil=True)
def calcMag(lattice):
    '''
    Magnetization of a given configuration
    '''
    mag = np.sum(lattice, dtype=np.int32)
    return mag

@nb.njit(nogil=True, parallel=True)
def ISING_model(nT, N, burnin, mcSteps):

    """ 
    nT      :         Number of temperature points.
    N       :         Size of the lattice, N x N.
    burnin  :         Number of MC sweeps for equilibration (Burn-in).
    mcSteps :         Number of MC sweeps for calculation.

    """


    T       = np.linspace(1.2, 3.8, nT)
    E,M,C,X = np.empty(nT, dtype= np.float32), np.empty(nT, dtype= np.float32), np.empty(nT, dtype= np.float32), np.empty(nT, dtype= np.float32)
    n1, n2  = 1/(mcSteps*N*N), 1/(mcSteps*mcSteps*N*N) 


    for temperature in nb.prange(nT):
        lattice = initialstate(N)         # initialise

        E1 = M1 = E2 = M2 = 0
        iT = 1/T[temperature]
        iT2= iT*iT
        
        for _ in range(burnin):           # equilibrate
            mcmove(lattice, iT, N)        # Monte Carlo moves

        for _ in range(mcSteps):
            mcmove(lattice, iT, N)           
            Ene = calcEnergy(lattice, N)  # calculate the Energy
            Mag = calcMag(lattice)        # calculate the Magnetisation
            E1 += Ene
            M1 += Mag
            M2 += Mag*Mag 
            E2 += Ene*Ene

        E[temperature] = n1*E1
        M[temperature] = n1*M1
        C[temperature] = (n1*E2 - n2*E1*E1)*iT2
        X[temperature] = (n1*M2 - n2*M1*M1)*iT

    return T,E,M,C,X


def main():
    
    N = 32
    start_time = timer()
    T,E,M,C,X = ISING_model(nT = 64, N = N, burnin = 8 * 10**4, mcSteps = 8 * 10**4)
    end_time = timer()

    print("Elapsed time: %g seconds" % (end_time - start_time))

    f = plt.figure(figsize=(18, 10)); #  

    # figure title
    f.suptitle(f"Ising Model: 2D Lattice\nSize: {N}x{N}", fontsize=20)

    _ =  f.add_subplot(2, 2, 1 )
    plt.plot(T, E, '-o', color='Blue') 
    plt.xlabel("Temperature (T)", fontsize=20)
    plt.ylabel("Energy ", fontsize=20)
    plt.axis('tight')


    _ =  f.add_subplot(2, 2, 2 )
    plt.plot(T, abs(M), '-o', color='Red')
    plt.xlabel("Temperature (T)", fontsize=20)
    plt.ylabel("Magnetization ", fontsize=20)
    plt.axis('tight')


    _ =  f.add_subplot(2, 2, 3 )
    plt.plot(T, C, '-o', color='Green')
    plt.xlabel("Temperature (T)", fontsize=20)
    plt.ylabel("Specific Heat ", fontsize=20)
    plt.axis('tight')


    _ =  f.add_subplot(2, 2, 4 )
    plt.plot(T, X, '-o', color='Black')
    plt.xlabel("Temperature (T)", fontsize=20)
    plt.ylabel("Susceptibility", fontsize=20)
    plt.axis('tight')


    plt.show()

if __name__ == '__main__':
    main()

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM