伝達関数のブロック線図結合

制御システムは、複数のサブシステム(伝達関数)を組み合わせて構成されます。ブロック線図はその接続関係を視覚的に表す強力なツールであり、結合規則を用いれば複雑なシステムを1つの等価伝達関数に簡約化できます。

本記事では、直列結合・並列結合・フィードバック結合の公式を信号の流れから丁寧に導出し、ブロック線図の等価変換規則を体系的に整理します。

本記事の内容

  • 直列結合(cascade connection)の伝達関数
  • 並列結合(parallel connection)の伝達関数
  • フィードバック結合(feedback connection)の伝達関数
  • ブロック線図の等価変換規則
  • Pythonでの結合計算と検証

前提知識

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

ブロック線図の基本要素

ブロック線図は以下の3つの基本要素から構成されます。

  1. ブロック(block): 伝達関数 $G(s)$ を表す矩形。入力に $G(s)$ を乗じて出力を生成する
  2. 加え合わせ点(summing junction): 複数の信号を加算(または減算)する
  3. 引き出し点(branch point): 信号を分岐させる(信号のコピー)

これらの要素の組み合わせにより、任意の線形システムの接続を表現できます。

直列結合(cascade connection)

構成

2つの伝達関数 $G_1(s)$ と $G_2(s)$ が直列に接続されている場合を考えます。入力 $U(s)$ がまず $G_1(s)$ を通り、その出力が $G_2(s)$ の入力となります。

$$ U(s) \xrightarrow{} \boxed{G_1(s)} \xrightarrow{V(s)} \boxed{G_2(s)} \xrightarrow{} Y(s) $$

導出

中間信号 $V(s)$ を用いると、

$$ V(s) = G_1(s) \, U(s) $$

$$ Y(s) = G_2(s) \, V(s) $$

$V(s)$ を消去すると、

$$ Y(s) = G_2(s) \, G_1(s) \, U(s) $$

したがって、等価伝達関数は

$$ \boxed{G(s) = \frac{Y(s)}{U(s)} = G_1(s) \, G_2(s)} $$

一般化: $n$ 個の直列結合

$n$ 個の伝達関数 $G_1(s), G_2(s), \ldots, G_n(s)$ が直列に接続されている場合、

$$ G(s) = \prod_{k=1}^{n} G_k(s) = G_1(s) \, G_2(s) \cdots G_n(s) $$

伝達関数は有理関数(多項式の比)であるため、積は可換です。つまり、直列結合の順序を入れ替えても等価伝達関数は変わりません。

$$ G_1(s) \, G_2(s) = G_2(s) \, G_1(s) $$

ただし、これは数学的な等価性であり、実装上は中間信号の物理的意味が異なる場合があります。

並列結合(parallel connection)

構成

2つの伝達関数 $G_1(s)$ と $G_2(s)$ が並列に接続されている場合、同じ入力 $U(s)$ がそれぞれに入り、出力が加え合わせ点で合算されます。

導出

それぞれの経路の出力は

$$ Y_1(s) = G_1(s) \, U(s) $$

$$ Y_2(s) = G_2(s) \, U(s) $$

加え合わせ点での合算により

$$ Y(s) = Y_1(s) + Y_2(s) = G_1(s) \, U(s) + G_2(s) \, U(s) $$

$$ Y(s) = \left[G_1(s) + G_2(s)\right] U(s) $$

したがって、等価伝達関数は

$$ \boxed{G(s) = \frac{Y(s)}{U(s)} = G_1(s) + G_2(s)} $$

一般化: 符号付き並列結合

減算の場合(一方の信号が負のフィードバック的に接続される場合)、

$$ G(s) = G_1(s) – G_2(s) $$

$n$ 個の並列結合では

$$ G(s) = \sum_{k=1}^{n} \sigma_k \, G_k(s) $$

ここで $\sigma_k = \pm 1$ は各経路の符号です。

フィードバック結合(feedback connection)

構成

フィードバック結合は制御工学の最も基本的な構造です。前向き経路に $G(s)$、フィードバック経路に $H(s)$ が配置されます。

入力 $R(s)$(目標値)から偏差信号 $E(s)$(誤差)を生成し、前向き経路を通った出力 $Y(s)$ をフィードバック経路 $H(s)$ を通して入力側に戻します。

導出(負のフィードバック)

加え合わせ点では入力から帰還信号を引きます。

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

前向き経路より

$$ Y(s) = G(s) \, E(s) $$

$E(s)$ を代入すると

$$ Y(s) = G(s) \left[R(s) – H(s) \, Y(s)\right] $$

展開して $Y(s)$ について整理します。

$$ Y(s) = G(s) \, R(s) – G(s) \, H(s) \, Y(s) $$

$$ Y(s) + G(s) \, H(s) \, Y(s) = G(s) \, R(s) $$

$$ \left[1 + G(s) \, H(s)\right] Y(s) = G(s) \, R(s) $$

したがって、閉ループ伝達関数は

$$ \boxed{G_{\text{cl}}(s) = \frac{Y(s)}{R(s)} = \frac{G(s)}{1 + G(s) \, H(s)}} $$

正のフィードバックの場合

加え合わせ点で帰還信号を加える場合(正のフィードバック)、

$$ E(s) = R(s) + H(s) \, Y(s) $$

同様に導出すると

$$ G_{\text{cl}}(s) = \frac{G(s)}{1 – G(s) \, H(s)} $$

統一表記

負のフィードバックを $+$、正のフィードバックを $-$ として

$$ G_{\text{cl}}(s) = \frac{G(s)}{1 \pm G(s) \, H(s)} $$

ここで $+$ が負のフィードバック(安定化に用いる一般的な構成)、$-$ が正のフィードバックです。

単位フィードバック

$H(s) = 1$ の場合を単位フィードバック(unity feedback)と呼びます。

$$ G_{\text{cl}}(s) = \frac{G(s)}{1 + G(s)} $$

この場合、偏差信号は

$$ E(s) = R(s) – Y(s) = \frac{1}{1 + G(s)} R(s) $$

$\frac{1}{1 + G(s)}$ を感度関数(sensitivity function)$S(s)$ と呼びます。$G(s)$ が大きいほど $S(s) \to 0$ となり、偏差が小さくなります。

開ループ伝達関数

フィードバック結合において、$G(s) \, H(s)$ を開ループ伝達関数(open-loop transfer function)と呼びます。

$$ L(s) = G(s) \, H(s) $$

閉ループ伝達関数は開ループ伝達関数を用いて

$$ G_{\text{cl}}(s) = \frac{G(s)}{1 + L(s)} $$

と書けます。ボード線図やナイキスト線図で解析するのは、この開ループ伝達関数 $L(s)$ です。

ブロック線図の等価変換規則

複雑なブロック線図を簡約化するための等価変換規則を整理します。

加え合わせ点の移動

ブロックの前から後へ移動:

加え合わせ点をブロック $G(s)$ の前から後ろに移動する場合、移動する信号経路に $G(s)$ を挿入します。

$$ Y(s) = G(s) \, X_1(s) + X_2(s) \quad \Longleftrightarrow \quad Y(s) = G(s)\left[X_1(s) + \frac{X_2(s)}{G(s)}\right] $$

つまり、$X_2$ の経路に $1/G(s)$ が必要になります。

ブロックの後から前へ移動:

逆に後ろから前に移動する場合、移動する信号経路に $1/G(s)$ を挿入します。実質的に、移動する経路に $G(s)$ を掛けます。

引き出し点の移動

ブロックの前から後へ移動:

引き出し点をブロック $G(s)$ の前から後ろに移動する場合、分岐した信号経路に $1/G(s)$ を挿入します。

ブロックの後から前へ移動:

逆に後ろから前に移動する場合、分岐した信号経路に $G(s)$ を挿入します。

変換規則の一覧

操作 変換前 変換後に追加する要素
加え合わせ点をブロック後方へ 加え合わせ→ブロック $G$ 移動経路に $1/G$
加え合わせ点をブロック前方へ ブロック $G$ →加え合わせ 移動経路に $G$
引き出し点をブロック後方へ 引き出し→ブロック $G$ 分岐経路に $1/G$
引き出し点をブロック前方へ ブロック $G$ →引き出し 分岐経路に $G$
加え合わせ点の入れ替え $A + B + C$ 順序自由(可換)
引き出し点の入れ替え 分岐の順序 順序自由

具体例: 複合系の等価伝達関数

次のシステムを考えます。前向き経路に $G_1(s)$ と $G_2(s)$ が直列に接続され、$G_2(s)$ の出力から $H(s)$ を通して $G_1(s)$ と $G_2(s)$ の間にフィードバックがかかっているとします。

ステップ1: 内側ループの簡約化

$G_2(s)$ と $H(s)$ でフィードバックループを形成しています。

$$ G_{\text{inner}}(s) = \frac{G_2(s)}{1 + G_2(s) \, H(s)} $$

ステップ2: 全体の伝達関数

$G_1(s)$ と $G_{\text{inner}}(s)$ の直列結合として

$$ G(s) = G_1(s) \cdot G_{\text{inner}}(s) = \frac{G_1(s) \, G_2(s)}{1 + G_2(s) \, H(s)} $$

Pythonでの実装

結合の計算と検証

import numpy as np
import matplotlib.pyplot as plt

# --- 伝達関数クラス(簡易実装) ---
class TransferFunction:
    """伝達関数 G(s) = num(s) / den(s) を多項式係数で表現"""

    def __init__(self, num, den):
        """
        num: 分子の係数リスト(高次→低次)
        den: 分母の係数リスト(高次→低次)
        """
        self.num = np.array(num, dtype=float)
        self.den = np.array(den, dtype=float)

    def evaluate(self, s):
        """s(複素数)における値を計算"""
        num_val = np.polyval(self.num, s)
        den_val = np.polyval(self.den, s)
        return num_val / den_val

    def __repr__(self):
        return f"TF(num={self.num}, den={self.den})"


def series(G1, G2):
    """直列結合: G = G1 * G2"""
    num = np.polymul(G1.num, G2.num)
    den = np.polymul(G1.den, G2.den)
    return TransferFunction(num, den)


def parallel(G1, G2, sign=1):
    """並列結合: G = G1 + sign * G2"""
    # G1.num/G1.den + sign * G2.num/G2.den
    # = (G1.num * G2.den + sign * G2.num * G1.den) / (G1.den * G2.den)
    num = np.polyadd(
        np.polymul(G1.num, G2.den),
        sign * np.polymul(G2.num, G1.den)
    )
    den = np.polymul(G1.den, G2.den)
    return TransferFunction(num, den)


def feedback(G, H, sign=-1):
    """フィードバック結合: G_cl = G / (1 - sign * G * H)
    sign=-1: 負のフィードバック(デフォルト)
    sign=+1: 正のフィードバック
    """
    # G_cl = G.num/G.den / (1 - sign * G.num*H.num/(G.den*H.den))
    # = G.num * H.den / (G.den * H.den - sign * G.num * H.num)
    num = np.polymul(G.num, H.den)
    den = np.polyadd(
        np.polymul(G.den, H.den),
        -sign * np.polymul(G.num, H.num)
    )
    return TransferFunction(num, den)


# --- 具体例 ---
# G1(s) = 10 / (s + 1)
G1 = TransferFunction([10], [1, 1])
# G2(s) = 5 / (s + 2)
G2 = TransferFunction([5], [1, 2])
# H(s) = 0.2
H = TransferFunction([0.2], [1])

# 各結合の計算
G_series = series(G1, G2)        # G1 * G2
G_parallel = parallel(G1, G2)    # G1 + G2
G_fb = feedback(G1, H, sign=-1)  # G1 / (1 + G1*H)

# 周波数応答(ボード線図)
omega = np.logspace(-2, 3, 500)
s = 1j * omega

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

# (1) 直列結合
mag_s = np.abs(G_series.evaluate(s))
axes[0, 0].semilogx(omega, 20 * np.log10(mag_s), 'b-', linewidth=2, label='$G_1 G_2$')
mag_1 = np.abs(G1.evaluate(s))
mag_2 = np.abs(G2.evaluate(s))
axes[0, 0].semilogx(omega, 20 * np.log10(mag_1), 'r--', linewidth=1, alpha=0.7, label='$G_1$')
axes[0, 0].semilogx(omega, 20 * np.log10(mag_2), 'g--', linewidth=1, alpha=0.7, label='$G_2$')
axes[0, 0].set_xlabel('Frequency [rad/s]', fontsize=11)
axes[0, 0].set_ylabel('Magnitude [dB]', fontsize=11)
axes[0, 0].set_title('Series Connection: $G = G_1 G_2$', fontsize=13)
axes[0, 0].legend(fontsize=10)
axes[0, 0].grid(True, alpha=0.3, which='both')

# (2) 並列結合
mag_p = np.abs(G_parallel.evaluate(s))
axes[0, 1].semilogx(omega, 20 * np.log10(mag_p), 'b-', linewidth=2, label='$G_1 + G_2$')
axes[0, 1].semilogx(omega, 20 * np.log10(mag_1), 'r--', linewidth=1, alpha=0.7, label='$G_1$')
axes[0, 1].semilogx(omega, 20 * np.log10(mag_2), 'g--', linewidth=1, alpha=0.7, label='$G_2$')
axes[0, 1].set_xlabel('Frequency [rad/s]', fontsize=11)
axes[0, 1].set_ylabel('Magnitude [dB]', fontsize=11)
axes[0, 1].set_title('Parallel Connection: $G = G_1 + G_2$', fontsize=13)
axes[0, 1].legend(fontsize=10)
axes[0, 1].grid(True, alpha=0.3, which='both')

# (3) フィードバック結合
mag_fb = np.abs(G_fb.evaluate(s))
axes[1, 0].semilogx(omega, 20 * np.log10(mag_fb), 'b-', linewidth=2,
                     label='$G_1/(1+G_1 H)$')
axes[1, 0].semilogx(omega, 20 * np.log10(mag_1), 'r--', linewidth=1, alpha=0.7,
                     label='$G_1$ (open)')
axes[1, 0].set_xlabel('Frequency [rad/s]', fontsize=11)
axes[1, 0].set_ylabel('Magnitude [dB]', fontsize=11)
axes[1, 0].set_title('Feedback Connection: $G_{cl} = G_1/(1+G_1 H)$', fontsize=13)
axes[1, 0].legend(fontsize=10)
axes[1, 0].grid(True, alpha=0.3, which='both')

# (4) ステップ応答の比較
dt = 0.001
t = np.arange(0, 5, dt)

def step_response(tf, t, dt):
    """オイラー法でステップ応答を計算(2次系まで対応)"""
    # 状態空間表現に変換して数値積分
    # ここでは周波数領域からの逆変換の近似として
    # 多数の周波数成分から合成する方法を使用
    N = len(t)
    y = np.zeros(N)
    # 台形法で逆ラプラス変換を近似(Talbot法の簡易版)
    M = 64
    for k in range(N):
        if t[k] == 0:
            continue
        total = 0.0
        for m in range(M):
            theta = -np.pi + (2 * m + 1) * np.pi / M
            sigma = 2.0 / t[k]
            s_val = sigma * (1 + 1j * theta / np.pi * M / 2)
            z = tf.evaluate(s_val) / s_val  # ステップ入力 = 1/s
            total += np.real(np.exp(s_val * t[k]) * z)
        y[k] = 2 * sigma / M * total
    return y

# 簡易インパルス応答 → ステップ応答(畳み込み近似)
# より簡潔に: 直接差分方程式で計算
def step_response_simple(num, den, t, dt):
    """1次系・2次系のステップ応答を差分方程式で計算"""
    N = len(t)
    # 入力: ステップ(u=1 for t>=0)
    u = np.ones(N)
    # 出力
    n_den = len(den)
    n_num = len(num)

    # 正規化(最高次の係数を1にする)
    num = np.array(num) / den[0]
    den = np.array(den) / den[0]

    order = n_den - 1

    if order == 1:
        # 1次系: den[0]*y' + den[1]*y = num * u
        a = den[1]
        b = num[-1] if len(num) == 1 else num[-1]
        y = np.zeros(N)
        for i in range(1, N):
            y[i] = y[i-1] + dt * (-a * y[i-1] + b * u[i])
        return y
    elif order == 2:
        # 2次系: y'' + den[1]*y' + den[2]*y = b*u
        a1 = den[1]
        a2 = den[2]
        b0 = num[-1] if len(num) == 1 else num[-1]
        y = np.zeros(N)
        yd = np.zeros(N)  # y'
        for i in range(1, N):
            yd[i] = yd[i-1] + dt * (-a1 * yd[i-1] - a2 * y[i-1] + b0 * u[i])
            y[i] = y[i-1] + dt * yd[i-1]
        return y
    else:
        return np.zeros(N)

# ステップ応答
y_open = step_response_simple(G1.num, G1.den, t, dt)
y_fb = step_response_simple(G_fb.num, G_fb.den, t, dt)

axes[1, 1].plot(t, y_open, 'r--', linewidth=1.5, label='Open loop $G_1$', alpha=0.7)
axes[1, 1].plot(t, y_fb, 'b-', linewidth=2, label='Closed loop $G_1/(1+G_1 H)$')

# 定常値の理論値
dc_open = G1.evaluate(0).real    # G1(0) = 10/1 = 10
dc_fb = G_fb.evaluate(0).real    # 10/(1+10*0.2) = 10/3
axes[1, 1].axhline(y=dc_open, color='red', linestyle=':', alpha=0.5)
axes[1, 1].axhline(y=dc_fb, color='blue', linestyle=':', alpha=0.5)
axes[1, 1].set_xlabel('Time [s]', fontsize=11)
axes[1, 1].set_ylabel('Output y(t)', fontsize=11)
axes[1, 1].set_title('Step Response: Open vs Closed Loop', fontsize=13)
axes[1, 1].legend(fontsize=10)
axes[1, 1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

# --- 結果の確認 ---
print("=== 結合結果の検証 ===")
print(f"G1(0) = {G1.evaluate(0).real:.4f}")
print(f"G2(0) = {G2.evaluate(0).real:.4f}")
print(f"直列 G1*G2(0) = {G_series.evaluate(0).real:.4f}  (理論: {10*5/1/2:.4f})")
print(f"並列 G1+G2(0) = {G_parallel.evaluate(0).real:.4f}  (理論: {10/1+5/2:.4f})")
print(f"FB G1/(1+G1*H)(0) = {G_fb.evaluate(0).real:.4f}  (理論: {10/(1+10*0.2):.4f})")

複合システムの等価変換

import numpy as np
import matplotlib.pyplot as plt

# 複合システム:
# 前向き経路: G1(s) -> G2(s) が直列
# 内部フィードバック: G2 の出力から H(s) を通して G1-G2 間に帰還

# G1(s) = 2/(s+1), G2(s) = 3/(s+3), H(s) = 0.5
G1_num, G1_den = [2], [1, 1]
G2_num, G2_den = [3], [1, 3]
H_num, H_den = [0.5], [1]

# 内側ループ: G_inner = G2 / (1 + G2*H)
# 分子: G2_num * H_den = [3] * [1] = [3]
# 分母: G2_den * H_den + G2_num * H_num = [1,3]*[1] + [3]*[0.5] = [1,3] + [1.5] = [1, 4.5]
G_inner_num = np.polymul(G2_num, H_den)
G_inner_den = np.polyadd(np.polymul(G2_den, H_den), np.polymul(G2_num, H_num))

# 全体: G = G1 * G_inner
G_total_num = np.polymul(G1_num, G_inner_num)
G_total_den = np.polymul(G1_den, G_inner_den)

print("=== 複合システムの等価伝達関数 ===")
print(f"G_inner = {G_inner_num} / {G_inner_den}")
print(f"G_total = {G_total_num} / {G_total_den}")

# G_total = 6 / (s+1)(s+4.5) = 6 / (s^2 + 5.5s + 4.5)

# 周波数応答の比較
omega = np.logspace(-2, 2, 500)
s = 1j * omega

G1_val = np.polyval(G1_num, s) / np.polyval(G1_den, s)
G2_val = np.polyval(G2_num, s) / np.polyval(G2_den, s)
G_total_val = np.polyval(G_total_num, s) / np.polyval(G_total_den, s)
G_open_val = G1_val * G2_val  # フィードバックなしの直列

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

# ゲイン線図
axes[0].semilogx(omega, 20*np.log10(np.abs(G_open_val)), 'r--', lw=1.5,
                 label='Open: $G_1 G_2$')
axes[0].semilogx(omega, 20*np.log10(np.abs(G_total_val)), 'b-', lw=2,
                 label='With FB: $G_1 G_2/(1+G_2 H)$')
axes[0].set_xlabel('Frequency [rad/s]', fontsize=11)
axes[0].set_ylabel('Magnitude [dB]', fontsize=11)
axes[0].set_title('Magnitude (Composite System)', fontsize=13)
axes[0].legend(fontsize=10)
axes[0].grid(True, alpha=0.3, which='both')

# 位相線図
axes[1].semilogx(omega, np.degrees(np.angle(G_open_val)), 'r--', lw=1.5,
                 label='Open: $G_1 G_2$')
axes[1].semilogx(omega, np.degrees(np.angle(G_total_val)), 'b-', lw=2,
                 label='With FB: $G_1 G_2/(1+G_2 H)$')
axes[1].set_xlabel('Frequency [rad/s]', fontsize=11)
axes[1].set_ylabel('Phase [deg]', fontsize=11)
axes[1].set_title('Phase (Composite System)', fontsize=13)
axes[1].legend(fontsize=10)
axes[1].grid(True, alpha=0.3, which='both')

plt.tight_layout()
plt.show()

# 定常ゲインの検証
dc_open = np.polyval(G1_num, 0) / np.polyval(G1_den, 0) * \
          np.polyval(G2_num, 0) / np.polyval(G2_den, 0)
dc_total = np.polyval(G_total_num, 0) / np.polyval(G_total_den, 0)
print(f"\n定常ゲイン(開ループ): {dc_open:.4f}")
print(f"定常ゲイン(フィードバック付き): {dc_total:.4f}")

内部フィードバックにより、低周波域のゲインが低下し、帯域幅が変化していることが確認できます。フィードバック結合はゲインと引き換えに応答特性を改善する典型的な構造です。

まとめ

本記事では、伝達関数のブロック線図結合について解説しました。

  • 直列結合: $G(s) = G_1(s) \cdot G_2(s)$(伝達関数の積)
  • 並列結合: $G(s) = G_1(s) + G_2(s)$(伝達関数の和)
  • フィードバック結合(負): $G_{\text{cl}}(s) = \dfrac{G(s)}{1 + G(s)H(s)}$
  • ブロック線図の等価変換規則(加え合わせ点・引き出し点の移動)を用いて複雑な系を簡約化できる
  • 開ループ伝達関数 $L(s) = G(s)H(s)$ がシステム解析の鍵となる

次のステップとして、以下の記事も参考にしてください。