FFT ইমপ্লিমেন্টেশন — বিট-রিভার্সাল ও বাটারফ্লাই
এই পাঠে যা শিখবেন
- বিট-রিভার্সাল পারমুটেশন কী এবং এটি কেন ইটারেটিভ 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-এর সাথে তুলনা করা হচ্ছে:
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}")
[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-এর গুণন ও যোগও গুনে
সরাসরি তুলনা করা যাক:
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 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 বাড়ার সাথে সাথে বাড়তেই থাকে।
একই গাণিতিক ফলাফল তিনটি সম্পূর্ণ ভিন্ন কোড-কাঠামোয় (ডিরেক্ট সামেশন, রিকার্সিভ ভাঙন, ইটারেটিভ বিট-রিভার্সাল + বাটারফ্লাই) বানিয়ে সংখ্যাগতভাবে মিলিয়ে দেখানো হলো — আর তারপর সত্যিকারের অপারেশন গুনে দেখানো হলো 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 পর্যন্তই সীমাবদ্ধ, কিন্তু
এই অনুপাত সূত্র উপরের কোডের গোনা মান থেকেই যাচাই করা।)
অনুশীলন
-
চিন্তা করুন: 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। -
পরীক্ষা করুন: উপরের দ্বিতীয় কোড সেলে
[8, 16, 32, 64, 128]-এর তালিকায়256যোগ করে Run চেপে আপনার হিসেব যাচাই করুন।রান করলে N=256-এর সারিতে সত্যিই FFT গুণন
1024, যোগ2048, মোট3072, আর DFT মোট130816দেখাবে — স্পিডআপ কলামে ৪২.৫৮x, যা প্রশ্ন ১-এর হাতে-করা হিসেবের সাথে হুবহু মিলে যায়।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- FFT-এর প্রয়োগ — বাস্তবে স্পেকট্রাল অ্যানালাইসিস পরবর্তী পাঠ এই পাঠের ইটারেটিভ FFT একটি নয়েজযুক্ত, দুই-ফ্রিকোয়েন্সির সিন্থেটিক সিগন্যালে প্রয়োগ করে সত্যিকারের পিক-ফাইন্ডিং দিয়ে লুকানো ফ্রিকোয়েন্সি খুঁজে বের করা হয়েছে।
- রেডিক্স-২ FFT অ্যালগরিদম — ডেসিমেশন ইন টাইম আগের পাঠ এই পাঠের ইটারেটিভ FFT যে বাটারফ্লাই সূত্রের উপর ভিত্তি করে বানানো, তার রিকার্সিভ ডেরিভেশন ও প্রথম ইমপ্লিমেন্টেশন।
- কেন FFT — DFT-এর কম্পিউটেশনাল কমপ্লেক্সিটি M6 · L26 DFT-এর O(N²) খরচের মূল বিশ্লেষণ, যার সাথে এই পাঠের FFT অপারেশন-কাউন্ট সরাসরি তুলনা করা হয়েছে।