機械系エンジニアの備忘録

20代独身社会人。仕事では機械・機構の研究開発を行っているエンジニアが、自分の専門分野ではないpythonを扱って楽しむブログです。

MENU

【python】PID制御による変化をリアルタイムで表示できるアプリに設定欄をタブで切り替えられる機能をつける

前回作ったアプリにメニューバーを追加。状態空間モデルから伝達関数を設定する機能と直接伝達関数を設定できる機能を追加し両者をタブで切り分けられるようにする

f:id:stjun:20200126172343p:plain

状態空間モデルから伝達関数を計算する画面

f:id:stjun:20200126172429p:plain

タブをクリックすると伝達関数を直接設定できる画面に移る

 

1. 誰に向けた記事か

pythontkinterを勉強してる人

tkinterでどんなアプリが作れるのか興味がある人

 ・様々な伝達関数でPID制御を試してみたい人

・大学の課題などでPID制御関係をやらないといけない人など

※初心者向けにpythonの勉強法とその手順を記事にしました。

www.stjun.com

 

 

2. はじめに

Part1はPID制御をGUIで操作できるアプリを作りました。

www.stjun.com

またPart2ではグラフの変化を可視化できる機能をつけました。

www.stjun.com
 part3では状態空間モデルの各行列の数値を変更できるようにし、様々なシステムでPIDを試せるようにしました。 

www.stjun.com

 通常は状態空間モデルで良いですが、大学の課題等では最初から伝達関数が与えられている場合があります。

そこで今回、伝達関数を直接設定できる機能をつけます。また前回作った機能と今回作る機能をタブで簡単に切り替えられるようにします。

 

3. コード・実行結果

3.1 コード
import tkinter as tk
import tkinter.ttk as ttk
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import (
    FigureCanvasTkAgg, NavigationToolbar2Tk)
import numpy as np
from control.matlab import *
import re

root = tk.Tk()#ウインドの作成
root.title("pid soft")#ウインドのタイトル
root.geometry("650x450") #ウインドの大きさ
frame_1 = tk.LabelFrame(root,labelanchor="nw",text="グラフ",foreground="green")
frame_1.grid(rowspan=2, column=0)
frame_2 = tk.LabelFrame(root,labelanchor="nw",text="パラメータ",foreground="green")
frame_2.grid(row=0, column=1, sticky="nwse")
frame3=tk.LabelFrame(root,text="履歴",foreground="green")
frame3.grid(row=1,column=1,sticky="nwse")
frame4=tk.LabelFrame(root,text="状態方程式から伝達関数を計算",foreground="green")
frame4.grid(row=2,column=0,sticky="nwse")
frame5=tk.LabelFrame(root,text="制御対象の伝達関数",foreground="green")
frame5.grid(row=2,column=1,sticky="nwse")
frame6=tk.LabelFrame(root,text="伝達関数を直接入力",foreground="green")
frame6.grid(row=2,column=0,sticky="nwse")

def change_page(page):
    page.tkraise()

#menuバーをrootに作る
menu=tk.Menu(root)
#個別のmenuを作る
menu_1=tk.Menu(menu,tearoff=False)
#menuを選択した時に出てくるドロップリストを作る
menu_1.add_command(label="状態方程式から入力",command=lambda :change_page(frame4))
menu_1.add_command(label="伝達関数から入力",command=lambda :change_page(frame6))
#menuバーに個別のmenuを足す
menu.add_cascade(label="伝達関数の計算方法を選択",menu=menu_1)

#スケールバーが動いたらその値を読み取りグラフを更新する
def graph(*args):
    global G
    graph_on_off=graph_var.get()
    if graph_on_off==0:
        ax.cla()
    Kp=scale_var.get()
    Ti=scale_var_Ti.get()
    Td=scale_var_Td.get()
    value_Kp=f"{Kp:.2f}"
    value_Ti=f"{Ti:.2f}"
    value_Td=f"{Td:.2f}"
    text_display.set(str(value_Kp))
    text_display_Ti.set(str(value_Ti))
    text_display_Td.set(str(value_Td))
    num=[Td,1,1/Ti]
    den=[1,0]
    G_ID=tf(num,den)
    G_all=feedback(G_ID*Kp*G,1)
    (y_s,t_s)=step(G_all,T=np.arange(0,10,0.01))
    (y_in,t_in)=step(1,T=np.arange(0,10,0.01))
    ax.set_xlabel('t / s')
    ax.set_ylabel('y')
    plt.style.use('ggplot')
    ax.plot(t_s,y_s)
    ax.plot(t_in,y_in,linestyle="dashed")
    ax.set_title('Kp='+str(value_Kp)+',Ti='+str(value_Ti)+',Td='+str(value_Td))
    canvas.draw()

#伝達関数の設定
#PID制御のパラメータ
Kp=30 #比例ゲイン
Ti=1.8 #積分ゲイン
Td=0.2 #微分ゲイン
num=[Td,1,1/Ti]
den=[1,0]
G_ID=tf(num,den)

#伝達関数の設定
num = [0.1]
den = [0.1, 1.0, 1]
G = tf(num, den)
G_all=feedback(G_ID*Kp*G,1)

#ステップ応答
(y_s,t_s)=step(G_all,T=np.arange(0,10,0.01))
(y_in,t_in)=step(1,T=np.arange(0,10,0.01))

#グラフの設定
fig=plt.Figure()
ax = fig.add_subplot(111) 
ax.plot(t_s,y_s)
ax.plot(t_in,y_in,linestyle="dashed")
ax.set_title('Kp='+str(Kp)+',Ti='+str(Ti)+',Td='+str(Td))
ax.set_xlabel('t / s')
ax.set_ylabel('y')
plt.style.use('ggplot')

#tkinterのウインド上部にグラフを表示する
canvas = FigureCanvasTkAgg(fig, master=frame_1)
canvas.draw()
canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1)
    
#比例ゲインのスケール作成
scale_var=tk.DoubleVar()
scale_var.set(Kp)
scale_var.trace("w",graph)
scale=ttk.Scale(frame_2,from_=0,to=10,length=150,orient="h",variable=scale_var)
scale.grid(row=1,column=0)

#比例ゲインのテキスト
text=tk.Label(frame_2,text="比例ゲイン:Kp")
text.grid(row=0,column=0)

#比例ゲインの数値表示テキスト
text_display=tk.StringVar()
text_display.set(str(Kp))
label=tk.Label(frame_2,textvariable=text_display)
label.grid(row=1,column=1)

#積分ゲインのスケール作成
scale_var_Ti=tk.DoubleVar()
scale_var_Ti.set(Ti)
scale_var_Ti.trace("w",graph)
scale_Ti=ttk.Scale(frame_2,from_=0.01,to=3,length=150,orient="h",variable=scale_var_Ti)
scale_Ti.grid(row=3,column=0)

#積分ゲインのテキスト
text_ti=tk.Label(frame_2,text="積分ゲイン:Ti")
text_ti.grid(row=2,column=0)

#積分ゲインの数値表示テキスト
text_display_Ti=tk.StringVar()
text_display_Ti.set(str(Ti))
label_Ti=tk.Label(frame_2,textvariable=text_display_Ti)
label_Ti.grid(row=3,column=1)

#微分ゲインのスケール作成
scale_var_Td=tk.DoubleVar()
scale_var_Td.set(Td)
scale_var_Td.trace("w",graph)
scale_Td=ttk.Scale(frame_2,from_=0,to=1.0,length=150,orient="h",variable=scale_var_Td)
scale_Td.grid(row=5,column=0)

#微分ゲインのテキスト
text_td=tk.Label(frame_2,text="微分ゲイン:Td")
text_td.grid(row=4,column=0)

#微分ゲインの数値表示テキスト
text_display_Td=tk.StringVar()
text_display_Td.set(str(Td))
label_Td=tk.Label(frame_2,textvariable=text_display_Td)
label_Td.grid(row=5,column=1)

#グラフ変化の履歴を残すか指定できるラジオボタンを作成
graph_var=tk.IntVar()
graph_var.set(0)
graph_on=tk.Radiobutton(frame3,value=0,variable=graph_var,text="なし")
graph_on.pack()
graph_off=tk.Radiobutton(frame3,value=1,variable=graph_var,text="あり")
graph_off.pack()

text_display_sys=tk.StringVar()
text_display_sys.set(str(G))
label=tk.Label(frame5,textvariable=text_display_sys)
label.pack()

label_A=tk.Label(frame4,text='A=',width=5)
label_A.grid(row=1,column=0)

label_A1=tk.Entry(frame4,width=5)
label_A1.insert(tk.END,'-10')
label_A1.grid(row=1,column=1)

label_A2=tk.Entry(frame4,width=5)
label_A2.insert(tk.END,'-10')
label_A2.grid(row=1,column=2)

label_A3=tk.Entry(frame4,width=5)
label_A3.insert(tk.END,'1')
label_A3.grid(row=2,column=1)

label_A4=tk.Entry(frame4,width=5)
label_A4.insert(tk.END,'0')
label_A4.grid(row=2,column=2)

label_B=tk.Label(frame4,text='B=',width=5)
label_B.grid(row=1,column=4)

label_B1=tk.Entry(frame4,width=5)
label_B1.insert(tk.END,'1')
label_B1.grid(row=1,column=5)

label_B2=tk.Entry(frame4,width=5)
label_B2.insert(tk.END,'0')
label_B2.grid(row=2,column=5)

label_C=tk.Label(frame4,text='C=',width=5)
label_C.grid(row=4,column=0,pady=10)

label_C1=tk.Entry(frame4,width=5)
label_C1.insert(tk.END,'0')
label_C1.grid(row=4,column=1,pady=10)

label_C2=tk.Entry(frame4,width=5)
label_C2.insert(tk.END,'1')
label_C2.grid(row=4,column=2,pady=10)

label_D=tk.Label(frame4,text='D=',width=5)
label_D.grid(row=4,column=4,pady=10)

label_D1=tk.Entry(frame4,width=5)
label_D1.insert(tk.END,'0')
label_D1.grid(row=4,column=5,pady=10)

def cal_sys():
    global G
    s2_num=float(num_s2.get())
    s1_num=float(num_s1.get())
    s0_num=float(num_s0.get())
    num=[s2_num,s1_num,s0_num]
    
    s2_den=float(den_s2.get())
    s1_den=float(den_s1.get())
    s0_den=float(den_s0.get())
    den=[s2_den,s1_den,s0_den]
    
    G = tf(num, den)
    
    text_display_sys.set(str(G))
    
    Kp=scale_var.get()
    Ti=scale_var_Ti.get()
    Td=scale_var_Td.get()
    value_Kp=f"{Kp:.2f}"
    value_Ti=f"{Ti:.2f}"
    value_Td=f"{Td:.2f}"
    text_display.set(str(value_Kp))
    text_display_Ti.set(str(value_Ti))
    text_display_Td.set(str(value_Td))
    num=[Td,1,1/Ti]
    den=[1,0]
    G_ID=tf(num,den)
    G_all=feedback(G_ID*Kp*G,1)
    ax.cla()
    (y_s,t_s)=step(G_all,T=np.arange(0,10,0.01))
    (y_in,t_in)=step(1,T=np.arange(0,10,0.01))
    ax.set_xlabel('t / s')
    ax.set_ylabel('y')
    plt.style.use('ggplot')
    ax.plot(t_s,y_s)
    ax.plot(t_in,y_in,linestyle="dashed")
    ax.set_title('Kp='+str(value_Kp)+',Ti='+str(value_Ti)+',Td='+str(value_Td))
    canvas.draw()


Button_cal=tk.Button(frame4,text='伝達関数を計算',width=15,command=cal_ABCD)
Button_cal.grid(row=1,column=7,padx=5)

def cal_ABCD():
    global G
    A_1=float(label_A1.get())
    A_2=float(label_A2.get())
    A_3=float(label_A3.get())
    A_4=float(label_A4.get())
    A=np.matrix([[A_1,A_2],[A_3,A_4]])
    
    B_1=float(label_B1.get())
    B_2=float(label_B2.get())
    B=np.matrix([[B_1],[B_2]])
    
    C_1=float(label_C1.get())
    C_2=float(label_C2.get())
    C=np.matrix([C_1,C_2])
    
    D_1=float(label_D1.get())
    D=D_1
    
    G=ss2tf(A,B,C,D)
    
    text_display_sys.set(str(G))
    
    Kp=scale_var.get()
    Ti=scale_var_Ti.get()
    Td=scale_var_Td.get()
    value_Kp=f"{Kp:.2f}"
    value_Ti=f"{Ti:.2f}"
    value_Td=f"{Td:.2f}"
    text_display.set(str(value_Kp))
    text_display_Ti.set(str(value_Ti))
    text_display_Td.set(str(value_Td))
    num=[Td,1,1/Ti]
    den=[1,0]
    G_ID=tf(num,den)
    G_all=feedback(G_ID*Kp*G,1)
    ax.cla()
    (y_s,t_s)=step(G_all,T=np.arange(0,10,0.01))
    (y_in,t_in)=step(1,T=np.arange(0,10,0.01))
    ax.set_xlabel('t / s')
    ax.set_ylabel('y')
    plt.style.use('ggplot')
    ax.plot(t_s,y_s)
    ax.plot(t_in,y_in,linestyle="dashed")
    ax.set_title('Kp='+str(value_Kp)+',Ti='+str(value_Ti)+',Td='+str(value_Td))
    canvas.draw()

label_num=tk.Label(frame6,text='分子=',width=5)
label_num.grid(row=1,column=0)

num_s2=tk.Entry(frame6,width=5)
num_s2.insert(tk.END,'0')
num_s2.grid(row=1,column=1)

label_num_s2=tk.Label(frame6,text='S^2+',width=5)
label_num_s2.grid(row=1,column=2)

num_s1=tk.Entry(frame6,width=5)
num_s1.insert(tk.END,'0')
num_s1.grid(row=1,column=3)

label_num_s1=tk.Label(frame6,text='S+',width=5)
label_num_s1.grid(row=1,column=4)

num_s0=tk.Entry(frame6,width=5)
num_s0.insert(tk.END,'0.1')
num_s0.grid(row=1,column=5)

label_den=tk.Label(frame6,text='分母=',width=5)
label_den.grid(row=4,column=0)

den_s2=tk.Entry(frame6,width=5)
den_s2.insert(tk.END,'0.1')
den_s2.grid(row=4,column=1)

label_den_s2=tk.Label(frame6,text='S^2+',width=5)
label_den_s2.grid(row=4,column=2)

den_s1=tk.Entry(frame6,width=5)
den_s1.insert(tk.END,'1')
den_s1.grid(row=4,column=3)

label_den_s1=tk.Label(frame6,text='S+',width=5)
label_den_s1.grid(row=4,column=4)

den_s0=tk.Entry(frame6,width=5)
den_s0.insert(tk.END,'1')
den_s0.grid(row=4,column=5)

Button_cal_sys=tk.Button(frame6,text='伝達関数を計算',width=15,command=cal_sys)
Button_cal_sys.grid(row=1,column=7,padx=5)


#状態空間モデルのフレームを上位層にする
frame4.tkraise()

root["menu"]=menu

root.mainloop()

 

3.2 実行結果

実行すると以下のアプリが表示されます。

上部にタブ「伝達関数の計算方法を選択」が追加されており、「状態空間モデルから伝達関数を計算する」か「直接伝達関数を設定する」かを選ぶことができます。

タブをクリックすると左下の欄が変化します。

f:id:stjun:20200126172429p:plain

ここでS^2、Sの係数や定数の入力を終えて「伝達関数を計算」のボタンを押すと、右下の制御対象の伝達関数が変わり、PID制御が再計算されます。

 

■ 読んで良かった本を紹介

私はAmazon kindle unlimitedというサービスを1年以上利用しています。

これは月額980円で 和書12万冊以上の電子書籍を読めるサービスです。

ビジネス本、雑誌、漫画、技術本など様々な本を読むことができます。

なおkindle unlimitedは最初の30日間無料のため、気軽に登録してみて、あまり読みたい本が無ければすぐに解約しても問題ありません。

それか30日に気になる本を全て読破すれば実質タダです。

ぜひ気になった方はチェックしてみて下さい。

なお学生さんであればkindle unlimitedよりも年2450円(月210円程度)で映画見放題、音楽聞き放題、本読み放題の「prime student」がおすすめです。

以下に私がkindle unlimitedで読んでよかった本を紹介します。kindle unlimitedに登録すれば何回読んでも何冊読んでも月額980円だけです。

1. 

tkinterでの基本的なパーツの作成方法(ボタンやテキストやスクロールバー等)について、初心者が読んでもすぐに分かるように丁寧に書かれています。私はtkinterを学んだ後に知りましたが、こんなに分かりやすい本があるなら初めからこの本を読んで勉強すればこんなに苦労するなかったのにと思ったくらいです...

 

2.

 投資信託について「投資信託とは?」「どうやって始めるか?」「口座はどこで開設するか?」など具体的なアクションまで書かれている本です。ど素人の著者が口座や銘柄を選ぶ過程が詳しく書いてるので非常に分かりやすかったです。

 

3. 

さおだけ屋はなぜ潰れないのか? 身近な疑問からはじめる会計学 (光文社新書)

さおだけ屋はなぜ潰れないのか? 身近な疑問からはじめる会計学 (光文社新書)

  • 作者:山田 真哉
  • 出版社/メーカー: 光文社
  • 発売日: 2005/02/16
  • メディア: 新書
 

 ジャンルは経済に入ると思いますが全然堅苦しくなく読める本です。身近な事例をもとに「成程、そういうことだったんだ」と思うような説明がされていて面白かったです。

 

4. 

多動力 (NewsPicks Book) (幻冬舎文庫)

多動力 (NewsPicks Book) (幻冬舎文庫)

 

既にベストセラーになってる本なので読んだ方も多いと思います。

堀江さんは時にキツイ物言いで炎上しますが、言ってることや考えてることはすごくまともだし、それを説明する能力にも長けているので読んでいてためになりました。 

 

5. 

「どういうお金の使い方をしたら幸せを感じれるのか?」「お金持ちになったら本当に幸せになれるのか?」を大きなテーマに、様々な体験談や研究事例を通してお金の賢い使い方を紹介している本です。

 

6. 雑誌系

家電、美容、アウトドア、経済など様々な雑誌があります。

こういった雑誌を買っていませんか?1つでも買っているならkindle unlimitedに登録した方が安上がりで更に読み放題です。

MONOQLO (モノクロ) 2020年 02月号 [雑誌]

MONOQLO (モノクロ) 2020年 02月号 [雑誌]

 
家電批評 2020年 1月号 [雑誌]

家電批評 2020年 1月号 [雑誌]

  • 作者: 
  • 出版社/メーカー: 晋遊舎
  • 発売日: 2019/12/03
  • メディア: Kindle
 
ロードバイク完全メンテナンス (エイムック 3544 BiCYCLE CLUB別冊)

ロードバイク完全メンテナンス (エイムック 3544 BiCYCLE CLUB別冊)

  • 作者: 
  • 出版社/メーカー: エイ出版社
  • 発売日: 2016/11/29
  • メディア: ムック
 
繰り返し作りたくなる!  ラク弁当レシピ (エイムック 3680)

繰り返し作りたくなる! ラク弁当レシピ (エイムック 3680)

 
Tarzan(ターザン) 2020年1月23日号 No.779 [内臓脂肪 皮下脂肪すっきり落とす!] [雑誌]

Tarzan(ターザン) 2020年1月23日号 No.779 [内臓脂肪 皮下脂肪すっきり落とす!] [雑誌]

  • 作者: 
  • 出版社/メーカー: マガジンハウス
  • 発売日: 2020/01/04
  • メディア: Kindle
 
DIME(ダイム) 2020年 03 月号 [雑誌]

DIME(ダイム) 2020年 03 月号 [雑誌]

  • 作者: 
  • 出版社/メーカー: 小学館
  • 発売日: 2019/12/16
  • メディア: 雑誌
 

 

7. 

この本は「1. 著者の筋トレ格言」「2. 筋トレ体験談」「3. 筋トレの効能を学術論文を引用して科学的に説明」の3部からなっています。この本で私が好きな著者の考え方は、

「弱いのはメンタルではない、フィジカルだ。メンタルを鍛えたいならまずフィジカルを鍛えろ。」

 です。

 

4.  説明

前回同様、PID制御の説明とpythonでの書き方は過去記事をご覧ください。

www.stjun.com

今回追加した部分は、

1. 伝達関数を直接設定できるGUI

2. タブで切り替える操作

です。

 

4.1 伝達関数を直接設定できるGUI

これは前回と全く同じです。

tkinterのLabel機能とEntry機能を使い、ユーザーがEntryに入力した数字を読み取り、その数値を用いて伝達関数を再計算しています。

前記事に詳しく書いてあります。

www.stjun.com

 

4.2 タブで切り替える操作

詳細は過去記事をどうぞ。

www.stjun.com

 

5.  最後に

こんなもの作って欲しいという要望がありましたら、ぜひコメントかHP下部のプライバシーポリシー内のお問合せフォームからご連絡ください。