位相スペクトルと群遅延の理論とPython実装

位相スペクトル・群遅延の数学的定義と物理的意味、FIRフィルタの線形位相特性、IIRフィルタの非線形位相問題、全域通過フィルタによる群遅延等化をPythonで解説します。

はじめに

FFT(高速フーリエ変換)でスペクトル解析を行うと、信号の各周波数成分が「どれだけのパワーを持つか」を表す振幅スペクトルが得られます。しかし、フーリエ変換の出力は複素数であり、振幅だけでなく位相スペクトルも同時に含まれています。

位相スペクトルは、実用上どのような意味を持つのでしょうか。

例えば、バターワースフィルタを音声信号に適用すると、「振幅特性は理想に近いのにフィルタ後の波形が歪んでいる」という現象が生じることがあります。これは位相の非線形性、すなわち周波数ごとに遅延量が異なることで引き起こされます。この「周波数依存の遅延」を定量的に表す概念が**群遅延(Group Delay)**です。

本記事では、位相スペクトルと群遅延の数学的定義・物理的解釈から、FIRフィルタとIIRフィルタの位相特性の違い、そして全域通過フィルタ(All-pass filter)による群遅延等化まで、Pythonを用いて体系的に解説します。

位相スペクトルの定義

複素周波数応答と位相

デジタルフィルタの周波数応答 \(H(e^{j\omega})\) は一般に複素数です。\(\omega\) は正規化角周波数(\(0 \leq \omega \leq \pi\) 、\(\omega = \pi\) がナイキスト周波数に対応)です。

この複素数を極形式で表すと:

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

ここで:

  • \(|H(e^{j\omega})|\) :振幅応答(Magnitude Response) — 各周波数成分の増幅率
  • \(\phi(\omega) = \angle H(e^{j\omega})\) :位相応答(Phase Response) — 各周波数成分の位相シフト量

位相応答は次式で計算されます。

\[\phi(\omega) = \angle H(e^{j\omega}) = \arctan\left(\frac{\mathrm{Im}[H(e^{j\omega})]}{\mathrm{Re}[H(e^{j\omega})]}\right) \tag{2}\]

折り返し位相(Wrapped Phase)とアンラップ位相(Unwrapped Phase)

\(\arctan\) 関数の値域は \((-\pi, \pi]\) に限られるため、\(\phi(\omega)\) を素直に計算すると位相が \(\pm\pi\) で不連続に折り返す「折り返し位相(wrapped phase)」が得られます。

例えば、連続的に変化するはずの位相が \(-3.1 \to -3.14 \to 3.12 \to 3.1\) のように見える場合、\(2\pi\) の不連続跳びが発生しています。

**アンラップ位相(unwrapped phase)**は、この不連続跳びを \(2\pi\) の整数倍の補正を加えることで滑らかな連続関数に変換したものです。

\[\phi_{\text{unwrap}}(\omega) = \phi(\omega) + 2\pi k(\omega), \quad k(\omega) \in \mathbb{Z} \tag{3}\]

Pythonでは np.unwrap() が自動的にこの処理を行います。群遅延を正確に計算するためにはアンラップ位相が必須です。

線形位相と非線形位相

フィルタの位相特性として特に重要なのが線形位相です。

\[\phi(\omega) = -\omega \cdot \tau_0 \tag{4}\]

このとき、位相は角周波数 \(\omega\) に比例して変化します。\(\tau_0\) (定数)は**位相遅延(Phase Delay)**と呼ばれます。

線形位相フィルタでは、すべての周波数成分が同じ時間だけ遅延するため、波形の形状が保たれます(単純な時間シフトのみが生じる)。一方、位相が \(\omega\) の非線形関数となる非線形位相の場合、周波数ごとに異なる遅延が生じ、複数の周波数成分からなる信号では波形が歪みます。

群遅延の定義と物理的解釈

数学的定義

**群遅延(Group Delay)**は位相応答の周波数に関する微分(符号を反転したもの)として定義されます。

\[\tau_g(\omega) = -\frac{d\phi(\omega)}{d\omega} \tag{5}\]

群遅延の単位はサンプル数(または秒)です。\(\tau_g(\omega)\) が \(\omega\) に依存する場合を非線形位相、\(\tau_g(\omega) = \tau_0\) (定数)の場合を線形位相と言います。

なお、式 \((4)\) の線形位相条件 \(\phi(\omega) = -\omega \cdot \tau_0\) を代入すると:

\[\tau_g(\omega) = -\frac{d(-\omega \cdot \tau_0)}{d\omega} = \tau_0 \tag{6}\]

となり、線形位相フィルタでは群遅延が一定値 \(\tau_0\) に等しいことが確認できます。

物理的解釈:信号の遅延

群遅延の物理的意味を理解するために、帯域制限された信号(変調波)を考えます。搬送波 \(\cos(\omega_0 t)\) に緩やかに変化する包絡線 \(a(t)\) を乗じた信号

\[x(t) = a(t) \cos(\omega_0 t)\]

をフィルタに通した場合、フィルタ出力の包絡線は以下のように遅延します:

\[y(t) \approx |H(e^{j\omega_0})| \cdot a(t - \tau_g(\omega_0)) \cdot \cos(\omega_0 t - \phi(\omega_0)) \tag{7}\]

ここで包絡線の遅延量は \(\tau_g(\omega_0)\) (群遅延)で与えられ、搬送波の位相シフトは \(\phi(\omega_0)\) (位相応答)で与えられます。

したがって群遅延は「ある周波数帯域の信号パケット(波束)が何サンプル(あるいは何秒)遅れて出力されるか」を表す指標です。

群遅延の歪みが引き起こす問題

群遅延が周波数によって大きく変化する場合(群遅延歪み)、複数の周波数成分からなる信号では以下の問題が生じます:

  • 波形歪み:例えば方形波は多数の高調波成分から構成されますが、各成分の遅延が異なると合成波形が変形します
  • エコー感・金属的音質:音声・音楽信号での位相歪みは知覚的な劣化を引き起こします
  • 制御系の不安定化:フィードバック制御系では群遅延の周波数依存性が安定性マージンに影響します
  • 通信系の符号間干渉:デジタル通信では群遅延歪みが隣接シンボルへの干渉(ISI)を引き起こします

FIRフィルタの線形位相特性

対称係数が線形位相をもたらす理由

FIRフィルタの伝達関数は以下のように書けます:

\[H(z) = \sum_{n=0}^{N-1} h[n] z^{-n} \tag{8}\]

このとき、インパルス応答 \(h[n]\) が**対称(symmetric)**な場合、すなわち

\[h[n] = h[N-1-n], \quad n = 0, 1, \ldots, N-1 \tag{9}\]

を満たすとき(Type I または Type II FIR)、フィルタは厳密な線形位相特性を持ちます。

証明の概略:

対称条件 \((9)\) を用いると、\(z = e^{j\omega}\) を代入した周波数応答は:

\[H(e^{j\omega}) = e^{-j\omega(N-1)/2} \cdot A(\omega) \tag{10}\]

の形に因数分解できます。ここで \(A(\omega)\) は実数値の**振幅関数(Amplitude Function)**です。したがって位相応答は:

\[\phi(\omega) = -\frac{N-1}{2}\omega + \begin{cases} 0 & (A(\omega) > 0) \\ \pi & (A(\omega) < 0) \end{cases} \tag{11}\]

\(A(\omega) > 0\) の通過域では \(\phi(\omega) = -\frac{N-1}{2}\omega\) (純粋な線形位相)となり、群遅延は一定値:

\[\tau_g = \frac{N-1}{2} \tag{12}\]

(単位:サンプル)になります。\(N-1\) 次(\(N\) タップ)の FIR フィルタでは群遅延は \(\frac{N-1}{2}\) サンプルです。

4種類の線形位相FIRフィルタ

係数の対称性とフィルタ長(奇数/偶数)によって4つのタイプに分類されます。

タイプ係数の対称性フィルタ長適用例制約
I偶対称 \(h[n] = h[N-1-n]\)奇数LPF, HPF, BPFなし(汎用)
II偶対称 \(h[n] = h[N-1-n]\)偶数LPF\(H(\pi) = 0\) (HPF不可)
III奇対称 \(h[n] = -h[N-1-n]\)奇数微分器、HT\(H(0) = H(\pi) = 0\)
IV奇対称 \(h[n] = -h[N-1-n]\)偶数HPF、微分器\(H(0) = 0\)

sosfiltfilt によるゼロ位相FIRフィルタリング

通常の因果FIRフィルタには群遅延 \(\frac{N-1}{2}\) サンプルの遅延が生じます。オフライン処理では 前後の2パス処理(forward-backward filtering) を用いることで群遅延をゼロにできます:

\[H_{\text{zero-phase}}(e^{j\omega}) = |H(e^{j\omega})|^2 \tag{13}\]

scipy.signal.filtfilt または scipy.signal.sosfiltfilt がこの処理を実装しています。

IIRフィルタの非線形位相

なぜIIRフィルタは非線形位相になるのか

IIRフィルタは有理伝達関数(フィードバック項を持つ)として表されます:

\[H(z) = \frac{B(z)}{A(z)} = \frac{\sum_{k=0}^{M} b_k z^{-k}}{1 + \sum_{k=1}^{N} a_k z^{-k}} \tag{14}\]

IIRフィルタは z 平面上の単位円内に極を持ちます。各極 \(p_k\) は位相応答に寄与し:

\[\phi(\omega) = \sum_k \angle (e^{j\omega} - z_k) - \sum_k \angle (e^{j\omega} - p_k) \tag{15}\]

ここで各項は単位円上の点 \(e^{j\omega}\) から零点・極への偏角です。極の配置が非対称(単位円に対して対称でない)であるため、位相応答は一般に \(\omega\) の非線形関数になります。

代表的なIIRフィルタの位相特性比較

フィルタ遷移帯域通過域群遅延用途
バターワース緩やか比較的滑らか汎用(波形保存重視)
チェビシェフI型急峻やや歪む振幅優先
チェビシェフII型急峻比較的滑らか阻止域優先
楕円(Cauer)最急峻大きく歪む最小次数設計
ベッセル緩やか最大限に平坦波形保存最優先

ベッセルフィルタは最大平坦群遅延(Maximally Flat Group Delay)を目標に設計されており、通過域での群遅延歪みが最小になります。ただし振幅の遷移帯域は緩やかです。

IIRフィルタの位相歪みの実害

通過域での振幅特性が平坦でも位相歪みが問題になる例:

  • 音声コーデック:バターワースで高域をフィルタリングすると、位相歪みにより自然音の過渡応答(子音の立ち上がり)が変化する
  • ECG信号処理:心電図の QRS 波形はシャープな過渡波形であり、位相歪みで波形形状が変化して診断精度が落ちる
  • オーディオイコライザ:スピーカーの特性補正で複数のIIRバンドフィルタを直列に接続すると群遅延が蓄積し、過渡応答が劣化する

全域通過フィルタによる群遅延等化

全域通過フィルタとは

**全域通過フィルタ(All-Pass Filter, APF)**は、振幅応答が全周波数で一定(\(|H_{AP}(e^{j\omega})| = 1\) )でありながら、位相応答(群遅延)を自由に制御できるフィルタです。

1次全域通過フィルタの伝達関数:

\[H_{AP}^{(1)}(z) = \frac{z^{-1} - a}{1 - az^{-1}}, \quad |a| < 1 \tag{16}\]

2次全域通過フィルタ(複素極に対応)の伝達関数:

\[H_{AP}(z) = \frac{z^{-1} - a^*}{1 - az^{-1}} \tag{17}\]

ここで \(a = r e^{j\theta}\) (\(r < 1\) )は極の位置を決める複素パラメータ(\(a^*\) はその複素共役)です。実数係数で実装する場合は1次全域通過フィルタを複素共役対として組み合わせた2次全域通過フィルタを用います。

\[H_{AP}^{(2)}(z) = \frac{z^{-2} - 2r\cos\theta \cdot z^{-1} + r^2}{1 - 2r\cos\theta \cdot z^{-1} + r^2 z^{-2}} \tag{18}\]

全域通過フィルタの位相・群遅延特性

全域通過フィルタの位相応答(1次):

\[\phi_{AP}(\omega) = -\omega - 2\arctan\left(\frac{a\sin\omega}{1 - a\cos\omega}\right) \tag{19}\]

極の位置パラメータ \(a\) (\(0 < a < 1\) )を調整することで、\(\omega \approx 0\) (低周波)または \(\omega \approx \pi\) (高周波)付近の群遅延ピークを制御できます。\(a\) の絶対値が大きいほど群遅延ピークが鋭くなります。

IIRフィルタ + 全域通過フィルタによる群遅延等化

**群遅延等化(Phase Equalization)**の手順:

  1. 目標とするフィルタ(例:バターワース)の群遅延特性 \(\tau_g^{\text{target}}(\omega)\) を計算する
  2. 全域通過フィルタ \(\tau_g^{AP}(\omega)\) を設計し、合計群遅延 \(\tau_g^{\text{total}} = \tau_g^{\text{target}} + \tau_g^{AP}\) が周波数に対して平坦になるよう最適化する
  3. 元のIIRフィルタと全域通過フィルタを直列に接続して使用する

振幅特性は全域通過フィルタを追加しても変化しないため、振幅特性を維持しながら群遅延を改善できます。

Python実装

準備:ライブラリのインポート

import numpy as np
import matplotlib.pyplot as plt
from scipy.signal import butter, sosfilt, freqz, firwin, group_delay, lfilter, iirfilter

位相応答の計算と可視化

# --- フィルタ設計 ---
fs = 1000          # サンプリング周波数 [Hz]
fc = 100           # カットオフ周波数 [Hz]
N_iir = 8          # IIRフィルタ次数
N_fir = 64         # FIRフィルタタップ数

# バターワースIIRフィルタ(ba形式)
b_iir, a_iir = butter(N_iir, fc, fs=fs, output='ba')

# 線形位相FIRフィルタ(hamming窓)
b_fir = firwin(N_fir, fc, fs=fs, window='hamming')
a_fir = np.array([1.0])

# --- 周波数応答の計算 ---
worN = 4096
w, H_iir = freqz(b_iir, a_iir, worN=worN, fs=fs)
_, H_fir = freqz(b_fir, a_fir, worN=worN, fs=fs)

# --- 位相応答の計算 ---
phase_iir_wrapped = np.angle(H_iir)              # 折り返し位相
phase_iir = np.unwrap(np.angle(H_iir))           # アンラップ位相
phase_fir = np.unwrap(np.angle(H_fir))           # FIR(線形位相)

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

# 振幅応答
axes[0].plot(w, 20 * np.log10(np.abs(H_iir) + 1e-12), label=f'IIR Butterworth N={N_iir}')
axes[0].plot(w, 20 * np.log10(np.abs(H_fir) + 1e-12), label=f'FIR Hamming {N_fir} taps', linestyle='--')
axes[0].set_xlabel('Frequency [Hz]')
axes[0].set_ylabel('Magnitude [dB]')
axes[0].set_title('Magnitude Response')
axes[0].set_xlim(0, fs / 2)
axes[0].set_ylim(-80, 5)
axes[0].axvline(fc, color='gray', linestyle=':', alpha=0.5, label='Cutoff')
axes[0].legend()
axes[0].grid(True, alpha=0.3)

# 折り返し位相(IIRのみ)
axes[1].plot(w, np.degrees(phase_iir_wrapped), label='IIR (wrapped)', color='orange')
axes[1].plot(w, np.degrees(phase_iir), label='IIR (unwrapped)', color='blue')
axes[1].set_xlabel('Frequency [Hz]')
axes[1].set_ylabel('Phase [degrees]')
axes[1].set_title('Phase Response: Wrapped vs Unwrapped (IIR)')
axes[1].set_xlim(0, fs / 2)
axes[1].legend()
axes[1].grid(True, alpha=0.3)

# IIR vs FIR 位相比較
axes[2].plot(w, np.degrees(phase_iir), label=f'IIR Butterworth N={N_iir} (nonlinear)')
axes[2].plot(w, np.degrees(phase_fir), label=f'FIR Hamming {N_fir} taps (linear)', linestyle='--')
axes[2].set_xlabel('Frequency [Hz]')
axes[2].set_ylabel('Phase [degrees]')
axes[2].set_title('Phase Response: IIR vs FIR')
axes[2].set_xlim(0, fs / 2)
axes[2].legend()
axes[2].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

群遅延の計算と比較

# --- scipy.signal.group_delay による群遅延計算 ---
worN = 4096
w_gd, gd_iir = group_delay((b_iir, a_iir), w=worN, fs=fs)
w_gd, gd_fir = group_delay((b_fir, a_fir), w=worN, fs=fs)

# --- 差分近似による群遅延計算(教育的目的) ---
w_arr, H_iir_arr = freqz(b_iir, a_iir, worN=worN * 2, fs=fs)
omega_arr = w_arr * 2 * np.pi / fs  # Hz → rad/sample
phase_arr = np.unwrap(np.angle(H_iir_arr))

# τ_g(ω) ≈ -Δφ/Δω
gd_diff = -np.diff(phase_arr) / np.diff(omega_arr)
w_diff = (w_arr[:-1] + w_arr[1:]) / 2  # 中点

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

# IIR vs FIR 群遅延比較
axes[0].plot(w_gd, gd_iir, label=f'IIR Butterworth N={N_iir}', color='blue')
axes[0].plot(w_gd, gd_fir, label=f'FIR Hamming {N_fir} taps (constant = {(N_fir-1)/2:.1f})',
             linestyle='--', color='red')
axes[0].axhline((N_fir - 1) / 2, color='red', linestyle=':', alpha=0.5)
axes[0].set_xlabel('Frequency [Hz]')
axes[0].set_ylabel('Group Delay [samples]')
axes[0].set_title('Group Delay Comparison: IIR vs FIR')
axes[0].set_xlim(0, fs / 2)
axes[0].set_ylim(-5, 50)
axes[0].legend()
axes[0].grid(True, alpha=0.3)

# 差分近似による群遅延(IIR)
axes[1].plot(w_diff, gd_diff, label='IIR (finite difference approximation)', color='purple', alpha=0.7)
axes[1].plot(w_gd, gd_iir, label='IIR (scipy.signal.group_delay)', color='blue', linestyle='--')
axes[1].set_xlabel('Frequency [Hz]')
axes[1].set_ylabel('Group Delay [samples]')
axes[1].set_title('Group Delay: Finite Difference vs scipy.signal.group_delay')
axes[1].set_xlim(0, fs / 2)
axes[1].set_ylim(-5, 50)
axes[1].legend()
axes[1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

FIRフィルタの群遅延が一定値 \(\frac{N-1}{2} = 31.5\) サンプルに保たれているのに対し、バターワースIIRフィルタは通過域から遷移域にかけて群遅延が大きく変化することが確認できます。

位相歪みによる波形変形の実証

# --- テスト信号の生成 ---
fs = 1000
t = np.arange(0, 0.5, 1/fs)

# 複数の周波数成分からなる矩形パルス的な信号
# (10Hz + 30Hz + 50Hz + 70Hz + 90Hz の重ね合わせ)
freqs_sig = [10, 30, 50, 70, 90]
x = sum(np.sin(2 * np.pi * f * t) / f for f in freqs_sig)

# --- フィルタリング ---
fc_test = 200  # カットオフを高くして通過させる

# IIRフィルタ(非線形位相)
b_iir_test, a_iir_test = butter(6, fc_test, fs=fs, output='ba')
y_iir = lfilter(b_iir_test, a_iir_test, x)

# FIRフィルタ(線形位相)
b_fir_test = firwin(101, fc_test, fs=fs, window='hamming')
y_fir = lfilter(b_fir_test, [1.0], x)

# FIRの群遅延補正(位相を合わせるためのサンプルシフト)
fir_delay = 50  # (101 - 1) / 2 = 50 サンプル
y_fir_aligned = np.concatenate([y_fir[fir_delay:], np.zeros(fir_delay)])

# --- プロット ---
fig, axes = plt.subplots(3, 1, figsize=(12, 8), sharex=True)

axes[0].plot(t, x, 'k', linewidth=1.5)
axes[0].set_title('Original Signal (sum of 10, 30, 50, 70, 90 Hz)')
axes[0].set_ylabel('Amplitude')
axes[0].grid(True, alpha=0.3)

axes[1].plot(t, y_iir, color='blue')
axes[1].plot(t, x, 'k--', alpha=0.3, linewidth=1, label='Original')
axes[1].set_title('After IIR Butterworth (nonlinear phase — waveform distortion)')
axes[1].set_ylabel('Amplitude')
axes[1].legend()
axes[1].grid(True, alpha=0.3)

axes[2].plot(t, y_fir_aligned, color='red')
axes[2].plot(t, x, 'k--', alpha=0.3, linewidth=1, label='Original')
axes[2].set_title('After FIR (linear phase, delay-corrected — shape preserved)')
axes[2].set_xlabel('Time [s]')
axes[2].set_ylabel('Amplitude')
axes[2].legend()
axes[2].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

全域通過フィルタの設計と群遅延等化

def allpass_filter_2nd(r, theta):
    """
    2次全域通過フィルタ係数の計算。

    H_AP(z) = (r^2 - 2r*cos(θ)*z^{-1} + z^{-2}) / (1 - 2r*cos(θ)*z^{-1} + r^2*z^{-2})

    Parameters
    ----------
    r : float
        極の半径(0 < r < 1)
    theta : float
        極の角度 [rad]

    Returns
    -------
    b, a : ndarray
        フィルタ係数(b = 分子, a = 分母)
    """
    c = 2 * r * np.cos(theta)
    r2 = r ** 2
    b = np.array([r2, -c, 1.0])
    a = np.array([1.0, -c, r2])
    return b, a


# --- 全域通過フィルタの位相・群遅延特性の可視化 ---
worN = 4096
fig, axes = plt.subplots(3, 1, figsize=(10, 10))

r_values = [0.5, 0.7, 0.9]
theta_ap = np.pi / 4  # 極の角度(ω = π/4 付近に群遅延ピーク)

for r in r_values:
    b_ap, a_ap = allpass_filter_2nd(r, theta_ap)
    w_ap, H_ap = freqz(b_ap, a_ap, worN=worN, fs=fs)
    w_ap_gd, gd_ap = group_delay((b_ap, a_ap), w=worN, fs=fs)

    # 振幅応答(常に 0 dB)
    axes[0].plot(w_ap, 20 * np.log10(np.abs(H_ap) + 1e-12), label=f'r={r}')

    # 位相応答
    axes[1].plot(w_ap, np.degrees(np.unwrap(np.angle(H_ap))), label=f'r={r}')

    # 群遅延
    axes[2].plot(w_ap_gd, gd_ap, label=f'r={r}')

axes[0].set_title('All-Pass Filter: Magnitude Response (should be 0 dB)')
axes[0].set_xlabel('Frequency [Hz]')
axes[0].set_ylabel('Magnitude [dB]')
axes[0].set_ylim(-3, 3)
axes[0].legend()
axes[0].grid(True, alpha=0.3)

axes[1].set_title('All-Pass Filter: Phase Response')
axes[1].set_xlabel('Frequency [Hz]')
axes[1].set_ylabel('Phase [degrees]')
axes[1].legend()
axes[1].grid(True, alpha=0.3)

axes[2].set_title('All-Pass Filter: Group Delay (controllable peak)')
axes[2].set_xlabel('Frequency [Hz]')
axes[2].set_ylabel('Group Delay [samples]')
axes[2].set_ylim(0, 30)
axes[2].legend()
axes[2].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()
# --- バターワース + 全域通過フィルタによる群遅延等化 ---
fs = 1000
fc = 100
N_butter = 6

b_butter, a_butter = butter(N_butter, fc, fs=fs, output='ba')
w_hz, gd_butter = group_delay((b_butter, a_butter), w=4096, fs=fs)

# 全域通過フィルタを設計して群遅延を平坦化
# バターワースの群遅延ピーク付近(カットオフ近傍)を補正するAPF
# 複数のAPFを直列に接続
r_ap = 0.65
theta_ap1 = 2 * np.pi * (fc * 0.9) / fs  # カットオフより少し低い周波数
theta_ap2 = 2 * np.pi * (fc * 0.7) / fs

b_ap1, a_ap1 = allpass_filter_2nd(r_ap, theta_ap1)
b_ap2, a_ap2 = allpass_filter_2nd(r_ap, theta_ap2)

# 直列接続の係数(畳み込みで合成)
b_total = np.convolve(np.convolve(b_butter, b_ap1), b_ap2)
a_total = np.convolve(np.convolve(a_butter, a_ap1), a_ap2)

w_hz, gd_total = group_delay((b_total, a_total), w=4096, fs=fs)
w_hz, gd_ap1 = group_delay((b_ap1, a_ap1), w=4096, fs=fs)
w_hz, gd_ap2 = group_delay((b_ap2, a_ap2), w=4096, fs=fs)

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

# 振幅応答(APFは振幅に影響しない)
w_hz_mag, H_butter = freqz(b_butter, a_butter, worN=4096, fs=fs)
_, H_total = freqz(b_total, a_total, worN=4096, fs=fs)
axes[0].plot(w_hz_mag, 20 * np.log10(np.abs(H_butter) + 1e-12),
             label='Butterworth only', color='blue')
axes[0].plot(w_hz_mag, 20 * np.log10(np.abs(H_total) + 1e-12),
             label='Butterworth + APF (equalized)', color='red', linestyle='--')
axes[0].set_xlabel('Frequency [Hz]')
axes[0].set_ylabel('Magnitude [dB]')
axes[0].set_title('Magnitude Response (APF does not change amplitude)')
axes[0].set_xlim(0, fs / 2)
axes[0].set_ylim(-80, 5)
axes[0].legend()
axes[0].grid(True, alpha=0.3)

# 群遅延の比較
axes[1].plot(w_hz, gd_butter, label=f'Butterworth N={N_butter}', color='blue')
axes[1].plot(w_hz, gd_ap1 + gd_ap2, label='APF contribution (ap1+ap2)', color='green', linestyle='--')
axes[1].plot(w_hz, gd_total, label='Butterworth + APF (equalized)', color='red', linewidth=2)
axes[1].set_xlabel('Frequency [Hz]')
axes[1].set_ylabel('Group Delay [samples]')
axes[1].set_title('Group Delay Equalization with All-Pass Filter')
axes[1].set_xlim(0, fc * 2)
axes[1].set_ylim(0, 30)
axes[1].axvline(fc, color='gray', linestyle=':', alpha=0.5, label='Cutoff')
axes[1].legend()
axes[1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

全域通過フィルタを追加することで振幅応答には変化がなく、群遅延の周波数依存性が緩和されることが確認できます。

複数IIRフィルタの群遅延比較

from scipy.signal import cheby1, ellip, bessel

fs = 1000
fc = 100
N = 6

# 各IIRフィルタの設計
filters = {
    'Butterworth': butter(N, fc, fs=fs, output='ba'),
    'Chebyshev I (1dB)': cheby1(N, 1, fc, fs=fs, output='ba'),
    'Elliptic (1dB, 60dB)': ellip(N, 1, 60, fc, fs=fs, output='ba'),
    'Bessel': bessel(N, fc, norm='mag', fs=fs, output='ba'),
}

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

for name, (b, a) in filters.items():
    w_hz, H = freqz(b, a, worN=4096, fs=fs)
    w_gd, gd = group_delay((b, a), w=4096, fs=fs)

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

axes[0].set_xlabel('Frequency [Hz]')
axes[0].set_ylabel('Magnitude [dB]')
axes[0].set_title(f'Magnitude Response: IIR Filter Comparison (N={N}, fc={fc} Hz)')
axes[0].set_xlim(0, 300)
axes[0].set_ylim(-80, 5)
axes[0].axvline(fc, color='gray', linestyle=':', alpha=0.5)
axes[0].legend()
axes[0].grid(True, alpha=0.3)

axes[1].set_xlabel('Frequency [Hz]')
axes[1].set_ylabel('Group Delay [samples]')
axes[1].set_title('Group Delay Comparison: IIR Filters')
axes[1].set_xlim(0, 300)
axes[1].set_ylim(0, 50)
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()

ベッセルフィルタが通過域で最も平坦な群遅延を持つことが明確に確認できます。楕円フィルタは急峻な振幅特性を持つ代償として群遅延歪みが最大になっています。

まとめ

本記事で解説した内容を以下の表にまとめます。

概念定義・特徴実用上の重要性
位相スペクトル\(\phi(\omega) = \angle H(e^{j\omega})\) 、アンラップ処理が必要フィルタの波形への影響を理解するための基礎
群遅延\(\tau_g(\omega) = -d\phi/d\omega\) 、単位はサンプル数周波数ごとの信号遅延量を定量評価
線形位相\(\phi(\omega) = -\omega\tau_0\) 、群遅延一定波形形状が保存される(単純な時間遅延のみ)
FIR線形位相対称係数 \(h[n] = h[N-1-n]\) で実現、群遅延 \(= (N-1)/2\) サンプル厳密な線形位相が保証、振幅と位相を独立設計可
IIR非線形位相極の配置により位相が非線形、遷移域付近で群遅延歪みが最大振幅特性優先時に位相歪みとのトレードオフが生じる
全域通過フィルタ$H_{AP}= 1$(全周波数)、位相・群遅延のみ制御IIRフィルタの後段に接続して群遅延を補正できる
群遅延等化APFの直列接続で群遅延を平坦化、振幅特性を変えずに位相特性を改善音声・通信・制御系での高品質フィルタリングに有効

フィルタ選択の実践ガイド

  • 波形の形状保存が最優先(心電図、インパルス応答計測)→ 線形位相FIRフィルタ
  • 急峻な遷移帯域が必要かつ波形歪みが許容できる(通信帯域分割)→ 楕円IIRフィルタ
  • 群遅延が一定に近いIIRが必要(フィードバック制御、低遅延処理)→ ベッセルフィルタ
  • IIRフィルタを使いつつ位相歪みを軽減したい(リアルタイム音声処理)→ IIR + 全域通過フィルタ
  • オフライン処理で遅延が許容できる(バッチ処理、研究用解析)→ sosfiltfilt(ゼロ位相フィルタリング)

関連記事

参考文献