Pythonによる制御工学入門 第2回
参考書の4.5「周波数応答」を自作の数値シミュレーションで再現する
専用モジュールを使えば一瞬だが、
今回は、各周波数の入力に対する応答を取得して、原始的に求める
実際の機器で求める場合も、同様の処理が必要なはず
周波数応答
まずは、controlモジュールを使って、周波数応答を確認する
1次遅れ系のボード線図
import matplotlib.pyplot as plt import numpy as np import pandas as pd import control.matlab as pyctrl K = 1 T = [1, 0.5, 0.1] fig = plt.figure(figsize=(6, 5)) ax1 = fig.add_subplot(2, 1, 1) ax2 = fig.add_subplot(2, 1, 2) for i in range(len(T)): P = pyctrl.tf([0, K], [T[i], 1]) gain, phase, w = pyctrl.bode(P, pyctrl.logspace(-2,2), plot=False) ax1.plot(w/(2*np.pi), 20*np.log10(gain), label='T=%.1f' % T[i]) ax2.plot(w/(2*np.pi), phase*180/np.pi, label='T=%.1f' % T[i]) ax1.set_ylim(-45, 10); ax1.set_yticks([-40, -20, 0]) ax2.set_ylim(-95, 5); ax2.set_yticks([-90, -45, 0]) ax1.legend(); ax1.grid(); ax1.set_xscale('log') ax1.set_ylabel('Gain [dB]') ax2.legend(); ax2.grid(); ax2.set_xscale('log') ax2.set_xlabel('[Hz]') ax2.set_ylabel('Phase [deg]') fig.tight_layout()
2次遅れ系のボード線図
import matplotlib.pyplot as plt import numpy as np import pandas as pd import control.matlab as pyctrl zeta = [1, 0.7, 0.4] omega_n = 1 fig = plt.figure(figsize=(6, 5)) ax1 = fig.add_subplot(2, 1, 1) ax2 = fig.add_subplot(2, 1, 2) for i in range(len(zeta)): P = pyctrl.tf([0, omega_n**2], [1, 2*zeta[i]*omega_n, omega_n**2]) gain, phase, w = pyctrl.bode(P, pyctrl.logspace(-2,2), plot=False) ax1.plot(w/(2*np.pi), 20*np.log10(gain), label='$\zeta$=%.1f' % zeta[i]) ax2.plot(w/(2*np.pi), phase*180/np.pi, label='$\zeta$=%.1f' % zeta[i]) ax1.set_ylim(-90, 10); ax1.set_yticks([-80, -40, 0]) ax2.set_ylim(-190, 10); ax2.set_yticks([-180, -90, 0]) ax1.legend(); ax1.grid(); ax1.set_xscale('log') ax1.set_ylabel('Gain [dB]') ax2.legend(); ax2.grid(); ax2.set_xscale('log') ax2.set_xlabel('[Hz]') ax2.set_ylabel('Phase [deg]') fig.tight_layout()
2次遅れ系のボード線図2
import matplotlib.pyplot as plt import numpy as np import pandas as pd import control.matlab as pyctrl zeta = 0.7 omega_n = [1, 5, 10] fig = plt.figure(figsize=(6, 5)) ax1 = fig.add_subplot(2, 1, 1) ax2 = fig.add_subplot(2, 1, 2) for i in range(len(omega_n)): P = pyctrl.tf([0, omega_n[i]**2],[1, 2*zeta*omega_n[i], omega_n[i]**2]) gain, phase, w = pyctrl.bode(P, pyctrl.logspace(-2,2), plot=False) ax1.plot(w/(2*np.pi), 20*np.log10(gain), label='$\omega_n$=%.1f' % omega_n[i]) ax2.plot(w/(2*np.pi), phase*180/np.pi, label='$\omega_n$=%.1f' % omega_n[i]) ax1.set_ylim(-90, 10); ax1.set_yticks([-80, -40, 0]) ax2.set_ylim(-190, 10); ax2.set_yticks([-180, -90, 0]) ax1.legend(); ax1.grid(); ax1.set_xscale('log') ax1.set_ylabel('Gain [dB]') ax2.legend(); ax2.grid(); ax2.set_xscale('log') ax2.set_xlabel('[Hz]') ax2.set_ylabel('Phase [deg]') fig.tight_layout()
数値演算による2次遅れ系のボード線図
今回の主題
各周波数の正弦波を入力としてモデルに入れて、
その際の出力からゲインの変化と位相の変化を求める
まずは、入れる入力の周波数を決める
0.01Hzから10Hzまでを等比で20分割するとすると、
なので、対数を取ると、
となり
となる
hz_s = [] hz = 0.01 while True: if hz > 10 * 10 ** (3 / 20): break hz_s.append(hz) hz = hz * 10 ** (3 / 20)
各周波数でシミュレーションを回す
時間分解能は0.1msで5sec回し、3~5secを保存する
モデルのパラメータは下記の設定
import numpy as np import pandas as pd (K, zeta, omega_n) = (1.0, 0.4, 5) dt = 0.0001 df_list = [] for hz in hz_s: t_s = [0.0] x_s = [0.0] v_s = [0.0] a_s = [0.0] F_s = [0.0] for i in range(50000): if i < 100: F = 0.0 else: F = 1.0 * np.sin(2.0 * np.pi * hz * t_s[-1]) F_s += [F] a_s += [- 2.0 * zeta * omega_n * v_s[-1] - omega_n**2 * x_s[-1] + K * omega_n**2 * F] v_s += [v_s[-1] + a_s[-1] * dt] x_s += [x_s[-1] + v_s[-1] * dt] t_s += [t_s[-1] + dt] df = pd.DataFrame() df['in'] = F_s[30000:] df['out'] = x_s[30000:] df.index = t_s[30000:] df_list += [df]
次に、解析のための関数を定義する
主に、Fittingで求めた振幅と位相を調整する関数
from scipy.optimize import curve_fit def sin_func(x, p0, p1, p2): return p0 * np.sin(2.0 * np.pi * p1 * x + p2) def _convert_phase(phase): phase += 180 phase %= 360 if phase < 0: phase += 180 else: phase -= 180 return phase def _convert_param(param): param = param.copy() param[2] = param[2] / np.pi * 180 if param[0] < 0: param[0] = -param[0] param[2] += 180 param[2] = _convert_phase(param[2]) return param def calc_f_func(param1, param2): param1 = _convert_param(param1) param2 = _convert_param(param2) freq = param1[1] gain = param2[0] / param1[0] phase = param2[2] - param1[2] phase = _convert_phase(phase) return param1, param2, freq, gain, phase
各周波数のデータに対して解析を行う
各データをFittingして、intとoutの振幅差からゲインを、位相差から位相変化を求める
data_s = [] for hz,df in zip(hz_s, df_list): p0 = [1.0, hz, 0.0] p1, conv = curve_fit(sin_func, np.array(df.index), np.array(df['in']), p0=p0) p0 = [1.0, hz, 0.0] p2, conv = curve_fit(sin_func, np.array(df.index), np.array(df['out']), p0=p0) param1, param2, freq, gain, phase = calc_f_func(p1, p2) data_s.append([freq, gain, phase]) df_res = pd.DataFrame(data_s) df_res.columns = ['freq', 'gain', 'phase']
最後に、controlモジュールとの結果を比較して表示する
import matplotlib.pyplot as plt import control.matlab as pyctrl fig = plt.figure(figsize=(6, 5)) ax1 = fig.add_subplot(2, 1, 1) ax2 = fig.add_subplot(2, 1, 2) ax1.plot(df_res['freq'], np.log10(df_res['gain'])*20.0, 'o-', label='sim') ax2.plot(df_res['freq'], df_res['phase'], 'o-', label='sim') zeta, omega_n = (0.4, 5) P = pyctrl.tf([0, omega_n**2],[1, 2*zeta*omega_n, omega_n**2]) gain, phase, w = pyctrl.bode(P, pyctrl.logspace(-2,2), plot=False) ax1.plot(w/(2*np.pi), 20*np.log10(gain), label='pyctrl') ax2.plot(w/(2*np.pi), phase*180/np.pi, label='pyctrl') ax1.set_ylim(-90, 10); ax1.set_yticks([-80, -40, 0]) ax2.set_ylim(-190, 10); ax2.set_yticks([-180, -90, 0]) ax1.legend(); ax1.grid(); ax1.set_xscale('log') ax1.set_ylabel('Gain [dB]') ax2.legend(); ax2.grid(); ax2.set_xscale('log') ax2.set_xlabel('[Hz]') ax2.set_ylabel('Phase [deg]') fig.tight_layout()
きちんと求めることができている
おまけ:確認方法
実際の機器で解析を行うと、ノイズなどで、正確にFittingが行えない場合がある
その場合は、きちんと in / out のデータを確認する
i = 15 hz = hz_s[i] df = df_list[i] p0 = [1.0, hz, 0.0] p1, conv = curve_fit(sin_func, np.array(df.index), np.array(df['in']), p0=p0) p0 = [1.0, hz, 0.0] p2, conv = curve_fit(sin_func, np.array(df.index), np.array(df['out']), p0=p0) param1, param2, freq, gain, phase = calc_f_func(p1, p2) fig = plt.figure(figsize=(6, 5)) ax1 = fig.add_subplot(2, 1, 1) ax2 = fig.add_subplot(2, 1, 2, sharex=ax1, sharey=ax1) ax1.plot(df.index, df['in'], label='raw') ax1.plot(df.index, sin_func(df.index, param1[0], param1[1], param1[2] / 180 * np.pi), label='fit') ax2.plot(df.index, df['out'], label='raw') ax2.plot(df.index, sin_func(df.index, param2[0], param2[1], param2[2] / 180 * np.pi), label='fit') ax1.set_xlabel('t'); ax1.set_ylabel('in'); ax1.grid(); ax1.legend() ax1.set_title('%.2f Hz' % hz) ax2.set_xlabel('t'); ax2.set_ylabel('out'); ax2.grid(); ax2.legend() fig.tight_layout()
今回の場合は、生データとFitting結果がほぼ一致している
また、今回は、全条件(周波数)で同じ条件(サンプル0.1ms/2sec分)でFittingを行ったが、
入力する周波数に合わせて、サンプル周期や時間を変えた方が精度が良くなる
参考文献
この記事は以下の書籍を参考にしましたが、
私の拙い知識で書いておりますので、誤り等ありましたらご指摘ください