ボード線図の読み方と作成:フィルタ周波数応答の対数スケール解析とPython実装

ボード線図で周波数応答(振幅[dB]・位相[deg])を対数スケールで読み解き、フィルタ設計(バターワース/チェビシェフ/ハイパス/ローパス)の評価に活かす方法を、伝達関数の数学的基礎からscipy.signalによるPython実装まで解説します。

はじめに

**ボード線図(Bode plot)**は、線形時不変システムの周波数応答 \(H(j\omega)\) を 振幅[dB]位相[deg] に分け、横軸を 対数スケール(log scale)の周波数 で表した2段グラフです。Hendrik Bodeが1930年代に制御工学のために体系化したこの可視化手法は、現在ではフィルタ設計、伝達関数解析、サーボ系の安定性評価など、信号処理と制御の現場で標準ツールになっています。

特にフィルタ設計では、バターワースフィルタの「最大平坦特性」、チェビシェフフィルタの「等リップル」、ハイパスフィルタローパスフィルタの「ロールオフ率」など、すべての特性がボード線図上で視覚化されます。本記事では、ボード線図の数学的基礎・読み方・Python実装を整理し、各種フィルタの周波数応答を横断的に理解できるハブとして使えるようにまとめます。

数学的基礎

伝達関数と周波数応答

連続時間 LTI システムの伝達関数を \(H(s)\) とします。\(s = j\omega\) を代入して得られる 周波数応答 は、複素数値関数として振幅と位相を持ちます。

\[H(j\omega) = |H(j\omega)| \, e^{j\angle H(j\omega)} \tag{1}\]

ボード線図はこの \(H(j\omega)\) を 2 つのプロットに分解します。

  • 振幅プロット: \(20\log_{10}|H(j\omega)|\) を縦軸に、\(\log_{10}\omega\) を横軸にとる
  • 位相プロット: \(\angle H(j\omega)\) (度)を縦軸に、\(\log_{10}\omega\) を横軸にとる

デシベル定義

振幅をデシベル(dB)で表す理由は、乗算が加算に変わることにあります。\(H_1\) と \(H_2\) を直列接続したシステムの振幅は

\[20\log_{10}|H_1 H_2| = 20\log_{10}|H_1| + 20\log_{10}|H_2| \tag{2}\]

となり、ボード線図上では 個別のプロットの和 として描けます。これにより、複数次の極・零点を持つフィルタも 1 次系の重ね合わせで近似的に読み解けるのです。

位相遅延と群遅延

位相プロットから直接読める量として、位相遅延群遅延 が重要です。

\[\tau_p(\omega) = -\frac{\angle H(j\omega)}{\omega}, \quad \tau_g(\omega) = -\frac{d\angle H(j\omega)}{d\omega} \tag{3}\]

群遅延 \(\tau_g\) は位相プロットの 傾き に直結し、波形歪みの大きさを表します。FIR フィルタの線形位相は群遅延が定数になることを意味し、ボード線図上では位相が周波数の一次関数で減少します。

1 次・2 次系のボード線図

1 次ローパス

\[H(s) = \frac{1}{1 + s/\omega_c} \tag{4}\]

の振幅は

\[|H(j\omega)| = \frac{1}{\sqrt{1 + (\omega/\omega_c)^2}} \tag{5}\]

であり、漸近線で読むと

  • \(\omega \ll \omega_c\) : \(|H|_{dB} \approx 0\) dB(通過域)
  • \(\omega = \omega_c\) : \(|H|_{dB} = -3\) dB(カットオフ点)
  • \(\omega \gg \omega_c\) : \(|H|_{dB} \approx -20\log_{10}(\omega/\omega_c)\) dB(-20 dB/dec のロールオフ

位相は \(\omega_c\) で \(-45^\circ\) 、高域漸近で \(-90^\circ\) になります。1 次ハイパスはこの 鏡像(低域で \(-20\) dB/dec の立ち上がり、\(+90^\circ \to 0^\circ\) への位相回転)として読めます。

2 次系の共振

2 次系

\[H(s) = \frac{\omega_n^2}{s^2 + 2\zeta\omega_n s + \omega_n^2} \tag{6}\]

の振幅プロットは、減衰係数 \(\zeta < 1/\sqrt{2}\) で 共振ピーク を持ちます。ピーク値は

\[M_p = \frac{1}{2\zeta\sqrt{1-\zeta^2}} \tag{7}\]

であり、\(\zeta\) が小さいほど鋭いピークが立ちます。チェビシェフフィルタの通過域リップルや楕円フィルタの両域リップルは、この 2 次系の共振が複数組み合わさった結果と見なせます。高域漸近のロールオフは -40 dB/dec で、1 次系の倍の急峻さです。

典型的フィルタとボード線図の対応

フィルタ通過域の振幅特性ロールオフ位相特性
バターワース \(N\) 次最大平坦\(-20N\) dB/dec比較的滑らか
チェビシェフ I 型等リップル\(-20N\) dB/dec通過域端で急峻に変化
楕円(Cauer)両域リップル最急峻最も急峻
ハイパス\(f > f_c\) で平坦低域 \(-20N\) /dec\(+90N^\circ \to 0^\circ\)
バンドパス帯域内で平坦両側 \(-20N\) /dec帯域中心で 0°
ノッチ\(f_0\) で深いディップ急峻\(f_0\) で \(\pm 90^\circ\) 回転

このようにボード線図は 「フィルタの種類を一目で見分ける視覚的辞書」 として機能します。次数 \(N\) はロールオフ率から、減衰特性は通過域・阻止域のリップル形状から、位相歪みは位相プロットの曲がりから読み取れます。

Python による作成

scipy.signal.bode による標準実装

scipy.signal.bode は連続時間伝達関数からボード線図のデータを直接生成できます。

import numpy as np
import matplotlib.pyplot as plt
from scipy import signal

# --- 1次ローパス: H(s) = 1 / (1 + s/wc) ---
wc = 2 * np.pi * 100        # カットオフ角周波数 [rad/s] (100 Hz)
num = [1.0]
den = [1.0 / wc, 1.0]
system = signal.TransferFunction(num, den)

# bode は 振幅[dB] と 位相[deg] を返す
w, mag, phase = signal.bode(system, w=np.logspace(0, 4, 1000))
f = w / (2 * np.pi)         # Hz に変換

fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 7), sharex=True)

ax1.semilogx(f, mag, linewidth=2)
ax1.axhline(-3, color='gray', linestyle=':', label='-3 dB')
ax1.axvline(wc / (2 * np.pi), color='r', linestyle='--', label=f'$f_c$ = {wc/(2*np.pi):.0f} Hz')
ax1.set_ylabel('Magnitude [dB]')
ax1.set_title('Bode Plot - 1st-order Lowpass')
ax1.grid(True, which='both', alpha=0.3)
ax1.legend()

ax2.semilogx(f, phase, linewidth=2, color='orange')
ax2.axvline(wc / (2 * np.pi), color='r', linestyle='--')
ax2.axhline(-45, color='gray', linestyle=':', label='-45°')
ax2.set_xlabel('Frequency [Hz]')
ax2.set_ylabel('Phase [degrees]')
ax2.grid(True, which='both', alpha=0.3)
ax2.legend()

plt.tight_layout()
plt.show()

semilogx で横軸を対数化することがポイントです。リニア軸では低域の構造が潰れてしまい、ボード線図本来の 「数十年(decade)にわたる広帯域挙動を一度に俯瞰する」 という長所が失われます。

デジタルフィルタ(IIR)のボード線図

バターワースチェビシェフ のような IIR デジタルフィルタには scipy.signal.freqz を使います。

import numpy as np
import matplotlib.pyplot as plt
from scipy import signal

fs = 1000.0           # サンプリング周波数 [Hz]
fc = 100.0            # カットオフ周波数 [Hz]
orders = [2, 4, 8]

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

for N in orders:
    sos = signal.butter(N, fc, btype='low', fs=fs, output='sos')
    w, h = signal.sosfreqz(sos, worN=4096, fs=fs)

    # 振幅プロット(対数周波数 + dB)
    ax1.semilogx(w, 20 * np.log10(np.abs(h) + 1e-12), label=f'N={N}', linewidth=2)
    # 位相プロット(unwrap で 360° 跳びを除去)
    ax2.semilogx(w, np.degrees(np.unwrap(np.angle(h))), label=f'N={N}', linewidth=2)

ax1.axhline(-3, color='gray', linestyle=':', label='-3 dB')
ax1.axvline(fc, color='r', linestyle='--', alpha=0.5)
ax1.set_ylabel('Magnitude [dB]')
ax1.set_title('Bode Plot - Butterworth Lowpass (digital)')
ax1.set_ylim(-100, 5)
ax1.grid(True, which='both', alpha=0.3)
ax1.legend()

ax2.axvline(fc, color='r', linestyle='--', alpha=0.5)
ax2.set_xlabel('Frequency [Hz]')
ax2.set_ylabel('Phase [degrees]')
ax2.set_xlim(1, fs / 2)
ax2.grid(True, which='both', alpha=0.3)
ax2.legend()

plt.tight_layout()
plt.show()

このプロットからは、次数 \(N\) を 1 段上げるごとに高域のロールオフが 20 dB/dec ずつ急峻になる こと、位相回転が \(-90^\circ\) ずつ深まることが確認できます。これが「次数から特性を予測する」感覚を養う最良のトレーニングです。

手動計算による複数フィルタの比較

bode / freqz を使わず手動で計算すると、各フィルタの違いを並列に可視化できます。

import numpy as np
import matplotlib.pyplot as plt
from scipy import signal

fs = 1000.0
fc = 100.0
N = 4
f = np.logspace(0, np.log10(fs / 2), 2000)
w = 2 * np.pi * f

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

filters = {
    'Butterworth':  signal.butter(N, fc, btype='low', fs=fs, output='ba'),
    'Chebyshev I':  signal.cheby1(N, 1.0, fc, btype='low', fs=fs, output='ba'),
    'Chebyshev II': signal.cheby2(N, 40.0, fc, btype='low', fs=fs, output='ba'),
    'Elliptic':     signal.ellip(N, 1.0, 40.0, fc, btype='low', fs=fs, output='ba'),
}

for name, (b, a) in filters.items():
    _, h = signal.freqz(b, a, worN=f, fs=fs)
    ax1.semilogx(f, 20 * np.log10(np.abs(h) + 1e-12), label=name, linewidth=2)
    ax2.semilogx(f, np.degrees(np.unwrap(np.angle(h))), label=name, linewidth=2)

ax1.axvline(fc, color='r', linestyle='--', alpha=0.5, label=f'$f_c$ = {fc} Hz')
ax1.axhline(-3, color='gray', linestyle=':', alpha=0.5)
ax1.set_ylabel('Magnitude [dB]')
ax1.set_title(f'Bode Plot Comparison (N={N})')
ax1.set_ylim(-80, 5)
ax1.grid(True, which='both', alpha=0.3)
ax1.legend()

ax2.set_xlabel('Frequency [Hz]')
ax2.set_ylabel('Phase [degrees]')
ax2.grid(True, which='both', alpha=0.3)
ax2.legend()

plt.tight_layout()
plt.show()

同じ次数・同じカットオフで並べると、バターワースの平坦な通過域、チェビシェフ I 型の通過域リップル、チェビシェフ II 型の阻止域リップル、楕円フィルタの両域リップルが ボード線図上で直接比較 できます。

設計指針

カットオフから次数を決める

通過域 \(f_p\) の最大減衰 \(A_p\) [dB] と阻止域 \(f_s\) の最小減衰 \(A_s\) [dB] が与えられたとき、バターワース次数は

\[N \geq \frac{\log_{10}\!\left(\dfrac{10^{A_s/10} - 1}{10^{A_p/10} - 1}\right)}{2\log_{10}(f_s / f_p)} \tag{8}\]

で求まります。これは ボード線図上の「2 点」から傾きを逆算して必要なロールオフ率を決める 操作です。scipy.signal.buttord がこの計算を自動化します。

位相回りに注意

ロールオフ率を急峻にするほど位相回転が深くなり、群遅延の周波数依存性が増します。

  • 音声処理・医療信号: 位相歪みを避けたい → FIR で線形位相を確保、または filtfilt でゼロ位相化
  • リアルタイム制御: 群遅延を最小化したい → ベッセルフィルタや低次バターワース
  • 通過域の平坦性が最優先: バターワース
  • 遷移帯域の急峻さが最優先: チェビシェフ・楕円

ボード線図の位相プロットは、この 「振幅性能と位相性能のトレードオフ」 を一望で示してくれる重要なツールです。

適応フィルタとの違い

ボード線図は 時不変な伝達関数を持つ固定フィルタ にのみ意味を持ちます。LMS/RLS などの適応フィルタ は係数が時間で変化するため、ある瞬間のスナップショットとしてのみボード線図を描けます。逆に、ボード線図で見た「目標の周波数応答」を適応フィルタが学習で近づける、という関係になります。

FFT / PSD との関係

ボード線図はフィルタ自体の特性を表しますが、実信号 がフィルタを通った結果を見るには FFT窓関数 + PSD と組み合わせます。具体的には、入力スペクトル \(X(f)\) と出力スペクトル \(Y(f) = H(f) X(f)\) から \(|H(f)| = |Y(f)/X(f)|\) を推定すれば、実測ボード線図 が得られます。これがシステム同定の基本原理です。

まとめ

  • ボード線図は周波数応答 \(H(j\omega)\) を 振幅[dB] + 位相[deg] の 2 段、対数周波数軸 で描く可視化
  • デシベルにより直列接続が加算に、対数軸により広帯域挙動が等間隔で読める
  • 1 次系のロールオフは \(\pm 20\) dB/dec、2 次系は \(\pm 40\) dB/dec、\(N\) 次は \(\pm 20N\) dB/dec
  • バターワース・チェビシェフ・楕円・ハイパス・バンドパス・ノッチなど すべてのフィルタの個性がボード線図に現れる
  • Python では scipy.signal.bode(連続時間)と scipy.signal.freqz(離散時間)が標準

各種フィルタの設計理論については以下の関連記事を参照してください。

関連記事

参考文献


関連ツール