PID制御のPython実装:シミュレーションとチューニング

PID制御をPythonで実装し、1次遅れ系のシミュレーションで動作を確認します。P/PI/PIDの応答比較とZiegler-Nicholsチューニング法も紹介します。

はじめに

PID制御は、産業用途で最も広く使われるフィードバック制御手法です。温度制御、モータ制御、プロセス制御など、あらゆる分野で用いられています。

PID制御の基礎理論についてはPID制御の基礎理論と各要素の役割を参照してください。本記事では、PID制御器をPythonで実装し、1次遅れ系に対するシミュレーションを通じて、P制御・PI制御・PID制御の応答特性を比較します。さらに、古典的なチューニング手法であるZiegler-Nichols法についても紹介します。

離散PID制御器の実装

連続時間のPID制御則は以下のように表されます。

\[ u(t) = K_P e(t) + K_I \int_0^t e(\tau)d\tau + K_D \frac{de(t)}{dt} \]

ここで、\(e(t) = r(t) - y(t)\) は目標値と出力の偏差です。計算機上で実装するためには、積分と微分を離散近似する必要があります。積分は累積和、微分は後退差分で近似すると、離散PID制御則は次のようになります。

\[ u[k] = K_P e[k] + K_I \Delta t \sum_{i=0}^{k} e[i] + K_D \frac{e[k] - e[k-1]}{\Delta t} \tag{1} \]

ここで、\(\Delta t\) はサンプリング周期です。

以下にPythonでの実装を示します。出力制限とアンチワインドアップ機構を含んでいます。

class PIDController:
    def __init__(self, kp, ki, kd, dt, output_limits=None):
        self.kp = kp
        self.ki = ki
        self.kd = kd
        self.dt = dt
        self.output_limits = output_limits
        self.integral = 0.0
        self.prev_error = 0.0

    def update(self, setpoint, measured):
        error = setpoint - measured

        # 比例項
        p_term = self.kp * error

        # 積分項(累積和による近似)
        self.integral += error * self.dt
        i_term = self.ki * self.integral

        # 微分項(後退差分による近似)
        derivative = (error - self.prev_error) / self.dt
        d_term = self.kd * derivative
        self.prev_error = error

        output = p_term + i_term + d_term

        # 出力制限(アンチワインドアップ)
        if self.output_limits is not None:
            lo, hi = self.output_limits
            if output > hi:
                output = hi
                self.integral -= error * self.dt
            elif output < lo:
                output = lo
                self.integral -= error * self.dt

        return output

    def reset(self):
        self.integral = 0.0
        self.prev_error = 0.0

アンチワインドアップは、操作量が上下限に達した場合に積分項の蓄積を停止する仕組みです。これがないと、制約下で積分値が際限なく増大し、制約解除後に大きなオーバーシュートを引き起こします。

制御対象のモデル

シミュレーションの制御対象として、1次遅れ系を用います。伝達関数は以下の通りです。

\[ G(s) = \frac{K}{1 + Ts} \tag{2} \]

ここで、\(K\) はプロセスゲイン、\(T\) は時定数です。この微分方程式を離散化すると、次の更新式が得られます。

\[ y[k+1] = y[k] + \frac{\Delta t}{T}(K \cdot u[k] - y[k]) \tag{3} \]

Pythonでの実装は以下の通りです。

class FirstOrderSystem:
    def __init__(self, gain, time_constant, dt):
        self.gain = gain
        self.time_constant = time_constant
        self.dt = dt
        self.y = 0.0

    def update(self, u):
        self.y += self.dt / self.time_constant * (self.gain * u - self.y)
        return self.y

    def reset(self):
        self.y = 0.0

P制御・PI制御・PID制御の応答比較

3種類の制御器でステップ応答を比較します。制御対象は \(K=1.0\)、\(T=1.0\) の1次遅れ系です。

import numpy as np
import matplotlib.pyplot as plt

dt = 0.01
t_end = 10.0
t = np.arange(0, t_end, dt)
setpoint = np.ones_like(t)  # ステップ入力

plant_params = {'gain': 1.0, 'time_constant': 1.0, 'dt': dt}

configs = [
    ('P control (Kp=2.0)', {'kp': 2.0, 'ki': 0.0, 'kd': 0.0}),
    ('PI control (Kp=2.0, Ki=1.0)', {'kp': 2.0, 'ki': 1.0, 'kd': 0.0}),
    ('PID control (Kp=2.0, Ki=1.0, Kd=0.5)', {'kp': 2.0, 'ki': 1.0, 'kd': 0.5}),
]

fig, axes = plt.subplots(2, 1, figsize=(10, 8), sharex=True)

for label, params in configs:
    pid = PIDController(**params, dt=dt)
    plant = FirstOrderSystem(**plant_params)
    y_hist = []
    u_hist = []

    for sp in setpoint:
        u = pid.update(sp, plant.y)
        y = plant.update(u)
        y_hist.append(y)
        u_hist.append(u)

    axes[0].plot(t, y_hist, label=label)
    axes[1].plot(t, u_hist, label=label)

axes[0].axhline(y=1.0, color='k', linestyle='--', alpha=0.5, label='Setpoint')
axes[0].set_ylabel('Output y(t)')
axes[0].set_title('Step Response Comparison')
axes[0].legend()
axes[0].grid(True, alpha=0.3)

axes[1].set_ylabel('Control input u(t)')
axes[1].set_xlabel('Time [s]')
axes[1].legend()
axes[1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

シミュレーション結果から、以下の特性が確認できます。

  • P制御: 応答は速いものの、定常偏差が残ります。\(K_P = 2.0\) のとき、理論的な定常偏差は \(\frac{1}{1 + K \cdot K_P} = \frac{1}{3} \approx 0.33\) です。
  • PI制御: 積分項の効果により定常偏差が解消されます。ただし、オーバーシュートが発生しやすくなります。
  • PID制御: 微分項によりオーバーシュートが抑制され、P制御の速応性とPI制御の定常特性を両立できます。

ゲインパラメータの影響

各ゲインパラメータの設定は制御性能に大きく影響します。

比例ゲイン \(K_P\) が大きすぎる場合

\(K_P\) を過大に設定すると、応答が振動的になり、最終的には不安定になります。例えば、P制御で \(K_P = 10.0\) とした場合、出力は目標値の周りで振動します。

積分ゲイン \(K_I\) とワインドアップ

\(K_I\) が大きい場合、または操作量に制限がある場合、積分項が過度に蓄積する「ワインドアップ」が発生します。操作量が飽和している間も積分値が増え続けるため、飽和から復帰した後に大きなオーバーシュートを引き起こします。前述のアンチワインドアップ機構はこの問題を軽減します。

微分ゲイン \(K_D\) とノイズ感度

微分項は偏差の変化率を使うため、測定ノイズに敏感です。ノイズが含まれる信号に対して大きな \(K_D\) を設定すると、操作量が激しく変動します。実用上は、微分項の入力にローパスフィルタを適用する「不完全微分」がよく用いられます。

パラメータ調整の指針

パラメータ増加させると注意点
\(K_P\)応答が速くなる、定常偏差が減少大きすぎると振動・不安定
\(K_I\)定常偏差が解消されるオーバーシュート増加、ワインドアップ
\(K_D\)オーバーシュート抑制、応答改善ノイズに敏感

Ziegler-Nicholsチューニング法

Ziegler-Nichols法は、制御対象のステップ応答特性から経験的にPIDゲインを決定する古典的な手法です。ここでは、ステップ応答法(開ループ法)を紹介します。

ステップ応答法

制御対象にステップ入力を加え、応答曲線から以下の3つのパラメータを読み取ります。

  • \(K\): プロセスゲイン(定常値/入力値)
  • \(L\): むだ時間(応答が始まるまでの遅れ)
  • \(T\): 時定数(応答の変曲点における接線が定常値に達するまでの時間)

これらのパラメータから、以下の表に基づいてPIDゲインを計算します。

制御器\(K_P\)\(T_I\)\(T_D\)
P\(\frac{T}{KL}\)--
PI\(\frac{0.9T}{KL}\)\(\frac{L}{0.3}\)-
PID\(\frac{1.2T}{KL}\)\(2L\)\(0.5L\)

ここで、\(T_I\) は積分時間、\(T_D\) は微分時間であり、\(K_I = K_P / T_I\)、\(K_D = K_P \cdot T_D\) の関係があります。

Ziegler-Nichols法は振動的な応答を出発点として設計されるため、そのままではオーバーシュートが大きくなる傾向があります。実用上は、この値を初期値として手動で微調整を行うことが一般的です。

外乱応答のシミュレーション

実際の制御系では、外乱(外部からの想定外の入力)が制御性能に影響を与えます。ここでは、シミュレーションの途中でステップ状の外乱を加え、P制御とPID制御の外乱抑制性能を比較します。

import numpy as np
import matplotlib.pyplot as plt

dt = 0.01
t_end = 20.0
t = np.arange(0, t_end, dt)
setpoint = np.ones_like(t)

# 外乱:t=10sで大きさ0.5のステップ外乱
disturbance = np.zeros_like(t)
disturbance[t >= 10.0] = 0.5

plant_params = {'gain': 1.0, 'time_constant': 1.0, 'dt': dt}

configs = [
    ('P control (Kp=2.0)', {'kp': 2.0, 'ki': 0.0, 'kd': 0.0}),
    ('PID control (Kp=2.0, Ki=1.0, Kd=0.5)', {'kp': 2.0, 'ki': 1.0, 'kd': 0.5}),
]

fig, axes = plt.subplots(2, 1, figsize=(10, 8), sharex=True)

for label, params in configs:
    pid = PIDController(**params, dt=dt)
    plant = FirstOrderSystem(**plant_params)
    y_hist = []
    u_hist = []

    for i, sp in enumerate(setpoint):
        u = pid.update(sp, plant.y)
        # 外乱をプラントの入力に加算
        y = plant.update(u + disturbance[i])
        y_hist.append(y)
        u_hist.append(u)

    axes[0].plot(t, y_hist, label=label)
    axes[1].plot(t, u_hist, label=label)

axes[0].axhline(y=1.0, color='k', linestyle='--', alpha=0.5, label='Setpoint')
axes[0].axvline(x=10.0, color='r', linestyle=':', alpha=0.5, label='Disturbance onset')
axes[0].set_ylabel('Output y(t)')
axes[0].set_title('Disturbance Rejection Comparison')
axes[0].legend()
axes[0].grid(True, alpha=0.3)

axes[1].set_ylabel('Control input u(t)')
axes[1].set_xlabel('Time [s]')
axes[1].legend()
axes[1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

シミュレーション結果から、以下のことがわかります。

  • P制御: 外乱に対して一定の抑制効果はありますが、外乱による定常偏差が残ります。
  • PID制御: 積分項の効果により、外乱が加わっても最終的に出力は目標値に復帰します。微分項により、外乱に対する初期応答も改善されます。

まとめ

本記事では、PID制御器をPythonで実装し、1次遅れ系に対するシミュレーションを通じて動作を確認しました。P制御・PI制御・PID制御の応答比較により、各要素の役割と効果を実験的に確認できました。

PID制御は構造がシンプルでありながら、適切なチューニングによって高い制御性能を実現できます。一方で、非線形性が強いシステムや多入力多出力系では、より高度な制御手法が必要になります。

関連記事

参考文献

  • Astrom, K. J., & Murray, R. M. (2021). Feedback Systems: An Introduction for Scientists and Engineers (2nd ed.). Princeton University Press.
  • Ziegler, J. G., & Nichols, N. B. (1942). “Optimum settings for automatic controllers”. Transactions of the ASME, 64(11), 759-768.