コタツで学ぶ P / PI / PID ―― 式より波形で、制御の効き目を確かめる

  • pid
  • 制御
  • python
  • 組み込み
  • シミュレーション

温調器やサーボモータを扱っていると、P・PI・PID という言葉に出会います。一方で、Web やアプリを中心に書いていると、名前は聞くけれど中身にふれる機会は少ないかもしれません。私も、はじめは「なんとなく安定させる魔法の三文字」くらいの理解でした。

身近なところだと、コタツやエアコンの温度調節がこれの仲間です。ただ、昔ながらのコタツのサーモスタットは、決めた温度でヒーターをカチッと切って、下がったらまた入れる、という ON/OFF です。これでもだいたいの温度は保てます。一方、エアコンの本体や、FA の現場で使う油温調機(オイルコントローラ)は、ヒーターや弁の効き具合を連続で変えて、目標温度に近づけます。この「連続で効き具合を変える」ところで、PID 系の制御がよく使われます。

この記事では、コタツを Python で作って、P → PI → PID と足していったときに温度の応答がどう変わるかを、波形で見ていきます。数式はできるだけ薄くして、微分は「傾き」、積分は「溜まり」くらいの感覚で進めます。

コタツを Python で作る

制御を試すには、まず制御される側 ―― コタツが要ります。ここでは、次の一行を毎秒くり返すだけのモデルにします。

次の温度 = 今の温度 + Δt/τ ×( −(今の温度 − 室温) + gain × ヒーター出力 )

τ(タウ)は変化のゆっくりさを表す時定数で、大きいほどゆっくり変わります。gain はヒーター全開で室温から何度上げられるか。ヒーターを切れば室温に向かって下がり、入れれば上がる。それだけです。コードにしても数行です。

def step(self, u):
    u = min(1.0, max(0.0, u))          # ヒーターは 0..100% しか出せない
    u_eff = self.delay(u)              # むだ時間ぶん遅らせて効かせる
    self.T += dt/tau * (-(self.T - T_amb) + gain * u_eff)
    return self.T

もう一つ、現実のコタツには「スイッチを入れてから熱が伝わるまでの遅れ」があります。これを むだ時間 として、ヒーター出力を数秒ぶん遅らせて効かせます(上の delay() は、数秒前の出力を返すだけの部分。実コードでは短いバッファで書いています)。この遅れが、あとで効いてきます。

設定は、室温 10℃ から目標 40℃ を目指す、でいきます。ヒーター出力は 0〜100% を連続で変えられる、と仮定します(本物のコタツの ON/OFF ではなく、エアコンや油温調機に近い前提です)。

P ―― 今ずれている分だけ押す

いちばん素直な考え方は、「目標との差(誤差)に比例して、ヒーターを強める」です。

u = Kp * (setpoint - T)                # 誤差に比例

これが P 制御。誤差が大きいほど強く、近づくほど弱く。走らせると、次の図の赤い線になります。

P/PI/PIDの温度応答とヒーター出力。赤=P、青=PI、緑=PID。上段が温度、下段がヒーター出力

赤い線が P だけの場合です。ぐんぐん上がって……40℃ に届く手前、27℃ くらいで止まってしまいました。目標との間に 12.8℃ の差が残っています。これが 定常偏差(オフセット) です。

なぜ届かないのか。下段のヒーター出力に答えが出ています。P だけだと、ヒーターは 38% くらいで落ち着いています。ところが 40℃ を保つのに必要な出力は 67%。P は「誤差 × 定数」なので、温度が上がって誤差が小さくなると、押す力も一緒に小さくなる。67% を出したいのに、そのぶんの誤差が残っていないと出せない ―― この釣り合ったところで止まります。だから、目標温度を保つのに 0 でないヒーター出力が要り、バイアスもフィードフォワードも持たないこの素朴な P 制御では、目標に届きません。

PI ―― 誤差が残るかぎり、溜め続ける

足りないのは、「誤差が小さくても出力を保つ何か」です。そこで 積分(I) を足します。積分といっても、やることは 誤差を毎回足し上げる だけです。

integ += Ki * (setpoint - T) * dt      # 誤差を溜め続ける
u = Kp * (setpoint - T) + integ

誤差が残っているかぎり、この「溜まり」は増え続けます。だから、たとえ誤差が小さくなっても、溜まった分がヒーターを押し続けてくれる。誤差が正に残るあいだ溜まりは増え、目標を保つのに要る出力へ近づいていきます。これが残差を消す力です(誤差が負に振れれば、溜まりは逆に減ります)。

青い線が PI。今度はちゃんと 40℃ に届きました。偏差はほぼゼロです。ただ、43℃ あたりまで行き過ぎ(オーバーシュート)、少し振れてから落ち着いています。溜め込んだ分が、目標に着いた後もしばらく押し続けるためです。

なお、この比較には、後半で説明する「積分の溜まりすぎ」への保護(アンチワインドアップ)をあらかじめ入れてあります。実際のコントローラでは標準的な保護で、これがないと PI も PID もこれより大きく行き過ぎます。

PID ―― 変化の速さでブレーキをかける

行き過ぎを抑えたい。そこで 微分(D) を足します。微分は 変化の速さ、つまり温度が今どれくらいの勢いで動いているか、です。急に上がっているならブレーキ、という役割。

slope = (T - T_prev) / dt              # 温度の傾き(=測定値の微分)
u = Kp*(setpoint - T) + integ - Kd*slope

温度が速く上がっているときは D が出力を引き、行き過ぎる前に緩めます。この ブレーキがあるおかげで、P をもっと強くして立ち上がりを速くしても、行き過ぎを抑えやすくなります(ただし D を上げすぎると、むだ時間やノイズを拾って、かえって振れやすくなります)。

緑の線が PID。Kp を 3 倍に上げて立ち上がりを速くしていますが、D の制動で行き過ぎは 0.2℃ に収まり、98 秒で目標に張り付きました。PI の 150 秒より速く、しかも行き過ぎも小さい。P で大きく押し、I で残差を消し、D で行き過ぎを抑える。三つの役割がそろいました。

実際に使うと出てくる、二つの落とし穴

ここまでが P/PI/PID の骨格です。ただ、この素朴なままだと現場で困ることが二つあります。基本編のうちに触れておきます。

積分の溜まりすぎ(ワインドアップ)

さきほどの「溜まり」には、気をつけたい癖があります。目標が遠くて、ヒーターが 100% に張り付いている間も、溜まりは誤差を足し続けます。ヒーターはもう 100% 以上は出せないのに、です。

アンチワインドアップの有無。赤=無しは53.9℃まで行き過ぎ、緑=有りは47℃で収まる。下段でヒーター出力の張り付き方が違う

赤い線がそれです。目標を 45℃ にして、しばらく 100% で加熱が続く状況。温度が 45℃ を超えても、ヒーターは 100% のまま(下段)。溜め込んだ分が大きすぎて、なかなか下がらないのです。結果、53.9℃ まで大きく行き過ぎました。

対策は単純で、ヒーターが振り切れて、なおその向きへ押し続けているあいだは溜まりを止める(アンチワインドアップ)。逆向きに戻したいときは、普通に溜まりを戻します。緑の線がそれで、47℃ を少し超えたあたりで素直に収まっています。

目標を急に変えると跳ねる(微分キック)

D は「変化の速さ」を見ます。もし 誤差の変化 をそのまま微分すると、目標を急に変えた瞬間、誤差が段差で飛ぶので、D も一瞬跳ね上がります。

微分キック。赤=誤差で微分は目標段差で指令が503%まで跳ねる、緑=測定値で微分は滑らか

赤い線は、目標を 40→44℃ に変えた瞬間、指令が 503% まで跳ねています(実際にはヒーターの上限で頭打ちになりますが、コントローラはこれだけの出力を要求している)。この一瞬の跳ね上がりを 微分キック と呼びます。

避け方は、D を 誤差ではなく測定値(温度そのもの) で微分すること。温度は急には飛ばないので、緑の線のように D 由来の大きな跳ねは出ません。目標が一定のあいだは誤差微分も測定値微分も同じ制動として働き、違いが出るのは目標を段差で変えた瞬間だけ ―― そこで D 項の跳ね上がりだけを避けられます。先ほどの PID で微分を T(測定値)から作ったのは、これを避けるためです。

おわりに ―― 温度は「ゆっくり」だから、素朴な PID でも追える

コタツの温度は、時定数が秒〜分の、ゆっくりした世界です。むだ時間はあるものの、素朴な PID でもちゃんと一周しました。数式もほとんど使わず、「傾き」と「溜まり」で最後まで来られました。

ですが、モータの位置決めのように 速く・正確に・振らさず を同時に求められる世界になると、単純な PID 一段では足りなくなります。そこでは、内側に速い電流のループ、その外に速度、さらに外に位置、とループを 入れ子(カスケード) にし、さらに三相交流をうまく扱うために 座標を回す(dq 変換) という道具が出てきます。次回は、同じ「波形で確かめる」やり方で、そこに踏み込んでみます。

コード一式(コタツのモデル、P/PI/PID、3 枚の図の生成)は、そのまま動く形で置いてあります。python pid_thermal.py で本文の比較表(定常偏差・行き過ぎ・整定時間)が、python plot_pid.py で 3 枚の図が出ます。

この記事は Zenn に初出したものを加筆・補足したものです ── Zenn の元記事を見る