본문 바로가기
수학의 재미/아름다운 이론

FDM #10, Laplace Equation의 풀이(2) Python Code

by hustler78 2023. 2. 2.
728x90
반응형

이번 글에서는, 지난 글

2023.02.02 - [수학의 재미/아름다운 이론] - FDM #9, Laplace Equation의 풀이(1)

 

FDM #9, Laplace Equation의 풀이(1)

지난 글 2023.01.27 - [수학의 재미/아름다운 이론] - FDM #8, Wave Equation(파동방정식)의 풀이(1) FDM #8, Wave Equation(파동방정식)의 풀이(1) 이 글은 FDM #7, Heat Equation의 풀이(3) FDM #7, Heat Equation의 풀이(3) 이

sine-qua-none.tistory.com

에서 알아본 Laplace Equation의 FDM 풀이법을 실제 예제를 들어 python coding으로 풀어보겠습니다. 

 

 

Laplace Equation 과 FDM풀이법 복습

 

2 변수 함수 u(x,y) , 0xa,0yb가 다음의 관계식과 경계조건

(L.E.)2ux2+2uy2=0  ,  0xa,0yb

(BC1)u(0,y)=0(BC2)u(a,y)=0(BC3)u(x,0)=0(BC4)u(x,b)=f(x)

을 만족할 때, 이를 Laplace Equation이라 불렀습니다. 이를 

 

0=x0<x1<x2<<xJ=a  ,  0=y0<y1<y2<<xK=b 

x,y축을 각각 분할 간격 hx,hy로 균등분할하고 편의를 위해 hx=hy라 두면


uj,k=14(uj+1,k+uj1,k+uj,k+1+uj,k1)

 

를 만족한다고 하였습니다. 또 여기에 Jacobi Iteration 기법을 도입하여 n번째 결과를 uj,k(n)이라 했을 때, 다음의 절차를 따라 값을 얻을 수 있다고 하였습니다.

FDM 절차

1. uj,k , 1jJ,1kK를 초기화한다(예를 들어 모든 값을 0이라 놓는다.)

2. u0,k=0 , uJ,k=0 ,uj,0=0,uj,K=f(xj) 같이 끝 격자들에 경계조건을 대입!

3. 새로운 격자판 uj,k(2)를 만들고, 이 경계조건은 2번과 똑같이 한다.

4.  아래 식을 이용하여 uj,k(2)의 값들을 모두 결정해 준다.
uj,k(2)=14(uj+1,k+uj1,k+uj,k+1+uj,k1)

5. 이제 uj,k(2)를 기준으로 하고 위의 1-4 번을 반복하여 uj,k(3) 값들을 만든다.

6. 반복적으로 uj,k(4),uj,k(5), 를 만들 수 있다.

7. 어떤 허용치(tolerance)를 설정하여 uj,knuj,k(n+1)의 값이 허용치 안에서 비슷하도록 하는, 즉 더 이상 값이 유의미하게 변하지 않는 상태를 찾으면 끝! 

또, 7번의 관계식은 수학적으로 

 

uj,k(n+1)=14(uj+1,k(n)+uj1,k(n)+uj,k+1(n)+uj,k1(n))

이고 종국에는 이 값이 거의 변화하지 않는 단계로 수렴한다는 설명을 하였습니다.

 

 

 

Laplace Equation 의 예시

 

다음의 예시를 풀어보죠.

2ux2+2uy2=0  ,  0<x<a,0<y<b,
u(0,y)=0u(a,y)=0u(x,0)=0u(x,b)=csin(πax)

위의 예시에서 a,b,c는 상수(constant)입니다.

 

위 함수의 실제 해는 다음과 같습니다(해가 맞는지는 간단한 계산을 통해 알 수 있습니다.)

 csinh(bπ/a)sin(πx/a)sinh(πy/a)

 

 

 

Python Code

 

def Solve_LaplaceEquation():
    # -----------------------------------------------------------------
    # u_{xx} + u_{yy} =0 for all 0<x<a and 0<y<b
    # u(0,y) = 0
    # u(x,0) = 0
    # u(a,y) = 0
    # u(x,b) = c sin(π/a)x
    #
    # solution :  (c/sinh(π/a *b)) * sin( π * x/a) * sinh(π *y/a)
    # -----------------------------------------------------------------

    a, b, c = 1, 1, 1   # a,b,c 에 1 대입
    xMax = a            # x 는 [0,a]
    yMax = b            # y 는 [0,b]
    dl = 0.01           # 분할간격은 0.01
    xSeq = np.arange(0, xMax + dl, dl)      # [0,a]를 균등분할
    ySeq = np.arange(0, yMax + dl, dl)      # [0,b]를 균등분할
    bdry_function = lambda x: c * np.sin(np.pi / a * x)   # y=b 의 boundary function 설정
    lx = len(xSeq)
    ly = len(ySeq)
    u = np.zeros((lx, ly))   # fdm grid 설정

    u[0, :] = 0        # x=0일 때의 boundary value
    u[-1, :] = 0       # x=a일 때의 boundary value
    u[:, 0] = 0        # y=0일 때의 boundary value
    u[:, -1] = np.array([bdry_function(x) for x in xSeq])  #y=b일 때 boundary value
    u_new = u.copy()   # 원본인 u를 건들지 않기 위해 u_new =u 대신, u_new =u.copy()를 써야함

    nIteration = 1       # 반복(iteration)의 최대치
    tolerance = 0.00001  # u_new와 u 값 사이의 허용오차(tolerance)
    error = 100          # error 초기 더미값 (tolerance보다 크기만 하면 됨)
    while (nIteration < 100000 and error > tolerance):   #최대 10만번 iteration & 허용오차 1/10만
        u_new[1: lx - 1, 1: ly - 1] = 0.25 * (u[0:lx - 2, 1:ly - 1] + u[2:lx, 1:ly - 1] +
                                              u[1:lx - 1, 0:ly - 2] + u[1:lx - 1, 2:ly])
        # 재귀적 산출 u에서 u_new 로
        error = np.linalg.norm(u_new - u)   # u와 u_new 사이의 거리(norm)
        u = u_new.copy()                    # u를 다시 u_new값으로 갱신
        nIteration += 1

    print('Iteration number: {} and  error: {}'.format(nIteration,error)) 
                                            # iteration 횟수와 norm error 출력하여 확인
    closed_solution = lambda x, y: c / np.sinh(np.pi / a * b) * np.sin(np.pi * x / a) * np.sinh(np.pi * y / a)
                                            # Laplace equation의 실제 exact formula
    for tg_index in range(ly):              
        # y=y0로 고정하고 u(x, y0) 함수를 x에 대해 그릴 예정
        # 위의 y0를 0에서 b까지 for문으로 돌림
        
        u_fixedy = u[:, tg_index]  # 해당 y0값의 index 를 tg_index로 하고 이때의 fdm 값
        tg_y = ySeq[tg_index]      # tg_index 인덱스에 할당된 y값, 즉, y0
        cs_fixedy = closed_solution(xSeq, tg_y)  #y=y0 에서의 exact solution

        plt.plot(xSeq, u_fixedy, color='b', label='fdm')
        plt.plot(xSeq, cs_fixedy, color='y',  label='exact_solution', linestyle='dashed')
        plt.title('At y={:.2f}'.format(tg_y))
        plt.legend()
        plt.xlabel('x')
        plt.ylim([0,1])
        plt.pause(0.01)            # 0.01초 동안 plotting pause
        plt.cla()                  # graph 지움
    plt.show()

if __name__ == '__main__':
    Solve_LaplaceEquation()

 

◆  코드 중간에 u_new = u.copy() 에 대해 첨언하면,  이것을 u_new = u라고 하면 u_new가 갱신되면서, u 또한 갱신되므로, 즉 배열 타입의 변수가 mutable 한 관계로 같은 주소값을 참조하기에 제대로 된 코드가 될 수 없습니다. 따라서 copy() method를 사용하여 복사를 해주어야 합니다. 관련 내용을 잘 아시는 분의 블로그가 큰 도움이 될 것 같습니다.

 

numpy.linalg.norm 은 행렬의 다양한 norm을 구해주는 함수입니다.  아무런 옵션도 취하지 않은 default값은 Frobenius norm으로서, 바로 전의 글을 참고해 보시기 바랍니다. 허용오차와 비교하여 u_new와 u사이의 norm값이 작아지면 for문이 멈춥니다.

 

 

이제 결과를 보시죠.

Iteration number: 13508 and  error: 9.999858290890539e-06

# iteration을 13500번 넘게 해야 허용오차 안에 들어오고, 허용오차는 1/10만인 상태

 

 

y값을 변화시켜 가며 fdm값과 실제값을 x에 대한 함수로 비교한 그림

 

어떻습니까? 모든 고정된 y값에 대해 FDM 값과 실제 closed form 값이 똑같습니다. 

(즉 y를 고정시키고 x에 대한 함수로서 FDM과 closed form을 비교한 결과)

 

 

 


사실 Laplace equation을 만족하는 함수 u(x,y)는 2 변수 함수이므로 3차원 공간 안에 그려지는 게 맞을 겁니다. 3D plotting 공부도 할 겸 한번 그려보겠습니다.

 

def Solve_LaplaceEquation():
    # -----------------------------------------------------------------
    # u_{xx} + u_{yy} =0 for all 0<x<a and 0<y<b
    # u(0,y) = 0
    # u(x,0) = 0
    # u(a,y) = 0
    # u(x,b) = c sin(π/a)x
    #
    # solution :  (c/sinh(π/a *b)) * sin( π * x/a) * sinh(π *y/a)
    # -----------------------------------------------------------------

    a, b, c = 1, 1, 1
    xMax = a
    yMax = b
    dl = 0.01
    xSeq = np.arange(0, xMax + dl, dl)
    ySeq = np.arange(0, yMax + dl, dl)
    bdry_function = lambda x: c * np.sin(np.pi / a * x)
    lx = len(xSeq)
    ly = len(ySeq)
    u = np.zeros((lx, ly))

    u[0, :] = 0
    u[-1, :] = 0
    u[:, 0] = 0
    u[:, -1] = np.array([bdry_function(x) for x in xSeq])
    u_new = u.copy()

    nIteration = 1
    tolerance = 0.00001
    error = 100
    while (nIteration < 100000 and error > tolerance):
        u_new[1: lx - 1, 1: ly - 1] = 0.25 * (u[0:lx - 2, 1:ly - 1] + u[2:lx, 1:ly - 1] +
                                              u[1:lx - 1, 0:ly - 2] + u[1:lx - 1, 2:ly])

        error = np.linalg.norm(u_new - u)
        u = u_new.copy()
        nIteration += 1

    print('Iteration number: {} and  error: {}'.format(nIteration,error))
    closed_solution = lambda x, y: c / np.sinh(np.pi / a * b) * np.sin(np.pi * x / a) * np.sinh(np.pi * y / a)
    
    # 위의 코드는 바로 전 코드와 똑같으므로 설명 생략
    #--------------------------------------------------------------------
    xval, yval = np.meshgrid(xSeq, ySeq)   # x값, y값으로 meshgrid 생성
    fig, axs = plt.subplots(ncols=2, figsize=(10, 5), subplot_kw={"projection": "3d"})
                                           # 그래프를 2개 그림, 3d로!

    axs[0].plot_surface(xval, yval, u.transpose(), alpha=0.5, color='blue')
                                           # (xval, yval) 점에 함숫값은 u.T의 대응값임
    axs[1].plot_surface(xval,yval,closed_solution(xval,yval),alpha=0.5,color='red')
                                           # x= xval, y=yval 에서의 closed form 값
    axs[0].view_init(elev=30, azim=120)    # 3차원 축의 높이(elevation)와 방위각(azimuth) 조절 기능
    axs[1].view_init(elev=30, azim=120)
    plt.show()

 

◆ 3차원 그래프는 축의 각도에 따라 그래프가 잘 보일 수도, 안보일 수도 있기에 view_init 함수로 조절해 줄 수 있습니다. 자세한 내용은 Tutorial을 참고해 보시기 바랍니다. 몇몇 예제는 다음과 같습니다.

 

결과를 볼까요?

값을 직접 비교해 불 순 없지만, FDM(좌측)과 Closed Form(우측)의 그래프가 똑같아 보임

 

 

 

 


이번엔 FDM 결과 그래프를 다양한 각도로 조망해 보기 위해 회전을 시켜보도록 하겠습니다.

 

from matplotlib import animation     # 애니메이션 효과를 주기위한 animation import

def Solve_LaplaceEquation():
    # -----------------------------------------------------------------
    # u_{xx} + u_{yy} =0 for all 0<x<a and 0<y<b
    # u(0,y) = 0
    # u(x,0) = 0
    # u(a,y) = 0
    # u(x,b) = c sin(π/a)x
    #
    # solution :  (c/sinh(π/a *b)) * sin( π * x/a) * sinh(π *y/a)
    # -----------------------------------------------------------------

    a, b, c = 1, 1, 1
    xMax = a
    yMax = b
    dl = 0.01
    xSeq = np.arange(0, xMax + dl, dl)
    ySeq = np.arange(0, yMax + dl, dl)
    bdry_function = lambda x: c * np.sin(np.pi / a * x)
    lx = len(xSeq)
    ly = len(ySeq)
    u = np.zeros((lx, ly))

    u[0, :] = 0
    u[-1, :] = 0
    u[:, 0] = 0
    u[:, -1] = np.array([bdry_function(x) for x in xSeq])
    u_new = u.copy()

    nIteration = 1
    tolerance = 0.00001
    error = 100
    while (nIteration < 100000 and error > tolerance):
        u_new[1: lx - 1, 1: ly - 1] = 0.25 * (u[0:lx - 2, 1:ly - 1] + u[2:lx, 1:ly - 1] +
                                              u[1:lx - 1, 0:ly - 2] + u[1:lx - 1, 2:ly])

        error = np.linalg.norm(u_new - u)
        u = u_new.copy()
        nIteration += 1

    print('Iteration number: {} and  error: {}'.format(nIteration,error))
    closed_solution = lambda x, y: c / np.sinh(np.pi / a * b) * np.sin(np.pi * x / a) * np.sinh(np.pi * y / a)
    
    # 위의 코드는 바로 전 코드와 똑같으므로 설명 생략
    #--------------------------------------------------------------------

    xval, yval = np.meshgrid(xSeq, ySeq)
    fig = plt.figure()
    ax = fig.add_subplot(projection='3d')
    def init():
        ax.plot_surface(xval, yval, u.transpose(), alpha=0.5, color='blue')
        ax.set_xlabel("x")
        ax.set_ylabel("y")
        ax.set_title('Laplace equation')
        return fig,

    def animate(i):
        ax.view_init(elev=30, azim=i)     # 30도 각도에서 방위각 i도 만큼 돌리는 animation
        return fig,

    anim = animation.FuncAnimation(fig, animate, init_func=init, frames=360, interval=20, blit=True)
    anim.save('scatter.gif', fps=30)

 

위 코드에 등장하는 함수들은 FuncAnimation에 관한 Codetorial의 설명이나 전문가분의 블로그를 우선 참고해 주시기 바랍니다. 결과를 gif로 저장가능하며 이때 사용하는 명령하는 save이고 fps를 설정하여 초당 프레임을 설정할 수 있습니다.

 

결과는 아래와 같습니다. 

 

 

 

신기하고도 유용한 기능입니다. 

 

어쨌든 Laplace Equation 역시 FDM으로 해결할 수 있다는 사실을 코드를 통해 알아보았습니다.

 

 

 

 

 

 

 

728x90
반응형