পাঠ ২৭ · ৫৭-এর মধ্যে · মডিউল ৬
Home / Courses / Digital Signal Processing / রেডিক্স-২ FFT

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

The radix-2 FFT algorithm — decimation in time
১১ মিনিট পড়া মধ্যম-কঠিন · Intermediate-Advanced Python কোডসহ সম্পূর্ণ বাংলায়

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

  • ডিরেক্ট 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 বের করলেই চলে, যদি এই সহজ যোগ-বিয়োগ সূত্র দিয়ে জোড়া লাগানো যায়।

ভাগ করো (divide)
N-পয়েন্ট সিগন্যালকে জোড় ও বিজোড় ইনডেক্সের দুটি N/2-পয়েন্ট সাব-সিগন্যালে ভাগ করো।
রিকার্স করো (recurse)
প্রতিটি সাব-সিগন্যালের DFT একই পদ্ধতিতে বের করো — যতক্ষণ না সাইজ ১-এ পৌঁছায় (একটি একক নম্বরের DFT নিজেই)।
জোড়া লাগাও (combine)
বাটারফ্লাই সূত্র (E[k] ± W_N^k O[k]) দিয়ে ছোট ফলাফলগুলো জোড়া লাগিয়ে বড় ফলাফল বানাও।

২ · একটি সত্যিকারের রিকার্সিভ ইমপ্লিমেন্টেশন

নিচের কোডে দুটো ফাংশন আছে: direct_dft (M5/L22-এর ডিরেক্ট সূত্র, এই পাঠে যাচাইয়ের জন্য সংক্ষেপে আবার লেখা হলো) আর 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]]
    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}")

    
কোডটি রান করলে N=8 সিগন্যালের ৮টি বিনেই 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 রূপভেদ ব্যবহার করা হয় (এই কোর্সের পরিধির বাইরে)।
মূল কথা · Key takeaway

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-এ অপারেশন-কাউন্ট টেবিলে এর প্রভাব স্পষ্ট দেখা যাবে।

অনুশীলন

  1. চিন্তা করুন: উপরের কোডে 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 ১-এ ম্যাগনিটিউড ঠিক ৪.০।

  2. পরীক্ষা করুন: কোড সেলে 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-এর কম্পিউটেশনাল কমপ্লেক্সিটি