ノッチフィルタの設計とPython実装:電源ノイズ除去の実践

scipy.signal.iirnotch / scipy.signal.iirpeak / scipy.signal.filtfilt でノッチフィルタをPython実装し電源ノイズ除去。50/60Hzハム除去のIIR型伝達関数設計、scipy.signal.tf2zpk による極零配置の解析とQ値の調整、バンドパス/バターワースとの比較、心電図・音声への応用までまとめます。

はじめに

心電図(ECG)や脳波(EEG)、加速度センサなどの計測では、50Hz(日本・欧州)や60Hz(米国)の電源ノイズが信号に混入する問題が頻繁に発生します。このノイズは特定の周波数に集中しているため、単純なローパスフィルタでは対処できないことがあります。たとえば心電図のR波は高周波成分を含むため、ローパスフィルタでノイズを除去しようとすると信号自体も劣化してしまいます。

**ノッチフィルタ(Notch Filter)**は、特定の周波数帯域のみを選択的に除去し、それ以外の周波数成分をそのまま通過させるフィルタです。本記事では、IIR型ノッチフィルタの理論的背景を解説し、SciPyを使ったPython実装で電源ノイズの除去を実践します。

フィルタの基礎知識については FIRフィルタとIIRフィルタの比較 、周波数解析の基礎については 高速フーリエ変換(FFT)の仕組みとPython実装 を参照してください。

ノッチフィルタの基本概念

ノッチフィルタは**帯域除去フィルタ(Band-Reject Filter)**の極端なケースで、非常に狭い周波数帯域だけを除去します。周波数応答は対象周波数において急峻な「ノッチ(切り込み)」を持ち、それ以外の帯域ではほぼ平坦なゲインを維持します。

ノッチフィルタの設計において重要なパラメータは以下の2つです。

  • ノッチ周波数 \(f_0\) : 除去対象の中心周波数(例: 50Hz、60Hz)
  • 品質係数 \(Q\) : ノッチの鋭さを制御するパラメータ

\(Q\) はノッチ周波数と帯域幅の比で定義されます。

\[Q = \frac{f_0}{\Delta f} \tag{1}\]

ここで \(\Delta f\) は \(-3\) dB帯域幅です。\(Q\) が大きいほどノッチが狭くなり、対象周波数のみをピンポイントで除去できます。

IIR型ノッチフィルタの伝達関数

2次IIR型ノッチフィルタの伝達関数は以下のように表されます。

\[H(z) = \frac{1 - 2\cos(\omega_0)z^{-1} + z^{-2}}{1 - 2r\cos(\omega_0)z^{-1} + r^2 z^{-2}} \tag{2}\]

ここで \(\omega_0 = 2\pi f_0 / f_s\) は正規化角周波数、\(r\) は極の半径(\(0 < r < 1\) )です。

分子(零点)の役割

分子の零点は単位円上の \(z = e^{\pm j\omega_0}\) に配置されます。零点が単位円上にあるため、周波数 \(\omega_0\) における伝達関数のゲインは正確にゼロになります。これがノッチフィルタの完全な除去特性の由来です。

分母(極)の役割

分母の極は \(z = r \cdot e^{\pm j\omega_0}\) に位置し、単位円の内側(半径 \(r < 1\) )に配置されます。極は零点と同じ角度に配置されますが、半径が \(r\) であるため単位円から離れています。\(r\) が1に近いほど極が零点に近づき、ノッチの影響範囲が狭くなります(高い \(Q\) )。

零点・極の配置と品質係数

極の半径 \(r\) と品質係数 \(Q\) の関係は近似的に以下のように表されます。

\[Q \approx \frac{\omega_0}{2(1-r)} \tag{3}\]

したがって帯域幅は次のようになります。

\[\Delta f = \frac{f_0}{Q} \approx \frac{2(1-r) \cdot f_s}{2\pi} \tag{4}\]

厳密な導出:近似式(3)はどこから来るのか

式(3)は「近似的に」としか説明されていませんが、scipy.signal.iirnotch が実際に使っている設計式から出発すると、この近似の由来と誤差の大きさを厳密に評価できます。SciPyの実装はOrfanidis (1996) の \(-3\) dB整合設計に基づき、次の手順で係数を決定しています。

正規化角周波数の帯域幅を \(\Delta\omega = \omega_0/Q\) とし、

\[\beta = \tan\!\left(\frac{\Delta\omega}{2}\right), \qquad G_0 = \frac{1}{1+\beta} \tag{5}\]

とおくと、伝達関数の分母係数・分子係数はそれぞれ

\[ a_1 = -2G_0\cos\omega_0, \qquad a_2 = 2G_0 - 1, \qquad b = G_0(1,\ -2\cos\omega_0,\ 1) \]

となります。分母の2次方程式

\[ z^2 + a_1 z + a_2 = 0 \]

において、共役複素根の積は定数項に等しい(解と係数の関係)ため、極の半径 \(r = |z|\) について

\[r^2 = a_2 = 2G_0 - 1 \tag{6}\]

が厳密に成り立ちます。これを \(G_0\) について解くと \(G_0 = (1+r^2)/2\) となり、\(\beta = 1/G_0 - 1\) に代入すれば

\[\Delta\omega = 2\arctan\!\left(\frac{1-r^2}{1+r^2}\right) \tag{7}\]

という、\(r\) と帯域幅 \(\Delta\omega\) を結ぶ厳密な関係式が得られます。ここで高 \(Q\) 極限(\(r \to 1\) )を考え、\(r = 1-\varepsilon\) (\(\varepsilon\) は微小量)とおくと、\(r^2 \approx 1-2\varepsilon\) より

\[\frac{1-r^2}{1+r^2} \approx \frac{2\varepsilon}{2} = \varepsilon = 1-r\]

となるため、式(7)は \(\Delta\omega \approx 2(1-r)\) に簡略化されます。\(Q = \omega_0/\Delta\omega\) に代入すれば、式(3)の近似式 \(Q \approx \omega_0/(2(1-r))\) が 高 \(Q\) 極限での1次近似 として導出されます。つまり式(3)は天下り的な公式ではなく、厳密な整合設計式(6)–(7)を \(r \to 1\) でテイラー展開した結果です。

この近似がどの程度の誤差を持つかを、実際にコードで検証します。

import numpy as np
from scipy import signal

fs = 1000
f0 = 50

for Q_set in [5, 15, 30, 60, 100]:
    b, a = signal.iirnotch(f0, Q_set, fs)
    _, poles, _ = signal.tf2zpk(b, a)
    r = np.abs(poles[0])

    # 式(3)による近似Q
    omega0 = 2 * np.pi * f0 / fs
    Q_formula = omega0 / (2 * (1 - r))

    # freqzで-3dB帯域幅を実測してQを逆算
    w, h = signal.freqz(b, a, worN=400_000, fs=fs)
    mag_db = 20 * np.log10(np.maximum(np.abs(h), 1e-16))
    li = np.where((mag_db[:-1] > -3) & (mag_db[1:] <= -3) & (w[:-1] < f0))[0]
    ri = np.where((mag_db[:-1] <= -3) & (mag_db[1:] > -3) & (w[:-1] > f0))[0]
    bw_measured = w[ri[0]] - w[li[-1]]
    Q_measured = f0 / bw_measured

    err_pct = (Q_formula - Q_measured) / Q_measured * 100
    print(f"Q_set={Q_set:4d}  r={r:.6f}  Q_formula={Q_formula:.4f}  "
          f"Q_measured={Q_measured:.4f}  誤差={err_pct:+.3f}%")

実行結果は以下の通りです。

設定 \(Q\)極半径 \(r\)式(3)近似 \(Q\)実測 \(Q\) (-3dB帯域幅から逆算)誤差
50.9690525.0764.988+1.76%
150.98958215.07814.965+0.76%
300.99477830.07829.940+0.46%
600.99738560.07859.880+0.33%
1000.998430100.078100.000+0.08%

\(Q\) が大きいほど式(3)の誤差は単調に縮小しており(\(Q=5\) で \(+1.76\%\) 、\(Q=100\) で \(+0.08\%\) )、これは式(3)が \(r \to 1\) の1次近似であることと整合します。実用域の \(Q = 20 \sim 50\) では誤差はいずれも \(0.5\%\) 未満であり、設計時の近似式としては十分実用的であることが確認できます。

以下のコードで、零点・極の配置を可視化します。

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

# --- パラメータ ---
fs = 1000       # サンプリング周波数 [Hz]
f0 = 50         # ノッチ周波数 [Hz]
Q = 30          # 品質係数

# --- ノッチフィルタの設計 ---
b, a = signal.iirnotch(f0, Q, fs)

# --- 零点・極の計算 ---
zeros, poles, _ = signal.tf2zpk(b, a)

# --- 零点・極配置図のプロット ---
fig, ax = plt.subplots(figsize=(6, 6))

# 単位円を描画
theta = np.linspace(0, 2 * np.pi, 200)
ax.plot(np.cos(theta), np.sin(theta), 'k-', linewidth=0.5)

# 零点と極をプロット
ax.scatter(zeros.real, zeros.imag, marker='o', s=100,
           facecolors='none', edgecolors='blue', linewidths=2, label='Zeros')
ax.scatter(poles.real, poles.imag, marker='x', s=100,
           color='red', linewidths=2, label='Poles')

ax.set_xlabel('Real')
ax.set_ylabel('Imaginary')
ax.set_title(f'Pole-Zero Plot (f0={f0} Hz, Q={Q})')
ax.set_aspect('equal')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

# 極の半径を表示
print(f"極の半径 r = {np.abs(poles[0]):.6f}")
print(f"零点の半径 = {np.abs(zeros[0]):.6f}")

実行結果は以下の通りです。

極の半径 r = 0.994778
零点の半径 = 1.000000

零点の半径は浮動小数点誤差の範囲で厳密に1(単位円上)であり、極の半径は式(6)から導かれる \(r=\sqrt{2G_0-1}=0.994778\) と一致します。両者がほぼ同じ角度 \(\omega_0\) にあるため、ノッチ周波数以外のゲインはほぼ1(0 dB)に保たれます。

ノッチフィルタ(f0=50Hz, Q=30)の零点・極配置図。零点(○)は単位円上、極(×)は半径0.994778の位置にある

scipy.signal.iirnotch による設計

SciPyの scipy.signal.iirnotch を使えば、ノッチフィルタの設計が簡潔に行えます。

from scipy.signal import iirnotch

# パラメータ
f0 = 50   # ノッチ周波数 [Hz]
Q = 30    # 品質係数
fs = 1000 # サンプリング周波数 [Hz]

# フィルタ係数の計算
b, a = iirnotch(f0, Q, fs)
  • f0: 除去したい周波数 [Hz]
  • Q: 品質係数(大きいほど狭いノッチ)
  • fs: サンプリング周波数 [Hz]
  • 戻り値: 分子係数 b、分母係数 a(2次IIRフィルタ)

以下のコードで、異なる \(Q\) 値における周波数応答を比較します。

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

fs = 1000
f0 = 50

fig, ax = plt.subplots(figsize=(10, 6))

for Q in [5, 15, 30, 60]:
    b, a = signal.iirnotch(f0, Q, fs)
    w, h = signal.freqz(b, a, worN=4096, fs=fs)
    ax.plot(w, 20 * np.log10(np.maximum(np.abs(h), 1e-12)),
            label=f'Q = {Q}')

ax.set_xlabel('Frequency [Hz]')
ax.set_ylabel('Magnitude [dB]')
ax.set_title('Notch Filter Frequency Response (f0 = 50 Hz)')
ax.set_xlim(0, 200)
ax.set_ylim(-60, 5)
ax.axvline(f0, color='gray', linestyle='--', alpha=0.5, label=f'f0 = {f0} Hz')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

\(Q\) が大きいほどノッチが狭く急峻になり、対象周波数のみをピンポイントで除去できることがわかります。実際に freqz の出力から \(-3\) dB帯域幅を測定すると、\(Q=5\) で \(10.01\) Hz、\(Q=15\) で \(3.36\) Hz、\(Q=30\) で \(1.65\) Hz、\(Q=60\) で \(0.85\) Hz となり、\(Q\) にほぼ反比例して帯域幅が狭くなることが確認できます(前節の表の実測 \(Q\) 列と対応)。

Q値の違いによるノッチフィルタの周波数応答比較(f0=50Hz)。Qが大きいほどノッチが狭く深くなる

実践:電源ノイズ除去のPython実装

実際の電源ノイズ除去を想定した完全なコード例を示します。50Hzの基本波に加え、100Hz・150Hzの高調波も同時に除去します。

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

# --- テスト信号の生成 ---
np.random.seed(42)
fs = 1000           # サンプリング周波数 [Hz]
T = 2.0             # 信号長 [s]
t = np.arange(0, T, 1/fs)
N = len(t)

# 有用信号: 10Hzと30Hzの正弦波
useful_signal = np.sin(2 * np.pi * 10 * t) + 0.5 * np.sin(2 * np.pi * 30 * t)

# 電源ノイズ: 50Hz基本波 + 100Hz, 150Hzの高調波
power_noise = (0.8 * np.sin(2 * np.pi * 50 * t)
               + 0.4 * np.sin(2 * np.pi * 100 * t)
               + 0.2 * np.sin(2 * np.pi * 150 * t))

# ランダムノイズ
random_noise = 0.3 * np.random.randn(N)

# 観測信号
observed = useful_signal + power_noise + random_noise

# --- カスケード接続のノッチフィルタ設計 ---
Q = 30
notch_freqs = [50, 100, 150]  # 基本波 + 高調波

# 各ノッチ周波数のフィルタ係数をSOS形式で結合
sos_list = []
for f_notch in notch_freqs:
    b, a = signal.iirnotch(f_notch, Q, fs)
    sos = signal.tf2sos(b, a)
    sos_list.append(sos[0])

sos_cascade = np.array(sos_list)

# --- フィルタ適用(ゼロ位相フィルタリング) ---
filtered = signal.sosfiltfilt(sos_cascade, observed)

# --- 周波数スペクトルの計算 ---
freqs = np.fft.rfftfreq(N, 1/fs)
spectrum_before = np.abs(np.fft.rfft(observed)) / N
spectrum_after = np.abs(np.fft.rfft(filtered)) / N

# --- プロット ---
fig, axes = plt.subplots(3, 1, figsize=(10, 10))

# (a) 時間領域: フィルタ適用前
axes[0].plot(t, observed, alpha=0.7, label='Observed')
axes[0].plot(t, useful_signal, 'k--', alpha=0.5, label='True signal')
axes[0].set_xlabel('Time [s]')
axes[0].set_ylabel('Amplitude')
axes[0].set_title('Before Notch Filtering')
axes[0].set_xlim(0, 0.5)
axes[0].legend()
axes[0].grid(True, alpha=0.3)

# (b) 時間領域: フィルタ適用後
axes[1].plot(t, filtered, alpha=0.7, label='Filtered')
axes[1].plot(t, useful_signal, 'k--', alpha=0.5, label='True signal')
axes[1].set_xlabel('Time [s]')
axes[1].set_ylabel('Amplitude')
axes[1].set_title('After Notch Filtering (50, 100, 150 Hz)')
axes[1].set_xlim(0, 0.5)
axes[1].legend()
axes[1].grid(True, alpha=0.3)

# (c) 周波数領域: 前後比較
axes[2].plot(freqs, 20 * np.log10(np.maximum(spectrum_before, 1e-12)),
             alpha=0.7, label='Before')
axes[2].plot(freqs, 20 * np.log10(np.maximum(spectrum_after, 1e-12)),
             alpha=0.7, label='After')
for f_notch in notch_freqs:
    axes[2].axvline(f_notch, color='red', linestyle='--', alpha=0.3)
axes[2].set_xlabel('Frequency [Hz]')
axes[2].set_ylabel('Magnitude [dB]')
axes[2].set_title('Frequency Spectrum: Before vs After')
axes[2].set_xlim(0, 200)
axes[2].legend()
axes[2].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

上記コードに以下の検証コードを追加し、フィルタ適用前後の定量的な効果を実測します。

# --- 定量評価 ---
rmse_before = np.sqrt(np.mean((observed - useful_signal) ** 2))
rmse_after = np.sqrt(np.mean((filtered - useful_signal) ** 2))
snr_before = 10 * np.log10(np.mean(useful_signal**2) / np.mean((observed - useful_signal) ** 2))
snr_after = 10 * np.log10(np.mean(useful_signal**2) / np.mean((filtered - useful_signal) ** 2))
print(f"RMSE: before={rmse_before:.5f}, after={rmse_after:.5f}")
print(f"SNR : before={snr_before:.3f} dB, after={snr_after:.3f} dB")

for f_notch in notch_freqs:
    idx = np.argmin(np.abs(freqs - f_notch))
    atten_db = 20 * np.log10(spectrum_after[idx] / spectrum_before[idx])
    print(f"  {f_notch} Hz 減衰量 = {atten_db:.2f} dB")

for f_useful in [10, 30]:
    idx = np.argmin(np.abs(freqs - f_useful))
    change_db = 20 * np.log10(spectrum_after[idx] / spectrum_before[idx])
    print(f"  有用信号 {f_useful} Hz の変化 = {change_db:.3f} dB")

実行結果は以下の通りです。

RMSE: before=0.70573, after=0.29683
SNR : before=0.986 dB, after=8.509 dB
  50 Hz 減衰量 = -26.67 dB
  100 Hz 減衰量 = -30.49 dB
  150 Hz 減衰量 = -33.32 dB
  有用信号 10 Hz の変化 = -0.001 dB
  有用信号 30 Hz の変化 = -0.011 dB

時間領域では、フィルタ適用後の信号が元の有用信号(10Hz + 30Hz)に近づいており、真値に対するRMSEは \(0.706 \to 0.297\) (約 \(58\%\) 減少)、SNRは \(0.99\) dB から \(8.51\) dB へと約 \(7.5\) dB改善しています。周波数領域では、50Hz・100Hz・150Hzの各ピークがそれぞれ \(-26.7\) dB、\(-30.5\) dB、\(-33.3\) dB減衰している一方、10Hzと30Hzの有用信号成分の変化はいずれも \(0.01\) dB未満に収まっており、対象周波数のみを選択的に除去できていることが定量的に確認できます。

電源ノイズ除去の前後比較。上段:フィルタ適用前の時間波形、中段:適用後の時間波形、下段:周波数スペクトルの前後比較(50/100/150Hzのピークが選択的に減衰)

設計上の注意点

品質係数Qの選択

  • Q が大きすぎる場合: ノッチが極端に狭くなり、わずかな周波数変動にも対応できません。また、過渡応答でリンギングが生じやすくなります。
  • Q が小さすぎる場合: ノッチが広くなり、対象周波数付近の有用な信号成分まで除去してしまいます。

実用的には \(Q = 20 \sim 50\) の範囲が電源ノイズ除去に適しています。

エッジケース:商用電源周波数のドリフトに対する脆弱性

これまでの例では電源ノイズをちょうど50Hzと仮定してきましたが、実際の商用電源周波数は需給バランスにより常に微小変動しており、日本の電力系統でも定格からずれることがあります。設計時の \(f_0\) と実際のノイズ周波数がわずかにずれた場合、高 \(Q\) のノッチほど急激に減衰量を失う点は見落とされがちな重要な弱点です。

import numpy as np
from scipy import signal

fs = 1000
f0_design = 50.0
drift_range = np.linspace(-1.0, 1.0, 201)  # 設計周波数からのずれ [Hz]

for Q in [10, 30, 60, 100]:
    b, a = signal.iirnotch(f0_design, Q, fs)
    for d in [0.1, 0.2, 0.5, 1.0]:
        idx = np.argmin(np.abs(drift_range - d))
        w, h = signal.freqz(b, a, worN=[2 * np.pi * (f0_design + d) / fs])
        atten_db = 20 * np.log10(np.abs(h[0]))
        print(f"Q={Q:3d}, drift={d:+.1f}Hz -> 減衰量 = {atten_db:.2f} dB")

実行結果(抜粋)は以下の通りです。

設計からのずれ\(Q=10\)\(Q=30\)\(Q=60\)\(Q=100\)
\(+0.1\) Hz\(-27.97\) dB\(-18.49\) dB\(-12.65\) dB\(-8.61\) dB
\(+0.2\) Hz\(-21.98\) dB\(-12.65\) dB\(-7.29\) dB\(-4.10\) dB
\(+0.5\) Hz\(-14.19\) dB\(-5.80\) dB\(-2.31\) dB\(-0.98\) dB
\(+1.0\) Hz\(-8.68\) dB\(-2.32\) dB\(-0.71\) dB\(-0.27\) dB

設計中心からわずか \(0.2\) Hzずれただけで、\(Q=100\) の減衰量は \(-4.10\) dBまで低下し、電源ノイズ除去としてはほぼ機能しなくなります。一方 \(Q=10\) は同じずれでも \(-21.98\) dBの減衰を維持します。つまりQを上げるほどノッチ周波数における理論上の減衰は深くなるが、その代償として実際の周波数変動に対する頑健性を失うというトレードオフが定量的に確認できます。

商用電源周波数のドリフトに対するノッチ減衰量の感度。Qが高いほど設計周波数からのずれに対して急激に減衰量を失う

この問題への実務的な対策としては、(1) 実測ノイズ周波数をゼロクロス法やFFTで推定し \(f_0\) を追従させる、(2) 適度に低い \(Q\) (\(20\sim30\) 程度)で頑健性を確保する、(3) 適応ノッチフィルタで中心周波数自体をオンラインに更新する、といった手法が使われます。実際、Mir & Singh (2024) は変分モード分解(VMD)でノイズ成分を狭帯域モードに分解し、その中心周波数の推定値に追従して通過帯域を動的に再設計する可変ノッチフィルタを提案し、固定周波数のノッチ設計よりも高いSNR改善を報告しています。また、アナログ回路側でも、Choubey et al. (2024) がVDCC(voltage differencing current conveyor)を用いた4次カスケードのコムフィルタ(50/150/250/350Hzの基本波・奇数次高調波を同時除去)を提案しており、単一ノッチで \(-45.7\) dBの減衰を実現しています。本記事のカスケード構成(式(2)を50/100/150Hzに適用)は、こうした研究で扱われるアナログ実装の考え方をデジタル領域で再現したものと位置づけられます。

ゼロ位相フィルタリングと因果フィルタリング

  • オフライン処理: scipy.signal.sosfiltfilt(ゼロ位相)を使用します。順方向・逆方向の2回フィルタリングにより位相歪みがゼロになります。
  • リアルタイム処理: scipy.signal.sosfilt(因果フィルタ)を使用します。未来のデータを参照できないため位相遅れが生じますが、逐次処理が可能です。

安定性

IIR型ノッチフィルタは、極の半径 \(r < 1\) であれば常に安定です。scipy.signal.iirnotch で設計したフィルタは自動的にこの条件を満たすため、安定性を個別に検証する必要はありません。

エッジケース:固定小数点実装における係数量子化

ただし、この安定性の保証は無限精度(倍精度浮動小数点)演算を前提としています。組み込みDSPやFPGAで固定小数点実装する場合、分母の係数(式(6)の \(a_2\) など)を有限ビット幅に丸める必要があり、高 \(Q\) ほど \(r\) が1に極めて近くなるため、量子化誤差が安定性マージンを食いつぶすリスクが高まります。

import numpy as np
from scipy import signal

fs = 1000
f0 = 50

def quantize(coeffs, bits):
    """[-2, 2) の範囲を bits ビットの固定小数点に丸める簡易モデル"""
    scale = 2 ** (bits - 2)
    return np.round(coeffs * scale) / scale

for Q in [30, 100, 300]:
    b, a = signal.iirnotch(f0, Q, fs)
    r_orig = np.max(np.abs(np.roots(a)))
    print(f"Q={Q}: 元の極半径 r = {r_orig:.6f}")
    for bits in [16, 10, 8]:
        a_q = quantize(a, bits)
        r_q = np.max(np.abs(np.roots(a_q)))
        print(f"  {bits}bit量子化後: r = {r_q:.6f}, 安定 = {r_q < 1.0}")

実行結果は以下の通りです。

\(Q\)元の \(r\)16bit量子化後10bit量子化後8bit量子化後
300.9947780.994768(安定)0.994123(安定)0.992157(安定)
1000.9984300.998442(安定)0.998045(安定)1.000000(不安定)
3000.9994770.999481(安定)1.000000(不安定)1.000000(不安定)

\(Q=30\) は8bit量子化でも安定を保ちますが、\(Q=100\) は8bit量子化で極半径がちょうど1に達して不安定化し、\(Q=300\) ではさらに緩い10bit量子化でも不安定化します。これは、高 \(Q\) 設計では極が単位円のごく近傍(\(1-r\) が \(10^{-3}\) 以下のオーダー)に位置するため、係数の丸め誤差がそのまま安定余裕を吸収してしまうためです。組み込み実装で高 \(Q\) のノッチフィルタを使う場合は、直接形(Direct Form)ではなく数値誤差に強い格子構造(lattice)や状態空間表現を用いる、あるいは倍精度以上のビット幅を確保するといった対策が必要です。

ノッチフィルタ vs バンドストップフィルタ

ノッチフィルタは帯域除去フィルタの特殊なケースですが、両者は使い分けが必要です。

特性ノッチフィルタバンドストップフィルタ
除去帯域非常に狭い(1つの周波数)比較的広い帯域
次数2次で実現可能高次数が必要な場合あり
用途電源ノイズ、特定の干渉信号除去周波数帯域全体の除去
設計iirnotchbutter + btype='bandstop'

以下に両者の周波数応答を比較するコードを示します。

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

fs = 1000
f0 = 50

# ノッチフィルタ(Q=30)
b_notch, a_notch = signal.iirnotch(f0, Q=30, fs=fs)
w_notch, h_notch = signal.freqz(b_notch, a_notch, worN=4096, fs=fs)

# バンドストップフィルタ(バターワース4次、40-60Hz除去)
sos_bs = signal.butter(4, [40, 60], btype='bandstop', fs=fs, output='sos')
w_bs, h_bs = signal.sosfreqz(sos_bs, worN=4096, fs=fs)

fig, ax = plt.subplots(figsize=(10, 6))
ax.plot(w_notch, 20 * np.log10(np.maximum(np.abs(h_notch), 1e-12)),
        label='Notch (Q=30)')
ax.plot(w_bs, 20 * np.log10(np.maximum(np.abs(h_bs), 1e-12)),
        label='Bandstop Butterworth (40-60 Hz)')
ax.set_xlabel('Frequency [Hz]')
ax.set_ylabel('Magnitude [dB]')
ax.set_title('Notch Filter vs Bandstop Filter')
ax.set_xlim(0, 200)
ax.set_ylim(-60, 5)
ax.axvline(f0, color='gray', linestyle='--', alpha=0.5)
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

ノッチフィルタは50Hzのみをピンポイントで除去するのに対し、バンドストップフィルタは40〜60Hzの帯域全体を抑制します。除去したい周波数が明確な場合はノッチフィルタ、ある帯域を広く除去したい場合はバンドストップフィルタが適しています。

まとめ

  • ノッチフィルタは特定の周波数のみを除去する帯域除去フィルタの特殊なケースで、電源ノイズ除去に最適である
  • 2次IIR型の伝達関数は、単位円上の零点でノッチ周波数を完全に除去し、内側のでノッチ幅を制御する
  • 品質係数 \(Q\) が大きいほどノッチが狭くなり、\(Q = 20 \sim 50\) が電源ノイズ除去の実用的な範囲である
  • scipy.signal.iirnotch で簡潔に設計でき、高調波(100Hz、150Hz等)にはカスケード接続で対応できる
  • オフライン処理では sosfiltfilt(ゼロ位相)、リアルタイム処理では sosfilt(因果フィルタ)を使い分ける
  • 極半径 \(r\) と \(Q\) の厳密な関係は \(r=\sqrt{2G_0-1}\) (\(G_0=1/(1+\tan(\Delta\omega/2))\) )であり、教科書的な近似式 \(Q\approx\omega_0/(2(1-r))\) は高 \(Q\) 極限での1次近似(実測誤差は \(Q=30\) で \(+0.46\%\) )である
  • 高 \(Q\) ノッチは商用電源周波数のわずかなドリフト(\(0.2\) Hzのずれで\(Q=100\) の減衰量は\(-4.10\) dBまで低下)や固定小数点実装の係数量子化(\(Q=100\) は8bit量子化で不安定化)に対して脆弱であり、頑健性重視なら \(Q=20\sim30\) 程度に抑えるか適応ノッチ化を検討する

関連記事

参考文献

  • Oppenheim, A. V., & Schafer, R. W. (2009). Discrete-Time Signal Processing (3rd ed.). Prentice Hall.
  • Haykin, S., & Van Veen, B. (2002). Signals and Systems (2nd ed.). Wiley.
  • Orfanidis, S. J. (1996). Introduction to Signal Processing. Prentice-Hall.(scipy.signal.iirnotch の \(-3\) dB整合設計はこの文献の式11.3.4/11.3.19/11.3.6/11.3.7/11.3.21に基づく)
  • Mir, H. Y., & Singh, B. (2024). Powerline interference reduction in ECG signals using variable notch filter designed via variational mode decomposition. Analog Integrated Circuits and Signal Processing, 118, 317–328. https://link.springer.com/article/10.1007/s10470-023-02200-9
  • Choubey, C. K., Kumar, S., & Pippal, S. K. (2024). Design method for VDCC-based analog comb filter for power line interference cancellation. MethodsX. https://pmc.ncbi.nlm.nih.gov/articles/PMC10912721/
  • SciPy signal.iirnotch documentation
  • SciPy Signal Processing documentation