フィードバック制御とは?基本概念をわかりやすく解説

シャワーを浴びるとき、あなたはどうやってお湯の温度を調節していますか?

もし「蛇口を45度回せば42°Cになるはずだ」と決め打ちして、そのまま目をつぶって浴びるなら、それは開ループ制御です。水圧が変わったり、誰かがキッチンで水を使い始めたりすれば、たちまち熱すぎるか冷たすぎるお湯を浴びることになります。

一方、手でお湯に触れて「熱い」と感じたら蛇口を少し戻し、「ぬるい」と感じたら少し開ける――この自然な行動こそがフィードバック制御(閉ループ制御)です。出力(お湯の温度)を感じ取り、目標(ちょうどよい温度)との差に基づいて入力(蛇口の開度)を修正する。人間が無意識にやっているこの仕組みを、工学的に定式化したものがフィードバック制御の理論です。

フィードバック制御を理解すると、次のような幅広い応用分野が見えてきます。

  • 自動車のクルーズコントロール: 坂道や向かい風があっても一定速度を保つ
  • ロケットの姿勢制御: 推力の揺らぎや大気の外乱があっても正しい方向を向き続ける
  • 産業用ロボットの位置制御: ワークの重さが変わっても正確な位置決めを行う
  • 電源回路の電圧安定化: 負荷が変動しても出力電圧を一定に保つ

これらはすべて「出力を測定して、目標値との差を修正する」というフィードバックの原理で動いています。本記事では、この原理を直感的な理解から数学的な定式化、そして Python シミュレーションによる検証まで、段階的に解説します。

本記事の内容

  • 制御とは何か――目標値・入力・出力・外乱の関係
  • 開ループ制御の仕組みと限界
  • 閉ループ制御(フィードバック制御)の原理と構成要素
  • 閉ループ伝達関数の導出(各ステップの日本語ガイド付き)
  • フィードバックの利点(外乱抑制・感度低減・不安定系の安定化)
  • 温度制御を題材にした具体例と Python シミュレーション

前提知識

この記事を読む前に、以下の記事を読んでおくと理解が深まります。

制御とは何か

日常に潜む「制御」

私たちの身の回りには、「ある量を望ましい状態に保つ」場面があふれています。エアコンで部屋の温度を25°Cに保つこと、自動車で速度を60 km/hに維持すること、ドローンをホバリングさせること。これらはすべて「制御」の具体例です。

では、制御とは何でしょうか。一言でいえば、「ある物理量(出力)を、望ましい状態(目標値)に近づけるために、システムへの入力を適切に操作すること」です。

制御を構成する4つの要素

制御を考えるとき、登場人物は大きく4つです。

  1. 目標値(reference / setpoint): 「こうなってほしい」という出力の理想値です。エアコンであれば設定温度、クルーズコントロールであれば設定速度がこれにあたります。
  2. 入力(input / manipulated variable): システムに与える操作量です。エアコンであればコンプレッサーの出力、自動車であればエンジンのスロットル開度です。
  3. 出力(output / controlled variable): 制御したい物理量そのものです。部屋の温度、車の速度、ロケットの姿勢角などです。
  4. 外乱(disturbance): 制御者の意図しない、システムに影響を与える要因です。エアコンであれば窓から入る外気、自動車であれば坂道や風です。

制御の目的は、外乱が存在する状況でも、出力を目標値に一致させることです。この目的を達成するためのアプローチとして、大きく2つの戦略があります。1つは出力を見ないで入力を決める「開ループ制御」、もう1つは出力を見て入力を調整する「閉ループ制御」です。まずは、よりシンプルな開ループ制御から見ていきましょう。

開ループ制御

「出力を見ない」制御

開ループ制御(Open-loop control)は、出力がどうなっているかを確認せず、あらかじめ計算や経験で決めた入力をそのまま与える制御方式です。

イメージとしては、「レシピ通りに作る」料理に似ています。「180°Cで15分焼く」とレシピに書いてあれば、焼き加減を見ずにその通りにする。レシピが正確で、オーブンの温度が安定していれば問題ありません。しかし、オーブンの温度にクセがあったり、材料の大きさが違ったりすれば、焦げたり生焼けになったりします。

信号の流れを数式で書くと、次のようになります。

$$ R(s) \xrightarrow{C(s)} U(s) \xrightarrow{G(s)} Y(s) $$

ここで $R(s)$ は目標値、$C(s)$ はコントローラの伝達関数、$G(s)$ はプラント(制御対象)の伝達関数、$U(s)$ は入力、$Y(s)$ は出力です。出力 $Y(s)$ から目標値 $R(s)$ への経路が存在しないことが、開ループの特徴です。

身近な開ループ制御の例

開ループ制御は、実は私たちの生活の中にもたくさんあります。

  • トースター: 「3分焼く」とタイマーを設定しますが、パンの焼き色をセンサーで検知しているわけではありません。パンの厚さや水分量が変われば、同じ3分でも焼き上がりは異なります。
  • 洗濯機の手動タイマー: 古い洗濯機では「30分回す」と設定しますが、衣類がきれいになったかどうかは確認しません。汚れがひどい場合、30分では不十分かもしれません。
  • 信号機: 時間ベースで赤・青を切り替えます。交通量が多くても少なくても、同じタイミングで切り替わります(交通感応式信号機を除く)。

開ループ制御の問題点

上の例からわかるように、開ループ制御には根本的な弱点があります。

外乱に対して脆弱です。トースターの例では、パンの種類(外乱)が変わるだけで結果が大きく変わります。制御系は外乱の存在を知ることすらできないため、修正のしようがありません。

モデルの不確実性に弱いという問題もあります。開ループ制御がうまく機能するには、プラントの数学モデル $G(s)$ が正確にわかっている必要があります。しかし現実のシステムでは、経年劣化や環境変化によってパラメータが変動します。モデルが不正確になった瞬間、制御性能は劣化します。

初期条件のずれを補正できないことも致命的です。想定と異なる初期状態からスタートしても、開ループ制御はそのずれに気づかず、当初の計画通りの入力を与え続けます。

これらの問題を解決するには、「出力を見て、目標値とのずれに応じて入力を修正する」仕組みが必要です。それが、次に解説するフィードバック制御です。

閉ループ制御(フィードバック制御)

「出力を見て修正する」制御

閉ループ制御(Closed-loop control)、すなわちフィードバック制御は、出力を測定し、目標値との偏差(error)に基づいて入力を動的に調整する制御方式です。

冒頭のシャワーの例を思い出してください。あなたは手でお湯の温度(出力)を感じ、「ちょうどよい温度」(目標値)との差を判断し、蛇口(入力)を調節します。この一連の流れ――「測定 → 比較 → 修正」――がフィードバック制御の本質です。

偏差は、目標値から出力を引いた値として定義されます。

$$ e(t) = r(t) – y(t) $$

ここで $r(t)$ は目標値、$y(t)$ は出力、$e(t)$ は偏差です。偏差が正であれば「出力が目標値に足りない」、偏差が負であれば「出力が目標値を超えている」ことを意味します。コントローラは、この偏差を小さくする方向に入力を生成します。

フィードバック制御系の構成要素

フィードバック制御系を構成する要素を、1つずつ確認しましょう。

プラント(Plant) は、制御対象のシステムです。部屋の温度を制御するならば部屋そのもの(ヒーターで温められ、外気で冷やされる系)がプラントです。数学的には伝達関数 $G(s)$ で記述します。

コントローラ(Controller) は、偏差 $e(t)$ を受け取り、プラントへの入力 $u(t)$ を生成する装置です。最もシンプルなコントローラは「偏差に比例した入力を出す」比例制御 $u(t) = K_p e(t)$ ですが、実用的には PID制御 がよく使われます。コントローラの伝達関数を $C(s)$ と書きます。

センサ(Sensor) は、出力を測定してフィードバック経路に戻す装置です。温度センサ、速度センサ、加速度センサなどが代表例です。センサの伝達関数を $H(s)$ と書きます。理想的なセンサであれば $H(s) = 1$ です。

比較器(Comparator / Summing junction) は、目標値 $r(t)$ とセンサ出力 $H(s)Y(s)$ の差を計算し、偏差 $e(t)$ を生成するノードです。ブロック線図上では、加え合わせ点として丸印で表されます。

ブロック線図で見る信号の流れ

フィードバック制御系の信号の流れを、ブロック線図として整理しましょう。

$$ R(s) \xrightarrow{+} E(s) \xrightarrow{C(s)} U(s) \xrightarrow{G(s)} Y(s) $$

$$ \qquad\qquad\qquad\qquad\qquad Y(s) \xrightarrow{H(s)} \text{フィードバック} \xrightarrow{-} $$

目標値 $R(s)$ とフィードバック信号 $H(s)Y(s)$ の差が偏差 $E(s)$ となり、コントローラ $C(s)$ を経て入力 $U(s)$ が生成され、プラント $G(s)$ を通って出力 $Y(s)$ が得られます。出力 $Y(s)$ はセンサ $H(s)$ を通って比較器に戻り、ループが閉じます。この「ループが閉じている」ことが「閉ループ」の名前の由来です。

ここで重要なのは、フィードバック信号は負の符号で加え合わされるという点です。これを負帰還(negative feedback)と呼びます。偏差 $e = r – Hy$ は「目標値に対してどれだけ足りないか」を表し、コントローラはこの不足分を埋める方向に入力を出します。もし正の符号で帰還すると(正帰還)、出力が増えるほどさらに出力が増えるという不安定な状態に陥ることが多くなります。

それでは、このブロック線図から閉ループ伝達関数を数学的に導出してみましょう。

閉ループ伝達関数の導出

導出のゴール

ここでの目標は、目標値 $R(s)$ から出力 $Y(s)$ までの伝達関数、すなわち閉ループ伝達関数 $T(s) = Y(s)/R(s)$ を求めることです。この伝達関数がわかれば、閉ループ系のステップ応答周波数特性を解析できるようになります。

ステップ1: 偏差の定義

まず、比較器における偏差 $E(s)$ を式で表します。目標値 $R(s)$ からセンサ出力 $H(s)Y(s)$ を引いたものが偏差です。

$$ E(s) = R(s) – H(s)Y(s) $$

これは「目標値に対して、現在の出力がどれだけずれているか」を $s$ 領域で表した式です。

ステップ2: 出力の表現

次に、出力 $Y(s)$ を偏差 $E(s)$ で表します。偏差 $E(s)$ はコントローラ $C(s)$ を通って入力 $U(s) = C(s)E(s)$ となり、プラント $G(s)$ を通って出力 $Y(s) = G(s)U(s)$ が得られますので、

$$ Y(s) = G(s) \cdot C(s) \cdot E(s) $$

です。

ステップ3: 偏差を消去する

ステップ1の偏差の式をステップ2に代入して、$E(s)$ を消去します。

$$ Y(s) = G(s)C(s)\bigl[R(s) – H(s)Y(s)\bigr] $$

右辺を展開すると、

$$ Y(s) = G(s)C(s)R(s) – G(s)C(s)H(s)Y(s) $$

$Y(s)$ が両辺に現れているので、$Y(s)$ を含む項を左辺に集めます。

$$ Y(s) + G(s)C(s)H(s)Y(s) = G(s)C(s)R(s) $$

左辺で $Y(s)$ をくくり出すと、

$$ Y(s)\bigl[1 + G(s)C(s)H(s)\bigr] = G(s)C(s)R(s) $$

ステップ4: 閉ループ伝達関数を得る

両辺を $\bigl[1 + G(s)C(s)H(s)\bigr]$ で割ると、閉ループ伝達関数が得られます。

$$ \begin{equation} T(s) = \frac{Y(s)}{R(s)} = \frac{C(s)G(s)}{1 + C(s)G(s)H(s)} \end{equation} $$

これがフィードバック制御系の最も基本的かつ重要な公式です。

単位フィードバックの場合

センサが理想的で $H(s) = 1$ のとき、これを単位フィードバック系と呼びます。この場合、閉ループ伝達関数はさらにシンプルになります。

$$ T(s) = \frac{C(s)G(s)}{1 + C(s)G(s)} $$

分母に現れる $1 + C(s)G(s)$ は特性多項式と呼ばれ、閉ループ系の安定性を決定づける極めて重要な式です。この特性多項式の根(特性方程式 $1 + C(s)G(s) = 0$ の解)が閉ループ系の極となり、すべての極が $s$ 平面の左半面にあれば系は安定です。安定性の詳細な判別には ラウス・フルヴィッツの安定判別法ゲイン余裕・位相余裕 を参照してください。

開ループ伝達関数との比較

閉ループ伝達関数の構造を、開ループの場合と比較してみましょう。

  • 開ループ: $Y(s) = C(s)G(s)R(s)$(入力 $\to$ 出力が一方通行)
  • 閉ループ: $Y(s) = \dfrac{C(s)G(s)}{1 + C(s)G(s)H(s)} R(s)$

閉ループでは分母に $1 + C(s)G(s)H(s)$ という項が加わります。この分母こそが、フィードバック制御の「修正力」の源です。開ループゲイン $L(s) = C(s)G(s)H(s)$ が大きいとき($|L(s)| \gg 1$)、閉ループ伝達関数は

$$ T(s) \approx \frac{C(s)G(s)}{C(s)G(s)H(s)} = \frac{1}{H(s)} $$

となり、プラント $G(s)$ やコントローラ $C(s)$ の特性に依存せず、センサ $H(s)$ のみで出力が決まります。これはフィードバック制御の本質的な強みです。プラントのパラメータが変動しても、十分なゲインがあれば出力は安定するのです。

ここまでで閉ループ伝達関数の導出が完了しました。では、この伝達関数が実際にどのような利点をもたらすのか、具体的なシナリオを通じて確認していきましょう。

フィードバックの利点

フィードバック制御が開ループ制御に比べて優れている点を、3つの観点から詳しく見ていきます。

利点1: 外乱の抑制

具体的シナリオ

エアコンで部屋の温度を25°Cに保っているとき、誰かが窓を開けたとしましょう。冷たい外気が流入して部屋の温度が下がり始めます。この「窓を開ける」という行為が外乱です。

開ループ制御であれば、エアコンは窓が開いたことに気づかず、同じ出力で運転し続けるため、部屋の温度はどんどん下がります。一方、フィードバック制御であれば、温度センサが「温度が下がった」ことを検知し、偏差が大きくなることでエアコンの出力が自動的に増加します。

数式による確認

外乱 $D(s)$ がプラントの入力側に加わる場合を考えます。このとき出力は次のようになります。

$$ Y(s) = \frac{C(s)G(s)}{1 + C(s)G(s)} R(s) + \frac{G(s)}{1 + C(s)G(s)} D(s) $$

右辺の第1項は目標値に対する応答、第2項は外乱に対する応答です。外乱から出力への伝達関数に注目しましょう。

$$ \frac{Y(s)}{D(s)}\bigg|_{R=0} = \frac{G(s)}{1 + C(s)G(s)} $$

開ループでは外乱から出力への伝達関数は $G(s)$ そのものですが、フィードバックにより $1/(1 + C(s)G(s))$ 倍に縮小されます。

開ループゲインが十分大きい周波数帯域、すなわち $|C(s)G(s)| \gg 1$ の条件が成り立つとき、外乱応答はさらにわかりやすくなります。

$$ \frac{G(s)}{1 + C(s)G(s)} \approx \frac{G(s)}{C(s)G(s)} = \frac{1}{C(s)} $$

つまり、外乱の影響はコントローラのゲインの逆数程度にまで抑えられます。コントローラのゲインを大きくすれば、外乱の影響をいくらでも小さくできるのです(ただし、ゲインを上げすぎると安定性の問題が生じます。これについては後述します)。

利点2: パラメータ変動に対する感度低減

具体的シナリオ

工場の生産ラインで、モーターを使ってベルトコンベアの速度を制御しているとします。モーターは使い続けるうちに摩擦が増えたり、温度によって特性が変わったりします。つまり、プラントの伝達関数 $G(s)$ が時間とともに変動するのです。開ループ制御では、この変動がそのまま出力のずれになります。

フィードバック制御では、プラントの特性が変動しても、出力を常に測定して修正をかけるため、性能の劣化が大幅に抑えられます。

数式による確認

プラントの伝達関数が $G(s)$ から $G(s) + \Delta G(s)$ に変化したとき、閉ループ伝達関数の変化を定量的に評価しましょう。

閉ループ伝達関数 $T(s) = \dfrac{CG}{1 + CG}$(単位フィードバック、$H = 1$)に対する感度関数 $S(s)$ は次のように定義されます。

$$ S(s) = \frac{\partial T / T}{\partial G / G} $$

これは「プラントが $1\%$ 変化したとき、閉ループ伝達関数が何 $\%$ 変化するか」を表す指標です。計算すると、

$$ S(s) = \frac{1}{1 + C(s)G(s)} $$

が得られます。以下に導出の過程を示します。

まず、$T(s) = \dfrac{CG}{1+CG}$ を $G$ で偏微分します。分数の微分公式(商の微分)を適用すると、

$$ \frac{\partial T}{\partial G} = \frac{C(1+CG) – CG \cdot C}{(1+CG)^2} = \frac{C}{(1+CG)^2} $$

次に、感度関数の定義に代入します。$\dfrac{G}{T} = \dfrac{G(1+CG)}{CG} = \dfrac{1+CG}{C}$ を利用すると、

$$ S = \frac{\partial T}{\partial G} \cdot \frac{G}{T} = \frac{C}{(1+CG)^2} \cdot \frac{1+CG}{C} = \frac{1}{1+CG} $$

開ループゲイン $|CG| \gg 1$ の帯域では $|S| \approx 0$ となり、プラントの変動が閉ループ特性にほとんど影響しません。つまり、フィードバック制御はプラントのモデル誤差に対してロバスト(頑健)です。

利点3: 不安定系の安定化

具体的シナリオ

倒立振子(逆さまに立てた棒)を台車の上でバランスさせることを考えてみましょう。棒を手で支えずに立たせようとすると、少しでも傾けばそのまま倒れてしまいます。これは、倒立振子が不安定なプラントだからです。

しかし、棒の傾きをセンサで測定し、傾いた方向に台車を素早く動かせば、棒を立たせ続けることができます。これがフィードバックによる不安定系の安定化です。セグウェイや二足歩行ロボットの姿勢制御も、同じ原理に基づいています。

数式による確認

不安定なプラント、たとえば $G(s) = \dfrac{1}{s – 1}$ を考えます。このプラントは極 $s = 1$ が右半面にあるため、開ループでは不安定です。すなわち、何もしなければ出力は指数関数的に発散します。

ここに比例コントローラ $C(s) = K_p$ を適用し、単位フィードバック系を構成すると、閉ループ伝達関数は次のようになります。

$$ T(s) = \frac{K_p \cdot \frac{1}{s-1}}{1 + K_p \cdot \frac{1}{s-1}} = \frac{K_p}{s – 1 + K_p} $$

閉ループの極は $s = 1 – K_p$ です。$K_p > 1$ とすれば極が左半面に移り、系は安定化します。開ループでは不可能だった安定化が、フィードバックの力で実現できるのです。

ただし、フィードバックが万能というわけではありません。ゲインを上げすぎると、特に高次のプラントでは位相遅れの影響で発振する可能性があります。安定性の限界はゲイン余裕と位相余裕で定量的に評価できます。

ここまでで、フィードバック制御の3つの大きな利点を確認しました。次に、これらの理論を具体的な温度制御の問題に当てはめて、数値的に理解を深めましょう。

具体例: 温度制御

問題設定

部屋の温度をヒーターで制御する状況を考えます。目標温度は $r = 25$°C、初期温度(外気温)は $T_{\text{outside}} = 10$°C です。

なぜ1次遅れ系でモデル化するのか

部屋の温度変化を物理的に考えてみましょう。ヒーターを点けると、まずヒーターの発熱体が温まり、そこから放射・対流によって部屋の空気が温まります。しかし同時に、壁や窓を通じて外気に熱が逃げていきます。この「流入する熱」と「流出する熱」のバランスで部屋の温度が決まります。

部屋の空気を一様な温度 $T(t)$ として、エネルギー保存則を立てると、

$$ C_{\text{th}} \frac{dT}{dt} = Q_{\text{in}}(t) – \frac{T(t) – T_{\text{outside}}}{R_{\text{th}}} $$

ここで $C_{\text{th}}$ は部屋の熱容量、$R_{\text{th}}$ は壁の熱抵抗、$Q_{\text{in}}(t)$ はヒーターからの入熱です。この微分方程式は1次遅れ系の形をしています。時定数は $\tau = C_{\text{th}} R_{\text{th}}$ で、これは「ヒーターを点けてから部屋が温まるまでにかかる時間のスケール」を表します。

ラプラス変換して整理すると、入力 $U(s)$(ヒーター出力)から出力 $Y(s)$(温度偏差 $T – T_{\text{outside}}$)への伝達関数は次のようになります。

$$ G(s) = \frac{K}{\tau s + 1} = \frac{K}{s + a} $$

ここで $a = 1/\tau$ です。本記事では $K = 1$, $a = 0.1$(時定数 $\tau = 10$ 秒)と設定します。

比例制御の閉ループ伝達関数

コントローラとして最もシンプルな比例制御 $C(s) = K_p$ を用い、単位フィードバック系を構成します。

閉ループ伝達関数を導出しましょう。一般公式 $T(s) = \dfrac{CG}{1+CG}$ に $C(s) = K_p$, $G(s) = \dfrac{K}{s+a}$ を代入します。

$$ T(s) = \frac{K_p \cdot \frac{K}{s + a}}{1 + K_p \cdot \frac{K}{s + a}} $$

分母・分子に $(s + a)$ を掛けて整理すると、

$$ T(s) = \frac{K_p K}{s + a + K_p K} $$

$K = 1$, $a = 0.1$ を代入すると、

$$ T(s) = \frac{K_p}{s + 0.1 + K_p} $$

この閉ループ系もまた1次遅れ系です。ただし、閉ループの時定数は $\tau_{\text{cl}} = \dfrac{1}{a + K_p K}$ であり、$K_p$ を大きくするほど時定数が短く(応答が速く)なります。

定常偏差の計算

目標値がステップ入力 $R(s) = 1/s$ のとき、最終値の定理を適用して定常値を求めましょう。

$$ y(\infty) = \lim_{s \to 0} s \cdot T(s) \cdot \frac{1}{s} = \lim_{s \to 0} \frac{K_p K}{s + a + K_p K} = \frac{K_p K}{a + K_p K} $$

$K = 1$, $a = 0.1$ のとき、

$$ y(\infty) = \frac{K_p}{0.1 + K_p} $$

定常偏差は $e_{\text{ss}} = 1 – y(\infty) = \dfrac{0.1}{0.1 + K_p}$ です。

$K_p = 0.5$ なら定常偏差は $\dfrac{0.1}{0.6} \approx 16.7\%$、$K_p = 5$ なら $\dfrac{0.1}{5.1} \approx 2.0\%$ です。$K_p$ を大きくすれば定常偏差は小さくなりますが、ゼロにはなりません。定常偏差を完全にゼロにするには、コントローラに積分動作を加える(PI制御やPID制御にする)必要があります。この点についてはPID制御の記事で詳しく解説しています。

ここまでの理論を、Python シミュレーションで確認してみましょう。

Pythonシミュレーション

伝達関数ベースの応答比較

まず、scipy.signal を使って、開ループと閉ループのステップ応答を比較します。このシミュレーションでは、比例ゲイン $K_p$ を変えたときに閉ループ応答がどう変化するか、また定常偏差がゲインにどう依存するかを視覚的に確認します。

import numpy as np
import matplotlib.pyplot as plt
from scipy.signal import lti, step

# プラントパラメータ: G(s) = K / (s + a)
a = 0.1   # 1/時定数
K = 1.0   # ゲイン

fig, axes = plt.subplots(2, 2, figsize=(14, 10))

# --- (1) 開ループのステップ応答 ---
num_open = [K]
den_open = [1, a]
sys_open = lti(num_open, den_open)
t_open, y_open = step(sys_open, T=np.linspace(0, 100, 1000))

axes[0, 0].plot(t_open, y_open, 'b-', linewidth=2, label='開ループ')
axes[0, 0].axhline(y=1, color='r', linestyle='--', alpha=0.5, label='目標値')
axes[0, 0].set_title('Open-loop step response', fontsize=14)
axes[0, 0].set_xlabel('Time [s]', fontsize=12)
axes[0, 0].set_ylabel('Output', fontsize=12)
axes[0, 0].legend(fontsize=11)
axes[0, 0].grid(True, alpha=0.3)

# --- (2) 閉ループのステップ応答(Kp を変化) ---
Kp_values = [0.5, 1.0, 2.0, 5.0]
for Kp in Kp_values:
    num_cl = [Kp * K]
    den_cl = [1, a + Kp * K]
    sys_cl = lti(num_cl, den_cl)
    t_cl, y_cl = step(sys_cl, T=np.linspace(0, 50, 1000))
    axes[0, 1].plot(t_cl, y_cl, linewidth=2, label=f'$K_p = {Kp}$')

axes[0, 1].axhline(y=1, color='r', linestyle='--', alpha=0.5, label='目標値')
axes[0, 1].set_title('Closed-loop step response (P control)', fontsize=14)
axes[0, 1].set_xlabel('Time [s]', fontsize=12)
axes[0, 1].set_ylabel('Output', fontsize=12)
axes[0, 1].legend(fontsize=10)
axes[0, 1].grid(True, alpha=0.3)

# --- (3) 定常偏差 vs Kp ---
Kp_range = np.linspace(0.1, 10, 100)
steady_state = Kp_range * K / (a + Kp_range * K)
error = 1 - steady_state

axes[1, 0].plot(Kp_range, error * 100, 'b-', linewidth=2)
axes[1, 0].set_xlabel('$K_p$', fontsize=12)
axes[1, 0].set_ylabel('Steady-state error [%]', fontsize=12)
axes[1, 0].set_title('Proportional gain vs steady-state error', fontsize=14)
axes[1, 0].grid(True, alpha=0.3)

# --- (4) 外乱応答の比較 ---
# 開ループ外乱応答: Y(s) = G(s) * D(s) → y(t) = (K/a)(1 - exp(-at))
# 閉ループ外乱応答: Y(s) = G(s)/(1+Kp*G(s)) * D(s)
t = np.linspace(0, 50, 1000)
y_dist_open = K / a * (1 - np.exp(-a * t))

Kp = 2.0
y_dist_closed = K / (a + Kp * K) * (1 - np.exp(-(a + Kp * K) * t))

axes[1, 1].plot(t, y_dist_open, 'b-', linewidth=2, label='Open-loop')
axes[1, 1].plot(t, y_dist_closed, 'r-', linewidth=2,
                label=f'Closed-loop ($K_p={Kp}$)')
axes[1, 1].set_xlabel('Time [s]', fontsize=12)
axes[1, 1].set_ylabel('Output deviation by disturbance', fontsize=12)
axes[1, 1].set_title('Disturbance response comparison', fontsize=14)
axes[1, 1].legend(fontsize=11)
axes[1, 1].grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig("feedback_control_analysis.png", dpi=150, bbox_inches="tight")
plt.show()

上のコードを実行すると、4つのグラフが得られます。それぞれの結果を読み取りましょう。

左上(開ループのステップ応答): 出力は $K/a = 10$ の定常値に向かって指数関数的に上昇します。目標値 $1$ を大きく超えてしまいます。これは開ループの伝達関数 $G(s)$ の直流ゲインが $K/a = 10$ であるためです。開ループ制御では、ゲインが1でなければ目標値に一致させるために入力を手動で調整する必要があります。

右上(閉ループのステップ応答): $K_p$ が大きいほど応答が速くなり、定常値も目標値 $1$ に近づきます。しかし、どの $K_p$ でも定常値は $1$ に完全には到達していません。これが比例制御の宿命である定常偏差です。$K_p = 5$ でも約 $2\%$ の偏差が残ります。

左下(定常偏差 vs $K_p$): $K_p$ が小さいうちは定常偏差が急速に減少しますが、$K_p$ を大きくするにつれて改善の幅が鈍化していきます。曲線は $e_{\text{ss}} = a/(a + K_p)$ という双曲線的な減衰を示しており、$K_p \to \infty$ でようやく $e_{\text{ss}} \to 0$ です。実用的には有限のゲインで運用するため、定常偏差を完全に消すには積分制御が必要であることが、このグラフから直感的に理解できます。

右下(外乱応答の比較): 開ループでは外乱がそのまま出力に影響し、定常偏差 $K/a = 10$ まで出力が変動します。一方、閉ループ($K_p = 2$)では定常偏差が $K/(a + K_p K) \approx 0.48$ にまで抑えられており、約20分の1に縮小されています。フィードバック制御の外乱抑制効果が明確に確認できます。

温度制御の時間領域シミュレーション

次に、より現実的なシナリオとして、時間領域での温度制御シミュレーションを行います。ここでは、シミュレーション途中($t = 100$ s)で外乱(窓を開ける)が発生する状況を再現します。開ループ制御とフィードバック制御で、外乱への応答がどのように異なるかを比較しましょう。

import numpy as np
import matplotlib.pyplot as plt

# シミュレーション設定
dt = 0.1          # タイムステップ [s]
T_total = 200     # 全シミュレーション時間 [s]
t = np.arange(0, T_total, dt)

# 物理パラメータ
tau = 20.0         # 時定数 [s](部屋の熱容量 × 壁の熱抵抗)
T_target = 25.0    # 目標温度 [°C]
T_outside = 10.0   # 外気温 [°C]
Kp = 2.0           # 比例ゲイン

# 外乱: t=100s で窓を開ける(外気の影響で温度が下がる)
disturbance = np.zeros_like(t)
disturbance[t >= 100] = -3.0  # 3°C 相当の冷却外乱

# --- 開ループ制御 ---
T_open = np.zeros_like(t)
T_open[0] = T_outside
# 目標温度に到達するような一定入力を計算
u_open = (T_target - T_outside) / 1.0

for i in range(1, len(t)):
    dT = (-T_open[i-1] + T_outside + u_open + disturbance[i]) / tau * dt
    T_open[i] = T_open[i-1] + dT

# --- 閉ループ制御(比例制御) ---
T_closed = np.zeros_like(t)
T_closed[0] = T_outside

for i in range(1, len(t)):
    error = T_target - T_closed[i-1]
    u = Kp * error
    dT = (-T_closed[i-1] + T_outside + u + disturbance[i]) / tau * dt
    T_closed[i] = T_closed[i-1] + dT

# 可視化
fig, ax = plt.subplots(figsize=(12, 6))

ax.plot(t, T_open, 'b-', linewidth=2, label='Open-loop', alpha=0.8)
ax.plot(t, T_closed, 'r-', linewidth=2, label='Feedback control (P)')
ax.axhline(y=T_target, color='k', linestyle='--', alpha=0.5,
           label=f'Target: {T_target}°C')
ax.axvline(x=100, color='gray', linestyle=':', alpha=0.5)
ax.annotate('Disturbance\n(window opened)',
            xy=(100, 12), fontsize=11, ha='center')

ax.set_xlabel('Time [s]', fontsize=12)
ax.set_ylabel('Temperature [°C]', fontsize=12)
ax.set_title('Temperature control: Open-loop vs Feedback', fontsize=14)
ax.legend(fontsize=11)
ax.grid(True, alpha=0.3)
ax.set_ylim(8, 30)

plt.tight_layout()
plt.savefig("temperature_control_comparison.png", dpi=150,
            bbox_inches="tight")
plt.show()

このグラフから、開ループ制御とフィードバック制御の根本的な違いが一目でわかります。

開ループ制御(青線) は、外乱がない前半($t < 100$ s)では目標温度 25°C 付近まで上昇しますが、$t = 100$ s で窓を開ける外乱が発生すると、温度が大きく低下してそのまま回復しません。コントローラは外乱の存在を知ることができないため、同じ入力を出し続けるだけです。

フィードバック制御(赤線) は、外乱発生後に一時的に温度が低下するものの、偏差が大きくなることでコントローラの出力が自動的に増加し、温度を引き戻します。完全には 25°C に戻りきらない(定常偏差が残る)のは比例制御の限界ですが、開ループと比較して外乱の影響が大幅に軽減されていることが明確です。

python-control ライブラリを使った解析

ここまでは scipy.signal と手計算ベースでシミュレーションを行いました。制御工学の専門的な解析には、python-control ライブラリを使うとさらに効率的です。以下では、開ループ・閉ループの伝達関数を定義し、ステップ応答とボード線図を一度に確認します。

import numpy as np
import matplotlib.pyplot as plt
import control as ct

# プラントとコントローラの定義
G = ct.tf([1], [1, 0.1])      # G(s) = 1/(s + 0.1)
Kp = 2.0
C = ct.tf([Kp], [1])          # C(s) = Kp

# 開ループ伝達関数
L = C * G                      # L(s) = Kp / (s + 0.1)

# 閉ループ伝達関数
T_cl = ct.feedback(L, 1)       # T(s) = CG / (1 + CG)

# 外乱から出力への伝達関数
T_dist = ct.feedback(G, C)     # G / (1 + CG)

print("開ループ伝達関数 L(s):")
print(L)
print("\n閉ループ伝達関数 T(s):")
print(T_cl)
print("\n外乱伝達関数:")
print(T_dist)
import numpy as np
import matplotlib.pyplot as plt
import control as ct

# プラントとコントローラ
G = ct.tf([1], [1, 0.1])
Kp = 2.0
C = ct.tf([Kp], [1])
L = C * G
T_cl = ct.feedback(L, 1)
T_dist = ct.feedback(G, C)

fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# --- ステップ応答の比較 ---
t = np.linspace(0, 50, 500)

t_ol, y_ol = ct.step_response(G, t)
t_cl_resp, y_cl_resp = ct.step_response(T_cl, t)
t_dist, y_dist = ct.step_response(T_dist, t)

axes[0].plot(t_ol, y_ol, 'b-', linewidth=2, label='Open-loop $G(s)$')
axes[0].plot(t_cl_resp, y_cl_resp, 'r-', linewidth=2,
             label='Closed-loop $T(s)$')
axes[0].plot(t_dist, y_dist, 'g--', linewidth=2,
             label='Disturbance $G/(1+CG)$')
axes[0].axhline(y=1, color='k', linestyle=':', alpha=0.3)
axes[0].set_xlabel('Time [s]', fontsize=12)
axes[0].set_ylabel('Output', fontsize=12)
axes[0].set_title('Step response comparison', fontsize=14)
axes[0].legend(fontsize=10)
axes[0].grid(True, alpha=0.3)

# --- 感度関数のボード線図 ---
S = 1 / (1 + L)   # 感度関数
T_comp = L / (1 + L)  # 相補感度関数

omega = np.logspace(-3, 2, 500)
mag_S, phase_S, omega_S = ct.frequency_response(S, omega)
mag_T, phase_T, omega_T = ct.frequency_response(T_comp, omega)

axes[1].semilogx(omega_S, 20 * np.log10(np.abs(mag_S)),
                 'b-', linewidth=2, label='$|S(j\\omega)|$')
axes[1].semilogx(omega_T, 20 * np.log10(np.abs(mag_T)),
                 'r-', linewidth=2, label='$|T(j\\omega)|$')
axes[1].axhline(y=0, color='k', linestyle=':', alpha=0.3)
axes[1].set_xlabel('Frequency [rad/s]', fontsize=12)
axes[1].set_ylabel('Magnitude [dB]', fontsize=12)
axes[1].set_title('Sensitivity and complementary sensitivity', fontsize=14)
axes[1].legend(fontsize=11)
axes[1].grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig("control_analysis_detail.png", dpi=150, bbox_inches="tight")
plt.show()

このコードでは、左のグラフでステップ応答、右のグラフで感度関数と相補感度関数のボード線図を表示しています。

左のグラフ(ステップ応答比較) では、3つの応答を重ねて表示しています。開ループ応答(青)は直流ゲイン $K/a = 10$ に向かってゆっくりと上昇し、目標値 $1$ を大きく超えます。閉ループ応答(赤)は $K_p K / (a + K_p K) \approx 0.95$ に収束し、目標値 $1$ にかなり近い値に落ち着いています。外乱伝達関数の応答(緑の破線)は $K/(a + K_p K) \approx 0.48$ に収束しており、開ループの $K/a = 10$ と比較して20分の1以下に抑制されていることが確認できます。

右のグラフ(感度関数・相補感度関数) では、感度関数 $|S(j\omega)|$ が低周波帯で小さく、高周波帯で $0$ dB に近づく様子が見えます。これは「低周波の外乱やモデル誤差はフィードバックで効果的に抑制されるが、高周波ではフィードバックの効果が薄れる」ことを示しています。一方、相補感度関数 $|T(j\omega)|$ は低周波帯で $0$ dB に近く(目標値追従が良い)、高周波帯で減衰しています。$|S| + |T| = 1$ という関係(ただし振幅ではなく伝達関数として $S + T = 1$)が成り立つため、感度の改善と追従性能の間にはトレードオフがあります。

開ループ vs 閉ループ: パラメータ変動の影響

最後に、プラントのパラメータが変動したときの影響を比較するシミュレーションを行います。これにより、フィードバック制御のロバスト性を視覚的に確認できます。

import numpy as np
import matplotlib.pyplot as plt
from scipy.signal import lti, step

# プラントのパラメータ変動を模擬
# 基準: G(s) = 1/(s + 0.1)
# 変動: a が 0.05 ~ 0.2 に変動する(時定数が変わる)

a_values = [0.05, 0.1, 0.15, 0.2]
K = 1.0
Kp = 2.0
t = np.linspace(0, 60, 1000)

fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# --- 開ループ ---
for a in a_values:
    sys_open = lti([K], [1, a])
    t_out, y_out = step(sys_open, T=t)
    axes[0].plot(t_out, y_out, linewidth=2, label=f'$a = {a}$')

axes[0].axhline(y=1, color='k', linestyle='--', alpha=0.3, label='Target')
axes[0].set_title('Open-loop: parameter variation', fontsize=14)
axes[0].set_xlabel('Time [s]', fontsize=12)
axes[0].set_ylabel('Output', fontsize=12)
axes[0].legend(fontsize=10)
axes[0].grid(True, alpha=0.3)
axes[0].set_ylim(0, 22)

# --- 閉ループ ---
for a in a_values:
    num_cl = [Kp * K]
    den_cl = [1, a + Kp * K]
    sys_cl = lti(num_cl, den_cl)
    t_out, y_out = step(sys_cl, T=t)
    axes[1].plot(t_out, y_out, linewidth=2, label=f'$a = {a}$')

axes[1].axhline(y=1, color='k', linestyle='--', alpha=0.3, label='Target')
axes[1].set_title('Closed-loop: parameter variation', fontsize=14)
axes[1].set_xlabel('Time [s]', fontsize=12)
axes[1].set_ylabel('Output', fontsize=12)
axes[1].legend(fontsize=10)
axes[1].grid(True, alpha=0.3)
axes[1].set_ylim(0, 1.2)

plt.tight_layout()
plt.savefig("parameter_variation.png", dpi=150, bbox_inches="tight")
plt.show()

この結果は、フィードバック制御のロバスト性を劇的に示しています。

左のグラフ(開ループ) では、パラメータ $a$ が $0.05$ から $0.2$ に変動すると、定常値が $K/a = 20$ から $K/a = 5$ まで4倍も変化しています。つまり、プラントの特性が少し変わるだけで、出力がまったく異なる値に収束するのです。応答速度も大きく変わっており、$a = 0.05$(時定数 20 秒)ではなかなか収束しません。

右のグラフ(閉ループ) では、同じパラメータ変動に対して、定常値が $K_p K/(a + K_p K)$ の範囲、すなわち約 $0.91$($a = 0.2$)から約 $0.98$($a = 0.05$)の間に収まっています。変動幅はわずか7%程度です。応答速度にも多少の違いはありますが、すべてのケースで概ね似た振る舞いを示しています。これが感度低減の効果であり、フィードバック制御がプラントの不確実性に対してロバストである理由です。

フィードバック制御の限界と次のステップ

ここまでフィードバック制御の利点を強調してきましたが、フィードバック制御にも限界があることを知っておくことは重要です。

比例制御の限界: 定常偏差

本記事で扱った比例制御 $C(s) = K_p$ では、1型以上の系(開ループ伝達関数に積分器 $1/s$ を含む系)でなければステップ入力に対して定常偏差が残ります。温度制御の例でも見た通り、$K_p$ を大きくしても定常偏差は完全にはゼロになりません。

この問題を解決するには、コントローラに積分項を追加して PI制御や PID制御 にする必要があります。積分項 $K_i/s$ が開ループ伝達関数に $1/s$ を追加するため、系の型が1つ上がり、ステップ入力に対する定常偏差がゼロになります。

安定性の問題

フィードバック制御は万能ではなく、設計を誤ると発振や不安定化を引き起こします。特に、ゲインを上げすぎると位相遅れの影響で系が不安定になることがあります。

2次遅れ系以上の高次プラントでは、ゲインと安定性のトレードオフが顕著になります。安定性を定量的に評価する手法として、ゲイン余裕と位相余裕ラウス・フルヴィッツの安定判別法 が用いられます。

感度と追従のトレードオフ

先ほどのボード線図で見た通り、感度関数 $S(s)$ と相補感度関数 $T(s)$ は $S(s) + T(s) = 1$ という関係を満たします。低周波帯で感度を下げる(外乱抑制を強化する)と、高周波帯での雑音増幅が大きくなり、逆もまた然りです。この制約は制御設計において常に意識する必要があり、ロバスト制御理論では $H_\infty$ 制御や $\mu$ 合成といった手法でこのトレードオフを体系的に扱います。

まとめ

本記事では、制御工学の最も基本的な概念であるフィードバック制御について、直感的な理解から数学的な定式化、そして Python シミュレーションによる検証まで一貫して解説しました。

  • 開ループ制御 は、出力を測定せずに入力を決める方式です。構造はシンプルですが、外乱やモデル誤差に対して脆弱です。
  • 閉ループ制御(フィードバック制御) は、出力を測定して目標値との偏差に基づき入力を動的に調整する方式です。外乱抑制・感度低減・不安定系の安定化という3つの大きな利点を持ちます。
  • 閉ループ伝達関数 $T(s) = \dfrac{CG}{1 + CGH}$ は、フィードバック制御系の特性を記述する最も基本的な式です。分母の $1 + CGH$ がフィードバックの「修正力」を生み出します。
  • 比例制御 は最もシンプルなコントローラですが、定常偏差が残るという限界があります。この限界を克服するには、積分動作を加えた PI 制御や PID 制御が必要です。
  • Python シミュレーションにより、フィードバック制御が外乱抑制やパラメータ変動に対するロバスト性を実現していることを視覚的に確認しました。

フィードバック制御は、制御工学のあらゆるトピックの基盤です。本記事の内容を踏まえて、次のステップとして以下の記事も参考にしてください。