Pythonプログラミング(その他のトピック・KdV方程式とソリトン)
このページでは、代表的なソリトンの例であるKdV方程式のシミュレーションを試みてみよう。併せて、時間変化する現象の可視化方法についても、何通りか試してみることにする。(ここはこれから)
津波を記述する方程式
底面が平らなプールに水が満たされているとき、その表面に立つ波を考えてみる。
波の波長がプールの深さよりも十分に長く、その振幅はプールの深さより十分に小さい状況で、
非圧縮(水の密度がどこでも一定)で渦はなく、粘性も無視できるなどの仮定を置くと、いわゆる浅水波を記述する方程式が得られる。
プールの縦の断面を取って、横方向をx軸として、水面の平均的な高さを
表面に立つ波の性質をさらに詳しく調べ、流体力学的な非線形効果まで考慮すると、波の振る舞い方が異なってくることが Korteweg-de Vriesらによって
明らかになった(再発見された)。
座標を線形波の速さで動いている座標系に取り直すと(
変数のスケールを取り直すことによって、各項の係数を調整することができる。
標準的な形として
(書きかけ)
KdV方程式とソリトン
KdV方程式には様々な(無限の)解が存在することが知られている。
ひと山のパルスが伝搬する解は1-ソリトン解と呼ばれ、位相速度を
解の導出は専門書に譲って、これが本当に方程式の解になっているかどうか、式に代入して確認してみることにしよう。 以下はsympyを使って、上記の解をKdV方程式に代入し、方程式が成り立つかどうか(結果が0になるかどうか)を調べるコードの例である:
from sympy import * x,t,u,kdveq = symbols("x, t, u, kdveq") k,c,d = symbols("k, c, d") c = 4*k**2 u = 2 * k**2 * sech(k*(x-c*t+d))**2 kdveq = diff(u,t) + 6 * u * diff(u,x) + diff(u,x,3) print(simplify(kdveq))
sympyには代表的な関数はすでに定義されており、双曲線関数
(書きかけ)
離散フーリエ変換による微分の計算
以下では、Numerical Solution of the KdVに述べられている方法を参考に、 KdV方程式を数値的に解くための具体的なコードを考えてみよう。
KdV方程式のシミュレーションには、一階および三階の空間微分を精度よく行う必要がある。
そのためには、スペクトル精度を持つFFTを使った微分計算が有効である。
具体的、関数
長さ
これを使うと、一階微分は
全く同様に、
微分方程式の時間発展の記述
解きたいKdV方程式は
フーリエ変換したKdV方程式は
ただし、このスキームをそのまま適用すると、誤差が累積して短時間で計算が破綻してしまう。
そこで、こちらのサイトでは、
Pythonによるコーディングの例
以下は
時間方向の積分は関数nextstep()
の中に書かれており、
前節のアルゴリズムそのままではなく、中点法を使って
このコードはGoogle Colaboratoryで実行することを想定。
単体で実行する場合は、末尾付近のplt.close()
の行はplt.show()
に変更すること。
import numpy as np import random import math import matplotlib.pyplot as plt from matplotlib import animation, rc from IPython.display import HTML def prep_3iq(n,dx): A = np.zeros((n,),dtype=np.complex64) for k in range(0,n): q = 2*math.pi*k/(dx*n) if k<=n//2 else 2*math.pi*(k-n)/(dx*n) A[k] = 3*1j*q return A def prep_iq3(n,dx): A = np.zeros((n,),dtype=np.complex64) for k in range(0,n): q = 2*math.pi*k/(dx*n) if k<=n//2 else 2*math.pi*(k-n)/(dx*n) A[k] = 1j*q*q*q return A def nextstep(): global u,t U = np.fft.fft(u) for cnt in range(500): U1 = U * np.exp(OP_iq3 * dt) K1 = - OP_3iq * np.fft.fft(u*u) U2 = U1 + K1 * dt/2 u2 = np.fft.ifft(U2).real K2 = - OP_3iq * np.fft.fft(u2*u2) U = U1 + K2 * dt u = np.fft.ifft(U).real t = t + dt ### N = 256 L = 20 dx = L/N OP_3iq = prep_3iq(N,dx) OP_iq3 = prep_iq3(N,dx) x = np.linspace(-L/2,L/2,N) u = np.exp(-x*x)*12 HSCALE=4 VSCALE=10 H = 800 fig, ax = plt.subplots() images = [] t = 0 dt = 0.00001 ax.set_xlabel('x') ax.set_ylabel('u') while t<0.1: img = ax.plot(x,u,color='red') label = plt.text(0.5,1, 't='+'{:.4f}'.format(t), ha='left',va='bottom',color='black', transform=ax.transAxes) images.append([img[0],label]) nextstep() anim = animation.ArtistAnimation(fig, images, interval=100) rc('animation', html='jshtml') plt.close() anim
上のコードをGoogle Colaboratoryのノートブック上で実行している様子。
上のコード例は、計算結果をプロットした画像をあらかじめ蓄えておいて、紙芝居のように表示するようにしているので、長時間にわたる表示には向いていない。
そこで、pygletを使って、画面に直接描画出力するコードの例も以下に示しておく。
このコードを実行するには pyglet(バージョン1.5.27以前) が必要
pyglet version 2をお使いの方は kdv-eq-pyglet2.pyをダウンロードしてお使いください。
# coding: utf-8 import numpy as np import random import math import pyglet from pyglet.gl import * def prep_3iq(n,dx): A = np.zeros((n,),dtype=np.complex64) for k in range(0,n): q = 2*math.pi*k/(dx*n) if k<=n//2 else 2*math.pi*(k-n)/(dx*n) A[k] = 3*1j*q return A def prep_iq3(n,dx): A = np.zeros((n,),dtype=np.complex64) for k in range(0,n): q = 2*math.pi*k/(dx*n) if k<=n//2 else 2*math.pi*(k-n)/(dx*n) A[k] = 1j*q*q*q return A ### N = 256 L = 20 dx = L/N OP_3iq = prep_3iq(N,dx) OP_iq3 = prep_iq3(N,dx) x = np.linspace(-L/2,L/2,N) u = np.exp(-x*x)*12 t = 0 dt = 0.00001 HSCALE=4 VSCALE=10 H = 800 config = pyglet.gl.Config(double_buffer=True) win = pyglet.window.Window(N*HSCALE,H,config=config,resizable=False) win.set_caption('KdV Equation') def nextstep(): global u,t U = np.fft.fft(u) for cnt in range(500): U1 = U * np.exp(OP_iq3 * dt) K1 = - OP_3iq * np.fft.fft(u*u) U2 = U1 + K1 * dt/2 u2 = np.fft.ifft(U2).real K2 = - OP_3iq * np.fft.fft(u2*u2) U = U1 + K2 * dt u = np.fft.ifft(U).real t = t + dt @win.event def on_draw(): glClear(GL_COLOR_BUFFER_BIT) glClearColor(0,0,0,1) glColor3d(0.2,0.2,0.3) glLineWidth(2.0) for j in range(-100,100,10): glBegin(GL_LINES) glVertex2d(0, H/2 + VSCALE*j) glVertex2d(N*HSCALE, H/2 + VSCALE*j) glEnd() str = '{0}'.format(j) label_y = pyglet.text.Label(str,font_name='Helvetica', color=(255,255,255,255), font_size=12, x=20, y=H/2+VSCALE*j-6) label_y.draw() glColor3d(1.0, 0.5, 0.1) glLineWidth(4.0) glBegin(GL_LINE_STRIP) for i in range(N): glVertex2d(i*HSCALE, H/2+u[i]*VSCALE) glEnd() str = '{0}'.format(-L/2) label_x0 = pyglet.text.Label(str,font_name='Helvetica', color=(255,255,255,255), font_size=12, x=0, y=H/2-25) label_x0.draw() str = '{0}'.format(L/2) label_x1 = pyglet.text.Label(str,font_name='Helvetica', color=(255,255,255,255), font_size=12, x=N*HSCALE-40, y=H/2-25) label_x1.draw() str = 'T={0:.3f}'.format(t) label_time = pyglet.text.Label(str,font_name='Helvetica', color=(255,255,255,255), font_size=12, x=10, y=10) label_time.draw() def update(deltat): nextstep() pyglet.clock.schedule_interval(update, 1/20.0) pyglet.app.run()
上のコードを実行した様子が以下の動画である(YouTube)。 初期条件として単峰の山(ガウス曲線)を与えると、それが複数の孤立波(ソリトン)に分裂し、進行する様子が観察できる。 振幅の大きな波ほど伝搬速度(位相速度)が大きいことも判る。
練習:積分スキームの比較
上のコードの関数 nextstep()
中の積分のスキームを
- オイラー差分(1次)
- 4次のルンゲクッタ
に変更し、それぞれの動作や安定性を確認してみなさい。
解説: Waterfall型のプロット
下図のように、波動の伝搬の様子を時空間的に可視化し、アニメーション表示するコードの例を示す。 時間が「上」から「下」に流れるように表示されるので waterfall plotと呼ぶことがある。 ソリトンが衝突し、再び分裂して進行する様子がよく確認できる(YouTube動画)。
計算部分は上記の例と全く同様であるが、計算結果を配列に格納・操作し、それを画像として表示することで、画面の更新をいくばくか高速化している。
NumPyの配列を画像として扱う際に、画像の横軸が「列」(axis=1)、画像の縦軸が「行」(axis=0)に対応する点に注意が必要である。
このコードを実行するには pyglet(version 1.5以下)が必要
pyglet version 2をお使いの方は kdv-eq-scroll-pyglet2.pyをダウンロードしてお使いください。
#coding utf-8 import numpy as np import random import math import pyglet from pyglet.gl import * def prep_3iq(n,dx): A = np.zeros((n,),dtype=np.complex64) for k in range(0,n): q = 2*math.pi*k/(dx*n) if k<=n//2 else 2*math.pi*(k-n)/(dx*n) A[k] = 3*1j*q return A def prep_iq3(n,dx): A = np.zeros((n,),dtype=np.complex64) for k in range(0,n): q = 2*math.pi*k/(dx*n) if k<=n//2 else 2*math.pi*(k-n)/(dx*n) A[k] = 1j*q*q*q return A ### N = 256 L = 20 dx = L/N OP_3iq = prep_3iq(N,dx) OP_iq3 = prep_iq3(N,dx) x = np.linspace(-L/2,L/2,N) u = np.exp(-x*x)*12 t = 0 dt = 0.00001 SCALE=1 H = 400 IMG_DATA = np.zeros(shape=(H,N*SCALE,3),dtype=np.uint8) config = pyglet.gl.Config(double_buffer=True) win = pyglet.window.Window(N*SCALE,H,config=config,resizable=False) win.set_caption('KdV Equation') def nextstep(): global u,t,umem U = np.fft.fft(u) for cnt in range(100): U1 = U * np.exp(OP_iq3 * dt) K1 = - OP_3iq * np.fft.fft(u*u) U2 = U1 + K1 * dt/2 u2 = np.fft.ifft(U2).real K2 = - OP_3iq * np.fft.fft(u2*u2) U = U1 + K2 * dt u = np.fft.ifft(U).real t = t + dt @win.event def on_draw(): glClear(GL_COLOR_BUFFER_BIT) glClearColor(0,0,0,1) global IMG_DATA,u IMG_DATA = np.roll(IMG_DATA,1,axis=0) for i in range(N): r = int((math.sin(u[i]*0.1)+1)/2*255) g = int((math.sin(2*u[i]*0.1)+1)/2*255) b = int((1.0+math.cos(u[i]*0.1))/2*255) IMG_DATA[0,i*SCALE:(i+1)*SCALE,0] = r IMG_DATA[0,i*SCALE:(i+1)*SCALE,1] = g IMG_DATA[0,i*SCALE:(i+1)*SCALE,2] = b pixels = IMG_DATA.ravel() rawData = (GLubyte * len(pixels))(*pixels) imageData = pyglet.image.ImageData(N*SCALE, H, 'RGB', rawData) imageData.blit(0, 0) glColor3d(1,1,1) glLineWidth(1) glBegin(GL_LINE_STRIP) glVertex2d(1, H-10) glVertex2d(1, H-20) glVertex2d(N*SCALE, H-20) glVertex2d(N*SCALE, H-10) glEnd() str = '{0}'.format(-L/2) label_x0 = pyglet.text.Label(str,font_name='Helvetica', color=(255,255,255,255), font_size=10, x=0, y=H-30) label_x0.draw() str = '{0}'.format(L/2) label_x1 = pyglet.text.Label(str,font_name='Helvetica', color=(255,255,255,255), font_size=10, x=N*SCALE-30, y=H-30) label_x1.draw() str = 'T={0:.3f}'.format(t) label_time = pyglet.text.Label(str,font_name='Helvetica', color=(255,255,255,255), font_size=10, x=N*SCALE/2-30, y=H-12) label_time.draw() def update(deltat): nextstep() pyglet.clock.schedule_interval(update, 1/20.0) pyglet.app.run()
(これから)