PID制御の基礎理論と各要素の役割

PID制御の基礎理論を、閉ループ伝達関数と最終値の定理から解説。P制御の定常偏差、内部モデル原理によるI項の効果、PI/PIDのランプ追従誤差を導出し、積分ワインドアップ・微分キック・微分項のノイズ増幅という3つの実装上の落とし穴をPythonで数値検証します。

PID制御とは

PID制御は、フィードバック制御の一種であり、制御対象の出力値 \(y(t)\) を目標値 \(r(t)\) に近づけるために広く用いられる制御手法です。制御対象の出力と目標値との偏差 \(e(t) = r(t) - y(t)\) に基づいて操作量 \(u(t)\) を計算し、制御対象に加えることで、制御系の安定化、目標値への追従性の向上、外乱抑制などを実現します。

本記事では、PID制御の理論的な基盤——閉ループ伝達関数の導出、最終値の定理による定常偏差の解析、積分ワインドアップ・微分キック・微分項のノイズ増幅という3つの実装上の落とし穴とその数学的な発生機序——を扱います。離散化やZiegler-Nichols法によるチューニング、外乱抑制の実践的なシミュレーションなど、実装寄りの内容は PID制御のPython実装:シミュレーションとチューニング で深掘りしているので、あわせて参照してください。

P制御 (比例制御)

P制御の操作量 \(u(t)\) は、偏差 \(e(t)\) に比例して決定されます。

\[ u(t) = K_P e(t) \]
  • \(K_P\) : 比例ゲイン。
  • 特徴: \(K_P\) を大きくすると、偏差に対する応答が速くなり、偏差を小さくする効果があります。しかし、\(K_P\) を大きくしすぎると、応答が振動的になり、最終的には制御が不安定になる可能性があります。また、偏差が小さくなると操作量も小さくなるため、目標値との間に定常偏差(定常状態での残差)が残ることがあります。この定常偏差がなぜ生じるかは、後述の「 定常偏差の解析:最終値の定理 」で厳密に導出します。

PI制御 (比例積分制御)

PI制御の操作量 \(u(t)\) は、偏差の比例項と積分項の和で決定されます。

\[ u(t) = K_P e(t) + K_I \int_0^t e(\tau)d\tau \]
  • \(K_I\) : 積分ゲイン。
  • 特徴: 積分項は、過去の偏差の累積を考慮するため、定常偏差を解消する効果があります。偏差がゼロになるまで操作量を出し続けるため、最終的に目標値に正確に追従できます。ただし、積分動作は応答に遅れを生じさせるため、急激な変化に対してオーバーシュート(目標値を超えてしまう現象)やハンチング(目標値の周りで振動する現象)が発生しやすくなることがあります。

PID制御 (比例積分微分制御)

PID制御の操作量 \(u(t)\) は、偏差の比例項、積分項、微分項の3つの要素の和で決定されます。

\[ u(t) = K_P e(t) + K_I \int_0^t e(\tau)d\tau + K_D \frac{de(t)}{dt} \]
  • \(K_D\) : 微分ゲイン。
  • 特徴: 微分項は、偏差の変化率(未来の偏差の傾向)を予測して操作量に反映させます。これにより、偏差の急激な変化に対して素早く反応し、オーバーシュートやハンチングを抑制し、応答の遅れを改善する効果があります。一方で、微分項は目標値の急変時に大きなスパイクを生んだり(微分キック)、測定ノイズを増幅したりする副作用も持ちます。これらは後述の「 PIDの3つの落とし穴:導出と数値検証 」で扱います。

PID制御は、偏差の「現在(P項)」、「過去(I項)」、「未来(D項)」に基づいて制御を行うことで、安定性、目標値への追従性、即応性をバランス良く満たす制御系を構成できます。PID制御を適切に機能させるためには、\(K_P, K_I, K_D\) の3つのパラメータを適切に調整する(チューニング)ことが非常に重要です。

ブロック線図

閉ループ伝達関数の導出

PID制御の定常偏差や安定性を厳密に議論するために、まず閉ループ伝達関数を導出します。図のブロック線図が表すのは、制御対象(プラント)\(G(s)\) に対する単一フィードバックループ(unity feedback)です。目標値を \(R(s)\) 、出力を \(Y(s)\) 、偏差を \(E(s)\) 、制御器の出力(操作量)を \(U(s)\) とすると、次の3つの関係式が成り立ちます。

\[ E(s) = R(s) - Y(s), \qquad U(s) = C(s)E(s), \qquad Y(s) = G(s)U(s) \]

ここで、\(C(s)\) はPID制御器の伝達関数であり、\(u(t)\) の定義をラプラス変換することで次のように得られます。

\[ C(s) = K_P + \frac{K_I}{s} + K_D s = \frac{K_D s^2 + K_P s + K_I}{s} \tag{1} \]

3つの関係式から \(Y(s)\) を消去すると、

\[ E(s) = R(s) - G(s)C(s)E(s) \quad\Longrightarrow\quad E(s)\big(1 + G(s)C(s)\big) = R(s) \]

したがって、偏差の伝達関数(目標値から偏差まで)と、出力の伝達関数(目標値から出力まで)は、それぞれ次のように求まります。

\[ \frac{E(s)}{R(s)} = \frac{1}{1 + L(s)}, \qquad \frac{Y(s)}{R(s)} = \frac{L(s)}{1 + L(s)}, \qquad L(s) \equiv C(s)G(s) \tag{2} \]

\(L(s)\) は開ループ伝達関数と呼ばれ、フィードバックループを切り開いたときの一巡伝達関数です。式(1)より、\(C(s)\) は \(s=0\) に極を持つ(\(K_I \ne 0\) のとき)ため、\(L(s)\) も同じ極を持ちます。この「原点の極」の有無が、次節で導く定常偏差の性質を決定づけます。

定常偏差の解析:最終値の定理

最終値の定理

信号 \(x(t)\) のラプラス変換を \(X(s)\) とするとき、\(sX(s)\) の全ての極が複素平面の左半平面(原点を含まない開LHP)にあれば、次の最終値の定理(Final Value Theorem, FVT)が成り立ちます。

\[ \lim_{t \to \infty} x(t) = \lim_{s \to 0} sX(s) \]

適用条件(\(sX(s)\) の安定性)を満たさない場合、この定理は使えません。以下では閉ループが安定であることを前提に、定常偏差 \(e_{ss} \equiv \lim_{t\to\infty} e(t)\) を式(2)のE(s)に適用して求めます。

P制御における定常偏差(Type-0プラント)

制御対象として、1次遅れ系 \(G(s) = K/(1+Ts)\) (プロセスゲイン \(K\) 、時定数 \(T\) )を考えます。この \(G(s)\) は \(s=0\) に極を持たない(\(G(0)=K\) は有限の値)ため、Type-0プラントと呼ばれます。

P制御では \(C(s) = K_P\) (定数)なので、\(L(s) = K_P G(s)\) です。目標値がステップ入力 \(r(t)=1 \;(t\ge0)\) 、すなわち \(R(s)=1/s\) のとき、式(2)より

\[ E(s) = \frac{1}{s\big(1 + K_P G(s)\big)} \]

FVTを適用すると、

\[ e_{ss} = \lim_{s \to 0} sE(s) = \frac{1}{1 + K_P G(0)} = \frac{1}{1 + K_P K} \tag{3} \]

\(K_P, K > 0\) である限り、この値はゼロになりません。これが「P制御には定常偏差が残る」という性質の厳密な根拠です。例えば \(K=1.0\) 、\(K_P=2.0\) のとき、\(e_{ss} = 1/(1+2) \approx 0.333\) となります( PID制御のPython実装 のシミュレーションで実測された値と一致します)。

積分項による定常偏差の解消:内部モデル原理

PI制御・PID制御では、\(C(s)\) が式(1)の形で \(s=0\) に極を持ちます。したがって \(L(s)=C(s)G(s)\) も \(s\to0\) で発散します(\(K_I \ne 0\) の限り、\(G(0)=K\ne0\) でも \(L(s) \sim K_I K/s \to \infty\) )。ステップ入力 \(R(s)=1/s\) に対して式(2)を適用すると、

\[ e_{ss} = \lim_{s \to 0} s \cdot \frac{1}{s\big(1+L(s)\big)} = \lim_{s \to 0} \frac{1}{1+L(s)} = 0 \tag{4} \]

\(L(s)\to\infty\) であるため、\(e_{ss}\) は厳密にゼロになります。閉ループ全体で見ると、\(L(s)\) が \(s=0\) に極を持つことは、\(Y(s)/R(s)\) の分母 \(1+L(s)\) の性質を通じて、閉ループが「原点に極を持つ入力(ステップ)を誤差なく通過させる」構造になっていることを意味し、これはType-1システムと呼ばれます。

この現象は、より一般的な内部モデル原理(Internal Model Principle; Francis & Wonham, 1976)の特殊ケースです。内部モデル原理は、「ある信号発生器(ステップならラプラス変換の分母 \(s\) 、ランプなら \(s^2\) )で生成される基準信号を、定常偏差ゼロで追従するためには、その信号発生器と同じ極を制御ループの一巡伝達関数 \(L(s)\) が内部に持たなければならない」というものです。ステップ入力の生成器は \(1/s\) ですから、これを追従するにはループのどこかに \(1/s\) (積分器)が必要であり、Type-0プラント自身は積分器を持たないため、その役割をI項が担うことになります。これが「積分項が定常偏差を解消する」ことの理論的な裏付けです。

ランプ入力への追従誤差:PI制御とPID制御の比較

では、ランプ入力 \(r(t) = t\) (\(R(s) = 1/s^2\) )に対する定常偏差はどうなるでしょうか。式(2)より、

\[ e_{ss} = \lim_{s \to 0} s \cdot \frac{1/s^2}{1+L(s)} = \lim_{s \to 0} \frac{1}{s\big(1+L(s)\big)} \]

PI制御では \(C(s) = K_P + K_I/s = (K_P s + K_I)/s\) なので、

\[ L(s) = \frac{(K_P s + K_I)K}{s(1+Ts)} \]

\(s\to0\) の極限では \(s \cdot L(s) \to K_I K\) に収束するため(\(K_v \equiv \lim_{s\to0} sL(s)\) を速度偏差定数と呼びます)、

\[ e_{ss} = \lim_{s\to0} \frac{1}{s + sL(s)} = \frac{1}{K_v} = \frac{1}{K_I K} \tag{5} \]

有限の値として残ります(ゼロにはなりません)。PID制御では \(C(s) = (K_D s^2+K_P s+K_I)/s\) となりますが、

\[ sL(s) = \frac{(K_D s^2+K_P s+K_I)K}{1+Ts} \;\xrightarrow{s\to0}\; K_I K \]

であり、\(K_D\) の項は \(s\to0\) で消えるため、\(K_v = K_I K\) はPI制御と全く同じになります。つまり、微分項 \(K_D\) はランプ追従の定常偏差には一切寄与しません。D項が改善するのはあくまで過渡応答(オーバーシュートや整定時間)であり、ランプ入力に対する定常偏差を減らすには \(K_I\) を大きくする(もしくはプラント自体に積分器を追加する)以外に手段はありません。

以下のシミュレーションで、この結果を数値的に確認します。\(G(s)=1/(1+s)\) (\(K=1,T=1\) )、\(K_P=2.0\) 、\(K_I=1.5\) のPI/PID制御器にランプ入力 \(r(t)=t\) を与え、\(K_D\) を \(0, 0.3, 0.6\) と変えて定常偏差を比較します。

import numpy as np

K_plant, T_plant = 1.0, 1.0
Kp, Ki = 2.0, 1.5

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 simulate_ramp(kp, ki, kd, dt=0.01, t_end=60.0, ramp_rate=1.0):
    plant = FirstOrderSystem(K_plant, T_plant, dt)
    integral = 0.0
    prev_error = 0.0
    t = 0.0
    e_hist = []
    while t < t_end:
        r = ramp_rate * t
        e = r - plant.y
        integral += e * dt
        d = (e - prev_error) / dt
        prev_error = e
        u = kp * e + ki * integral + kd * d
        plant.update(u)
        e_hist.append(e)
        t += dt
    return np.array(e_hist)

for kd in [0.0, 0.3, 0.6]:
    e_hist = simulate_ramp(Kp, Ki, kd)
    e_ss_numeric = np.mean(e_hist[-200:])  # last 2s average
    e_ss_theory = 1.0 / (Ki * K_plant)
    print(f"Kd={kd:4.1f}: e_ss(numeric)={e_ss_numeric:.5f}, "
          f"theory 1/(Ki*K)={e_ss_theory:.5f}")
Kd= 0.0: e_ss(numeric)=0.66667, theory 1/(Ki*K)=0.66667
Kd= 0.3: e_ss(numeric)=0.66667, theory 1/(Ki*K)=0.66667
Kd= 0.6: e_ss(numeric)=0.66667, theory 1/(Ki*K)=0.66667

理論値 \(1/(K_I K) = 1/1.5 \approx 0.6667\) と数値シミュレーションが完全に一致し、かつ \(K_D\) を変えても \(e_{ss}\) が変化しないことが確認できます。

PIDの3つの落とし穴:導出と数値検証

理想的な連続時間PID則は式(1)のように単純ですが、実装・運用の現場では次の3つの現象が性能を大きく損ないます。ここではそれぞれの発生メカニズムを理論的に導出し、簡潔な数値実験で効果を確認します。より詳細な対策手法の比較(back-calculation法・clamping法の導出、離散化誤差の解析など)は PID制御のPython実装 で扱っているので、実装に踏み込む際はそちらを参照してください。

積分ワインドアップ

実際のアクチュエータには必ず出力範囲の限界があります。非飽和の制御指令

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

は、実際には次のように飽和させられます。

\[ u(t) = \begin{cases} u_{max} & (u_{unsat}(t) > u_{max}) \\ u_{unsat}(t) & (u_{min} \le u_{unsat}(t) \le u_{max}) \\ u_{min} & (u_{unsat}(t) < u_{min}) \end{cases} \]

素朴な実装では、積分状態 \(I(t)=\int_0^t e(\tau)d\tau\) は偏差 \(e(t)\) のみに依存し、実際に印加された \(u(t)\) が飽和で \(u_{unsat}(t)\) と乖離しているかどうかを一切参照しません。したがって、アクチュエータが飽和している間も \(e(t)\) が同符号であり続ける限り、\(I(t)\) は制御が実際には効いていないにもかかわらず際限なく増加し続けます。これが積分ワインドアップです。出力がようやく目標値に近づき \(e(t)\) の符号が反転しても、蓄積された過剰な \(I(t)\) を打ち消すには反対符号の偏差を長時間積分し続ける必要があり、その間 \(u_{unsat}(t)\) は飽和限界を超えたままになるため、結果として大きなオーバーシュートを招きます。

代表的な対策の一つが条件付き積分(conditional integration)です。飽和が発生していて、かつ偏差の符号が飽和をさらに深める方向(=積分を止めるべき方向)と一致する場合にのみ積分を停止し、それ以外は通常通り積分します。

\[ \frac{dI}{dt} = \begin{cases} 0 & \big(\text{飽和中かつ } \mathrm{sign}(e(t)) \text{ が飽和方向と一致}\big) \\ e(t) & (\text{それ以外}) \end{cases} \]

以下のシミュレーションで、通常のPID(アンチワインドアップなし)と条件付き積分を比較します。制御対象は \(G(s)=1/(1+s)\) 、ゲインは \(K_P=1.0\) 、\(K_I=3.5\) 、\(K_D=0.3\) 、操作量制限を \(u \in [-1.15, 1.15]\) とし(定常状態で必要な操作量は \(u_{ss}=1.0\) )、目標値 \(r=1.0\) へのステップ応答を比較します。

import numpy as np

K_plant, T_plant = 1.0, 1.0
Kp, Ki, Kd = 1.0, 3.5, 0.3
u_lo, u_hi = -1.15, 1.15
dt, t_end, setpoint = 0.01, 15.0, 1.0

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

class PIDNoAntiWindup:
    """ナイーブなPID:飽和中も積分を加算し続ける"""
    def __init__(self, kp, ki, kd, dt, limits):
        self.kp, self.ki, self.kd, self.dt = kp, ki, kd, dt
        self.lo, self.hi = limits
        self.integral = 0.0
        self.prev_error = None
    def update(self, setpoint, measured):
        error = setpoint - measured
        if self.prev_error is None:
            self.prev_error = error
        self.integral += error * self.dt
        p = self.kp * error
        i = self.ki * self.integral
        d = self.kd * (error - self.prev_error) / self.dt
        self.prev_error = error
        u_unsat = p + i + d
        return min(max(u_unsat, self.lo), self.hi), u_unsat

class PIDConditionalIntegration:
    """条件付き積分:飽和方向と偏差の符号が一致する間だけ積分を凍結"""
    def __init__(self, kp, ki, kd, dt, limits):
        self.kp, self.ki, self.kd, self.dt = kp, ki, kd, dt
        self.lo, self.hi = limits
        self.integral = 0.0
        self.prev_error = None
    def update(self, setpoint, measured):
        error = setpoint - measured
        if self.prev_error is None:
            self.prev_error = error
        p = self.kp * error
        i_tentative = self.ki * (self.integral + error * self.dt)
        d = self.kd * (error - self.prev_error) / self.dt
        self.prev_error = error
        u_unsat = p + i_tentative + d
        saturated_high = u_unsat > self.hi
        saturated_low = u_unsat < self.lo
        pushing_further = (saturated_high and error > 0) or (saturated_low and error < 0)
        if not pushing_further:
            self.integral += error * self.dt
        u = min(max(u_unsat, self.lo), self.hi)
        return u, u_unsat

def run(controller_cls, t_end, dt, setpoint_val):
    t = np.arange(0, t_end, dt)
    plant = FirstOrderSystem(K_plant, T_plant, dt)
    ctrl = controller_cls(Kp, Ki, Kd, dt, (u_lo, u_hi))
    y_hist, int_hist = [], []
    for _ in t:
        u, _ = ctrl.update(setpoint_val, plant.y)
        y = plant.update(u)
        y_hist.append(y)
        int_hist.append(ctrl.integral)
    return t, np.array(y_hist), np.array(int_hist)

results = {
    "No anti-windup": run(PIDNoAntiWindup, t_end, dt, setpoint),
    "Conditional integration": run(PIDConditionalIntegration, t_end, dt, setpoint),
}

for name, (t, y, integral) in results.items():
    overshoot = (np.max(y) - setpoint) / setpoint * 100
    tol = 0.02 * setpoint
    settle_idx = next(i for i in range(len(y)) if np.all(np.abs(y[i:] - setpoint) < tol))
    print(f"{name}: overshoot={overshoot:.2f}%, settle_t={t[settle_idx]:.2f}s, "
          f"peak_integral={np.max(integral):.4f}")
No anti-windup: overshoot=14.46%, settle_t=8.04s, peak_integral=0.7217
Conditional integration: overshoot=4.79%, settle_t=3.67s, peak_integral=0.3377
手法オーバーシュート整定時間(2%以内)積分状態のピーク値
アンチワインドアップなし14.46%8.04 s0.7217
条件付き積分4.79%3.67 s0.3377

積分ワインドアップと条件付き積分の効果比較

条件付き積分により、積分状態のピーク値が約53%減少し、オーバーシュートも14.46%から4.79%へ、整定時間も8.04秒から3.67秒へと大幅に改善しています。back-calculation法・clamping法との比較や、より一般的な離散実装の詳細は PID制御のPython実装 の該当節で導出しています。

微分キック(Derivative Kick)

微分項 \(K_D \, de(t)/dt\) は偏差 \(e(t)=r(t)-y(t)\) の変化率を用いるため、次のように分解できます。

\[ \frac{de(t)}{dt} = \frac{dr(t)}{dt} - \frac{dy(t)}{dt} \]

目標値 \(r(t)\) がステップ状に変化する瞬間には、右辺第1項 \(dr/dt\) が理想的にはディラックのデルタ関数(離散時間では \(\Delta r/\Delta t\) という有限だが \(\Delta t\) が小さいほど非常に大きな値)になります。これが操作量に瞬間的な大きなスパイクを生む微分キックです。標準的な対策は、微分項を偏差 \(e\) ではなく観測値 \(y\) にのみ作用させる微分先行型(derivative-on-measurement)です。

\[ d(t) = -K_D \frac{dy(t)}{dt} \]

目標値が一定に保たれている区間では \(de/dt = -dy/dt\) が成り立つため、この形は通常の微分項と完全に同じ値を返します。つまり、外乱抑制やノイズ応答の特性を犠牲にすることなく、目標値変化時のキックだけを取り除けます。

以下のシミュレーションで、目標値が \(t=2\) sで \(0 \to 1\) にステップ変化する場合の操作量を比較します(\(K_P=2.5\) 、\(K_I=1.2\) 、\(K_D=0.6\) 、\(G(s)=1/(1+s)\) )。

import numpy as np

K_plant, T_plant = 1.0, 1.0
Kp, Ki, Kd = 2.5, 1.2, 0.6
dt = 0.01

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

class PIDErrorDerivative:
    """通常形式:微分項が偏差 e = r - y に作用する"""
    def __init__(self, kp, ki, kd, dt):
        self.kp, self.ki, self.kd, self.dt = kp, ki, kd, dt
        self.integral = 0.0
        self.prev_error = None
    def update(self, setpoint, measured):
        error = setpoint - measured
        if self.prev_error is None:
            self.prev_error = error
        self.integral += error * self.dt
        p = self.kp * error
        i = self.ki * self.integral
        d = self.kd * (error - self.prev_error) / self.dt
        self.prev_error = error
        return p + i + d

class PIDMeasurementDerivative:
    """微分先行型:微分項が観測値 -y のみに作用する"""
    def __init__(self, kp, ki, kd, dt):
        self.kp, self.ki, self.kd, self.dt = kp, ki, kd, dt
        self.integral = 0.0
        self.prev_measured = None
    def update(self, setpoint, measured):
        error = setpoint - measured
        if self.prev_measured is None:
            self.prev_measured = measured
        self.integral += error * self.dt
        p = self.kp * error
        i = self.ki * self.integral
        d = -self.kd * (measured - self.prev_measured) / self.dt
        self.prev_measured = measured
        return p + i + d

t = np.arange(0, 8.0, dt)
step_time = 2.0
setpoint = np.where(t < step_time, 0.0, 1.0)

for name, cls in [("Derivative-on-error", PIDErrorDerivative),
                   ("Derivative-on-measurement", PIDMeasurementDerivative)]:
    pid = cls(Kp, Ki, Kd, dt)
    plant = FirstOrderSystem(K_plant, T_plant, dt)
    u_hist = []
    for sp in setpoint:
        u = pid.update(sp, plant.y)
        plant.update(u)
        u_hist.append(u)
    u_hist = np.array(u_hist)
    idx_step = np.argmax(t >= step_time)
    jump = u_hist[idx_step] - u_hist[idx_step - 1]
    print(f"{name}: u just before step={u_hist[idx_step-1]:.4f}, "
          f"u just after step={u_hist[idx_step]:.4f}, jump={jump:.4f}")
Derivative-on-error: u just before step=0.0000, u just after step=62.5120, jump=62.5120
Derivative-on-measurement: u just before step=0.0000, u just after step=2.5120, jump=2.5120
方式ステップ直前の \(u\)ステップ直後の \(u\)変化量
derivative-on-error(通常)0.000062.5120+62.512
derivative-on-measurement0.00002.5120+2.512

微分キックの比較(derivative-on-error vs. derivative-on-measurement)

通常の微分項では目標値ステップの瞬間に操作量が約62.5まで跳ね上がる激しいキックが発生しますが、微分先行型ではおよそ2.51(比例項の変化分 \(K_P \Delta r = 2.5\) にほぼ一致)に収まっています。図のズームパネルでは、微分キック後にリンギング(振動的な減衰)が続く様子も確認できます。実際のアクチュエータでこの種のスパイクが発生すると機械的な衝撃や飽和を招くため、微分先行型は実務上ほぼ標準的な実装です。

微分項のノイズ増幅

微分項は周波数領域で見ると \(D(j\omega) = K_D \cdot j\omega\) という利得を持ち、その大きさ \(\lvert D(j\omega) \rvert = K_D \omega\) は角周波数 \(\omega\) に比例して無限に増大します。測定ノイズは一般に広い周波数帯域に成分を持つため、理想的な(フィルタなしの)微分は高周波ノイズを大きく増幅してしまいます。標準的な対策は、微分項に一次のローパスフィルタを付加したフィルタ付き微分です。

\[ D(s) = \frac{K_D s}{1 + s/N} = \frac{K_D N s}{s + N} \]

\(N\) は微分フィルタ係数と呼ばれ、フィルタの折れ点角周波数(時定数 \(\tau_f=1/N\) )を決めます。周波数応答は \(\lvert D(j\omega) \rvert = K_D N \omega / \sqrt{\omega^2+N^2}\) となり、\(\omega \ll N\) では理想微分に一致する一方、\(\omega \to \infty\) では \(K_D N\) に飽和するため、高周波ノイズの増幅が有限に抑えられます。

以下のシミュレーションで、\(G(s)=1/(1+s)\) に標準偏差 \(0.02\) の正規分布ノイズを観測値に加えた状態で \(K_P=2.5\) 、\(K_I=0.8\) 、\(K_D=0.6\) のPID制御を行い、フィルタなし・\(N=8\) ・\(N=1.5\) (強フィルタ)の3通りで、定常状態(\(t>5\) s)における操作量の変動を比較します。

import numpy as np

K_plant, T_plant = 1.0, 1.0
Kp, Ki, Kd = 2.5, 0.8, 0.6
dt, noise_std = 0.01, 0.02

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

class PIDFilteredDerivative:
    """微分先行型に、時定数1/Nの一次ローパスフィルタ(Noneならフィルタなし)を追加"""
    def __init__(self, kp, ki, kd, dt, N=None):
        self.kp, self.ki, self.kd, self.dt = kp, ki, kd, dt
        self.N = N
        self.integral = 0.0
        self.prev_measured = None
        self.d_filt = 0.0
    def update(self, setpoint, measured):
        error = setpoint - measured
        if self.prev_measured is None:
            self.prev_measured = measured
        self.integral += error * self.dt
        p = self.kp * error
        i = self.ki * self.integral
        d_raw = -(measured - self.prev_measured) / self.dt
        self.prev_measured = measured
        if self.N is None:
            d_used = d_raw
        else:
            tau_f = 1.0 / self.N
            alpha = min(self.dt / tau_f, 1.0)
            self.d_filt += alpha * (d_raw - self.d_filt)
            d_used = self.d_filt
        return p + i + self.kd * d_used

dt_arr = np.arange(0, 15.0, dt)
setpoint = np.ones_like(dt_arr)

std_ref = None
for label, N in [("No filter", None), ("N=8", 8.0), ("N=1.5 (stronger filter)", 1.5)]:
    np.random.seed(7)
    pid = PIDFilteredDerivative(Kp, Ki, Kd, dt, N=N)
    plant = FirstOrderSystem(K_plant, T_plant, dt)
    u_hist = []
    for sp in setpoint:
        noisy_measurement = plant.y + np.random.normal(0, noise_std)
        u = pid.update(sp, noisy_measurement)
        plant.update(u)
        u_hist.append(u)
    u_hist = np.array(u_hist)
    mask = dt_arr > 5.0
    u_std = np.std(u_hist[mask])
    if std_ref is None:
        std_ref = u_std
    print(f"{label}: std(u)={u_std:.4f}, ptp(u)={np.ptp(u_hist[mask]):.4f}, "
          f"amplification_factor={u_std/std_ref:.3f}")
No filter: std(u)=2.7418, ptp(u)=19.1303, amplification_factor=1.000
N=8: std(u)=0.1476, ptp(u)=0.9491, amplification_factor=0.054
N=1.5 (stronger filter): std(u)=0.0681, ptp(u)=0.4360, amplification_factor=0.025
フィルタ設定操作量の標準偏差操作量のPeak-to-Peakフィルタなし比
フィルタなし2.741819.13031.000
\(N=8\)0.14760.94910.054
\(N=1.5\) (強フィルタ)0.06810.43600.025

微分項のノイズ増幅とフィルタリングの効果

フィルタなしでは操作量の標準偏差が2.74まで増幅されていますが、\(N=8\) のフィルタで約18.6分の1(0.148)に、\(N=1.5\) まで強めると約40分の1(0.068)まで低減できています。フィルタの時定数 \(\tau_f=1/N\) をプラントの応答速度に対して大きくしすぎると位相遅れが無視できなくなり安定余裕を損なうため、\(N\) の選定にはトレードオフが伴います。

PIDチューニングとロバスト性の最新研究動向

古典的なPID制御理論は成熟した分野ですが、2024年以降も理論的な再解釈や実装指針の刷新が続いています。

アンチワインドアップの体系的な整理: Caparroz, Soltész, Hägglund, Guzmán(arXiv:2606.01959, 2026)は、本記事で扱ったback-calculation法・条件付き積分を含む複数のアンチワインドアップ手法を、目標値追従・外乱抑制の両シナリオで横断的に比較し、条件付き積分と動的back-calculationを組み合わせた新しいハイブリッド手法を提案しています。特に、back-calculation法のtracking time constantの最適値が積分時間より小さくなる傾向があり、飽和比率や制御器の攻撃性によって大きく変動することを実験的に示しており、実務者が計算コストをかけずに準最適な性能を得るための具体的な指針を与えています。

内部モデル原理の再解釈: Shi(arXiv:2411.14678, 2024)は、PID制御器を「系を安定化する同次系コントローラ」と「未知の集中外乱を推定・補償する外乱オブザーバ」という2つの機構の組み合わせとして再解釈し、本記事で導出した内部モデル原理に基づくI項の役割を、外乱補償の観点から補強する枠組みを提示しています。測定ノイズがパラメータ選択に与える影響の解析や、劣駆動VTOL機・車両の横方向制御への応用例も示されています。

ゲインとロバスト性の理論的関係: Zhu, Yu, Liu(arXiv:2504.15081, 2025)は、PIDゲインを3つの補助パラメータによる非線形写像として再定式化する「PID-GM」を提案し、特異摂動理論を用いて、不確かさを持つ2次系に対する閉ループの安定性と定常偏差の性能が、単一の仮想特異摂動パラメータに明示的に関係づけられることを示しています。3つのゲインすべてがこのパラメータに対して単調に変化するという結果は、本記事のゲイン・チューニング指針(\(K_P, K_I, K_D\) を個別に調整する方法)に対して、より体系的な代替アプローチを提供するものです。この分野は変化が速いため、具体的な数値や適用範囲については各論文の一次情報を参照することをお勧めします。

まとめ

本記事では、PID制御の3つの基本形(P・PI・PID)の役割を確認したのち、閉ループ伝達関数を導出し、最終値の定理を用いてP制御の定常偏差・I項による定常偏差の解消(内部モデル原理)・PI/PIDのランプ追従誤差(\(K_D\) が寄与しないこと)を厳密に示しました。さらに、積分ワインドアップ・微分キック・微分項のノイズ増幅という3つの実装上の落とし穴について、発生メカニズムを数式で導出し、それぞれ数値シミュレーションと図で効果を確認しました。

PID制御の理論はシンプルでありながら、これらの性質を正しく理解して初めて、適切なゲイン設計とアンチワインドアップ・フィルタリングの実装が可能になります。より実装に踏み込んだ内容(離散化、Ziegler-Nichols法、外乱抑制の実験など)は PID制御のPython実装 を、安定余裕の評価には ボード線図ナイキスト線図・根軌跡 を、モデルの不確かさに対するロバスト性を保証したい場合は H∞制御 を参照してください。

参考文献

  • Astrom, K. J., & Murray, R. M. (2021). Feedback Systems: An Introduction for Scientists and Engineers (2nd ed.). Princeton University Press.
  • Åström, K. J., & Hägglund, T. (1995). PID Controllers: Theory, Design, and Tuning (2nd ed.). Instrument Society of America.
  • Francis, B. A., & Wonham, W. M. (1976). “The internal model principle of control theory”. Automatica, 12(5), 457-465.
  • Caparroz, M., Soltész, K., Hägglund, T., & Guzmán, J. L. (2026). “Anti-Windup in PID Control: Review, Analysis, and New Tuning Directions”. arXiv:2606.01959.
  • Shi, X. (2024). “Reinterpreting PID Controller From the Perspective of State Feedback and Lumped Disturbance Compensation”. arXiv:2411.14678.
  • Zhu, B., Yu, W., & Liu, H. H. T. (2025). “PID-GM: PID Control with Gain Mapping”. arXiv:2504.15081.

関連記事