grbl1.1+Arduino CNCシールドV3.5+bCNCを使用中。
BluetoothモジュールおよびbCNCのPendant機能でスマホからもワイヤレス操作可能。
その他、電子工作・プログラミング、機械学習などもやっています。
MacとUbuntuを使用。

CNCマシン全般について:
国内レーザー加工機と中国製レーザー加工機の比較
中国製レーザーダイオードについて
CNCミリングマシンとCNCルーターマシンいろいろ
その他:
利用例や付加機能など:
CNCルーター関係:



*CNCマシンの制作記録は2016/04/10〜の投稿に書いてあります。


2019年12月9日月曜日

ローレンツ・アトラクタ: オイラー法 / ルンゲクッタ法

カオス理論によく出てくるローレンツアトラクタをPython/Jupyter Notebookで描いてみました。

ローレンツ方程式:
ローレンツ方程式は、

dx/dt = -p*x + p*y
dy/dt = -x*z + r*x - y
dz/dt = x*y - b*z

であり、これら三つの微分方程式に任意の初期値x,y,zを与えると(x=0, y=0, z=0以外)、係数p=10, r=28, b=8/3の時にカオス的な振る舞いをするというもの。通常の数式なら発散/収束/周期的になりますが、そうはならないというところが面白い。また初期値がほんの少し違うだけで結果が大きく変わるという初期値鋭敏性も持つ。

初期値x=1, y=1, z=1を与えて、1ステップ0.001で50000ステップ分計算させたもの。
コードは以下。
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
dt = 0.001
T = 50
p, r, b = 10, 28, 8/3
print('steps:', int(T/dt))

def lorenz_euler(V):
    x, y, z = V[0], V[1], V[2]
    X, Y, Z = [], [], []
    t = 0
    while t <= T:
        dx = (-p*x + p*y) * dt
        dy = (-x*z + r*x - y) * dt
        dz = (x*y - b*z) * dt
        x += dx
        y += dy
        z += dz
        X.append(x)
        Y.append(y)
        Z.append(z)
        t += dt
    return X, Y, Z

V0 = [1.0, 1.0, 1.0]
X, Y, Z = lorenz_euler(V0)

fig = plt.figure(figsize=(8, 8))
ax = fig.add_subplot(111, projection='3d')
g = 25
ax.set_xlim(-g,g)
ax.set_ylim(-g,g)
ax.set_zlim(0, g*2)
ax.plot(X, Y, Z, color='blue', alpha=0.6)
plt.show()

とりあえず、xの式に注目すると、

dx/dt = -p*x + p*y

の式を変形して、

dx = (-p*x + p*y) * dt

にすれば、dxはxにおける1ステップ分の変化量なので、whileループ内で変化量dxをxに加算していき、設定した限度を越えればwhileループを打ち切り。

x += dx

という感じで、yやzについても同様に処理し、プログラミング的には簡単な手続き。

しかし調べてみると、これは「オイラー法」という計算方法であり、あまり精度が保てないらしい。精度を出すには1ステップの刻み幅をより細かくすれば単純に向上しますが、そうすると全体のステップ数も増え計算量が多くなります。そのための工夫としてルンゲクッタ法というのがあります。以下では、各計算方法の比較をしてみます。


オイラー法:
まず、簡単な例としてオイラー法から。

f(x) = x*sin(x)

という式で試してみます。
f(x)を微分すると、

f'(x) = sin(x) + x*cos(x)

になります。
xの変化量dxは、

dx = f'(x)*dt = (sin(x) + x*cos(x))*dt

となり、

x += dx

で、xの値を更新してループ処理させます。
以下コード。
h = 0.1
T = np.pi * 2
print('steps:', int(T/h))

def f(x):
    return x * np.sin(x)

X0 = np.arange(0, T, h)
Y0 = f(X0)

def df(x): # derivative of x*sin(x) : (x*sin(x))' = sin(x) + x*cos(x) 
    return np.sin(x) + x * np.cos(x)

x = 0
y = 0
X = [x]
Y = [y]
while x <= T - h:
    dx = h
    dy = df(x) * h
    x += dx
    y += dy
    X.append(x)
    Y.append(y)

fig = plt.figure(figsize=(8, 4))
ax = fig.add_subplot(111)
ax.grid()
ax.plot(X0, Y0, label='True ', alpha=0.8, color='C0')
ax.plot(X, Y, label='Euler', alpha=0.8, color='C1')
ax.legend()
1ステップの刻み幅は0.1。合計で62ステップ。X, Yが積分したオイラー法の結果で、X0, Y0は積分を使わないで求めた結果。

わずかですが誤差があります。刻み幅をもっと細かくすれば誤差も減りますが、ローレンツアトラクタの場合は初期値鋭敏性もあるためこの誤差は致命的かもしれません。
y値最大誤差:0.3043499782


2次のルンゲクッタ法:
この方法は改良オイラー法とも呼ばれているようで、その名の通りオイラー法よりは誤差が少ない。オイラー法では刻み幅とその時のy値(高さ)を掛け合わせているため、細長い長方形を積算していますが、2次のルンゲクッタ法では長方形上端を斜め(変化量の傾き)にした台形で計算します。その分精度向上するというもの。
h = 0.1
T = np.pi * 2
print('steps:', int(T/h))

def f(x):
    return x * np.sin(x)

X0 = np.arange(0, T, h)
Y0 = f(X0)

def df(x): # derivative of sin(x)
    return np.sin(x) + x * np.cos(x)

x = 0
y = 0
X2 = [x]
Y2 = [y]

while x <= T - h:
    k1 = df(x)
    k2 = df(x + h)
    x += h
    y += h / 2 * (k1 + k2)
    X2.append(x)
    Y2.append(y)

fig = plt.figure(figsize=(8, 4))
ax = fig.add_subplot(111)
ax.grid()
ax.plot(X0, Y0, label='True ', alpha=0.8)
ax.plot(X2, Y2, label='RK2', alpha=0.8)
ax.legend()
k1とk2が台形の異なる二辺の長さ。あとは台形面積を求める計算。
今回はdtの代わりにhを刻み幅にしていますが同じ0.1です。誤差は0(h^3)程度。

かなり誤差はなくなりました。
y値最大誤差:0.0042499318


4次のルンゲクッタ法:
さらに精度をあげた方法。通常この4次のルンゲクッタ法くらいの精度が必要らしい。先ほどの2次ではk1、k2の2点でしたが、今回はk1〜k4の4点をとり、その平均を求めます。
h = 0.1
T = np.pi * 2
print('steps:', int(T/h))

def f(x):
    return x * np.sin(x)

X0 = np.arange(0, T, h)
Y0 = f(X0)

def df(x): # derivative of x*sin(x) : (x*sin(x))' = sin(x) + x*cos(x) 
    return np.sin(x) + x * np.cos(x)

x = 0
y = 0
X4 = [x]
Y4 = [y]

while x < T - h:
    k1 = df(x) * h
    k2 = df(x + h/2) * h
    k3 = df(x + h/2) * h
    k4 = df(x + h) * h
    x += h
    y += (k1 + 2*k2 + 2*k3 + k4) / 6
    X4.append(x)
    Y4.append(y)

fig = plt.figure(figsize=(8, 4))
ax = fig.add_subplot(111)
ax.grid()
ax.plot(X0, Y0, label='True ', alpha=0.8)
ax.plot(X4, Y4, label='RK4', alpha=0.8)
ax.legend()
テイラー展開により導出できるようですが、そのまま公式を覚えた方がよさそう。式中では刻み幅の両端点と中点をとって、1:2:2:1の割合で平均近似しているようです。そうすると、曲線上の中点の値df(x+h/2)と刻み幅hの両端を直線で結んだ時の中間平均値:(df(x) + df(x+h))/2には差分があるので、その2点の2/3の地点が計算で求まるようです。

グラフを見ただけでは違いはわかりにくいですが、数値的に誤差を求めてみるとさらに精度が上がっているようです。理論上、誤差は0(h^5)。
y値最大誤差:0.0000003046


scipyのライブラリ:
自前でルンゲクッタ法をコーディングするかわりに、scipyのライブラリを使ってもいいかもしれません。

scipy.integrate.solve_ivp (methodでRK45など選択可)
scipy.integrate.odeint (古いタイプ?)
scipy.integrate.RK4 (使いにくい)

solve_ivpでは計算法としてRK23やRK45も選択可能。t_evalで刻み幅を配列で指定。さらにオプションのrtol, atolで精度を指定することもでできるようなので、アルゴリズム的な工夫はほぼ必要なし。デフォルトではrtol=1e-3、atol=1e-6になっており、そのままでも大丈夫そう。


RK2、RK4、Odeint、Solve_ivpの比較:
ルンゲクッタ法(RK2、RK4)、ならびにライブラリのOdeint、Solver_ivp(RK45)をローレンツアトラクで比較してみることに(とは言っても、初期値鋭敏性のあるローレンツアトラクタなので、精度の比較はしにくいですが)。
さらに、3D/2D表示、XYZの各値の変化を1D表示するファンクションも追加しておきました。
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from scipy.integrate import odeint
from scipy.integrate import solve_ivp

%matplotlib inline
#%matplotlib notebook

def df(t, xyz):
    x, y, z = xyz
    return np.array([-p*x + p*y, -x*z + r*x - y, x*y - b*z])

def lorenz_RK2(V):
    xyz = V
    t = 0
    X, Y, Z = [], [], []
    while t <= T:
        k1 = df(t, xyz)
        k2 = df(t+h, xyz + h*k1)
        xyz += (k1 + k2) * h / 2
        X.append(xyz[0])
        Y.append(xyz[1])
        Z.append(xyz[2])
        t += h
    return X, Y, Z

def lorenz_RK4(V):
    xyz = V
    t = 0
    X, Y, Z = [], [], []
    while t <= T:
        k1 = df(t, xyz)
        k2 = df(t+h/2, xyz + h*k1/2)
        k3 = df(t+h/2, xyz + h*k2/2)
        k4 = df(t+h, xyz + h*k3)
        xyz += (k1 + 2*k2 + 2*k3 + k4) * h / 6
        X.append(xyz[0])
        Y.append(xyz[1])
        Z.append(xyz[2])
        t += h
    return X, Y, Z

def lorenz_Odeint(V):
    xyz = V
    t = np.arange(0, T, h)
    X, Y, Z = odeint(df, xyz, t, tfirst=True).T 
    #X, Y, Z = odeint(f2, xyz, t, rtol=1e-9, atol=1e-9).T
    return X, Y, Z

def lorenz_ivp(V):
    xyz_0 = np.array(V)
    t = np.arange(0, T, h) 
    res = solve_ivp(df, (0, T), xyz_0, method='RK45', t_eval=t)#, rtol=1e-7, atol=1e-9)
    X = res.y[0]
    Y = res.y[1]
    Z = res.y[2]
    print(X[-1], Y[-1], Z[-1])
    return X, Y, Z

def plot3D(methods, labels):
    from matplotlib.colors import BASE_COLORS
    colorlist = list(BASE_COLORS.keys())
    
    fig = plt.figure(figsize=(10,10))
    ax = fig.add_subplot(111, projection='3d')
    g = 30 
    ax.set_xlim(-g, g)
    ax.set_ylim(-g, g)
    ax.set_zlim(0, 2*g)
    ax.xaxis.pane.set_facecolor('white')
    ax.yaxis.pane.set_facecolor('white')
    ax.zaxis.pane.set_facecolor('white')

    for i, XYZ in enumerate(methods):
        X = XYZ[0]
        Y = XYZ[1]
        Z = XYZ[2]
        
        #color = 'C' + str(i)
        color = colorlist[i]
        ax.plot(X, Y, Z, color=color, alpha=0.5, label=labels[i]) # plot method
        ax.scatter(X[0], Y[0], Z[0], color=color, s=60, marker='x') # start point
        ax.text(X[0], Y[0], Z[0], s='START  ', color=color, verticalalignment='top', horizontalalignment='right')
        ax.scatter(X[-1], Y[-1], Z[-1], color=color, s=30, marker='o') # end point

    plt.legend()
    plt.show()

def plot2D(methods, labels, axis):
    from matplotlib.colors import BASE_COLORS
    colorlist = list(BASE_COLORS.keys())
    
    fig = plt.figure(figsize=(8,8))
    ax = fig.add_subplot(111)
    ax.axis([-30, 30, 0, 60], 'equal')
    if axis == 'XY':
        ax.axis([-30, 30, -30, 30], 'equal')     
    ax.grid()
    
    for i, XYZ in enumerate(methods):
        if axis == 'XY':
            A = XYZ[0]
            B = XYZ[1]
        elif axis == 'XZ':
            A = XYZ[0]
            B = XYZ[2]
        else:
            A = XYZ[1]
            B = XYZ[2]
        
        color = colorlist[i]
        ax.plot(A, B, color=color, alpha=0.5, label=labels[i])  # plot line
        ax.scatter(A[0], B[0], color=color, s=60, marker='x')   # start point
        ax.text(A[0], B[0], s='START  ', color=color, verticalalignment='center', horizontalalignment='right')
        ax.scatter(A[-1], B[-1], color=color, s=30, marker='o') # end point
    plt.legend()
    plt.show()
    print(axis + ' coordinates')

def plot1D(methods, labels, axis):
    plt.figure(figsize=(14, 3))
    for i, XYZ in enumerate(methods):
        if axis == 'X':
            A = XYZ[0]
        elif axis == 'Y':
            A = XYZ[1]
        else:
            A = XYZ[2]
        plt.plot(list(range(len(A))), A, color='C'+str(i), alpha=0.6, label=labels[i])
    plt.legend()
    plt.show()

実行用のコードは以下。
h = 1e-3 # dt
T = 30   # total time
p, r, b = 10, 28, 8/3
print('steps:', int(T/h))

xyz_0 = [10.0, 10.0, 10.0] # initial values for the methods 
methods = [lorenz_RK2(xyz_0), lorenz_RK4(xyz_0), lorenz_Odeint(xyz_0), lorenz_ivp(xyz_0)]
labels = ['RK2', 'RK4', 'ODE', 'IVP']

plot3D(methods, labels)
# plot2D(methods, labels, 'XY')
# plot2D(methods, labels, 'XZ')
plot2D(methods, labels, 'YZ')
plot1D(methods, labels, 'X')

表示結果。
 4種類の異なる計算方法。x印が開始点、●印が終着点(水色のIVPは見えにくくなっていますが)。


YZ座標。この方向からみるとほぼ線対称の形になります。


縦軸はX値、横軸はステップ数。
6000ステップくらいまでは一緒ですが、その後は次第にずれてきています。単体だけ見ると、規則的に見える部分もありますが、やはり非周期的になっています。


初期値鋭敏性:
計算方法は同じで、初期値をほんの少しだけ変えて実験してみました。
初期値A:X=10.0、Y=10.0、Z=10.0
初期値B:X=10.001、Y=10.0、Z=10.0
BのXの初期値だけ0.001違います。あとは同じ条件で50000ステップ。
以下が実行コード。
h = 1e-3
T = 50
p, r, b = 10, 28, 8/3
print('steps:', int(T/h))

xyz_0 = [10.0, 10.0, 10.0]
xyz_1 = [10.001, 10.0, 10.0]
methods = [lorenz_RK4(xyz_0), lorenz_RK4(xyz_1)]
labels = ['RK4:'+str(xyz_0), 'RK4:'+str(xyz_1)]

plot3D(methods, labels)
# plot2D(methods, labels, 'XY')
# plot2D(methods, labels, 'XZ')
# plot2D(methods, labels, 'YZ')
plot1D(methods, labels, 'X')
plot1D(methods, labels, 'Z')

結果表示。
軌道に差があるため、青と緑のムラができています。✖️が開始点、●は終着点。


上が50000ステップにおけるX値。下はZ値。
10000ステップを超えたあたりから差がでてきます。ずれるというより、異なったパターンになってきています。
完璧なランダムとは言えませんが、数多くのステップ数を繰り返しても周期的にならないという部分が興味深い。


分布について:
ランダムの指標として、値全体の分布もとってみました。
これはオイラー法による結果ですが、上から、X値、Y値、Z値の分布です(50000ステップ)。
基本的には中央に寄っていますが、正規分布とも違う。


まとめ:
元々はカオス的な振る舞いをするローレンツアトラクタを利用して乱数生成できないかということを考えておりましたが、計算方法であるルンゲクッタ法やライブラリについての実験となりました。これはこれでいろいろ勉強になります。精度を高める計算方法にはいくつかあるようですが、PID制御もそのうちの一つという感じです。ということで、引き続き調査してみたいと思います。




2019年11月27日水曜日

フーリエ級数を用いて正多角形を描く(その2)

前回の続き、フーリエ級数を用いた正多角形を描くアルゴリズムについて。

正n角形を描く:
正n角形を描くには、

y = Σ(1/C2 * sin(C * θ))
θ = 0〜2π

Cは無限級数であり、例えば正方形(n=4)を描くには、

C = [..., -3*n+1, -2*n+1, -1*n+1, 0*n+1, 1*n+1, 2*n+1, ...]
C = [..., -11, -7, -3, 1, 5, 9, ...]

になり、1を基準としてn=4ずつ加算、あるいは負の方向であれば4ずつ減算した級数。
あるいは、k%n==1 (k=-k, ..., -3, -2, -1, 0, 1, 2, 3, ... k)のときのkの値。
仮に級数を6つに限定すれば、

C = [-11, -7, -3, 1, 5, 9]

となります。そして各波形を

y = 1/C2 * sin(C * θ)

とすれば以下のようになります。
横軸はθ、縦軸の値はCの値。C=9やC=-11のときはほぼ直線に見えますが、僅かながら振幅しています。
さらに、これらの波形を足し合わせると、

y = Σ(1/C2 * sin(C * θ)),  C = [-11, -7, -3, 1, 5, 9]

以下のような波形になります。
この波形を横軸x、縦軸yに置き換えると、

x = Σ(1/C2 * cos(C * θ)) / Σ(1/C2), C = [-11, -7, -3, 1, 5, 9]
y = Σ(1/C2 * sin(C * θ)) / Σ(1/C2), C = [-11, -7, -3, 1, 5, 9]

正方形に近似した軌跡になります。まだ正方形としては不完全なかたちですが、Cの個数を増やすとより正方形に近似していきます。
仮にCの個数を20個にすると、

C = [-39, -35, -31, -27, -23, -19, -15, -11, -7, -3, 1, 5, 9, 13, 17, 21, 25, 29, 33, 37]

になります。
sin()、cos()の代わりにexp()をつかって複素数であらわせば、

V = Σ(1/C2 * exp(i * C * θ)) / Σ(1/C2),

になります。iは虚数、Vは複素数でVの実部がX座標、Vの虚部がY座標になります。
これをコーディングすると以下。

正方形の場合(n=4):
import numpy as np
import matplotlib.pyplot as plt

n = 4
p = n * 10
theta = np.linspace(0, 2*np.pi, p+1)
F = 20
C = np.array([(1 + f * n) for f in range(-F//2, F//2)])
print('C =', C)
V = sum([1 / c**2 * np.exp(1j * c * theta) for c in C]) / sum(1 / C**2)

X = V.real
Y = V.imag

fig = plt.figure(figsize=(6, 6))
ax = fig.add_subplot(111)
ax.grid()
ax.axis('equal')
ax.plot(X, Y)
ax.scatter(X, Y, s=10, c='red')
nは正n角形、pはtheta:0〜2πにおける分解能。Pythonでは虚数iは1j。
Cは無限フーリエ級数の係数ですが、20個(F=20)にしておきます。
V.realはVの実部でX座標、V.imagはVの虚部でY座標。

n=4(正方形)、 p=n*10=40、F=20で描くと以下。
赤い点は設定した分解能pの数だけありますが、より正確な図形を描く場合はFの数を増やします。F=20であればかなり正方形に近似します。

アニメーションで表現:
n=4(正方形)、 p=n*10=40、F=20

コード:
中心角thetaを分解能pで分けアニメーションにしています。
nを変えれば、正三角形(n=3)、正五角形(n=5)などになります。また、pの分解能をあげればより細かいフレームレートになります。あるいはFで級数の数を増やせば、より正確な図形の軌跡を描きます。
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import animation
from IPython.display import HTML

n = 4
p = n * 10
theta = np.linspace(0, 2*np.pi, p+1)
F = 20
C = np.array([(1 + f * n) for f in range(-F//2, F//2)])
print('C =', C)

fig = plt.figure(figsize=(6, 6))
ax = fig.add_subplot(111)
ax.grid()
ax.axis('equal')

ims = []
X = []
Y = []
for t in theta:
    V = sum([1 / c**2 * np.exp(1j * c * t) for c in C]) / sum(1 / C**2)
    x = V.real
    y = V.imag
    X.append(x)
    Y.append(y)
    P1 = ax.plot(X, Y, c='tab:blue')
    P2 = ax.scatter(x, y, s=40, c='red')
    ims.append(P1 + [P2])
    
ani = animation.ArtistAnimation(fig, ims, interval=150)
ani.save('ani0.gif', writer="imagemagick")
#ani.save('ani0.mp4', writer="ffmpeg")
plt.close()
HTML(ani.to_jshtml())


Epicycleで表現:
正n角形の軌跡は複数の波形の重ね合わせで生成されているので、それぞれの波形の動きがわかるようにEpicycleで表現すると以下。
複数の円があり、その中で半径の直線が回転しています。F=20という設定なので20種類の波形があり、正方形の中心から外側に向けて円も20個あります(表示上小さすぎて見えないですが)。各円の中で独自の係数Cに対応した周期で回転しています。

C = [-39, -35, -31, -27, -23, -19, -15, -11, -7, -3, 1, 5, 9, 13, 17, 21, 25, 29, 33, 37]

ですが、絶対値順に並び替えて、

C = [1, -3, 5, -7, 9, -11, 13, -15, 17, -19, 21, -23, 25, -27, 29, -31, 33, -35, 37, -39]

にしています。
円の半径はCの要素をcとすれば、

r = abs(1/c2 * exp(i * c * θ) / sum(1/C2))
 = abs(1/c2 / sum(1/C2))

で求めているため、正方形の中心から外側にいくほど小さな円になります。
c=1なら反時計回りに周期1、c=-3なら時計回りに周期3となります(cが正なら反時計回り、負なら時計回り)。

上記アニメーションのコード:
p = n * 20
theta = np.linspace(0, 2*np.pi, p+1)
F = 20
C = np.array([(1 + f * n) for f in range(-F//2, F//2)])
AS = np.argsort(abs(C))
C = C[AS]
print('C =', C)

fig = plt.figure(figsize=(6, 6))
ax = fig.add_subplot(111)
ax.grid()
ax.axis('equal')

ims = []
cir = np.linspace(0, 2*np.pi, 65)
LN = np.array([])
for t in theta:
    VR = np.array([])
    V = np.array([0])
    v = 0
    for c in C:
        vr = 1 / c**2 * np.exp(1j * c * t) / sum(1 / C**2)
        VR = np.append(VR, vr)
        v += vr
        V = np.append(V, v)
     
    P1 = ax.plot(V.real, V.imag, c='blue')
    
    cr = abs(VR) * np.exp(1j * C * np.vstack(cir))
    P2 = ax.plot(V[:-1].real + cr.real, V[:-1].imag + cr.imag, c='gray', alpha=0.5)
    
    LN = np.append(LN, V[-1])
    P3 = ax.plot(LN.real, LN.imag, c='tab:blue')
    P4 = ax.scatter(V[-1].real, V[-1].imag, s=40, c='red')

    ims.append(P1 + P2 + P3 + [P4])
    
ani = animation.ArtistAnimation(fig, ims, interval=100)
ani.save('epi.gif', writer="imagemagick")
#ani.save('epi.mp4', writer="ffmpeg")
plt.close()
HTML(ani.to_jshtml())


フーリエ級数の数が少ない場合:
F=2、C = [1, -3]であれば、二つの波形の合成(二つの円による軌跡)になり、フーリエ級数が少ないため正方形の形は以下のようにやや丸くなってしまいます(不完全な正方形)。
C = [1, -3]であるため、大きい円は周期1で反時計回りで回っているのに対して、小さな円は周期3で時計回りに回っているのがわかります。
それぞれの円の半径は、

r(1) = (1/1)2/ ((1/1)2 + (1/(-3))2) = 0.9
r(-3) = (1/(-3))2 / ((1/1)2 + (1/(-3))2) = 0.1

になります。

人気の投稿