2024-3S 計算数理演習(東京大学理学部・教養学部) [齊藤宣一] [top] [0] [1] [2] [3] [4] [5] [6] [7] [8] [UTOL]

8. 偏微分方程式:熱伝導,波動の数値計算

熱方程式

一様な材質でできた,長さが L の針金の熱伝導現象は,次の偏微分方程式で記述される: (8.1a)ut=κ2ux2(0<x<L, t>0). ここで,u=u(x,t) は時刻 t における点 x での針金の温度である.また,κ は針金の比熱や密度などから定まる正定数である.(8.1a)は,熱伝導方程式,熱拡散方程式,あるいは,単に,熱方程式と呼ばれる.

針金の温度変化を論じるには針金の端点(境界)での物理的な状況設定が必要である. もし,この両端が一定の温度,たとえば,0 に保たれているのであれば,u は境界条件 (8.1b)u(0,t)=0,u(L,t)=0(t>0) を満たす.これを,Dirichlet境界条件と呼ぶ.

もし,端点で熱の出入りがない状態が保たれている,すなわち,断熱の状態であれば, 境界条件として, Neumann境界条件 ux(0,t)=0,ux(L,t)=0(t>0) を採用しなけばならない.

温度変化の考察を t=0 から始めるとすると,この初期時刻での温度分布は既知であるとする必要がある.すなわち,u には初期条件 (8.1c)u(x,0)=a(0xL) が課せられる.ただし,a=a(x) は区間 [0,L] 上で与えられた関数である.特に何も断らなければ,考えている境界条件を満たすような連続関数であるとしておく(実際には,この条件は,それほど本質的ではない).

当面,熱方程式(8.1a),Dirichlet境界条件(8.1b),初期条件(8.1c)からなる,熱方程式の初期値境界値問題(8.1a,b,c)を扱う.Neumann境界条件については,次節で,詳しく扱う.

熱方程式の初期値境界値問題(8.1a,b,c)は,Fourierの方法を用いて,解を得ることができる.しかし,ここでは,数値的な方法による近似解法を考えたい.

差分近似

高等学校で学んだように,滑らかな関数 v(x)x=α における微分係数は, v(α)=dvdx(α)=limh0v(α+h)v(α)h で定義される.したがって,h0 に十分近ければ, 分数 (8.2a)v(α+h)v(α)h の値は v(α) に十分に近いことが期待される.実際, h>0 とし,Iαα±h を含むような有界な閉区間とする. このとき,Taylorの定理により, v(α+h)=v(α)+v(α)h+12v(α+θh)h2 を満たす θ[0,1] が存在するので,これを変形して, (8.2b)|v(α)v(α+h)v(α)h|=|12v(α+θh)h|12hmaxxI|v(x)| が得られる.これは,v(x)I 上で C2 級であれば,v(α)[v(α+h)v(α)]/h の差は h に比例して小さくなることを示している.分数(8.2a)のことを,前進差分商,あるいは,前進Euler近似と呼ぶ.同じように,v(α) を近似する量として, (8.3)v(α)v(αh)h,(8.4)v(α+h2)v(αh2)h,v(α+h)v(αh)2h を,それぞれ,後退差分商(後退Euler近似)中心差分商と呼ぶ.実際, |v(α)v(α)v(αh)h|12hmaxxI|v(x)|,|v(α)v(α+h2)v(αh2)h|124h2maxxI|v(3)(x)| を導くことができる.

2階の微分係数 v(α) の近似ついては, (8.5a)v(αh)2v(α)+v(α+h)h2 が得られる.これを,v(α)2階の中心差分商と呼ぶ.実際, (8.5b)|v(α)v(αh)2v(α)+v(α+h)h2|112h2maxxI|v(4)(x)| を導くことができる.

熱方程式に対する陽的スキーム

あらためて,Dirichlet境界条件下での熱方程式の初期値境界値問題(8.1a,b,c)を考えよう.N を正の整数として,h=LN+1とおき,xi=ih (i=0,1,,N+1) と定める.このとき,x0=0xN+1=Lであることに注意せよ.次に,正の定数τ を固定して,tn=nτ (n=0,1,)とおく.この格子点の各点 (xi,tn) で,初期値境界値問題の解 u(xi,tn) の近似値 uin を求めたい.

図8.1
図8.1

ut(xi,tn) に前進差分商(8.2a),2ux2(xi,tn) に2階中心差分商(8.5a)を適用することで, (8.1a)に対する近似方程式として, (8.6a)uin+1uinτ=κui1n2uin+ui+1nh2(1iN, n0) が得られる.境界条件と初期条件は,単純に, (8.6b)u0n=uN+1n=0(n1),(8.6c)ui0=a(xi)(0iN+1) とする.

(8.6a,b)は, (8.7)λ=def.κτh2 と置くことで, (8.8){u0n+1=0,uin+1=(12λ)uin+λ(ui1n+ui+1n)(1iN,n0),uN+1n+1=0 と表現できる.したがって,近似方程式(8.6a,b,c)は,図8.2のような模式図(スキーム,scheme)で表現できる. すなわち.uin+1 を計算する際には,「その真下の値」uin と「左右の下の値」ui1n,ui+1n を加重平均しているのである.このことから,(8.6a,b,c)のような差分近似から導かれた近似方程式を差分近似スキーム,あるいは,差分スキームと呼ぶ.特に,(8.6a,b,c)を陽的差分スキーム(explicit finite difference scheme),あるいは単に,陽的スキーム陽的解法と呼ぶ. (この他に,「陰的」差分スキームがあるが,本講義では扱わない.)

図8.2
図8.2

注意. h=L/(N+1) としているのは, 1iN に対して,xi を 区間 [0,L] の内点にするためである. 結果的に,各 tn (n1) に対して,uin (1iN) が求めるべき未知数となる. h=L/N とした場合には,0=x0<x1<<xN1<xN=L となり, uin (1iN1) が求めるべき未知数となる.実際のところ,陽的スキームを考える限りは,h=L/(N+1) と定める恩恵は,さほどではない.あくまで,個人的な好みの問題であるので,h=L/N としたい人は,(その定義に合わせて,しかるべく修正して)そのようにしても良い.

実際に,(8.6a.b.c)を計算してみよう.この節で使う予定のモジュールをすべてインポートしておく.

In [1]:  
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import animation, rc
from IPython.display import HTML
import plotly.graph_objects as go
import plotly.offline as pyo

(8.6a,b,c)を計算する関数を作成する. ただし,時間区間については,0tT で考えるものとする. 次は,関数を実行する際に,ユーザが指定するものである:

理由は後で明らかになるが,τ を直接指定せず,先に,λ を指定して,τ=λh2/κ により,τ を算出する.そして,nmax=[T/τ] と定める.すると,0tnT (n=0,1,,nmax),かつ,tnmax+1>T となる(tnmax=T が成立するか否かは,場合による).このとき,一般には,nmax の値は,とても大きくなる.一方で,解の可視化の一つの方法として, 各 tn (n=0,1,,nmax) に対して,xu 平面に曲線 (xi,uin) (0iN+1) を描く とする.すると,nmax の値が大きいとき,とても見辛い. そのために,補助的に,20から100くらいの整数 num を予め指定して(num<nmaxであることを前提に), 各 tmstep (m=0,1,,num) に対して,xu 平面に曲線 (xi,uim) (0iN+1) を描く とする.ここで,step=[nmax/num] としている. この場合も,一般には, tnumsteptnmaxT である.

xgrid=(x0,x1,,xN+1)RN+2 とする. 「現在時刻」における uni を記憶する配列として, u=(u0n,u1n,,uN+1n)RN+2 を導入する.すると, u[1:N+1]=(u1n,,uNn),u[0:N]=(u0n,,uN1n),u[2:N+2]=(u2n,,uN+1n) であるから,(8.6a,b,c),すなわち,(8.8)の更新式は, u[1:N+1]=(12lam)u[1:N+1]+lam(u[0:N]+u[2:N+2]) とすれば良い.ここで,右辺の u[1:N+1] などは, (u1n,,uNn) などを, 左辺の u[1:N+1] は,(u1n+1,,uNn+1) を意味することに注意せよ.

その上で,tgrid=(t0,tstep,t2step,,tnumstep)Rnum+1,および, sol=(u00u10uN0uN+10u0stepu1stepuNstepuN+1stepu0numstepu1numstepuNnumstepuN+1numstep)R(num+1)×(N+2) と定める.関数の計算結果としては, xgridtgridsol を返す.

以上を行う関数が,In [2]に示した heat_0dbc_ex1 である.

In [2]:  
#(斉次)熱方程式、零Dirichlet境界条件、陽的スキーム
def heat_0dbc_ex1(L, kappa, initial, N, T, lam, num):
    
  h = L/(N+1)
  tau = lam*h*h/kappa
  nmax = int(T/tau)
  step=int(max(1, nmax/num))
  
  xgrid=np.linspace(0.0, L, N+2)
  tgrid=np.array([0.0])
  u=np.vectorize(initial)(xgrid)
  sol=np.copy(u)
  
  for n in range(nmax):
    u[1:N+1] = (1-2*lam)*u[1:N+1]+lam*(u[0:N]+u[2:N+2])
    tnow = (n+1)*tau
    if (n+1)%step==0:
      sol=np.vstack((sol,u))
      tgrid=np.append(tgrid, tnow)

  return xgrid, tgrid, sol

次に可視化のために,関数を3つ用意する.

In [3]:  
#図のプロット、一枚の図に、グラフを重ねる
def plot_solution(xgrid, uvect):
  # 描画に関するパラメータの設定
  umax = np.max(sol)
  umin = np.min(sol)
  ulength = umax - umin
  umax += 0.05 * ulength
  umin -= 0.05 * ulength
  # t = 0                  
  plt.plot(xgrid,uvect[0,:],color='#ff8c00')
  # t > 0
  for n in range(1,uvect.shape[0]):
    plt.plot(xgrid,uvect[n,:],color='#00bfff')
  
  plt.xlabel('x')
  plt.ylabel('u')
  plt.ylim(umin, umax)
  plt.grid('on')
  plt.show()
In [4]:  
# xtu空間での曲線の描画
# plotly.graph_objects,plotly.offline モジュールを使う
# xgrid.shape <= tgrid.shape でないと上手く描けない
def plot_solution3d(xgrid, tgrid, uvect):  
  # 3Dプロットの作成
  fig = go.Figure()
  
  Nt = tgrid.shape[0]
  # t = 0
  n = 0
  tval = tgrid[n]*np.ones(Nt)
  fig.add_trace(go.Scatter3d(x=xgrid, y=tval, z=uvect[n,:], mode='lines',line=dict(color='#ff8c00', width=2)))
  # t > 0 
  for n in range(1, Nt):
    tval = tgrid[n]*np.ones(Nt)
    fig.add_trace(go.Scatter3d(x=xgrid, y=tval, z=uvect[n,:], mode='lines',line=dict(color='#00bfff', width=2)))

  # レイアウトの設定
  fig.update_layout(scene=dict(xaxis_title='x', yaxis_title='time', zaxis_title='u'))
  fig.update_layout(scene=dict(camera=dict(eye=dict(x=1.5, y=-1.5, z=1))))
  fig.update_layout(showlegend=False)

  # プロットの表示
  pyo.plot(fig)
  with open('temp-plot.html', 'r') as file:
    html_content = file.read()
  display(HTML(html_content))
  # Google Colaboratoryでなければ、上の4行は、以下の1行で置き換えられる(はず)
  # fig.show()
In [5]: 
#アニメーションの作成
# IPython.displayモジュールを使う
def plot_animation(xgrid, tgrid, uvect):
  # 描画に関するパラメータの設定
  umax = np.max(sol)
  umin = np.min(sol)
  xmax = np.max(xgrid)
  xmin = np.min(xgrid)
  ulength = umax - umin
  utop = umax + 0.1*ulength
  umax += 0.05*ulength
  umin -= 0.05*ulength
  xmid = (xmax+xmin)/2
  xlength = xmax - xmin
  xmax += 0.05*xlength
  xmin -= 0.05*xlength
  
  # fig, axオブジェクトを作成
  fig, ax = plt.subplots()
  ims = []
  
  # t = 0
  n = 0
  im, = ax.plot(xgrid, uvect[n, :], color='#ff8c00')
  title = ax.text(xmid, utop, f'time={tgrid[n]:.4f}', ha='center', va='center', fontsize=15)
  ims.append([im, title])
    
  # t > 0
  for n in range(1, uvect.shape[0]):
    im, = ax.plot(xgrid, uvect[n, :], color='#00bfff')
    title = ax.text(xmid, utop, f'time={tgrid[n]:.4f}', ha='center', va='center', fontsize=15)
    ims.append([im, title])
    
  # 各軸のラベル
  ax.set_xlabel(r"$x$", fontsize=15)
  ax.set_ylabel(r"$u$", fontsize=15)
  # グラフの範囲を設定
  ax.set_xlim([xmin, xmax])
  ax.set_ylim([umin, umax])
  ax.grid(True)

  # ArtistAnimationにfigオブジェクトとimsを代入してアニメーションを作成
  return animation.ArtistAnimation(fig, ims)

これらを,次のIn [6]ように使う.熱方程式に対する差分法の最初の例としては,初期関数として, a(x)=min{x, Lx} を用いるのが,慣例になっている. ここでもその慣例に従おう(この関数に対して,配列 xgrid を渡したときに,同じ次元の配列 u が返ってくるように,u=np.vectorize(initial)(xgrid)としている) .

In [6]:  
# 初期値
def initial1(x):
  return min(x,1-x)
  
#パラメータの設定
L = 1.0
kappa = 1.0
N = 51
T = 1.0
lam = 0.4
num = 60

#熱方程式の計算
x, tn, sol = heat_0dbc_ex1(L, kappa, initial1, N, T, lam, num)

#図の表示
plot_solution(x, sol)

#3D図の表示
plot_solution3d(x, tn, sol)

#アニメーションを表示
anim = plot_animation(x, tn, sol)
#結果をheat.gifに保存(Googleドライブをマウントしておくこと!)
anim.save('/content/drive/MyDrive/Colab Notebooks/fig/heat.gif', writer='pillow')
rc('animation', html='jshtml')
plt.close()
anim
図8.3a 図8.3b
図8.3a, b:Out [6]
図8.3c
図8.3c Out [6]

以下では,可視化にはplot_solutionのみを使う.

まずは,Lκ を固定して,Nλ を色々変えて,計算をしてみよ.すると,例えば,N=23λ=0.51T=0.18として計算してみると,図8.4のようになる.

図8.4
図8.4 N=23λ=0.51T=0.18num=30(他は In [6] と同じ)

実は,(8.1)の解 u は,初期関数 a が連続で,a(0)=a(L)=0 満たす限りにおいて, (8.9)max0xL|u(x,t)|max0xL|a(x)|(t0) を満たす(例えば,[藤田1977]の第1章を見よ).これを,熱方程式の解の L 安定性という. しかしながら,図8.4では,このような安定性は観察できない.すなわち,図8.4における振動現象は,近似の対象としている微分方程式の解の挙動を反映しているわけではない.

この例から,(8.9)と同様の性質は,陽的スキームの解については,一般には成り立たないことがわかる.それでは,どうすれば良いであろうか? 実は, 対応する性質の成立を保証するためには,λ,すなわち,hτκ の間に, (8.10)λ(=κτh2)12 を仮定する必要がある.実際,(8.6a)の両辺の絶対値をとり,12λ0 に注意して, 三角不等式を使うと |uin+1|(12λ)|uin|+λ(|ui1n|+|ui+1n|)(1iN) となる.ここで,Mn=max1iN|uin| とおくと,この不等式に より,|uin+1|(12λ)Mn+λ(Mn+Mn)=Mn となるから, 両辺の i についての最大値をとると,Mn+1Mn を得る.したがって, MnM0,すなわち, (8.11)max1iN|uin|max1iN|a(xi)|(n0) が得られる.これを,差分解の L 安定性, あるいは,離散 L 安定性などという.特に,条件(8.10)が満たされている時には, 陽的スキームの解は発散しないことがわかる.

In [2]のheat_0dbc_ex1において, Nτ の値でなく, Nλ の値をはじめに指定して,τ を,τ=λh2/κ で求めた理由は,(8.10)を容易に成立させるためである.

非斉次の熱方程式

針金に熱の湧き出しや吸収がある場合には,(8.1a)の代わりに,非斉次の熱方程式, (8.12)ut=κ2ux2+f(0<x<L, t>0) を考えることになる.ここで,f=f(x,t) は,与えられた関数である. 境界条件(8.1b)と初期条件(8.1c)は同じとする. このときには,(8.6a)を, (8.13)uin+1uinτ=κui1n2uin+ui+1nh2+f(xi,tn)(1iN, n0), すなわち,(8.7)と同じ λ を使って, uin+1=(12λ)uin+λ(ui1n+ui+1n)+τf(xi,tn)(1iN, n0) と変えれば良い.(8.6b,c)はそのまま使える.

In [7]:  
#非斉次熱方程式、零Dirichlet境界条件、陽的スキーム
def heat_0dbc_ex2(L, kappa, initial, righthand, N, T, lam, num):
    
  h = L/(N+1)
  tau = lam*h*h/kappa
  nmax = int(T/tau)
  step=int(max(1, nmax/num))
  
  xgrid=np.linspace(0.0, L, N+2)
  tgrid=np.array([0.0])
  u=np.vectorize(initial)(xgrid)
  sol=np.copy(u)
  
  n=0
  tnow=n*tau
  for n in range(nmax):
    u[1:N+1] = (1-2*lam)*u[1:N+1]+lam*(u[0:N]+u[2:N+2]) + tau*righthand(xgrid[1:N+1], tnow)
    tnow = (n+1)*tau
    if (n+1)%step==0:
      sol=np.vstack((sol,u))
      tgrid=np.append(tgrid, tnow)

  return xgrid, tgrid, sol

fa を, (8.14)f(x,t)=et(x4+x3+12x26x),a(x)=x3(1x) として計算をしてみよう.結果は,図8.5の通りである. 今度は,解 u は,0 に減衰しない.

In [8]:  
# 初期値 a
def initial2(x):
  return x**3*(1.0 - x)

# 右辺の関数 f
def righthand2(x, t):
  return np.exp(t)*(-x**4 + x**3 + 12*x**2 - 6*x)
  
#パラメータの設定
L = 1.0
kappa = 1.0
N = 51
T = 1.0
lam = 0.4
num = 30

#熱方程式の計算
x, tn, sol = heat_0dbc_ex2(L, kappa, initial2, righthand2, N, T, lam, num)

#図の表示
plot_solution(x, sol)
図8.5
図8.5 Out [8]

次に,fa を, (8.15)f(x,t)=f3(x)=4π2sin(2πx),a(x)=x(1x)sin(4πx) として計算をしてみよう.

In [9]:  
# 初期値 a
def initial3(x):
  return np.sin(4.0*np.pi*x)*x*(1.0 - x)

# 右辺の関数 f
def righthand3(x, t):
  return 4.0*np.pi**2*np.sin(2.0*np.pi*x)
  
#パラメータの設定
L = 1.0
kappa = 1.0
N = 51
T = 1.0
lam = 0.4
num = 30

#熱方程式の計算
x, tn, sol = heat_0dbc_ex2(L, kappa, initial3, righthand3, N, T, lam, num)

#図の表示
plot_solution(x, sol)  
図8.6
図8.6 Out [9]

結果は,図8.6の通りである. このとき.数値解は,時間が十分に経過した際に,ある関数 w=w(x) に漸近しているように見える.実際,(各点ごとに)u(x,t)w(x) (t) を仮定すると,(8.12)と(8.1b)により,w は, 0=κw+f3(x),w(0)=w(1)=0 を満たすはずである.したがって,w(x)=sin(2πx) であることがわかる.実際, u が,t の際に,w[0,1] で一様収束することを示すのは難しくない(例えば,[藤田1977]を見よ).

誤差の考察

非斉次熱方程式の初期値境界値問題(8.12),(8.1b,c)と,その陽的スキーム(8.13),(8.6b,c)の誤差について考察する.その前に,まず,(8.13),(8.6b,c)の解 uin についても, (8.11)に対応した結果を導いておこう.実際, λ1/2 である限りにおいて, |uin+1|(12λ)|uin|+λ(|ui1n|+|ui+1n|)+τ|f(xi,tn)|(1iN) と評価できるので,離散 L 安定性 (8.16)max1iN|uin|max1iN|a(xi)|+τk=0n1max1iN|f(xi,tk)|(n1) が得られる.これを見やすくするために,一般に,v=(v1,,vN)RN に対して, ノルム v=max1iN|vi| を導入し,u(n)=(u1n,,uNn)a=(a(x1),,a(xN))f(n)=(f(x1,tn),,f(xi,tn)) とおく.すると,(8.16)は, (8.16)u(n)a+τk=0n1f(k)(n1) と書ける.

さて,改めて,誤差 ein=uinu(xi,tn) について考察しよう. 常微分方程式の誤差解析の時と同様に,この誤差が,どのような差分スキームを満たすのかを考えることが出発点となる.Uin=u(xi,tn) とおくと, ein+1einτκei1n2ein+ei+1nh2=(uin+1uinτκui1n2uin+ui+1nh2)(Uin+1UinτκUi1n2Uin+Ui+1nh2)=f(xi,tn)(Uin+1UinτκUi1n2Uin+Ui+1nh2)=rinとおく=rin. すなわち,ein は, ein+1einτ=κei1n2ein+ei+1nh2+rin(1iN, n0) と境界条件 e0n=eN+1n=0,および,初期条件 ei0=0 を満たす. したがって,λ が(8.10)を満たすならば,離散 L 安定性(8.16)により, e(n)τk=0n1r(k)(n1) が得られる.ただし,e(n)=(e1n,,eNn)r(n)=(r1n,,rNn) である.

一方で,rin は,方程式(8.12)を使って, rin=ut(xi,tn)κ2ux2(xi,tn)(Uin+1UinτκUi1n2Uin+Ui+1nh2)=(ut(xi,tn)Uin+1Uinτ)κ(2ux2(xi,tn)Ui1n2Uin+Ui+1nh2) と書ける.ここで,T>0 を任意に固定して,Q=[0,L]×[0,T] とおく.すると,差分商に対する誤差評価(8.2b)と(8.5b)を用いて, |rin|12τmax(x,t)Q|2ut2(x,t)|+κ12h2max(x,t)Q|4ux4(x,t)| と評価できる.したがって,さらに, M=12max(x,t)Q|2ut2(x,t)|+κ12max(x,t)Q|4ux4(x,t)| とおくと, r(n)(τ+h2)M(0tnT) が成り立つ.これに,上で導いた, e(n)r(n) の関係式を使うと,次の定理が証明できたことになる.

定理. T>0 を任意に固定する. 非斉次熱方程式の初期値境界値問題(8.12),(8.1b,c)の解 u は,M< となるほど滑らかであるとする.λ について,(8.10)を仮定する. このとき,u と陽的スキーム(8.13),(8.6b,c)の解 uin の誤差は, max0tnTe(n)TM(τ+h2) を満たす.

上の計算を実験的に確かめてみよう. そのために,(8.14)の fa を使う.このとき,厳密解は, u(x,t)=etx3(1x) となる.τ=λh2/κ とするため,誤差の挙動としては,O(h2) が予想される.

In [10]:  
#誤差の計算、非斉次熱方程式、零Dirichlet境界条件、陽的スキーム
def error_heat(L, kappa, initial, righthand, exact, T, lam):
    
  N = 10
  num = 200

  kmax = 6
  hv = np.zeros(kmax)
  ev = np.zeros(kmax)

  for k in range(kmax): 
    N = N*2
    x, tn, sol = heat_0dbc_ex2(L, kappa, initial, righthand, N, T, lam, num)
    err = 0.0
    for n in range(tn.shape[0]):
      tval = tn[n]
      err = max(err, np.linalg.norm(sol[n,:] - exact(x, tval), ord=np.inf))
    hv[k] = L/(N+1)
    ev[k] = err

  rate=(np.log(ev[1:]) - np.log(ev[:-1])) / (np.log(hv[1:]) - np.log(hv[:-1]))
  
  return hv, ev, rate
In [11]:  
# 初期値 a
def initial2(x):
  return x**3*(1.0 - x)

# 右辺の関数 f
def righthand2(x, t):
  return np.exp(t)*(-x**4 + x**3 + 12*x**2 - 6*x)
  
# 厳密解 u
def exact2(x, t):
  return np.exp(t)*(1.0 - x)*x**3
  

#パラメータの設定
L = 1.0
kappa = 1.0
N = 51
T = 1.0
lam = 0.4
num = 30

#熱方程式の計算
hv, ev, rate = error_heat(L, kappa, initial2, righthand2, exact2, T, lam)

#収束の速さの出力
for i in range(rate.shape[0]-1):
  print(f'{hv[i+1]:.3f}, {rate[i]:.3f}')

#結果の描画(両対数目盛)
plt.plot(hv, ev, 'bo-')   
plt.xscale('log')
plt.yscale('log')
plt.legend(['explicit'])  
plt.xlabel('h')
plt.ylabel('error')
plt.grid('on')
plt.gca().set_aspect('equal', adjustable='box') 
plt.show()
  
Out [11]: 
0.024, 1.995
0.012, 1.999
0.006, 2.000
0.003, 2.000
図8.7
図8.7 Out [11]

波動方程式

ギターの弦のように真っ直ぐに張った弦の横振動は,波動方程式 (8.17a)2ut2=c22ux2(0<x<L,t>0),(8.17b)u(0,t)=u(L,t)=0(t>0),(8.17c)u(x,0)=f(x),ut(x,0)=g(x)(0xL) で記述される.u=u(x,t) は弦の鉛直方向の変位,c>0 は伝搬速度, f は弦の初期形状,g は弦の初期速度である.

引き続き,格子点の集合 (xi,tn) (0iN+1, n0) を考える. ただし,N を正の整数として,h=L/(N+1) とおき,xi=ih (i=0,1,,N+1) と,正の定数 τ を固定して,tn=nτ (n=0,1,) としている.

熱方程式の場合と同様に考えると,(8.17a)の差分近似は, (8.18a)uin12uin+uin+1τ2=c2ui1n2uin+ui+1nh2(1iN, n1) とすればよい.境界条件と一つ目の初期条件は, (8.18b)u0n=uN+1n=0(n1),(8.18c)ui0=f(xi)(0iN+1) とする.二つ目の初期条件は前進差分商を応用して, (8.19a)ui1ui0τ=g(xi)(0iN+1) とするのが簡単であるが,次のように考えてもよい.すなわち,t=0 を中心にして,中心差分商を適用して,(仮に n=1 を認めて) (8.19b)ui1ui12τ=g(xi)(0iN+1) が得られる.ここに現れる ui1 を消去するために,n=0 において差分方程式(8.18a) の成立を要請すると, ui12ui0+ui1τ2=c2ui102ui0+ui+10h2 となる.ここで, (8.20)λ=def.cτh と定義する.熱方程式の(8.7)とは異なるので注意せよ. そして,上の関係式から,ui1 を消去すると, ui1=2ui0ui1+λ2(ui102ui0+ui+10)=2ui0ui1+2g(xi)τ+λ2(ui102ui0+ui+10)=2(1λ2)f(xi)ui1+2g(xi)τ+λ2(f(xi1)+f(xi+1)), すなわち,二つ目の初期条件の近似として, (8.18d)ui1=g(xi)τ+(1λ2)f(xi)+λ22(f(xi1)+f(xi+1))(0iN+1) が得られる.

まとめると, 波動方程式に対する初期値境界値問題(8.17a,b,c)に対する差分解法は,次のようになる.まず, (8.18c)と(8.18d)で, {ui0}1iN{ui1}1iN を計算し,次に,n1 に対して, uin+1=2(1λ2)uin+λ2(ui1n+ui+1n)uin1(1iN){uin+1}1iN を計算するわけである.

In [12]:  
#非斉次波動程式、零Dirichlet境界条件、陽的スキーム
def wave_0dbc_ex(L, c, funcf, funcg, N, T, lam, num):
    
  h = L/(N+1)
  tau = lam*h/c
  nmax = int(T/tau)
  step=int(max(1, nmax/num))
  
  xgrid=np.linspace(0.0, L, N+2)
  tgrid=np.array([0.0])
  unew = np.zeros_like(xgrid)
  
  # n = 0
  u = np.vectorize(funcf)(xgrid)
  n = 0
  tnow = n*tau
  sol = np.copy(u)
  upast = np.copy(u)
  
  # n = 1
  u[1:N+1]=tau*np.vectorize(funcg)(xgrid[1:N+1])+(1-lam**2)*u[1:N+1]+(lam**2/2.0)*(u[0:N]+u[2:N+2])
  n = 1 
  tnow=n*tau
  sol=np.vstack((sol,u))
  tgrid=np.append(tgrid, tnow)
  
  # n >= 2 
  for n in range(1, nmax, 1):
    unew[1:N+1] = 2*(1-lam**2)*u[1:N+1] + lam**2*(u[0:N]+u[2:N+2]) - upast[1:N+1]
    upast=np.copy(u)
    u=np.copy(unew)  
    tnow=(n+1)*tau 
    if (n+1)%step==0:
      sol=np.vstack((sol,u))
      tgrid=np.append(tgrid, tnow)

  return xgrid, tgrid, sol
In [13]:  
# 初期値 f
def displacement(x):
  return np.sin(2.0*np.pi*x)
      
# 初期値 g
def velocity(x):
  return 0.0
  
#パラメータの設定
L = 1.0
c = 1.0
N = 51
T = 2
lam = 1.0
num = 60

#波動方程式の計算
x, tn, sol = wave_0dbc_ex(L, c, displacement, velocity, N, T, lam, num)

#図の表示
plot_solution(x, sol)

#アニメーションを表示
anim = plot_animation(x, tn, sol)
#結果をheat.gifに保存(Googleドライブをマウントしておくこと!)
anim.save('/content/drive/MyDrive/Colab Notebooks/fig/wave.gif', writer='pillow')
rc('animation', html='jshtml')
plt.close()
anim

fg を, (8.21)f(x,t)=sin(2πx),g(x,t)=0 とした際の計算結果を.図8.8a, bに示す. c=1N=51λ=1T=2 としている.

図8.
図8.8a Out [13]c=1N=51λ=1T=2
図8.
図8.8b Out [13]c=1N=51λ=1T=2

しかしながら,図8.9に示した通り,λ=1.1 とすると, 差分スキームの解は破綻してしまう.

図8.
図8.9 c=1N=51λ=1.1T=0.95

実は,λ が, (8.22)λ1 を満たすならば,差分スキームの解は(ある意味において)安定であり,また,波動方程式の解に収束する.この(8.22)を,Courant--Friedrichs--Lewy (CFL) 条件という(これは,[CFL]を起源としている).

しかしながら,熱方程式の(8.16)のような顕著な安定性が得られないため,誤差解析は,案外厄介である.興味のある人は,[齊藤2023]や[John]を見よ.したがって,本講義では,数値実験のみで誤差の挙動を確かめておく.c=1 として,(8.21)の fg を用いたとき, u(x,t)=cos(2πt)sin(2πx) が厳密解となる. Out [15]により,誤差の挙動は O(τ2+h2) であることが予想される(そして実際にこの予想は正しい).

In [14]:  
#誤差の計算、波動方程式、零Dirichlet境界条件、陽的スキーム
def error_wave(L, c, funcf, funcg, exact, T, lam):
    
  N = 10
  num = 100

  kmax = 6
  hv = np.zeros(kmax)
  ev = np.zeros(kmax)

  for k in range(kmax): 
    N = N*2
    x, tn, sol = wave_0dbc_ex(L, c, funcf, funcg, N, T, lam, num)
    err = 0.0
    for n in range(tn.shape[0]):
      tval = tn[n]
      err = max(err, np.linalg.norm(sol[n,:] - exact(x, tval), ord=np.inf))
    hv[k] = L/(N+1)
    ev[k] = err
 
  rate=(np.log(ev[1:]) - np.log(ev[:-1])) / (np.log(hv[1:]) - np.log(hv[:-1]))

  return hv, ev, rate
In [15]:  
# 初期値 f
def displacement(x):
  return np.sin(2.0*np.pi*x)
      
# 初期値 g
def velocity(x):
  return 0.0
  
# 厳密解 u
def exact_wave(x, t):
  return np.sin(2.0*np.pi*x)*np.cos(2.0*np.pi*t)
  
#パラメータの設定
L = 1.0
c = 1.0
T = 1.0
lam = 0.9

#波動方程式の計算
hv, ev, rate = error_wave(L, c, displacement, velocity, exact_wave, T, lam)

#収束の速さの出力
for i in range(rate.shape[0]-1):
  print(f'{hv[i+1]:.3f}, {rate[i]:.3f}')

#結果の描画(両対数目盛)
plt.plot(hv, ev, 'bo-')   
plt.xscale('log')
plt.yscale('log')
plt.legend(['explicit'])  
plt.xlabel('h')
plt.ylabel('error')
plt.grid('on')
plt.gca().set_aspect('equal', adjustable='box') 
plt.show()  

Out [15]: 
0.024, 2.004
0.012, 2.000
0.006, 2.000
0.003, 2.000
図8.7
図8.10 Out [15]

注意. In [15] において,λ=1 とすると,「精度が良すぎて」逆に精度が観察できない.各自,試してみよ.

注意. (8.22)を,Courant--Friedrichs--Lewy (CFL) 条件と言うと述べた. 一方で,熱方程式に対する(8.10)も,CFL条件と言われることがあるが,私個人は,適切な用語法とは思えないため,使わない.

問題

  1. 上記の入力と出力をすべて確かめよ.
  2. パラメータ値を変えて,計算を再試行せよ.
  3. 例題を自分で設定して,上記の方法を試せ.

課題

課題8.1

熱方程式の初期値境界値問題(8.1a,b,c)に対して,厳密解を自分で作り,λ=1/6 のとき, 陽的スキーム(8.6a,b,c)の誤差について調べよ. 可能ならば,観察した事実を,数学的に説明せよ. さらに, 非斉次の場合(8.12)と(8.13)についても同様の観察をせよ.

課題8.2

熱方程式(8.1a)に対して,初期条件(8.1c)と,非斉次のDirichlet境界条件 u(0,t)=1,u(L,t)=0(t>0) を与えた時の初期値境界値問題を考え,差分スキームを導け.また, tn の際の数値解 uin の挙動を観察せよ.

課題8.3

初期条件 ut(x,0)=g(x) の近似を,(8.19b)でなく(8.19a)とした場合の,波動方程式の初期値境界値問題(8.17a,b,c)に対する差分スキームを導け.また,実際に計算を遂行して,誤差の挙動を調べよ.

課題8.4

波動方程式の初期値境界値問題(8.17a,b,c)において,境界条件を,周期境界条件 u(0,t)=u(L,t),ux(0,t)=ux(L,t)(t>0) に代えた問題に対して差分法を適用せよ(fg は境界条件に応じて,適切に選ぶこと).

ヒント: この場合は,xi=ih (i=0,1,,N)h=L/N と定めた方が良い. そして,uinu(xi,tn) に対して,境界条件により,uin=ui+Nn (i) とする.

参考文献

---「8. 偏微分方程式:熱伝導,波動の数値計算」はこれで終了---