فهرست مقاله
نیازمندی «دامنهٔ سیگنال را با FFT محاسبه کن» هنوز معیار پذیرش قابل آزمون نیست. دامنهٔ قله است یا RMS؟ مؤلفهٔ ثابت چه میشود؟ طول رکورد، نرخ نمونهبرداری و پنجره چه فرضی دارند؟ در این آموزش، همین ابهام را به قرارداد اجرایی تبدیل میکنیم. یک سیگنال ولتاژ کاملاً ساختگی میسازیم، پاسخ بعضی خانههای تبدیل را با دست تعیین میکنیم و نتیجه را با محاسبهای مستقل میسنجیم. هیچ اسیلوسکوپ، حسگر واقعی یا کارایی محصول اندازهگیری نمیشود؛ هدف، دقیق کردن معنای موفقیت نرمافزار پردازش سیگنال است.
۱. نیازمندی را با واحد، طول رکورد و فرضها بنویسید

قرارداد نمونه شامل ۶۴ نمونهٔ حقیقی با فاصلهٔ یکنواخت و نرخ ۶۴۰۰ نمونه بر ثانیه است. سیگنال از مؤلفهٔ ثابت ۰٫۲۵ ولت، کسینوس ۷۰۰ هرتز با دامنهٔ قلهٔ ۱٫۶ ولت و دنبالهٔ متناوب مثبت و منفی ۰٫۱ ولت تشکیل میشود. جزء آخر، کسینوس نایکوئیست همین نمونههاست؛ دربارهٔ دامنهٔ یک موج آنالوگ با فاز دلخواه در فرکانس نایکوئیست ادعا نمیکنیم. نویز، زمانبندی نامنظم و تبدیل ADC در مسئله وجود ندارند.
زمان نمونهها از صفر تا ۶۳ تقسیم بر ۶۴۰۰ ثانیه است. آخرین زمان ۹٫۸۴۳۷۵ میلیثانیه میشود، ولی طول پنجرهٔ تحلیل، یعنی ۶۴ تقسیم بر نرخ، ۱۰ میلیثانیه است. اشتباه گرفتن این دو، محور فرکانس را تغییر میدهد. فاصلهٔ خانههای فرکانسی ۱۰۰ هرتز است؛ این فاصله، بهتنهایی تضمین تفکیک هر دو موج نزدیک یا دقت تخمین فرکانس نیست.
تابع آموزشی فقط فهرست یا تاپل دقیق با طول زوج و حداقل چهار عضو میپذیرد. نمونهها باید اعداد حقیقی متناهی از نوع دقیق int یا float پایتون باشند؛ مقدار بولی و عدد مختلط رد میشوند. نرخ و معکوس آن نیز باید مثبت و متناهی باشند. این محدود کردن دامنه، قانون عمومی FFT نیست؛ برای طول فرد باید رفتار آخرین خانه را جدا طراحی کرد.
۲. دامنهٔ یکطرفه و استثنای دو انتها

در قرارداد نرمالسازی backward در NumPy، تبدیل مستقیم بدون ضریب تقسیم و تبدیل معکوس با ضریب یک بر تعداد انجام میشود. برای نمونهٔ حقیقی، تابع rfft بخش غیرتکراری تبدیل را نگه میدارد. اینجا ۳۳ مقدار مختلط از صفر تا نایکوئیست داریم. محور را با rfftfreq با فاصلهٔ زمانی برحسب ثانیه میسازیم تا واحد هرتز باشد.
برای طیف دامنهٔ این قرارداد، قدرمطلق خروجی را بر ۶۴ تقسیم میکنیم و فقط خانههای داخلی را دو برابر میکنیم. صفر و نایکوئیست برای طول زوج، جفت مستقل منفی ندارند. مثال رسمی دامنهٔ یکطرفهٔ MathWorks همین تفاوت را توضیح میدهد؛ کد این مقاله مستقل و با NumPy اجرا میشود و اجرای MATLAB نیست.
مرجع دستی سه خروجی خام را مشخص میکند: خانهٔ صفر برابر ۱۶، خانهٔ هفت برابر ۵۱٫۲ و خانهٔ ۳۲ برابر ۶٫۴ است. پس دامنههای متناظر ۰٫۲۵، ۱٫۶ و ۰٫۱ ولت میشوند. نتیجه قدرمطلق است؛ فاز و علامت مؤلفهٔ ثابت را از این آرایه بازسازی نمیکنیم. همچنین این طیف، چگالی توان برحسب ولت مربع بر هرتز یا توان بار برحسب وات نیست. نام ستون گزارش باید همین تفاوت را روشن نگه دارد.
۳. پنجره، بهرهٔ همدوس و یک مثال نقض

برای یک بررسی جدا، فقط کسینوس ۷۰۰ هرتز را در پنجرهٔ Hann تناوبی ضرب میکنیم. فرمول استفادهشده 0.5-0.5*cos(2*pi*n/N) برای اندیس صفر تا ۶۳ است. مستندات Hann تناوبی در MathWorks ساخت پنجرهٔ یک عضو بلندتر و حذف عضو نهایی را توضیح میدهد؛ راهنمای SciPy نیز حالت تناوبی را از حالت متقارن جدا میکند. SciPy در برنامه اجرا نمیشود.
میانگین وزنهای این پنجره ۰٫۵ است. در کسینوس همخانه و دور از دو انتها، دامنهٔ خانهٔ هفت پس از پنجره تقریباً ۰٫۸ ولت میشود؛ تقسیم آن بر میانگین وزنها، ۱٫۶ ولت را برمیگرداند. این شاهد به همین تکموج و خانهٔ انتخابشده وابسته است. شاخهٔ پنجره را روی سیگنال خالص اجرا کردهایم تا آن را با مؤلفهٔ ثابت و نایکوئیست مثال اول مخلوط نکنیم.
اکنون فرکانس را ۷۵۰ هرتز، میان دو خانه، میگذاریم. حتی پس از همان تصحیح، بزرگترین خانه حدود ۱٫۳۵۸۲۸۷ ولت است و دامنهٔ تألیفی هنوز ۱٫۶ ولت باقی میماند. بنابراین «تقسیم بر میانگین پنجره» تخمینزن عمومی دامنه از بلندترین خانه نمیسازد. این مثال نقض، حد خطای همهٔ پنجرهها یا همهٔ فرکانسها نیست. معیار پذیرش باید شرط همخانه بودن، روش انتخاب خانه و معنای خروجی را همراه خود داشته باشد.
۴. کد کامل، مرجع مستقل و خروجی ثبتشده

کد با Python 3.14.5 و NumPy 2.4.6 اجرا شده است. علاوه بر مرجع دستی، هر ۳۳ ضریب را با جمع مستقیم DFT و کتابخانهٔ استاندارد دوباره محاسبه میکند. این مسیر از FFT کتابخانه استفاده نمیکند؛ با این حال هر دو به تعریف مشترک تبدیل وابستهاند. بازسازی معکوس، کنترل مکمل دیگری است و بهتنهایی کافی نیست، چون دو مقیاسبندی غلطِ سازگار ممکن است یکدیگر را خنثی کنند.
تلورانس مطلق دامنه یک پیکوولت و حد مقایسهٔ خروجی خام ۱۰ به توان منفی ده در مقیاس مجموع نمونههای ولتاژ است. این حدود انتخاب آزمون همین دادهاند و دقت دستگاه نیستند. پس از تبدیل با asarray، شکل و نوع دادهٔ آرایههای مقایسه سختگیرانه بررسی میشوند؛ این بررسی جای قرارداد نوع پایتونی و ظرف ورودی را نمیگیرد.
"""Explicit amplitude acceptance criteria for authored finite voltage samples."""
import cmath
import json
import math
import platform
import numpy as np
def real_number(value):
if type(value) not in (int, float):
raise TypeError('Exact int or float required; bool/complex/string excluded')
try:
value = float(value)
except OverflowError as error:
raise ValueError('Finite float64 representation required') from error
if not math.isfinite(value):
raise ValueError('Finite value required')
return value
def amplitude_spectrum(samples_v, sample_rate_hz):
if type(samples_v) not in (list, tuple):
raise TypeError('Exact list or tuple required')
n = len(samples_v)
if n < 4 or n % 2:
raise ValueError('This educational API accepts even lengths at least four only')
rate = real_number(sample_rate_hz)
if rate <= 0:
raise ValueError('Positive sample rate required')
interval_s = 1.0 / rate
if not math.isfinite(interval_s) or interval_s <= 0:
raise ValueError('Finite positive reciprocal sample interval required')
values = np.array([real_number(value) for value in samples_v], dtype=np.float64)
transformed = np.fft.rfft(values, norm='backward')
amplitude_v = np.abs(transformed) / n
amplitude_v[1:-1] *= 2.0 # Even N: neither DC nor Nyquist has a separate partner.
frequency_hz = np.fft.rfftfreq(n, d=interval_s)
if not (np.isfinite(amplitude_v).all() and np.isfinite(frequency_hz).all()):
raise ArithmeticError('Nonfinite output is outside this API contract')
return {'n': n, 'frequency_hz': frequency_hz, 'amplitude_v': amplitude_v,
'transformed': transformed}
def direct_dft(samples, k):
n = len(samples)
terms = [value * cmath.exp(-2j * math.pi * k * index / n)
for index, value in enumerate(samples)]
return complex(math.fsum(term.real for term in terms),
math.fsum(term.imag for term in terms))
def close(actual, expected, atol):
# Check array dtype/shape after asarray, not the original Python container type.
np.testing.assert_allclose(np.asarray(actual), np.asarray(expected), rtol=0,
atol=atol, equal_nan=False, strict=True)
return True
def rejects(call, exception):
try:
call()
except exception:
return True
return False
def run_example():
n, rate_hz, tone_hz, peak_v = 64, 6400.0, 700.0, 1.6
times_s = [index / rate_hz for index in range(n)]
pure_tone = [peak_v * math.cos(2 * math.pi * tone_hz * t) for t in times_s]
samples_v = [0.25 + tone + 0.1 * (-1)**index
for index, tone in enumerate(pure_tone)]
before = list(samples_v)
result = amplitude_spectrum(samples_v, rate_hz)
raw, amplitude = result['transformed'], result['amplitude_v']
direct_reference = np.array([direct_dft(samples_v, k) for k in range(n // 2 + 1)])
unused = [k for k in range(33) if k not in (0, 7, 32)]
time_energy_v2_samples = math.fsum(value * value for value in samples_v)
spectral_energy_v2_samples = float((abs(raw[0])**2 + abs(raw[-1])**2
+ 2 * np.sum(np.abs(raw[1:-1])**2)) / n)
window = [0.5 - 0.5 * math.cos(2 * math.pi * index / n) for index in range(n)]
coherent_gain = math.fsum(window) / n
windowed = amplitude_spectrum([value * weight for value, weight in zip(pure_tone, window)], rate_hz)
raw_window_peak = float(windowed['amplitude_v'][7])
corrected_window_peak = raw_window_peak / coherent_gain
off_bin_tone = [peak_v * math.cos(2 * math.pi * 750.0 * t) for t in times_s]
off_bin = amplitude_spectrum([value * weight for value, weight in zip(off_bin_tone, window)], rate_hz)
off_bin_corrected = off_bin['amplitude_v'] / coherent_gain
off_bin_peak_index = int(np.argmax(off_bin_corrected))
alias_samples = [peak_v * math.cos(2 * math.pi * 5700.0 * t) for t in times_s]
doubled = amplitude_spectrum([-2.0 * value for value in samples_v], rate_hz)
wrong_endpoints = amplitude.copy()
wrong_endpoints[[0, -1]] *= 2
wrong_no_pairing = np.abs(raw) / n
checks = {
'authored_count_duration_and_last_sample_time_are_distinct': len(times_s) == 64
and math.isclose(n / rate_hz, 0.01) and math.isclose(times_s[-1], 0.00984375),
'frequency_grid_has_33_bins_from_zero_to_3200_hz': result['frequency_hz'].shape == (33,)
and close(result['frequency_hz'], np.arange(33, dtype=np.float64) * 100.0, 1e-10),
'three_raw_dft_coefficients_match_hand_reference': close(raw[[0, 7, 32]], np.array([16+0j, 51.2+0j, 6.4+0j]), 1e-10),
'dc_is_not_doubled_and_retains_025_v': math.isclose(float(amplitude[0]), 0.25, rel_tol=0, abs_tol=1e-12),
'coherent_700_hz_tone_has_16_v_peak_amplitude': math.isclose(float(amplitude[7]), 1.6, rel_tol=0, abs_tol=1e-12),
'nyquist_is_not_doubled_and_retains_01_v': math.isclose(float(amplitude[-1]), 0.1, rel_tol=0, abs_tol=1e-12),
'all_other_authored_bins_are_near_zero': bool(np.all(amplitude[unused] < 1e-12)),
'all_33_fft_coefficients_match_independent_direct_dft': close(raw, direct_reference, 1e-10),
'inverse_transform_recovers_all_64_original_values': close(np.fft.irfft(raw, n=n, norm='backward'), np.array(samples_v), 1e-12),
'sample_square_sum_matches_hand_8656_v2_samples': math.isclose(time_energy_v2_samples, 86.56, rel_tol=0, abs_tol=1e-10),
'parseval_weights_endpoints_once_and_interior_twice': math.isclose(spectral_energy_v2_samples, time_energy_v2_samples, rel_tol=0, abs_tol=1e-10),
'negative_double_signal_doubles_magnitude_not_sign': close(doubled['amplitude_v'], 2.0 * amplitude, 1e-12),
'periodic_hann_has_half_gain_and_nonzero_final_sample': math.isclose(coherent_gain, 0.5, rel_tol=0, abs_tol=1e-15)
and window[0] == 0.0 and window[-1] > 0.0,
'coherent_pure_tone_hann_peak_corrects_08_to_16_v': math.isclose(raw_window_peak, 0.8, rel_tol=0, abs_tol=1e-12)
and math.isclose(corrected_window_peak, 1.6, rel_tol=0, abs_tol=1e-12),
'half_bin_tone_peak_remains_biased_after_gain_correction': abs(float(off_bin_corrected[off_bin_peak_index]) - 1.6) > 0.05,
'700_and_5700_hz_cosines_have_indistinguishable_samples': close(np.array(pure_tone), np.array(alias_samples), 1e-12),
'unsupported_container_length_and_odd_length_are_rejected': all(rejects(call, exception) for call, exception in (
(lambda: amplitude_spectrum(np.array(samples_v), rate_hz), TypeError),
(lambda: amplitude_spectrum([], rate_hz), ValueError),
(lambda: amplitude_spectrum([0, 1, 0], rate_hz), ValueError))),
'bad_scalar_types_nonfinite_values_and_rates_are_rejected': all(
rejects(lambda value=value: amplitude_spectrum([value, 0, 0, 0], rate_hz), (TypeError, ValueError))
for value in (True, '1', 1+0j, math.nan, math.inf, 10**400)) and all(
rejects(lambda rate=rate: amplitude_spectrum([0, 0, 0, 0], rate), (TypeError, ValueError))
for rate in (False, 0, -1, math.nan, math.inf, 1e-320)),
'hand_reference_rejects_wrong_endpoint_and_pair_scaling': rejects(lambda: close(wrong_endpoints[[0, -1]], np.array([0.25, 0.1]), 1e-12), AssertionError)
and rejects(lambda: close(float(wrong_no_pairing[7]), 1.6, 1e-12), AssertionError),
'original_input_is_unchanged_and_tuple_input_agrees': samples_v == before
and close(amplitude_spectrum(tuple(samples_v), rate_hz)['amplitude_v'], amplitude, 1e-12),
}
assert len(checks) == 20 and all(type(value) is bool for value in checks.values())
assert all(checks.values()), checks
return {'python': platform.python_version(), 'numpy': np.__version__,
'scope': 'authored_uniform_voltage_samples_no_instruments_or_noise',
'fixture': {'sample_count': n, 'sample_rate_hz': rate_hz, 'tone_hz': tone_hz,
'tone_peak_v': peak_v, 'dc_v': 0.25, 'nyquist_cosine_coefficient_v': 0.1,
'record_duration_s': n / rate_hz, 'last_sample_time_s': times_s[-1],
'frequency_grid_spacing_hz': rate_hz / n, 'normalization': 'backward'},
'observed': {'dc_v': float(amplitude[0]), 'tone_peak_v': float(amplitude[7]),
'nyquist_v': float(amplitude[-1]),
'max_unused_bin_v': float(np.max(amplitude[unused])),
'max_abs_fft_direct_dft_difference_v_samples': float(np.max(np.abs(raw-direct_reference))),
'sample_square_sum_v2_samples': time_energy_v2_samples,
'parseval_square_sum_v2_samples': spectral_energy_v2_samples},
'hann_pure_tone_probe': {'formula': '0.5-0.5*cos(2*pi*n/N), n=0..N-1',
'coherent_gain': coherent_gain, 'uncorrected_peak_v': raw_window_peak,
'corrected_peak_v': corrected_window_peak},
'off_bin_probe': {'authored_tone_hz': 750.0, 'true_authored_peak_v': peak_v,
'largest_corrected_grid_bin_hz': float(result['frequency_hz'][off_bin_peak_index]),
'largest_corrected_grid_peak_v': float(off_bin_corrected[off_bin_peak_index]),
'not_a_universal_bias_bound': True},
'alias_probe_hz': [700.0, 5700.0],
'absolute_tolerances': {'amplitude_v': 1e-12, 'raw_dft_v_samples': 1e-10,
'sample_square_sum_v2_samples': 1e-10, 'frequency_hz': 1e-10},
'checks': checks}
if __name__ == '__main__':
print(json.dumps(run_example(), indent=2))
هر ۲۰ بررسی نامدار موفق شد. بیشترین اختلاف FFT و جمع مستقیم حدود ۱٫۴۴ ضربدر ده به توان منفی سیزده بود. مجموع مربع نمونهها حدود ۸۶٫۵۶ ولت مربع شد و محاسبهٔ متناظر در حوزهٔ فرکانس همان مقدار را داد. این جمع گسسته، انرژی برحسب ژول نیست. آزمونهای عمدی، دو برابر کردن دو انتها و فراموش کردن دو برابر کردن خانهٔ داخلی را رد میکنند. گزارش کامل، فرضها و مقدارهای مشاهدهشده را نگه میدارد.
{
"python": "3.14.5",
"numpy": "2.4.6",
"scope": "authored_uniform_voltage_samples_no_instruments_or_noise",
"fixture": {
"sample_count": 64,
"sample_rate_hz": 6400.0,
"tone_hz": 700.0,
"tone_peak_v": 1.6,
"dc_v": 0.25,
"nyquist_cosine_coefficient_v": 0.1,
"record_duration_s": 0.01,
"last_sample_time_s": 0.00984375,
"frequency_grid_spacing_hz": 100.0,
"normalization": "backward"
},
"observed": {
"dc_v": 0.2500000000000005,
"tone_peak_v": 1.6000000000000003,
"nyquist_v": 0.09999999999999999,
"max_unused_bin_v": 1.8541675650869462e-15,
"max_abs_fft_direct_dft_difference_v_samples": 1.4377088998462502e-13,
"sample_square_sum_v2_samples": 86.56000000000004,
"parseval_square_sum_v2_samples": 86.56000000000004
},
"hann_pure_tone_probe": {
"formula": "0.5-0.5*cos(2*pi*n/N), n=0..N-1",
"coherent_gain": 0.5,
"uncorrected_peak_v": 0.8000000000000003,
"corrected_peak_v": 1.6000000000000005
},
"off_bin_probe": {
"authored_tone_hz": 750.0,
"true_authored_peak_v": 1.6,
"largest_corrected_grid_bin_hz": 700.0,
"largest_corrected_grid_peak_v": 1.3582867623188384,
"not_a_universal_bias_bound": true
},
"alias_probe_hz": [
700.0,
5700.0
],
"absolute_tolerances": {
"amplitude_v": 1e-12,
"raw_dft_v_samples": 1e-10,
"sample_square_sum_v2_samples": 1e-10,
"frequency_hz": 1e-10
},
"checks": {
"authored_count_duration_and_last_sample_time_are_distinct": true,
"frequency_grid_has_33_bins_from_zero_to_3200_hz": true,
"three_raw_dft_coefficients_match_hand_reference": true,
"dc_is_not_doubled_and_retains_025_v": true,
"coherent_700_hz_tone_has_16_v_peak_amplitude": true,
"nyquist_is_not_doubled_and_retains_01_v": true,
"all_other_authored_bins_are_near_zero": true,
"all_33_fft_coefficients_match_independent_direct_dft": true,
"inverse_transform_recovers_all_64_original_values": true,
"sample_square_sum_matches_hand_8656_v2_samples": true,
"parseval_weights_endpoints_once_and_interior_twice": true,
"negative_double_signal_doubles_magnitude_not_sign": true,
"periodic_hann_has_half_gain_and_nonzero_final_sample": true,
"coherent_pure_tone_hann_peak_corrects_08_to_16_v": true,
"half_bin_tone_peak_remains_biased_after_gain_correction": true,
"700_and_5700_hz_cosines_have_indistinguishable_samples": true,
"unsupported_container_length_and_odd_length_are_rejected": true,
"bad_scalar_types_nonfinite_values_and_rates_are_rejected": true,
"hand_reference_rejects_wrong_endpoint_and_pair_scaling": true,
"original_input_is_unchanged_and_tuple_input_agrees": true
}
}
۵. چهار تمرین پاسخدار و محدودیت پذیرش

تمرین اول: اگر همهٔ نمونهها در منفی دو ضرب شوند، دامنه چه میشود؟ پاسخ: قدرمطلق دامنهها دو برابر میشود؛ علامت منفی در فاز اثر دارد. برنامه این ویژگی را میآزماید. انتظار دامنهٔ منفی، با قرارداد قدرمطلق ناسازگار است.
تمرین دوم: چرا آخرین خانه را همیشه نباید مانند حالت زوج پردازش کرد؟ پاسخ: در طول فرد، آخرین خروجی مثبت rfft روی نایکوئیست نیست و معمولاً مختلط است. تابع این درس طول فرد را صریحاً رد میکند؛ حذف این کنترل بدون تغییر مقیاسبندی، گسترش معتبر محسوب نمیشود.
تمرین سوم: آیا تصحیح بهرهٔ Hann دامنهٔ ۷۵۰ هرتز را دقیق میکند؟ پاسخ: در اجرای حاضر خیر؛ بیشترین مقدار حدود ۱٫۳۵۸۲۸۷ ولت است. شرط پذیرش تکموج همخانه را نباید بیتغییر به فرکانس میانخانه تعمیم داد.
تمرین چهارم: آیا مشاهدهٔ خانهٔ ۷۰۰ هرتز، فرکانس فیزیکی منبع را ثابت میکند؟ پاسخ: خیر؛ کسینوس ۵۷۰۰ هرتز در همین زمانهای نمونهبرداری، نمونههایی برابر کسینوس ۷۰۰ میسازد، چون ۵۷۰۰ برابر نرخ منهای ۷۰۰ است. این همارزی عددی نیز آزموده میشود؛ دربارهٔ پهنای باند ورودی واقعی شاهدی نداریم. برای تحویل آزمایش، ثبت داده و محیط آزمایش تکرارپذیر و برای مرزهای مدل گسسته، آزمون کوانتیزهساز با مرجع مستقل مکملاند. منابع در ۲۴ مهر ۱۴۰۵ بازخوانی شدهاند؛ تصاویر مفهومیاند و نمودار اندازهگیری نیستند.
منابع و مطالعهٔ بیشتر
- NumPy2.4 FFT؛ تعریف تبدیل و نرمالسازی backward
- NumPy2.4 rfft؛ خروجی حقیقی، زوج و فرد و نایکوئیست
- NumPy2.4 rfftfreq؛ محور فرکانس و واحد فاصلهٔ زمانی
- MathWorks fft؛ دامنهٔ یکطرفه و دو انتهای صفر و نایکوئیست
- MathWorks hann؛ تعریف پنجرهٔ تناوبی با حذف عضو پایانی
- SciPy hann؛ تمایز پنجرهٔ متقارن و تناوبی
- NumPy2.4 assert_allclose؛ تلورانس و شکل و نوع آرایه
بررسی منابع: ۲۴ مهر ۱۴۰۵. تحلیلها و پیشنهادهای اجرایی، جمعبندی نویسندهاند.

