何かあれば GitHub のリポジトリに issue を作るか ryukau@gmail.com までお気軽にどうぞ。


インデックスに戻る

Update: 2026-07-05

Table of Contents

ダウンサンプリングにつかう IIR (infinite impulse response) フィルタをポリフェイズ (polyphase) 化して direct form による実装と比べます。

IIR フィルタの伝達関数から始めます。

\[ H(z) = \frac{B(z)}{A(z)} = \frac{\sum_{i=0}^{L} b_i z^{-i}}{1 + \sum_{i=1}^{N} a_i z^{-i}}. \]

分母の多項式 \(A\) を因数分解します。 \(p_i\)\(H\) の極です。 \(H\) は有理関数なので、その極とは \(A(z)=0\) の解のことです。

\[ A(z) = \prod_{i=1}^{N} (1 - p_i z^{-1}). \]

\(z^M\) にあわせて極をスケーリングした伝達関数 \(A_M\) を定義します。 \(M\) はダウンサンプリングの倍率です。

\[ A_M(z^M) = \prod_{i=1}^{N} (1 - p_i^M z^{-M}). \]

伝達関数に \(A_M\) をねじこみます。

\[ H(z) = \frac{B(z)}{A(z)} \cdot \frac{A_M(z^{-M})}{A_M(z^{-M})} = \frac{Q(z)}{A_M(z^{-M})}, \quad \text{where} \enspace Q(z) = B(z) \dfrac{A_M(z^{-M})}{A(z)}. \]

ここで \(Q\) について以下の変形ができます。

\[ \begin{aligned} B(z) \frac{A_M(z^M)}{A(z)} &= B(z) \prod_{i=1}^{N} \frac{1 - (p_i z^{-1})^M}{1 - p_i z^{-1}} \\ &= B(z) \prod_{i=1}^{N} \frac{ \cancel{(1 - (p_i z^{-1}))}(1 + (p_i z^{-1}) + (p_i z^{-1})^2 + (p_i z^{-1})^3 + \dots + (p_i z^{-1})^{M-1}) }{ \cancel{1 - p_i z^{-1}} } \\ &= B(z) \prod_{i=1}^{N} \left( \sum_{k=0}^{M-1} p_i^k z^{-k} \right). \end{aligned} \]

分母が 1 となって消えたので FIR です。 FIR である \(Q\) はポリフェイズ分解できます。

\[ Q(z) = \sum_{k=0}^{M-1} z^{-k} Q_k(z^M). \]

ここでは FIR のポリフェイズ分解の詳細は省略します。ざっくり言えばインデックス \(n\) について \(z^{-(nM+k)}\) の項を集めたものがポリフェイズ分解後の FIR となる \(Q_k\) です。例えば \(M=2\) で係数を \(q_n\) とすると、 \(Q_0 = q_0 z + q_2 z^{-2} + q_4 z^{-4}, \dots\)\(Q_1 = q_1 z^{-1} + q_3 z^{-3} + q_5 z^{-5}, \dots\) となります。

\(H\) に戻ります。

\[ H(z) = \sum_{k=0}^{M-1} z^{-k} \frac{Q_k(z^M)}{A_M(z^M)}. \]

分母を含めてポリフェイズ分解できています。

プロトタイプとなる zpk 形式の伝達関数から \(A_M\)\(Q_k\) を求めれば計算できる形になります。 zpk は MATLAB や SciPy で使われている伝達関数のゼロ、極、ゲイン (zero, pole, gain) の組です。 SciPy では scipy.signal.butterscipy.signal.ellip の引数に output="zpk" を指定すれば zpk の組が得られます。

実装」の節にコード例と、完全な設計コードへのリンクを掲載しています。

高速で正確な計算方法を検討します。計算方法のバリエーションには以下の軸があります。

以降では、軸 1 の分子と分母を分けて計算する方法のことをハイブリッド (hybrid) と呼ぶことにします。ハイブリッド形式では分子を FIR 、分母を sos として計算します。分子を別に計算するため、分母の sos は 1 セクションあたりの係数を \(a_1, a_2\) の 2 つに減らせます。

軸 2 の Kahan summation はポリフェイズ出力と、ハイブリッドの FIR の計算に適用できます。単純加算に比べると遅くなりますが、その遅さが許容範囲内か、また意味のある正確さの向上が得られるかについて調べます。

以下は C++20 での実装へのリンクです。リンク先のコードには、いくつか FMA の無しの実装が含まれています。この文章では FMA 有りの実装のみについて検討します。

数が多いので折りたたんでいます。

速度と正確さの比較
周波数特性
速度と正確さの比較
周波数特性
速度と正確さの比較
周波数特性
速度と正確さの比較
周波数特性

Butterworth フィルタについては、どの polyphase IIR の実装であっても速度、正確さ共に transposed direct form II (TDF-II) を超える性能と言えそうです。

Hybrid Simple が速いです。 Hybrid Simple は伝達関数の分子を FIR として計算しており、また FIR 内で Kahan summation を使っていない実装です。

SOS Simple が二番目に早いです。 clang++ では order = 4 かつ f64 のフィルタで Hybrid Simple を上回る性能になっています。

Kahan summation については予想通り遅くなっています。正確さの向上については大きく向上するとは言い難く、一部のケースを除いて SOS Simple のほうが正確に見えます。

f64 と f32 で実行速度がほぼ同じです。 IIR フィルタと Kahan summation は 1 サンプル前の計算結果を利用するので自動ベクトル化が入らないことが原因と考えられます。 Hybrid Simple は FIR 部分で自動ベクトル化が入っていると捉えれば納得できる結果と言えます (g++ では確認) 。複数の信号を SIMD で並列計算するときは f32 のほうが速くなると予想されます。

f32 では振幅特性がざっくりと 120 dB を下回る範囲で、周波数特性が歪んでいます。周波数特性のプロットで暴れているのはすべて f32 を表す点線です。

以下は軸 1 のフィルタ係数の表現の違いによる係数の数を比較した表です。ハイブリッド形式のほうがフィルタ係数の数が少なくなる傾向があります。 SOS 形式については \(a_0=1\) とすることで、 1 セクションあたりの係数の数を 5 として数えています。

ハイブリッド形式の係数の数の表
↓ Order  M → 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
1
2 7
3 14
4 13 21
5 32
6 19 25 43
7 58
8 25 41 73
9 38 92
10 31 61 111
11 134
12 37 49 61 85 157
13 184
14 43 113 211
15 62 92 242
16 49 81 145 273
SOS 形式の係数の数の表
Order  M 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
1
2 10
3 30
4 20 40
5 75
6 30 45 90
7 140
8 40 80 160
9 75 225
10 50 125 250
11 330
12 60 90 120 180 360
13 455
14 245 490
15 120 200 600
16 160 320 640

以下は Python による設計コードと C++20 による Hybrid Simple のフィルタ実装です。

Python によるフィルタ設計の実装例

次数が高いフィルタは正確に設計できないことがあるので mpmath を使っています。 tf2sos_mp を省略しているので、そのままでは動きません。

from mpmath import mp

def design_polyphase_iir(
    z_mp,
    p_mp,
    k_mp,
    M: int,
    output: str = "ba",
    as_float: bool = False,
    workdps: int | None = None,
):
    """
    Decomposes an arbitrary IIR filter (given by its zeros, poles, and gain)
    for polyphase down-sampling.

    Parameters:
    -----------
    z_mp : list or iterable
        Zeros of the filter as mpmath numbers.
    p_mp : list or iterable
        Poles of the filter as mpmath numbers.
    k_mp : mpmath number
        System gain.
    M : int
        Down-sampling factor.
    output : str, optional
        Format of the output filter: "ba", "sos", or "hybrid".
    as_float : bool, optional
        Cast the coefficients to Python float.
    workdps : int, optional
        Working precision in decimal places (dps). If None, the current
        ambient mpmath precision context is used.

    Returns:
    --------
    tuple or list
        The returned structure depends on the value of the `output` parameter:
        * If "ba" (default):
          Returns a tuple `(q_polyphase, a_low_poly)` where:
            - `q_polyphase` (list of lists): Numerator coefficients for each of the `M` polyphase branches.
            - `a_low_poly` (list): Denominator coefficients shared by all branches (expressed in powers of z^-M).
        * If "sos":
          Returns `sos_polyphase` (list of lists of lists): A list of length `M` containing the Second-Order Section (SOS) representations for each polyphase branch.
        * If "hybrid":
          Returns a tuple `(q_polyphase, sos_sections)` where:
            - `q_polyphase` (list of lists): Numerator coefficients for each of the `M` polyphase branches.
            - `sos_sections` (list of lists): Denominator coefficients `[a1, a2]` for each pole pair.

        Coefficients are returned as standard Python floats if `as_float` is True; otherwise, they are `mpmath` types.
    """
    if output not in ("ba", "sos", "hybrid"):
        raise ValueError("output must be one of 'ba', 'sos', or 'hybrid'")

    with mp.workdps(workdps):

        def poly_from_roots(roots):
            """
            Expands (x - r1)(x - r2)... into polynomial coefficients.
            Returns coefficients in descending order of powers (highest power first).
            """
            c = [mp.mpf(1)]
            for r in roots:
                c += [0]
                for i in range(len(c) - 1, 0, -1):
                    c[i] -= r * c[i - 1]
            return c

        def mp_convolve(a, b):
            len_a = len(a)
            len_b = len(b)
            out = [mp.mpf(0.0)] * (len_a + len_b - 1)
            for i in range(len_a):
                for j in range(len_b):
                    out[i + j] += a[i] * b[j]
            return out

        def mp_deconvolve(num, den):
            """
            Polynomial division: num(z) / den(z).
            Returns (quotient, remainder).
            Assumes coefficients are in descending order of powers (highest power first).
            """
            div = list(num)
            sz = len(div) - len(den) + 1
            if sz <= 0:
                return [mp.mpf(0)], div

            q = [mp.mpf(0)] * sz
            for k in range(sz):
                q[k] = div[k] / den[0]
                for j, val in enumerate(den):
                    div[k + j] -= q[k] * val

            return q, div

        b_poly = [mp.re(x * k_mp) for x in poly_from_roots(z_mp)]
        a_poly = [mp.re(x) for x in poly_from_roots(p_mp)]

        p_new = [p**M for p in p_mp]
        a_low_poly = [mp.re(x) for x in poly_from_roots(p_new)]

        deg_low = len(a_low_poly) - 1
        a_high_poly = [mp.mpf(0.0)] * (deg_low * M + 1)
        for i, val in enumerate(a_low_poly):
            a_high_poly[i * M] = val

        s_poly, remainder = mp_deconvolve(a_high_poly, a_poly)
        q_poly = mp_convolve(b_poly, s_poly)

        q_polyphase = []
        for k in range(M):
            q_polyphase.append(q_poly[k::M])

        if output == "ba":
            if as_float:
                q_polyphase = apply(q_polyphase, float)
                a_low_poly = apply(a_low_poly, float)
            return q_polyphase, a_low_poly

        elif output == "sos":
            sos_polyphase = []
            for k in range(M):
                q_k = q_poly[k::M]
                sos_section = tf2sos_mp(q_k, a_low_poly)
                sos_polyphase.append(sos_section)

            if as_float:
                sos_polyphase = apply(sos_polyphase, float)
            return sos_polyphase

        elif output == "hybrid":
            sos_sections = []
            pool = list(p_new)
            while len(pool) > 0:
                p1 = pool.pop(0)

                if abs(mp.im(p1)) < 1e-20:
                    a1 = -mp.re(p1)
                    a2 = mp.mpf(0.0)
                    sos_sections.append([a1, a2])
                    continue

                best_idx = -1
                best_err = mp.mpf("inf")

                for i, p2 in enumerate(pool):
                    err = abs(p1 - mp.conj(p2))
                    if err < best_err:
                        best_err = err
                        best_idx = i

                if best_idx != -1:
                    p2 = pool.pop(best_idx)
                    a1 = -2 * mp.re(p1)
                    a2 = abs(p1) ** 2
                    sos_sections.append([a1, a2])
                else:
                    a1 = -mp.re(p1)
                    a2 = mp.mpf(0.0)
                    sos_sections.append([a1, a2])

            if as_float:
                q_polyphase = apply(q_polyphase, float)
                sos_sections = apply(sos_sections, float)
            return q_polyphase, sos_sections
C++20 による Hybrid Simple の実装
#pragma once

#include <array>
#include <cmath>
#include <concepts>
#include <cstddef>
#include <ranges>
#include <tuple>
#include <utility>

// Hybrid 形式の polyphase IIR の係数。 (Butterworth, order=8, cutoff=0.3125, M=4)
template<typename T> struct Hybrid {
  static constexpr int nPhase = 4; // 文中の M に対応。

  // 分母の SOS: {a1, a2} for 1 / (1 + a1*z^-1 + a2*z^-2)
  static constexpr std::array<std::array<T, 2>, 4> denom{{
    { T(7.60834734050858330e-01), T(2.70020217408328600e-01) },
    { T(2.45817342985895443e-01), T(1.83487154523652787e-02) },
    { T(6.21709429503566474e-02), T(1.10913984116065879e-03) },
    { T(-8.69548164131942261e-03), T(1.06677160707193438e-04) },
  }};

  // 分子の FIR.
  static constexpr std::array<std::array<T, 9>, nPhase> branches{{
    { T(4.67360371460534638e-04), T(1.78965525913988399e-01), T(2.66265704737198072e-01), T(1.82695165925812308e-01), T(3.59985359464480154e-02), T(3.47770758488648735e-03), T(9.76101633490265749e-05), T(5.13705983926379128e-07), T(5.56796711341444609e-11) },
    { T(5.13399690923943292e-03), T(2.82921645103193231e-01), T(2.25756981966161091e-01), T(1.35987195285923984e-01), T(2.25319762725637897e-02), T(1.58374704330561684e-03), T(3.19637728170234209e-05), T(9.14850632661454864e-08), T(0.00000000000000000e+00) },
    { T(2.62245483078802598e-02), T(3.38960281490826409e-01), T(2.16172793820407716e-01), T(8.99252575894855938e-02), T(1.32404246635418957e-02), T(6.76027869152339453e-04), T(9.30032662397550523e-06), T(1.25270724575848982e-08), T(0.00000000000000000e+00) },
    { T(8.25615432363466933e-02), T(3.22027248938468458e-01), T(2.10495757082402951e-01), T(5.67774689416133613e-02), T(7.09628583197262353e-03), T(2.68501683273136621e-04), T(2.36627742844577689e-06), T(1.17002404623042308e-09), T(0.00000000000000000e+00) },
  }};
};

template<std::floating_point T, auto Coefficients> class FirSection {
private:
  static constexpr std::size_t N = Coefficients.size();

  alignas(64) std::array<T, (N > 0 ? N - 1 : 0)> s{};

public:
  void reset() { s.fill(T(0)); }

  inline T process(T input) {
    if constexpr (N == 0) { return T(0); }

    constexpr auto b0 = Coefficients[0];
    if constexpr (N == 1) { return b0 * input; }

    T y = std::fma(b0, input, s[0]);
    for (std::size_t i = 0; i < N - 2; ++i) {
      s[i] = std::fma(Coefficients[i + 1], input, s[i + 1]);
    }
    s[N - 2] = Coefficients[N - 1] * input;
    return y;
  }
};

template<std::floating_point T, auto Coefficients> class IirSection {
private:
  static constexpr std::size_t nSections = Coefficients.size();
  std::array<std::array<T, 2>, nSections> s{};

  template<std::size_t... I> inline void process_sections(T& val, std::index_sequence<I...>) {
    ((process_one_section<I>(val)), ...);
  }

  template<std::size_t Index> inline void process_one_section(T& val) {
    T y_out = val + s[Index][0];
    s[Index][0] = std::fma(-Coefficients[Index][0], y_out, s[Index][1]);
    s[Index][1] = -Coefficients[Index][1] * y_out;
    val = y_out;
  }

public:
  void reset() {
    for (auto& sec : s) { sec.fill(T(0)); }
  }

  inline T process(T input) {
    if constexpr (nSections == 0) { return input; }
    T y = input;
    process_sections(y, std::make_index_sequence<nSections>{});
    return y;
  }
};

template<std::floating_point T, typename Coefficients> class PolyphaseIir {
public:
  static constexpr int nPhase = Coefficients::nPhase;

private:
  template<std::size_t... I> static auto make_fir_tuple(std::index_sequence<I...>) {
    return std::tuple<FirSection<T, Coefficients::branches[I]>...>{};
  }
  using FirTuple = decltype(make_fir_tuple(std::make_index_sequence<nPhase>{}));

  FirTuple fir_branches;
  IirSection<T, Coefficients::denom> iir_filter;

  template<std::size_t Index> inline T process_branch(T input) {
    return std::get<Index>(fir_branches).process(input);
  }

  template<std::size_t... I>
  inline T sum_branches(const std::array<T, nPhase>& inputs, std::index_sequence<I...>) {
    return (... + process_branch<I>(inputs[I]));
  }

  template<std::size_t... I> inline void reset_branches(std::index_sequence<I...>) {
    ((std::get<I>(fir_branches).reset()), ...);
  }

public:
  void reset() {
    reset_branches(std::make_index_sequence<nPhase>{});
    iir_filter.reset();
  }

  inline T process(const std::array<T, nPhase>& inputs) {
    T fir_sum = sum_branches(inputs, std::make_index_sequence<nPhase>{});
    return iir_filter.process(fir_sum);
  }
};

// 使用例。
template<std::floating_point T> auto down_sample(std::vector<T>& inputs) {
  PolyphaseIir<T, Hybrid<T>> filter;

  constexpr size_t M = Hybrid<T>::nPhase;
  constexpr size_t n_sample = inputs.size() / M;

  std::vector<T> outputs;
  outputs.reserve(n_sample);

  for (size_t n = 0; n < n_sample; n += M) {
    std::array<T, M> frame;
    for (size_t k = 0; k < M; ++k) { frame[k] = inputs[n + k]; }
    outputs.push_back(filter.process(frame));
  }

  return outputs;
}

完全な形のコードを以下のリンク先に掲載しています。

Python による完全な設計コードはリンク先の design.pysignal_mp.py に書かれています。 design.pydesign_polyphase_iir を含むメインの設計コード、 signal_mp.py は主に SciPy の実装を mpmath に移植したものです。

C++20 による他のフィルタ形式の実装は polyphaseiir.hpp に書かれています。