পাঠ ২৮ · ৫৭-এর মধ্যে · মডিউল ৬
Home / Courses / Digital Signal Processing / FFT ইমপ্লিমেন্টেশন

FFT ইমপ্লিমেন্টেশন — বিট-রিভার্সাল ও বাটারফ্লাই

Implementing FFT — bit-reversal & butterfly
১১ মিনিট পড়া মধ্যম-কঠিন · Intermediate-Advanced Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • বিট-রিভার্সাল পারমুটেশন কী এবং এটি কেন ইটারেটিভ FFT-এর প্রথম ধাপ
  • একটি সম্পূর্ণ ইটারেটিভ বাটারফ্লাই-লুপ FFT ইমপ্লিমেন্টেশন, ধাপে ধাপে (stage by stage)
  • একই ফলাফল তিনটি ভিন্ন পদ্ধতিতে (direct DFT, recursive FFT, iterative FFT) বের করে সংখ্যাগতভাবে যাচাই করা
  • সত্যিকারের গণনা করা অপারেশন-কাউন্ট দিয়ে O(N²) বনাম O(N log N)-এর পার্থক্য প্রত্যক্ষভাবে দেখা

১ · বিট-রিভার্সাল পারমুটেশন কী

L27-এর রিকার্সিভ FFT প্রতিবার সিগন্যালকে জোড়/বিজোড়ে ভাগ করেছিল — এবং প্রতিটি রিকার্সিভ কলে x[0::2], x[1::2] স্লাইসিং করে নতুন লিস্ট বানিয়েছিল। একটি ইটারেটিভ সংস্করণে আমরা এই একই জোড়/বিজোড় ভাগ প্রক্রিয়া আগে থেকেই একবারে করে ফেলি — সিগন্যালের ইনডেক্সগুলোকে একটি নির্দিষ্ট ক্রমে সাজিয়ে (পুনর্বিন্যাস করে), যাতে পরে শুধু সরল লুপ দিয়ে বাটারফ্লাই চালানো যায়।

এই পুনর্বিন্যাসের নাম বিট-রিভার্সাল — প্রতিটি ইনডেক্স i-কে তার বাইনারি প্রতিনিধিত্বে লিখে, বিটগুলো উল্টে (reverse করে) নতুন ইনডেক্স বের করা হয়। যেমন N=8 (৩-বিট ইনডেক্স, যেহেতু log₂8 = 3)-এ ইনডেক্স ১ = বাইনারি 001, উল্টালে 100 = ৪। তাই মূল ইনডেক্স ১-এর স্যাম্পল বিট-রিভার্সড ক্রমে অবস্থান ৪-এ যায়।

কেন এই ক্রম দরকার
L27-এর রিকার্সিভ ভাঙন বারবার জোড়/বিজোড়ে ভাগ করে যে চূড়ান্ত ক্রম তৈরি করত, বিট-রিভার্সাল ঠিক সেই একই ক্রম এক ধাপেই তৈরি করে দেয়।
শুধু একটি পুনর্বিন্যাস
বিট-রিভার্সাল কোনো নতুন গণনা করে না — শুধু বিদ্যমান স্যাম্পলগুলোকে ভিন্ন ক্রমে সাজায়, তাই এটি নিজে গণনার খরচে যোগ হয় না।
এরপর শুধু বাটারফ্লাই
একবার সঠিক ক্রমে সাজানো হয়ে গেলে, বাকি কাজ শুধু ধাপে ধাপে (size = 2, 4, 8, ...) বাটারফ্লাই সূত্র প্রয়োগ করা — কোনো রিকার্সন ছাড়াই।

২ · সম্পূর্ণ ইটারেটিভ FFT ও তিন-মুখী যাচাই

নিচের কোডে bit_reverse_indices ইনডেক্সের বিট-রিভার্সড ক্রম বের করে, আর iterative_fft সেই ক্রমে সাজানো স্যাম্পল দিয়ে শুরু করে size = 2 থেকে size = N পর্যন্ত ধাপে ধাপে বাটারফ্লাই চালায় (প্রতি ধাপে size দ্বিগুণ হয় — এটাই log₂Nটি ধাপের কারণ)। এরপর L27-এর direct_dft ও recursive_fft-এর সাথে তুলনা করা হচ্ছে:

Python
import cmath, math

def direct_dft(x):
    N = len(x)
    return [sum(x[n] * cmath.exp(-2j * math.pi * k * n / N) for n in range(N)) for k in range(N)]

def recursive_fft(x):
    N = len(x)
    if N == 1:
        return [x[0]]
    even = recursive_fft(x[0::2])
    odd = recursive_fft(x[1::2])
    X = [0j] * N
    for k in range(N // 2):
        t = cmath.exp(-2j * math.pi * k / N) * odd[k]
        X[k] = even[k] + t
        X[k + N // 2] = even[k] - t
    return X

def bit_reverse_indices(N):
    bits = N.bit_length() - 1
    order = []
    for i in range(N):
        rev = 0
        v = i
        for _ in range(bits):
            rev = (rev << 1) | (v & 1)
            v >>= 1
        order.append(rev)
    return order

def iterative_fft(x, count_ops=False):
    N = len(x)
    if N & (N - 1) != 0:
        raise ValueError("length must be a power of 2")
    order = bit_reverse_indices(N)
    X = [x[order[i]] for i in range(N)]
    mults = 0
    adds = 0
    size = 2
    while size <= N:
        half = size // 2
        twiddles = [cmath.exp(-2j * math.pi * j / size) for j in range(half)]
        for start in range(0, N, size):
            for j in range(half):
                w = twiddles[j]
                even_part = X[start + j]
                odd_part = X[start + j + half] * w
                mults += 1
                X[start + j] = even_part + odd_part
                X[start + j + half] = even_part - odd_part
                adds += 2
        size *= 2
    if count_ops:
        return X, mults, adds
    return X

print("বিট-রিভার্সাল ক্রম, N=8:", bit_reverse_indices(8))

N = 8
fs = 8.0
x = [math.sin(2 * math.pi * 1 * n / fs) + 0.5 * math.cos(2 * math.pi * 2 * n / fs) for n in range(N)]

X_dft = direct_dft(x)
X_fft_rec = recursive_fft(x)
X_fft_iter = iterative_fft(x)

print()
print(f"{'k':>3} | {'direct DFT':>10} | {'রিকার্সিভ FFT':>13} | {'ইটারেটিভ FFT':>13} | {'|DFT-iter|':>10} | {'|rec-iter|':>10}")
max_diff_dft_iter = 0.0
max_diff_rec_iter = 0.0
for k in range(N):
    d1 = abs(X_dft[k] - X_fft_iter[k])
    d2 = abs(X_fft_rec[k] - X_fft_iter[k])
    max_diff_dft_iter = max(max_diff_dft_iter, d1)
    max_diff_rec_iter = max(max_diff_rec_iter, d2)
    print(f"{k:>3} | {abs(X_dft[k]):>10.6f} | {abs(X_fft_rec[k]):>13.6f} | {abs(X_fft_iter[k]):>13.6f} | {d1:>10.2e} | {d2:>10.2e}")

print()
print(f"সর্বোচ্চ |DFT - ইটারেটিভ FFT|: {max_diff_dft_iter:.3e}")
print(f"সর্বোচ্চ |রিকার্সিভ FFT - ইটারেটিভ FFT|: {max_diff_rec_iter:.3e}")

    
কোডটি রান করলে প্রথমে দেখা যায় N=8-এর বিট-রিভার্সাল ক্রম: [0, 4, 2, 6, 1, 5, 3, 7] (উদাহরণ: ইনডেক্স ১ → ৪, ঠিক আগের যুক্তি অনুযায়ী)। এরপর তুলনা টেবিলে দেখা যায় — direct DFT আর ইটারেটিভ FFT-এর মধ্যে সর্বোচ্চ পার্থক্য ৩.২৪ × ১০⁻¹⁵ (L27-এর recursive FFT-এর সাথে direct DFT-এর পার্থক্যের সমান, যেহেতু এটি একই সিগন্যাল), কিন্তু recursive FFT ও iterative FFT-এর মধ্যে পার্থক্য সবগুলো বিনেই একদম ০.০০ × ১০⁺⁰⁰ — দুটো সম্পূর্ণ ভিন্ন কাঠামোর অ্যালগরিদম (একটি রিকার্সিভ, একটি ইটারেটিভ) বিট-ফর-বিট অভিন্ন সংখ্যাসূচক ফলাফল দিচ্ছে, কারণ দুটোই একই ক্রমে একই গাণিতিক অপারেশন করছে — শুধু কোড-কাঠামো ভিন্ন।

৩ · সত্যিকারের অপারেশন-কাউন্ট — DFT বনাম FFT

L26-এ আমরা direct DFT-এর অপারেশন গুনেছিলাম (N=8-এ ১২০, N=128-এ ৩২৬৪০)। এখন একই সিগন্যাল সাইজগুলোতে iterative_fft-এর count_ops=True মোড ব্যবহার করে FFT-এর গুণন ও যোগও গুনে সরাসরি তুলনা করা যাক:

Python
def counted_dft(x):
    N = len(x)
    mults = 0
    adds = 0
    for k in range(N):
        for n in range(N):
            _ = x[n] * cmath.exp(-2j * math.pi * k * n / N)
            mults += 1
            if n > 0:
                adds += 1
    return mults, adds

print("--- অপারেশন কাউন্ট: direct DFT বনাম FFT ---")
print(f"{'N':>5} | {'DFT মোট':>9} | {'FFT গুণন':>9} | {'FFT যোগ':>8} | {'FFT মোট':>8} | {'স্পিডআপ':>8}")

for N in [8, 16, 32, 64, 128]:
    xN = [math.sin(2 * math.pi * 3 * n / N) for n in range(N)]
    dft_mults, dft_adds = counted_dft(xN)
    dft_total = dft_mults + dft_adds
    _, fft_mults, fft_adds = iterative_fft(xN, count_ops=True)
    fft_total = fft_mults + fft_adds
    speedup = dft_total / fft_total
    print(f"{N:>5} | {dft_total:>9} | {fft_mults:>9} | {fft_adds:>8} | {fft_total:>8} | {speedup:>7.2f}x")

    
এই কোডটি আগের কোড সেলের ফাংশনগুলোর উপর নির্ভরশীল (একই "নোটবুক-স্টাইল" কোষ, আগের সেল আগে রান করা দরকার)। ফলাফল: N=8-এ FFT-এর ১২টি গুণন + ২৪টি যোগ = ৩৬টি মোট অপারেশন (DFT-এর ১২০-এর তুলনায় ৩.৩৩x কম); N=16-এ ৯৬ (DFT ৪৯৬, ৫.১৭x); N=32-এ ২৪০ (DFT ২০১৬, ৮.৪০x); N=64-এ ৫৭৬ (DFT ৮১২৮, ১৪.১১x); আর N=128-এ ১৩৪৪ (DFT ৩২৬৪০, ২৪.২৯x)। লক্ষ্য করুন — স্পিডআপ N বাড়ার সাথে সাথে বাড়তেই থাকে, এবং এটি ঠিক তত্ত্বীয় প্রত্যাশার সাথে মেলে: FFT-এর মোট অপারেশন N log₂N-এর সমানুপাতিক (যাচাই: N=128-এ FFT গুণন সংখ্যা ৪৪৮ = ঠিক (N/2)×log₂N = 64×7, আর যোগ ৮৯৬ = ঠিক N×log₂N = 128×7), যেখানে DFT-এর N²-এর সমানুপাতিক — তাই অনুপাত N²/(N log N) = N/log N, যা N বাড়ার সাথে সাথে বাড়তেই থাকে।
মূল কথা · Key takeaway

একই গাণিতিক ফলাফল তিনটি সম্পূর্ণ ভিন্ন কোড-কাঠামোয় (ডিরেক্ট সামেশন, রিকার্সিভ ভাঙন, ইটারেটিভ বিট-রিভার্সাল + বাটারফ্লাই) বানিয়ে সংখ্যাগতভাবে মিলিয়ে দেখানো হলো — আর তারপর সত্যিকারের অপারেশন গুনে দেখানো হলো FFT কেন দ্রুত। এই অপারেশন-কাউন্ট পদ্ধতিটি (ওয়াল-ক্লক টাইমিং নয়) M9-এর পলিফেজ ফিল্টার লেসনেও (L42) একইভাবে ব্যবহার করা হবে দক্ষতা তুলনার জন্য। পরবর্তী পাঠে (L29) আমরা এই ইটারেটিভ FFT-কে একটি বাস্তবসম্মত স্পেকট্রাল অ্যানালাইসিস সমস্যায় প্রয়োগ করব — নয়েজযুক্ত একটি সিগন্যাল থেকে দুটি লুকানো ফ্রিকোয়েন্সি খুঁজে বের করে।

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।

প্র ০১ recursive FFT আর iterative FFT-এর মধ্যে পার্থক্য একদম 0.0, কিন্তু direct DFT-এর সাথে উভয়েরই পার্থক্য 10⁻¹⁵-এর কাছাকাছি — কেন এই পার্থক্য?

recursive আর iterative FFT দুটোই ঠিক একই ক্রমে একই গুণন-যোগ অপারেশন করে (শুধু কোড ভিন্নভাবে লেখা — একটিতে রিকার্সিভ কল, অন্যটিতে লুপ), তাই কোনো রাউন্ডিং পার্থক্যই তৈরি হয় না। কিন্তু direct DFT সম্পূর্ণ ভিন্ন ক্রমে (একটি বড় যোগফল সরাসরি) গণনা করে, তাই সামান্য ভিন্ন রাউন্ডিং জমে — এটাই L27-এর প্র ০১-এ আলোচিত floating-point ত্রুটির উৎস।

প্র ০২ কোডে while size <= N: লুপ কতবার চলবে N=128-এর জন্য, আর কেন?

size শুরু হয় ২ থেকে এবং প্রতি ধাপে দ্বিগুণ হয় (২, ৪, ৮, ১৬, ৩২, ৬৪, ১২৮) — তাই এটি চলবে log₂128 = 7বার। এই ৭টি ধাপই "ধাপে ধাপে ছোট থেকে বড় DFT জোড়া লাগানো" প্রক্রিয়া প্রতিনিধিত্ব করে — L27-এর রিকার্সনের প্রতিটি "স্তর" এখানে একটি করে while লুপ পুনরাবৃত্তি হিসেবে প্রকাশ পেয়েছে।

প্র ০৩ টেবিলে স্পিডআপ N=8-এ ৩.৩৩x থেকে N=128-এ ২৪.২৯x পর্যন্ত বেড়েছে। N আরও বড় হলে (যেমন N=1024) এই স্পিডআপ কি সীমাহীন বাড়তেই থাকবে?

হ্যাঁ, তত্ত্বীয়ভাবে — যেহেতু স্পিডআপ অনুপাত N²/(N log₂N) = N/log₂N-এর সমানুপাতিক, আর N/log₂N N বাড়ার সাথে সাথে (ধীরে হলেও) সীমাহীনভাবে বাড়তে থাকে। যেমন N=1024-এ 1024/10 ≈ 102, আর N=4096-এ 4096/12 ≈ 341 — অর্থাৎ N যত বড়, FFT-এর সুবিধা তত বেশি নাটকীয়। (এই কোর্সে Pyodide sandbox দ্রুত রাখতে কোড N=128 পর্যন্তই সীমাবদ্ধ, কিন্তু এই অনুপাত সূত্র উপরের কোডের গোনা মান থেকেই যাচাই করা।)

অনুশীলন

  1. চিন্তা করুন: N=256-এর জন্য FFT-এর গুণন সংখ্যা (N/2)×log₂N সূত্র দিয়ে হাতে হিসেব করুন। DFT-এর তুলনায় (যা L26-এর অনুশীলনে 65536 গুণন হিসেব করা হয়েছিল) স্পিডআপ কত হবে বলে আপনার ধারণা?

    FFT গুণন: (256/2) × log₂256 = 128 × 8 = 1024টি। FFT যোগ: 256 × 8 = 2048টি। মোট FFT অপারেশন = 1024 + 2048 = 3072। DFT-এর মোট অপারেশন (L26-এর সূত্র 2N²-N থেকে) = 2×256² - 256 = 130816। স্পিডআপ = 130816 / 3072 ≈ 42.58x।

  2. পরীক্ষা করুন: উপরের দ্বিতীয় কোড সেলে [8, 16, 32, 64, 128]-এর তালিকায় 256 যোগ করে Run চেপে আপনার হিসেব যাচাই করুন।

    রান করলে N=256-এর সারিতে সত্যিই FFT গুণন 1024, যোগ 2048, মোট 3072, আর DFT মোট 130816 দেখাবে — স্পিডআপ কলামে ৪২.৫৮x, যা প্রশ্ন ১-এর হাতে-করা হিসেবের সাথে হুবহু মিলে যায়।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

আগের পাঠ
রেডিক্স-২ FFT অ্যালগরিদম — ডেসিমেশন ইন টাইম