ベッセルフィルタの設計原理とPython実装

ベッセルフィルタ(Thomson-Besselフィルタ)の数学的基礎から、群遅延の最大平坦特性の導出、バターワース・チェビシェフ・楕円フィルタとの比較、SciPyを使ったローパス/ハイパス/バンドパスフィルタのPython実装までを解説します。

はじめに

ベッセルフィルタ(Bessel Filter)は、通過域で群遅延が最大限に平坦なIIRフィルタです。W. E. Thomsonが1952年に提案したことからThomson-Besselフィルタとも呼ばれます。

代表的なIIRフィルタの群遅延の平坦性を比較すると次のようになります(同次数の場合):

\[\text{ベッセル} > \text{バターワース} > \text{チェビシェフ} > \text{楕円}\]

ベッセルフィルタは通過帯域内で群遅延がほぼ一定であるため、異なる周波数成分の信号が同じ時間遅延でフィルタを通過します。これによりパルス波形の形状を保持できます。本記事では数学的背景から、SciPyを使った実践的な設計・実装までを解説します。

群遅延とは

群遅延(Group Delay)とは、フィルタの位相特性 \(\phi(\omega)\) の周波数に対する微分の負値です:

\[\tau(\omega) = -\frac{d\phi(\omega)}{d\omega} \tag{1}\]

群遅延が一定(周波数によらず等しい)であることは、フィルタが線形位相特性を持つことを意味します。線形位相フィルタでは、すべての周波数成分が同じ時間だけ遅延するため、信号の波形がそのまま保たれます。

音声・音楽処理、パルス波形の伝送、測定機器など、波形の形状保持が重要な用途でベッセルフィルタが選ばれます。

ベッセルフィルタの数学的背景

ベッセル多項式

ベッセルフィルタの伝達関数はベッセル多項式 \(B_n(s)\) に基づいています。逆数正規化されたベッセル多項式(\(y_n(s)\) とも書く)は漸化式で定義されます:

\[B_0(s) = 1 \tag{2}\]\[B_1(s) = s + 1 \tag{3}\]\[B_n(s) = (2n-1)B_{n-1}(s) + s^2 B_{n-2}(s) \tag{4}\]

低次のベッセル多項式を具体的に示します:

次数 \(n\)\(B_n(s)\)
1\(s + 1\)
2\(s^2 + 3s + 3\)
3\(s^3 + 6s^2 + 15s + 15\)
4\(s^4 + 10s^3 + 45s^2 + 105s + 105\)

伝達関数

\(n\) 次ベッセルローパスフィルタの伝達関数は次の形をとります:

\[H_n(s) = \frac{B_n(0)}{B_n(s/\omega_0)} = \frac{(2n-1)!!}{B_n(s/\omega_0)} \tag{5}\]

ここで \(\omega_0\) は正規化角周波数、\((2n-1)!! = 1 \cdot 3 \cdot 5 \cdots (2n-1)\) は二重階乗です。

この構成により、\(s \to 0\) すなわち低周波数域での群遅延が最大限に平坦になります。

群遅延の最大平坦性

ベッセルフィルタの群遅延 \(\tau(\omega)\) を \(\omega=0\) でテイラー展開すると、\(2n-1\) 次まで導関数がゼロになります:

\[\tau(\omega) = \tau_0 \left[ 1 + O(\omega^{2n}) \right] \tag{6}\]

これが「群遅延が最大平坦」の意味です。\(n\) が大きいほど平坦な領域が広がります。

他のIIRフィルタとの比較

フィルタ振幅平坦性遷移帯域の急峻さ群遅延特性主な用途
ベッセル最も劣る最もなだらか最大平坦波形保持・パルス伝送
バターワース最大平坦中程度比較的良好汎用ローパス
チェビシェフI等リップルバターワースより急峻非線形急峻な遮断が必要な場合
楕円(Cauer)等リップル最も急峻最も劣る最小次数で仕様を満たす場合

ベッセルフィルタは振幅特性(遮断の鋭さ)を犠牲にして位相・群遅延特性を最大化します。遷移帯域が非常になだらかなため、同じ遮断仕様を満たすには高い次数が必要です。

SciPyによる実装

SciPyでは scipy.signal.bessel() 関数でベッセルフィルタを設計できます。デフォルトでは群遅延を \(\omega=0\) で正規化します(norm='delay')。

基本的なローパスフィルタ設計

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

# ===== フィルタ設計パラメータ =====
fs = 1000      # サンプリング周波数 [Hz]
fc = 100       # カットオフ周波数 [Hz]
orders = [2, 4, 6]

fig, axes = plt.subplots(3, 1, figsize=(10, 10))

for N in orders:
    # ベッセルローパスフィルタ(群遅延正規化)
    sos = signal.bessel(N, fc, btype='low', fs=fs, norm='delay', output='sos')
    w, h = signal.sosfreqz(sos, worN=4096, fs=fs)

    # 群遅延の計算
    w_gd, gd = signal.group_delay((signal.bessel(N, fc, btype='low', fs=fs, norm='delay')), w=4096, fs=fs)

    axes[0].plot(w, 20 * np.log10(np.abs(h) + 1e-12), label=f'N={N}')
    axes[1].plot(w, np.degrees(np.unwrap(np.angle(h))), label=f'N={N}')
    axes[2].plot(w_gd, gd, label=f'N={N}')

axes[0].set_xlim(0, 500)
axes[0].set_ylim(-60, 5)
axes[0].set_xlabel('周波数 [Hz]')
axes[0].set_ylabel('ゲイン [dB]')
axes[0].set_title('ベッセルフィルタ ゲイン特性')
axes[0].axvline(fc, color='gray', linestyle='--', alpha=0.5, label=f'fc={fc}Hz')
axes[0].legend()
axes[0].grid(True, alpha=0.3)

axes[1].set_xlim(0, 500)
axes[1].set_xlabel('周波数 [Hz]')
axes[1].set_ylabel('位相 [度]')
axes[1].set_title('ベッセルフィルタ 位相特性')
axes[1].legend()
axes[1].grid(True, alpha=0.3)

axes[2].set_xlim(0, fs / 2)
axes[2].set_ylim(0, 0.02)
axes[2].set_xlabel('周波数 [Hz]')
axes[2].set_ylabel('群遅延 [s]')
axes[2].set_title('ベッセルフィルタ 群遅延特性')
axes[2].legend()
axes[2].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

他フィルタとの群遅延比較

ベッセルフィルタの群遅延平坦性を他フィルタと視覚的に比較します:

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

fs = 1000
fc = 100
N = 4

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

filters = {
    'Bessel': signal.bessel(N, fc, btype='low', fs=fs, norm='delay'),
    'Butterworth': signal.butter(N, fc, btype='low', fs=fs),
    'Chebyshev I (rp=1dB)': signal.cheby1(N, 1.0, fc, btype='low', fs=fs),
    'Elliptic (rp=1,rs=60)': signal.ellip(N, 1.0, 60.0, fc, btype='low', fs=fs),
}

colors = ['royalblue', 'tomato', 'green', 'purple']

for (name, (b, a)), color in zip(filters.items(), colors):
    w, h = signal.freqz(b, a, worN=4096, fs=fs)
    w_gd, gd = signal.group_delay((b, a), w=4096, fs=fs)

    axes[0].plot(w, 20 * np.log10(np.abs(h) + 1e-12), label=name, color=color)
    axes[1].plot(w_gd, gd, label=name, color=color)

axes[0].set_xlim(0, 500)
axes[0].set_ylim(-80, 5)
axes[0].set_xlabel('周波数 [Hz]')
axes[0].set_ylabel('ゲイン [dB]')
axes[0].set_title(f'ゲイン特性の比較(N={N})')
axes[0].axvline(fc, color='gray', linestyle='--', alpha=0.5, label=f'fc={fc}Hz')
axes[0].legend()
axes[0].grid(True, alpha=0.3)

axes[1].set_xlim(0, 500)
axes[1].set_ylim(0, 0.025)
axes[1].set_xlabel('周波数 [Hz]')
axes[1].set_ylabel('群遅延 [s]')
axes[1].set_title(f'群遅延特性の比較(N={N})')
axes[1].axvline(fc, color='gray', linestyle='--', alpha=0.5)
axes[1].legend()
axes[1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

ハイパス・バンドパスフィルタへの拡張

btype パラメータを変更するだけでハイパス・バンドパスフィルタを設計できます:

from scipy import signal

fs = 1000
N = 4

# ===== ハイパスフィルタ =====
sos_hp = signal.bessel(N, 200, btype='high', fs=fs, norm='delay', output='sos')
print("ハイパスフィルタ: 設計完了")

# ===== バンドパスフィルタ =====
sos_bp = signal.bessel(N, [100, 300], btype='bandpass', fs=fs, norm='delay', output='sos')
print("バンドパスフィルタ: 設計完了")

# ===== バンドストップフィルタ =====
sos_bs = signal.bessel(N, [100, 300], btype='bandstop', fs=fs, norm='delay', output='sos')
print("バンドストップフィルタ: 設計完了")

パルス波形への適用例

ベッセルフィルタの最大の利点を示す例として、矩形パルス信号を各フィルタに通した場合の波形比較を示します:

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

fs = 1000
fc = 100
N = 4
t = np.linspace(0, 0.1, int(fs * 0.1), endpoint=False)

# 矩形パルス信号(幅5ms)
x = np.zeros(len(t))
x[(t >= 0.02) & (t < 0.025)] = 1.0

filters = {
    'Bessel': signal.bessel(N, fc, btype='low', fs=fs, norm='delay', output='sos'),
    'Butterworth': signal.butter(N, fc, btype='low', fs=fs, output='sos'),
    'Chebyshev I': signal.cheby1(N, 1.0, fc, btype='low', fs=fs, output='sos'),
    'Elliptic': signal.ellip(N, 1.0, 60.0, fc, btype='low', fs=fs, output='sos'),
}

fig, axes = plt.subplots(len(filters) + 1, 1, figsize=(10, 14), sharex=True)

axes[0].plot(t * 1000, x, 'k', label='入力パルス')
axes[0].set_ylabel('振幅')
axes[0].set_title('入力信号(矩形パルス)')
axes[0].legend()
axes[0].grid(True, alpha=0.3)

for idx, (name, sos) in enumerate(filters.items()):
    y = signal.sosfilt(sos, x)  # lfilter相当(因果的、リアルタイム)
    axes[idx + 1].plot(t * 1000, y, label=name)
    axes[idx + 1].plot(t * 1000, x, 'k--', alpha=0.3, label='元のパルス')
    axes[idx + 1].set_ylabel('振幅')
    axes[idx + 1].set_title(f'{name} フィルタ後(N={N})')
    axes[idx + 1].legend()
    axes[idx + 1].grid(True, alpha=0.3)

axes[-1].set_xlabel('時間 [ms]')
plt.tight_layout()
plt.show()

このコードを実行すると、ベッセルフィルタが最も波形の形状を保持していること、楕円フィルタでは大きなリンギングが発生することが確認できます。

norm パラメータの意味

scipy.signal.bessel() には norm パラメータがあり、正規化方法を選べます:

norm意味用途
'delay'\(\omega=0\) での群遅延が \(1/\omega_c\) になるよう正規化群遅延の比較(デフォルト)
'critical'-3dBカットオフが指定周波数になるよう正規化他フィルタとの振幅比較
'mag'通過域端のゲインを指定値に合わせる特定ゲイン要件がある場合

他のフィルタ(バターワース等)と同じカットオフ周波数で比較したい場合は norm='critical' を使用します:

from scipy import signal

fs = 1000
fc = 100
N = 4

# -3dBカットオフをfc=100Hzに設定
sos_bessel = signal.bessel(N, fc, btype='low', fs=fs, norm='critical', output='sos')
sos_butter = signal.butter(N, fc, btype='low', fs=fs, output='sos')

ベッセルフィルタを選ぶべきシナリオ

シナリオ推奨フィルタ
パルス波形の形状を保持したいベッセルフィルタ
音声信号の位相歪みを最小化したいベッセルフィルタ
遷移帯域をできるだけ急峻にしたい楕円フィルタ
通過域の振幅を最大限に平坦にしたいバターワースフィルタ
計算コスト(次数)を最小化したい楕円フィルタ
リップルを許容して遮断を急峻にしたいチェビシェフフィルタ

関連記事

参考文献

  • Thomson, W. E. (1952). Delay networks having maximally flat frequency characteristics. Proceedings of the IEE - Part III: Radio and Communication Engineering, 96(44), 487–490.
  • Proakis, J. G., & Manolakis, D. G. (2007). Digital Signal Processing (4th ed.). Pearson.
  • SciPy scipy.signal.bessel documentation

関連ツール