রেডিক্স-২ FFT অ্যালগরিদম — ডেসিমেশন ইন টাইম
এই পাঠে যা শিখবেন
- ডিরেক্ট DFT সূত্রকে জোড়/বিজোড় ইনডেক্সে ভাগ করে কীভাবে বাটারফ্লাই সম্পর্ক বের হয়
- একটি সম্পূর্ণ, সত্যিকারের রিকার্সিভ radix-2 DIT FFT ইমপ্লিমেন্টেশন — নো NumPy, শুধু
cmath - কীভাবে একটি নতুন অ্যালগরিদমের ফলাফল একটি পুরনো, বিশ্বস্ত পদ্ধতির (এখানে ডিরেক্ট DFT) সাথে সংখ্যাগতভাবে মিলিয়ে যাচাই করতে হয়
- কেন এই পদ্ধতি শুধু power-of-2 সাইজের সিগন্যালের জন্য প্রযোজ্য
১ · সিগন্যালকে জোড় ও বিজোড় ভাগে ভাঙা
DFT সূত্র থেকে শুরু করা যাক:
$$X[k] = \sum_{n=0}^{N-1} x[n] \, e^{-j2\pi kn/N}$$
এখন যোগফলটিকে জোড় ইনডেক্স (n = 0, 2, 4, ...) আর বিজোড় ইনডেক্স (n = 1, 3, 5,
...) — এই দুই ভাগে ভাগ করা যাক। n = 2m (জোড়) আর n = 2m+1 (বিজোড়)
লিখে:
$$X[k] = \underbrace{\sum_{m=0}^{N/2-1} x[2m]\, e^{-j2\pi km/(N/2)}}_{E[k]\ \text{— জোড় স্যাম্পলের } N/2\text{-পয়েন্ট DFT}} \;+\; e^{-j2\pi k/N} \underbrace{\sum_{m=0}^{N/2-1} x[2m+1]\, e^{-j2\pi km/(N/2)}}_{O[k]\ \text{— বিজোড় স্যাম্পলের } N/2\text{-পয়েন্ট DFT}}$$
এখানে E[k] আর O[k] প্রতিটি নিজেই একটি ছোট, স্বাধীন N/2-পয়েন্ট DFT —
যথাক্রমে জোড় ও বিজোড় স্যাম্পলের উপর। আর W_N^k = e^{-j2\pi k/N}-কে বলা হয়
টুইডল ফ্যাক্টর (twiddle factor)। যেহেতু E[k] ও O[k]
পর্যায়ক্রমিক (E[k+N/2] = E[k], একই যুক্তিতে O-এর জন্যও), তাই পুরো N-পয়েন্ট
ফলাফল মাত্র দুটো সূত্র থেকেই বের করা যায়:
$$X[k] = E[k] + W_N^k\, O[k], \qquad X[k + N/2] = E[k] - W_N^k\, O[k], \qquad k = 0, \dots, N/2-1$$
এই জোড়া সূত্রকেই বলা হয় বাটারফ্লাই অপারেশন (গ্রাফিক্যালি আঁকলে প্রজাপতির ডানার
মতো দেখায় বলে এই নাম)। মূল কথা হলো — N-পয়েন্ট DFT বের করতে দুটো N/2-পয়েন্ট
DFT বের করলেই চলে, যদি এই সহজ যোগ-বিয়োগ সূত্র দিয়ে জোড়া লাগানো যায়।
N-পয়েন্ট সিগন্যালকে জোড় ও বিজোড় ইনডেক্সের দুটি N/2-পয়েন্ট সাব-সিগন্যালে ভাগ করো।
প্রতিটি সাব-সিগন্যালের DFT একই পদ্ধতিতে বের করো — যতক্ষণ না সাইজ ১-এ পৌঁছায় (একটি একক নম্বরের DFT নিজেই)।
বাটারফ্লাই সূত্র (
E[k] ± W_N^k O[k]) দিয়ে ছোট ফলাফলগুলো জোড়া লাগিয়ে বড় ফলাফল বানাও।২ · একটি সত্যিকারের রিকার্সিভ ইমপ্লিমেন্টেশন
নিচের কোডে দুটো ফাংশন আছে: direct_dft (M5/L22-এর ডিরেক্ট সূত্র, এই পাঠে যাচাইয়ের জন্য
সংক্ষেপে আবার লেখা হলো) আর 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]]
if N % 2 != 0:
raise ValueError("length must be a power of 2")
even = recursive_fft(x[0::2])
odd = recursive_fft(x[1::2])
X = [0j] * N
for k in range(N // 2):
twiddle = cmath.exp(-2j * math.pi * k / N) * odd[k]
X[k] = even[k] + twiddle
X[k + N // 2] = even[k] - twiddle
return X
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 = recursive_fft(x)
print(f"{'k':>3} | {'direct DFT':>12} | {'recursive FFT':>14} | {'পার্থক্য':>10}")
max_diff = 0.0
for k in range(N):
d = abs(X_dft[k] - X_fft[k])
max_diff = max(max_diff, d)
print(f"{k:>3} | {abs(X_dft[k]):>12.8f} | {abs(X_fft[k]):>14.8f} | {d:>10.2e}")
print()
print(f"সর্বোচ্চ পার্থক্য (৮ বিন মিলিয়ে): {max_diff:.3e}")
N2 = 16
x2 = [math.sin(2 * math.pi * 3 * n / N2) + 0.3 * math.cos(2 * math.pi * 5 * n / N2) for n in range(N2)]
X_dft2 = direct_dft(x2)
X_fft2 = recursive_fft(x2)
max_diff2 = max(abs(a - b) for a, b in zip(X_dft2, X_fft2))
print(f"N=16-এর জন্য সর্বোচ্চ পার্থক্য: {max_diff2:.3e}")
direct DFT আর recursive FFT-এর
ম্যাগনিটিউড হুবহু মিলে যায় (k=1-এ ৪.০, k=2 ও k=6-এ ২.০, বাকিগুলোতে ০) — প্রতিটি বিনের পার্থক্য
10⁻¹⁶ থেকে 10⁻¹⁵-এর মধ্যে, আর সর্বোচ্চ পার্থক্য ৩.২৪ ×
১০⁻¹⁵। এরপর N=16 সিগন্যালেও একইভাবে যাচাই করলে সর্বোচ্চ পার্থক্য ২.৪৬ ×
১০⁻¹⁴। এই মানগুলো কোনোভাবেই শূন্য নয়, কিন্তু float-এর সাধারণ নির্ভুলতার সীমা
(প্রায় 10⁻¹⁶) থেকে মাত্র কয়েক গুণ বড় — অর্থাৎ এগুলো প্রকৃত গাণিতিক পার্থক্য নয়, শুধু
ভিন্ন ক্রমে ভাসমান-বিন্দু (floating-point) যোগ-বিয়োগ করার কারণে জমে থাকা রাউন্ডিং ত্রুটি। সিদ্ধান্ত:
রিকার্সিভ FFT আর ডিরেক্ট DFT সংখ্যাগতভাবে একই ফলাফল দেয় — শুধু FFT অনেক কম
অপারেশনে করে (L28-এ এটি নির্দিষ্টভাবে গোনা হবে)।
৩ · কেন এই ভাঙন বৈধ — একটি সংক্ষিপ্ত যুক্তি
এই পদ্ধতি কাজ করে কারণ W_N = e^{-j2\pi/N} একটি পর্যায়ক্রমিক (periodic) ও প্রতিসাম্যপূর্ণ
সংখ্যা — বিশেষভাবে W_N^{k+N/2} = -W_N^k (কারণ e^{-j\pi} = -1)। এই একটি
বীজগাণিতিক সত্যই বাটারফ্লাই সূত্রের + আর - চিহ্নদ্বয়ের পেছনের কারণ — দুটো
সূত্রই আসলে একই W_N^k O[k] পদ ব্যবহার করে, শুধু চিহ্ন বদলায়। এই একটি পর্যবেক্ষণই পুরো
অ্যালগরিদমের দক্ষতার মূল ভিত্তি: একই গুণফল W_N^k O[k] দুইবার আলাদা করে গণনা করার
দরকার নেই।
N একটি power of 2 (২, ৪, ৮, ১৬, ৩২, ...)
— কোডেও if N % 2 != 0: raise ValueError(...) লাইনটি এই শর্ত নিশ্চিত করে। যদি সিগন্যালের
দৈর্ঘ্য power-of-2 না হয়, তখন হয় শূন্য (zero) যোগ করে (M5/L25-এর zero-padding, ভিন্ন উদ্দেশ্যে হলেও
একই কৌশল) দৈর্ঘ্য পরবর্তী power-of-2-এ বাড়ানো হয়, অথবা mixed-radix/Bluestein-এর মতো ভিন্ন FFT
রূপভেদ ব্যবহার করা হয় (এই কোর্সের পরিধির বাইরে)।
Radix-2 decimation-in-time FFT একটি N-পয়েন্ট DFT সমস্যাকে বারবার অর্ধেক করে ছোট সমস্যায় ভাঙে, আর একটি সাধারণ বাটারফ্লাই সূত্র দিয়ে ফলাফল জোড়া লাগায়। উপরের কোড প্রমাণ করেছে এই পদ্ধতি ডিরেক্ট DFT-এর সাথে সংখ্যাগতভাবে অভিন্ন ফলাফল দেয়। পরবর্তী পাঠে (L28) আমরা এই একই অ্যালগরিদম একটি ভিন্ন, ইটারেটিভ কাঠামোয় (বিট-রিভার্সাল + বাটারফ্লাই লুপ) পুনরায় বানাব, তিন-মুখী তুলনা করব, এবং প্রথমবারের মতো সত্যিকারের অপারেশন-কাউন্ট দিয়ে দেখাব FFT আসলে কতটা দ্রুত।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১
উপরের ডেমোতে পার্থক্য একেবারে 0.0 নয়, বরং 10⁻¹⁵-এর কাছাকাছি। এর
মানে কি FFT ভুল ফলাফল দিচ্ছে?
না। এই পার্থক্য floating-point (ভাসমান-বিন্দু) সংখ্যার সাধারণ রাউন্ডিং ত্রুটি — কম্পিউটার
বাস্তব সংখ্যাকে সসীম নির্ভুলতায় সংরক্ষণ করে (সাধারণত প্রায় ১৫-১৬ সিগনিফিক্যান্ট ডিজিট)। ডিরেক্ট
DFT আর রিকার্সিভ FFT ঠিক একই যোগ-গুণগুলো ভিন্ন ক্রমে করে (FFT আগে ছোট অংশ যোগ করে, তারপর জোড়া
লাগায়; DFT সরাসরি বড় যোগফল করে), আর ভিন্ন ক্রমে যোগ করলে সামান্য ভিন্ন রাউন্ডিং জমে — কিন্তু এই
পার্থক্য 10⁻¹৪-এর মাত্রার, যেখানে প্রকৃত সিগন্যাল মান ০-৪-এর মধ্যে, তাই ব্যবহারিক
অর্থে এটি সম্পূর্ণ উপেক্ষণযোগ্য।
প্র ০২
recursive_fft ফাংশনে N == 1 হলে সরাসরি [x[0]] রিটার্ন
করা হয়েছে — কেন এটি একটি বৈধ "১-পয়েন্ট DFT"?
DFT সূত্রে N = 1 বসালে যোগফলে মাত্র একটি পদ থাকে (n = 0, k =
0), আর e^{-j2\pi \times 0 \times 0/1} = e^0 = 1 — তাই
X[0] = x[0] × 1 = x[0]। অর্থাৎ একটি একক নম্বরের DFT নিজেই সেই নম্বর — এটাই
রিকার্সনের base case, যেখানে ভাঙন থামে এবং সরাসরি উত্তর দেওয়া হয়।
প্র ০৩
বাটারফ্লাই সূত্রে X[k] আর X[k+N/2] দুটোই E[k] ও
W_N^k O[k] থেকে বের হয় — এখানে W_N^k O[k] কতবার গণনা করা হচ্ছে?
মাত্র একবার — কোডে twiddle = cmath.exp(...) * odd[k] একবার গণনা
করে একটি চলকে রাখা হয়েছে, তারপর সেটি যোগ করে X[k] আর বিয়োগ করে
X[k+N//2] বের করা হয়েছে। এই "একবার গণনা করে দুইবার ব্যবহার করা" কৌশলটিই radix-2
FFT-এর দক্ষতার একটি বড় অংশ — L28-এ অপারেশন-কাউন্ট টেবিলে এর প্রভাব স্পষ্ট দেখা যাবে।
অনুশীলন
-
চিন্তা করুন: উপরের কোডে
N = 8সিগন্যালটিsin(2π×1×n/8) + 0.5×cos(2π×2×n/8)। এতে দুটি ফ্রিকোয়েন্সি উপাদান আছে (bin ১ আর bin ২, এবং তাদের প্রতিসম প্রতিরূপ bin ৭ ও bin ৬)। DFT ম্যাগনিটিউড টেবিলে bin ১-এ কত মান আসবে বলে আপনার ধারণা (ইঙ্গিত: একটি বিশুদ্ধ সাইন উপাদানের অ্যামপ্লিটিউড ১, আর N-পয়েন্ট DFT-তে ম্যাগনিটিউড সাধারণত N/2 × অ্যামপ্লিটিউড হয়)?N/2 × amplitude = 8/2 × 1 = 4। কোডের আউটপুট এটিই নিশ্চিত করে — bin ১-এ ম্যাগনিটিউড ঠিক ৪.০। -
পরীক্ষা করুন: কোড সেলে
N = 8-এর সিগন্যাল সংজ্ঞায় সহগ0.5-কে0.8-এ বদলে Run চেপে দেখুন bin ২ ও bin ৬-এর ম্যাগনিটিউড কীভাবে বদলায়, আর direct DFT ও recursive FFT তখনও মিলে কিনা।0.8-এর অ্যামপ্লিটিউডে bin ২ ও bin ৬-এর ম্যাগনিটিউডN/2 × 0.8 = 3.2হয়ে যাবে (আগে ছিল ২.০)। বাকি বিনগুলো (বিশেষ করে bin ১ ও bin ৭, যেগুলো সাইন উপাদানের জন্য) অপরিবর্তিত থাকবে, এবংdirect DFTওrecursive FFTতখনও পূর্বের মতোই ফ্লোটিং-পয়েন্ট নির্ভুলতার সীমায় মিলে যাবে — অ্যালগরিদম যেকোনো ইনপুট সিগন্যালের জন্যই সমানভাবে বৈধ, শুধু এই একটি নির্দিষ্ট উদাহরণের জন্য নয়।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- FFT ইমপ্লিমেন্টেশন — বিট-রিভার্সাল ও বাটারফ্লাই পরবর্তী পাঠ একই বাটারফ্লাই সূত্র, কিন্তু ইটারেটিভভাবে (বিট-রিভার্সাল পারমুটেশন + লুপ) বানিয়ে তিন-মুখী সংখ্যাগত যাচাই এবং একটি সত্যিকারের DFT-বনাম-FFT অপারেশন-কাউন্ট তুলনা টেবিল।
- কেন FFT — DFT-এর কম্পিউটেশনাল কমপ্লেক্সিটি আগের পাঠ ডিরেক্ট DFT-এর O(N²) খরচ কোথা থেকে আসে, এবং কেন একটি দ্রুততর অ্যালগরিদম দরকার।
- ডিসক্রিট ফুরিয়ার ট্রান্সফর্ম (DFT) M5 · L22 এই পাঠে যাচাইয়ের জন্য ব্যবহৃত ডিরেক্ট DFT সূত্রের মূল সংজ্ঞা ও প্রথম ইমপ্লিমেন্টেশন।