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

scipy.signal.group_delay・scipy.signal.freqz・numpy.unwrap で位相スペクトルと群遅延をPython実装。位相の数学的定義と物理的意味、位相アンラップ、FIRフィルタの線形位相条件、IIRフィルタの非線形位相歪み、全域通過フィルタによる群遅延等化まで解説します。

はじめに

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フィルタと全域通過フィルタを直列に接続して使用する

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

研究動向: 群遅延等化は現在も研究が続く分野です。Zhang ら(2025)は、全域通過フィルタとは異なるアプローチとして、欠陥地板構造(DGS)による負群遅延(Negative Group Delay, NGD)回路をバンドパスフィルタに組み込み、通過域端で増大する正の群遅延をNGDで相殺する等化法を提案しています。3次バンドパスフィルタの試作機でこの手法を適用した結果、群遅延の変動が0.55 nsから0.25 nsへと54.5%低減したことが報告されています(Zhang et al., “A group delay equalization method for filters based on DGS NGD structure,” Electromagnetics, 2025)。本記事で扱う離散時間の全域通過フィルタによる等化と、連続時間・マイクロ波回路のNGD構造による等化は、いずれも「フィルタ本体の群遅延の偏りを、追加の回路要素の群遅延で打ち消す」という共通原理に基づいています。

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.savefig('phase_response_comparison.png')

# --- 折り返しの検証:|Δφ| > 0.9π となる不連続点の個数 ---
wrap_jumps = np.sum(np.abs(np.diff(phase_iir_wrapped)) > np.pi * 0.9)
print(f"折り返し位相の不連続ジャンプ数: {wrap_jumps}")
print(f"アンラップ位相の範囲: {np.degrees(phase_iir.max()):.2f}度 〜 {np.degrees(phase_iir.min()):.2f}度")

実行結果:

折り返し位相の不連続ジャンプ数: 13
アンラップ位相の範囲: 0.00度 〜 -905.56度

8次バターワースIIRフィルタと64タップFIRフィルタの振幅応答(上)、IIRの折り返し位相とアンラップ位相の比較(中)、IIR・FIRの位相応答比較(下)を示すグラフ

中段のグラフから、折り返し位相(オレンジ)が \(\pm 180°\) で13回不連続にジャンプしている一方、アンラップ処理後(青)は滑らかに単調減少していることが確認できます。下段では、FIR(緑破線)の位相が原点を通る直線であるのに対し、IIR(青実線)はナイキスト周波数付近で最終的に約 \(-905.6°\) まで達する非線形カーブを描いており、両者の質的な違いが視覚的に明らかです。

群遅延の計算と比較

# --- 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.savefig('group_delay_comparison.png')

print(f"FIR群遅延: 最小={gd_fir.min():.6f}, 最大={gd_fir.max():.6f}, 標準偏差={gd_fir.std():.2e}")
print(f"理論値 (N_fir-1)/2 = {(N_fir - 1) / 2}")
mask_pb = (w_gd >= 0) & (w_gd <= 90)
print(f"IIR群遅延(通過域 0-90Hz): 最小={gd_iir[mask_pb].min():.3f}, 最大={gd_iir[mask_pb].max():.3f} サンプル")

gd_iir_interp = np.interp(w_diff, w_gd, gd_iir)
mask_valid = (w_diff > 5) & (w_diff < fs / 2 - 5)
diff_err = np.abs(gd_diff[mask_valid] - gd_iir_interp[mask_valid])
print(f"差分近似とscipy.group_delayの誤差: 平均={diff_err.mean():.4f}, 最大={diff_err.max():.4f} サンプル")

実行結果:

FIR群遅延: 最小=31.499999, 最大=31.500002, 標準偏差=3.59e-08
理論値 (N_fir-1)/2 = 31.5
IIR群遅延(通過域 0-90Hz): 最小=7.888, 最大=14.584 サンプル
差分近似とscipy.group_delayの誤差: 平均=0.0009, 最大=0.9531 サンプル

8次バターワースIIRフィルタと64タップFIRフィルタの群遅延比較(上)、差分近似とscipy.signal.group_delayの一致度比較(下)を示すグラフ

FIRフィルタの群遅延は標準偏差 \(3.59 \times 10^{-8}\) サンプルという浮動小数点誤差レベルの精度で理論値 \(\frac{N-1}{2} = 31.5\) サンプルに一致しており、式 \((12)\) が数値的にも厳密に成立することが確認できます。一方、バターワースIIRフィルタの群遅延は通過域(0〜90Hz)だけでも最小7.888サンプルから最大14.584サンプルまで変化しており、周波数依存性が明確です。また、差分近似 \(-\Delta\phi/\Delta\omega\) とscipyの group_delay は平均誤差0.0009サンプルとほぼ完全に一致しますが、最大誤差は0.9531サンプルまで広がる箇所があり、これは折り返し位相の不連続点近傍でアンラップ処理の数値誤差が蓄積するためです。

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

# --- テスト信号の生成 ---
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.savefig('waveform_distortion.png')

# --- 定量評価:過渡部を除く中央区間でのRMSE ---
mask_mid = (t >= 0.15) & (t <= 0.35)
rmse_iir = np.sqrt(np.mean((y_iir[mask_mid] - x[mask_mid]) ** 2))
rmse_fir = np.sqrt(np.mean((y_fir_aligned[mask_mid] - x[mask_mid]) ** 2))
print(f"元信号との一致度(中央区間0.15-0.35秒, RMSE): IIR={rmse_iir:.5f}, FIR(遅延補正後)={rmse_fir:.5f}")
print(f"信号のRMS振幅: {np.sqrt(np.mean(x[mask_mid] ** 2)):.5f}")

実行結果:

元信号との一致度(中央区間0.15-0.35秒, RMSE): IIR=0.02584, FIR(遅延補正後)=0.00011
信号のRMS振幅: 0.07694

10+30+50+70+90Hzの重ね合わせ信号(上)、6次バターワースIIR通過後の波形歪み(中)、101タップFIR通過後(遅延補正済み)の波形保存(下)を示すグラフ

信号のRMS振幅0.07694に対し、IIRバターワース通過後の波形は元信号とのRMSE誤差が0.02584(振幅比で約34%)と大きく波形が変形しているのに対し、群遅延補正後のFIR出力はRMSE誤差0.00011(振幅比で約0.14%)とほぼ完全に元信号を再現しています。これは線形位相FIRフィルタが単純な時間シフトのみを生じることの直接的な数値的裏付けです。

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

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 付近に群遅延ピーク)

peak_info = []
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)
    mag_db = 20 * np.log10(np.abs(H_ap) + 1e-12)
    axes[0].plot(w_ap, mag_db, 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}')

    peak_freq = w_ap_gd[np.argmax(gd_ap)]
    peak_info.append((r, mag_db.max(), mag_db.min(), gd_ap.max(), peak_freq))

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.savefig('allpass_response.png')

for r in r_values:
    idx = r_values.index(r)
    r_v, mag_max, mag_min, gd_peak, peak_freq = peak_info[idx]
    print(f"r={r_v}: 振幅範囲=[{mag_min:.2e}, {mag_max:.2e}] dB, "
          f"群遅延ピーク={gd_peak:.3f}サンプル @ {peak_freq:.2f}Hz")

実行結果:

r=0.5: 振幅範囲=[8.68e-12, 8.69e-12] dB, 群遅延ピーク=3.610サンプル @ 118.16Hz
r=0.7: 振幅範囲=[8.68e-12, 8.69e-12] dB, 群遅延ピーク=6.010サンプル @ 124.39Hz
r=0.9: 振幅範囲=[8.67e-12, 8.70e-12] dB, 群遅延ピーク=19.105サンプル @ 125.00Hz

2次全域通過フィルタの振幅応答(常に0dB、上)、位相応答(中)、極半径r=0.5/0.7/0.9における群遅延ピーク(下)を示すグラフ

上段のグラフから、極半径 \(r\) に関わらず振幅応答は数値誤差レベル(\(10^{-12}\) dB オーダー)で厳密に0dBに保たれていることが確認できます。下段では、\(r\) が0.5→0.7→0.9と大きくなるにつれて群遅延ピークが3.610→6.010→19.105サンプルへと急激に増大しており、極が単位円に近づくほど群遅延のピークが鋭く・大きくなるという理論的性質が数値的に裏付けられます。ピーク位置(約118〜125Hz)は設計時に指定した極角度 \(\theta_{ap} = \pi/4\) (正規化周波数)に対応する周波数帯に一致しています。

バターワースIIR + 全域通過フィルタによる群遅延等化の実証

全域通過フィルタで群遅延を平坦化するには、元のフィルタの群遅延が低い周波数帯にAPFの群遅延ピークを配置することが要点です。バターワースIIRフィルタの群遅延は通過域の低周波数側で最も小さく、カットオフに近づくほど増大します(前掲の実行結果で確認した0〜90Hz帯で7.888〜14.584サンプル)。したがって、低周波数側にAPFの群遅延ピークを配置して底上げすれば、全体として平坦化できます。

# --- バターワース + 全域通過フィルタによる群遅延等化 ---
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.5
theta_ap1 = 2 * np.pi * 5 / fs   # 5Hz付近に群遅延ピークを配置
theta_ap2 = 2 * np.pi * 10 / fs  # 10Hz付近に群遅延ピークを配置

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.savefig('group_delay_equalization.png')

mask_pb = (w_hz >= 0) & (w_hz <= fc)
gd_butter_range = gd_butter[mask_pb].max() - gd_butter[mask_pb].min()
gd_total_range = gd_total[mask_pb].max() - gd_total[mask_pb].min()
mag_diff = np.abs(20 * np.log10(np.abs(H_butter) + 1e-12) - 20 * np.log10(np.abs(H_total) + 1e-12))
print(f"通過域(0-{fc}Hz)の群遅延変動幅: Butterworthのみ={gd_butter_range:.3f}サンプル, "
      f"Butterworth+APF={gd_total_range:.3f}サンプル")
print(f"振幅応答差の最大値(全帯域): {mag_diff.max():.6f} dB")

実行結果:

通過域(0-100Hz)の群遅延変動幅: Butterworthのみ=4.994サンプル, Butterworth+APF=1.473サンプル
振幅応答差の最大値(全帯域): 0.000000 dB

バターワースIIRのみとバターワース+全域通過フィルタ等化後の振幅応答比較(上、両者は完全に重なる)、群遅延の比較(下、APF追加により通過域での変動が抑制される)を示すグラフ

上段のグラフから、バターワースIIR単体と全域通過フィルタ追加後の振幅応答は完全に重なっており、実測でも差の最大値が0.000000dB(浮動小数点誤差以下)であることが確認できます。下段では、通過域(0〜100Hz)における群遅延の変動幅がバターワース単体の4.994サンプルから、全域通過フィルタ追加後は1.473サンプルへと約3.4分の1に縮小しており、振幅特性を一切変えずに群遅延の周波数依存性を大幅に緩和できることが実測値で裏付けられます。

注意点: APFの群遅延ピークの配置場所は結果を左右する重要な設計パラメータです。試しに元のバターワースの群遅延が高い周波数帯(カットオフ近傍)にAPFピークを配置すると、群遅延の変動幅はかえって拡大します(実測で4.994→7.041サンプルに悪化することを確認済み)。これは、フラット化には「対象フィルタの群遅延が低い帯域を底上げする」配置が必要であり、単にAPFを直列接続するだけでは自動的に等化されないことを示しています。実務では最小二乗法などで群遅延誤差を最小化するようAPFのパラメータ(\(r\) , \(\theta\) )を最適化する設計手法が用いられます。

複数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))

gd_stats = {}
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)

    mask_pb = (w_gd >= 0) & (w_gd <= fc)
    gd_stats[name] = (gd[mask_pb].min(), gd[mask_pb].max(), gd[mask_pb].std())

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.savefig('iir_filter_comparison.png')

print("通過域(0-100Hz)群遅延統計(最小, 最大, 標準偏差)[サンプル]:")
for name, (mn, mx, sd) in gd_stats.items():
    print(f"  {name}: 最小={mn:.3f}, 最大={mx:.3f}, 標準偏差={sd:.3f}")

実行結果:

通過域(0-100Hz)群遅延統計(最小, 最大, 標準偏差)[サンプル]:
  Butterworth: 最小=5.946, 最大=10.940, 標準偏差=1.676
  Chebyshev I (1dB): 最小=6.858, 最大=31.263, 標準偏差=4.781
  Elliptic (1dB, 60dB): 最小=5.412, 最大=46.929, 標準偏差=7.203
  Bessel: 最小=4.160, 最大=4.596, 標準偏差=0.129

バターワース・チェビシェフI型・楕円・ベッセルの4種IIRフィルタの振幅応答比較(上)、群遅延比較(下、楕円が最大のピークを示す)を示すグラフ

通過域(0〜100Hz)における群遅延の標準偏差を比較すると、ベッセルフィルタが0.129サンプルと圧倒的に小さく(最小4.160〜最大4.596サンプルとほぼ一定)、最大平坦群遅延という設計目標が数値的に裏付けられます。一方、楕円フィルタは標準偏差7.203サンプル(最小5.412〜最大46.929サンプル)と群遅延ピークが最大であり、急峻な振幅特性(最小次数設計)とのトレードオフが顕著です。チェビシェフI型は標準偏差4.781サンプルとその中間に位置し、前掲の「代表的なIIRフィルタの位相特性比較」表で整理した定性的な序列(緩やか↔急峻、平坦↔歪む)が実測データで裏付けられました。

まとめ

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

概念定義・特徴実用上の重要性
位相スペクトル\(\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非線形位相極の配置により位相が非線形、遷移域付近で群遅延歪みが最大振幅特性優先時に位相歪みとのトレードオフが生じる
全域通過フィルタ\(\lvert H_{AP} \rvert = 1\) (全周波数)、位相・群遅延のみ制御IIRフィルタの後段に接続して群遅延を補正できる
群遅延等化APFの直列接続で群遅延を平坦化、振幅特性を変えずに位相特性を改善音声・通信・制御系での高品質フィルタリングに有効

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

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

関連記事

参考文献