فهرست مقاله
فرض کنید ماژول تحلیلِ ولتاژ، ستونی با نام «طیف» تحویل میدهد. از این نام نمیتوان فهمید عددها دامنهاند، مربع دامنهاند یا چگالی بر واحد هرتز. در این درس، پریودوگرامِ یک رکورد تألیفی را با واحد و ترازِ قابل بررسی میسازیم. مسئله، کنترل نرمالسازی نرمافزار است؛ دادهٔ اندازهگیریشده، نویز تصادفیِ واقعی یا تخمینِ مشخصات یک دستگاه در اختیار نداریم.
۱. چگالی طیفی با دامنهٔ FFT فرق دارد

چهار نمونهٔ چهار، صفر، دو و دو ولت را با نرخ هشت نمونه بر ثانیه میگیریم. فاصلهٔ زمانی نمونهها یکنواخت است و رکورد چهار نمونه دارد. قراردادِ حاضر هیچ میانگینی را حذف نمیکند؛ مؤلفهٔ ثابت باید در گزارش بماند. همین تصمیم با پیشفرضِ حذفِ ثابت در تابع periodogram در SciPy متفاوت است و باید هنگام مقایسه صریح باشد.
در آزمون دامنهٔ FFT ولتاژ، خروجی با واحد ولت و اصلاحِ دامنهٔ پنجره بررسی میشد. اینجا خروجی، چگالی با واحد ولتِ مربع بر هرتز است. مربعِ یک دامنهٔ یکطرفه را نباید بیتغییر چگالی نامید؛ هم مخرجِ نرخ نمونهبرداری و هم نحوهٔ جمعکردن سهم فرکانسهای منفی اهمیت دارند.
تبدیل مستقیمِ نمونهٔ چهارتایی، در سه خانهٔ نامنفی به ترتیب هشت، دوبهعلاوهٔ دو واحد موهومی، و چهار است. این عددهای خام به تعریف تبدیل وابستهاند. تعریف و نرمالسازی DFT در NumPy 2.4، تبدیل مستقیمِ بدون تقسیم و معکوسِ دارای عامل یکبرطول را توضیح میدهد؛ برنامه نیز صریحاً حالت backward را به کار میبرد.
با پنجرهٔ واحد، مجموع مربعِ وزنها چهار و حاصلضرب آن در نرخ، سیودو است. مربعِ قدرمطلقِ ضریبها را بر این عدد تقسیم میکنیم و سهمِ جفتِ داخلی را جمع میزنیم. چگالیها دو، نیم و نیم ولتِ مربع بر هرتزند. فرکانسهای متناظر صفر، دو و چهار هرتز و فاصلهٔ خانهها دو هرتز است؛ جمعِ چگالیها ضربدر این فاصله، شش ولتِ مربع میشود.
۲. دو انتها، طول فرد و وزنِ پنجره

برای ورودی حقیقی، ضریبهای مثبت و منفیِ متناظر مزدوجاند و مربعِ اندازهٔ یکسانی دارند. در نمایش یکطرفه، سهمِ دو طرفِ داخلی با هم جمع میشود. خانهٔ صفر، جفتِ جداگانه ندارد؛ در طولِ تبدیلِ زوج، خانهٔ نایکوئیست نیز جدا دوباره شمرده نمیشود. دو برابر کردنِ همهٔ خانهها، ترازِ همین مثال را خراب میکند.
شرطِ آخرین خانه به طولِ تبدیل مربوط است. برای سه نمونهٔ یک، صفر و صفر ولت با نرخ سه نمونه بر ثانیه، دو خانهٔ نامنفی داریم. آخرین فرکانس یک هرتز است، نه نایکوئیستِ یکونیم هرتز؛ پس سهمِ منفیاش باید اضافه شود. چگالیها یکنهم و دونهم و جمعِ وزندارشان یکسوم ولتِ مربع است. کد حالتِ تکنمونه را نیز با تنها خانهٔ صفر میپذیرد.
اگر وزنهای حقیقیِ پنجره را w بنامیم، ابتدا نمونهها در همین وزنها ضرب میشوند. مخرجِ چگالی برابر نرخ ضربدر مجموع مربعِ وزنهاست؛ مخرجِ اصلاحِ دامنه، کمیتِ دیگری است. بخش کاملِ سیگنال نمونهبرداریشده در راهنمای SciPy این دو ثابتِ نرمالسازی و تعریف چگالی را جدا عرضه میکند. پنجرهٔ این آزمایش برداری صریح است و ساختِ خودکارِ Hann یا اجرای SciPy نداریم.
از رابطهٔ گسستهٔ Parseval نتیجه میگیریم که جمعِ چگالیِ یکطرفه ضربدر فاصلهٔ خانهها، برابر مجموع مربعِ نمونههای پنجرهخورده تقسیم بر مجموع مربعِ وزنهاست. این هویتِ حسابِ دقیق، برای رکوردِ دلخواه الزاماً میانگین مربعِ نمونههای اصلی نیست. در اینجا از روایتِ نرمالسازیِ پنجره، برابریِ عمومیِ توانِ هر سیگنالِ قطعی پیش و پس از پنجره را استنباط نمیکنیم.
شاهد روشن، وزنهای یک، صفر، یک و صفر است. برای دادهٔ اصلی، میانگین مربع شش است؛ میانگین مربعِ نسخهٔ پنجرهخورده با مخرج چهار، پنج میشود؛ اما ترازِ پریودوگرام با مخرجِ انرژیِ پنجره، ده ولتِ مربع است. این سه عدد، سه تعریف متفاوتاند. آموزش RMS نمونههای ولتاژ تعریفِ وزنبرابر را بررسی میکند و جای این وزنگذاریِ تازه را نمیگیرد.
۳. کد کاملِ پریودوگرامِ بدون حذف میانگین

تابع، فهرست یا تاپلِ غیرخالی از int یا float واقعی پایتون را میپذیرد و آنها را به float تبدیل میکند. بولی، رشته، مختلط و مقدار نامتناهی پذیرفته نیست. نمونهها برحسب ولت و وزنها بیبعدند. پنجرهٔ اختیاری باید همطول باشد و انرژیِ غیرصفر داشته باشد؛ وزنِ منفیِ حقیقی ممنوع نیست، ولی پنجرهٔ مختلط در این رابط پوشش داده نمیشود.
نرخ باید مثبت و متناهی باشد. طولِ تبدیل، عدد صحیحِ حداقل برابر تعدادِ نمونههاست؛ کوتاهکردنِ خاموشِ رکورد نداریم. در طول بزرگتر، فقط صفرهای قراردادی به انتها افزوده میشوند. برای تعدادِ نمونه یا بُعدِ دامنه، سقفِ کوچکِ ساختگی نگذاشتهایم؛ محدودیتِ حافظه و محدودهٔ محاسبات همچنان واقعیاند و این کد تضمینِ پردازش هر اندازهای نیست.
"""Finite real voltage periodogram; no detrending and no statistical PSD claim."""
from dataclasses import dataclass, asdict
from fractions import Fraction
import cmath
import json
import math
import platform
import numpy as np
def real_number(value):
if type(value) not in (int, float):
raise ValueError("actual int/float required, excluding bool")
try:
result = float(value)
except OverflowError as exc:
raise ValueError("value not representable as finite float") from exc
if not math.isfinite(result):
raise ValueError("finite number required")
return result
def vector(values):
if type(values) not in (list, tuple) or not values:
raise ValueError("nonempty list/tuple required")
return tuple(real_number(v) for v in values)
@dataclass(frozen=True)
class Density:
frequency_hz: tuple
density_v2_per_hz: tuple
df_hz: float
integrated_v2: float
window_square_sum: float
n_samples: int
nfft: int
detrend: str = "off"
def periodogram(samples_v, fs_hz, window=None, nfft=None):
"""Real finite samples in V, dimensionless real window; nfft >= len(samples)."""
x = vector(samples_v)
fs = real_number(fs_hz)
if fs <= 0:
raise ValueError("positive sampling frequency in Hz required")
w = (1.0,) * len(x) if window is None else vector(window)
if len(w) != len(x) or not any(v != 0 for v in w):
raise ValueError("same-length, nonzero-energy window required")
size = len(x) if nfft is None else nfft
if type(size) is not int or size < len(x):
raise ValueError("actual integer nfft >= sample count required")
df = fs / size
if not math.isfinite(df) or df <= 0:
raise ArithmeticError("frequency grid is not finite and positive")
try:
with np.errstate(over="raise", invalid="raise", divide="raise", under="ignore"):
xa = np.array(x, dtype=np.float64)
wa = np.array(w, dtype=np.float64)
energy = float(wa @ wa)
denominator = fs * energy
if not math.isfinite(denominator) or denominator <= 0:
raise ArithmeticError("window normalization out of floating range")
transform = np.fft.rfft(xa * wa, n=size, norm="backward")
if not np.isfinite(transform).all():
raise ArithmeticError("nonfinite FFT")
density = (transform.real ** 2 + transform.imag ** 2) / denominator
if size % 2 == 0:
density[1:-1] *= 2
else:
density[1:] *= 2
frequencies = np.arange(len(density), dtype=np.float64) * df
if not np.isfinite(density).all() or not np.isfinite(frequencies).all():
raise ArithmeticError("nonfinite spectrum or frequencies")
area = math.fsum(float(v) for v in density) * df
if not math.isfinite(area):
raise ArithmeticError("nonfinite integrated density")
except (FloatingPointError, OverflowError) as exc:
raise ArithmeticError("intermediate floating-point range exceeded") from exc
return Density(tuple(map(float, frequencies)), tuple(map(float, density)),
df, area, energy, len(x), size)
def full_dft_oracle(x, w, fs, size):
"""All complex DFT bins, then pair opposite bins; no FFT or doubling slice."""
spectrum = []
norm = float(fs) * math.fsum(float(t) ** 2 for t in w)
for k in range(size):
terms = [float(a) * float(b) * cmath.exp(-2j * math.pi * k * i / size)
for i, (a, b) in enumerate(zip(x, w, strict=True))]
value = complex(math.fsum(t.real for t in terms),
math.fsum(t.imag for t in terms))
spectrum.append(abs(value) ** 2 / norm)
return tuple(spectrum[k] if k == 0 or 2 * k == size
else spectrum[k] + spectrum[size - k]
for k in range(size // 2 + 1))
def exact_weighted_square(x, w):
xf, wf = map(lambda v: tuple(Fraction(float(t)) for t in v), (x, w))
return sum((a * b) ** 2 for a, b in zip(xf, wf, strict=True)) / sum(b * b for b in wf)
def close(a, b):
return math.isclose(float(a), float(b), rel_tol=2e-11, abs_tol=2e-12)
def rejects(exc, *args, **kwargs):
try:
periodogram(*args, **kwargs)
except exc:
return True
return False
def run():
checks = {}
def check(name, passed):
checks[name] = bool(passed)
assert checks[name], name
x, fs = (4, 0, 2, 2), 8
p = periodogram(x, fs)
check("literal_DC_interior_Nyquist_density", p.density_v2_per_hz == (2., .5, .5))
check("literal_bin_width_and_mean_square", p.df_hz == 2 and p.integrated_v2 == 6)
odd = periodogram((1, 0, 0), 3)
check("odd_last_bin_is_paired_not_Nyquist", all(close(a, b) for a, b in zip(odd.density_v2_per_hz, (1/9, 2/9))) and close(odd.integrated_v2, 1/3))
single = periodogram((2,), 8)
check("single_sample_has_only_DC", single.density_v2_per_hz == (.5,) and single.integrated_v2 == 4)
w = (1, 0, 1, 0)
weighted = periodogram(x, fs, w)
check("weighted_square_is_not_ordinary_or_windowed_mean", weighted.integrated_v2 == 10 and exact_weighted_square(x, w) == 10 and sum((a*b)**2 for a,b in zip(x,w))/len(x) == 5 and p.integrated_v2 == 6)
cases, max_density_difference, max_parseval_difference = 0, 0., 0.
for n in range(1, 10):
values = tuple(((3*i) % 7 - 3) / 2 for i in range(n))
windows = [(1.,)*n, tuple(.5 + .25*(i % 3) for i in range(n)), tuple((-1.)**i for i in range(n))]
for weights in windows:
for size in (n, n+1, 2*n):
result = periodogram(values, 12, weights, size)
expected = full_dft_oracle(values, weights, 12, size)
difference = max(abs(a-b) for a,b in zip(result.density_v2_per_hz, expected, strict=True))
target = float(exact_weighted_square(values, weights))
max_density_difference = max(max_density_difference, difference)
max_parseval_difference = max(max_parseval_difference, abs(result.integrated_v2-target))
assert all(close(a,b) for a,b in zip(result.density_v2_per_hz,expected,strict=True)) and close(result.integrated_v2,target)
cases += 1
check("full_DFT_and_Fraction_time_oracles_81_fixtures_ONE_group", cases == 81)
padded = periodogram(x, fs, nfft=8)
check("zero_padding_changes_grid_not_integrated_square", padded.df_hz == 1 and close(padded.integrated_v2,6) and all(close(padded.density_v2_per_hz[2*i],v) for i,v in enumerate(p.density_v2_per_hz)))
rate2 = periodogram(x, fs*2)
check("sampling_rate_scales_density_inverse_and_frequency_direct", all(close(a,2*b) for a,b in zip(p.density_v2_per_hz,rate2.density_v2_per_hz)) and rate2.integrated_v2 == p.integrated_v2 and rate2.df_hz == 2*p.df_hz)
signal2 = periodogram(tuple(-2*a for a in x),fs)
check("signal_gain_squared_in_density", all(close(a,4*b) for a,b in zip(signal2.density_v2_per_hz,p.density_v2_per_hz)))
window3 = periodogram(x,fs,tuple(3*a for a in w))
check("common_nonzero_window_gain_cancels", window3.density_v2_per_hz == weighted.density_v2_per_hz)
check("zero_signal_remains_zero_without_detrending", periodogram((0,0,0),3).integrated_v2 == 0 and periodogram((2,2),8).integrated_v2 == 4 and p.detrend == "off")
data, weights = [4,0,2,2], [1,0,1,0]
periodogram(data,8,weights)
check("input_lists_are_not_mutated", data == [4,0,2,2] and weights == [1,0,1,0])
check("container_shape_type_and_nfft_rejections", all(rejects(ValueError,*args,**kw) for args,kw in [(([],8),{}),(((True,0),8),{}),(((1+0j,0),8),{}),((("1",0),8),{}),(((1,2),8),{'window':(1,)}),(((1,2),8),{'nfft':1}),(((1,2),8),{'nfft':True}),(((1,2),8),{'nfft':2.0})]))
check("sampling_frequency_rejections", all(rejects(ValueError,(1,2),f) for f in (0,-1,True,float('inf'),float('nan'),"8")))
check("zero_and_nonfinite_window_rejections", rejects(ValueError,x,8,(0,0,0,0)) and rejects(ValueError,x,8,(1,float('nan'),1,1)))
check("finite_input_intermediate_overflow_is_explicit", rejects(ArithmeticError,(1e308,1e308),8) and rejects(ArithmeticError,(1,2),5e-324))
return {'python':platform.python_version(),'numpy':np.__version__,'units':{'samples':'V','frequency':'Hz','density':'V^2/Hz','integrated_density':'V^2'},'detrending':'off; supplied samples are not centered','hand':asdict(p),'odd_example':asdict(odd),'window_counterexample':{'weighted_integral_v2':weighted.integrated_v2,'ordinary_mean_square_v2':6,'windowed_mean_square_v2':5},'zero_padded_integral_v2':padded.integrated_v2,'independent_oracle_fixtures_in_ONE_group':cases,'max_density_difference_v2_per_hz':max_density_difference,'max_Parseval_difference_v2':max_parseval_difference,'comparison_relative_tolerance':2e-11,'comparison_absolute_tolerance_in_corresponding_units':2e-12,'checks':checks}
if __name__ == '__main__':
print(json.dumps(run(),ensure_ascii=False,indent=2,allow_nan=False))
حتی ورودیِ متناهی ممکن است در ضرب، FFT یا مربعکردن از محدودهٔ float بیرون برود. تابع در این وضعیت خطای صریح میدهد و نتیجهٔ نامتناهی را گزارش موفق نمیکند. وزنهای غیرصفرِ بسیار ریز هم ممکن است هنگام ساخت مخرج به صفر گرد شوند. روش، الگوریتمِ مقیاسبندیشده برای پوششِ همهٔ دامنههای دینامیکی نیست؛ این مرزِ عددی در قرارداد و آزمون ثبت شده است.
خروجی، فرکانسها، چگالیها، فاصلهٔ خانهها و جمعِ چگالیِ وزندار را نگه میدارد. جمعِ مورد نظر، جمعِ خانههای همین DFT است؛ قانون ذوزنقهای با نصفکردنِ دو انتها را جای آن نمیگذاریم. برنامه با Python 3.14.5 و NumPy 2.4.6 واقعاً اجرا شده است. نسخهٔ مستندات SciPy 1.18.0 است؛ اجرای MATLAB یا SciPy ادعا نمیشود.
۴. مرجع DFT و ترازِ میانگین مربع

مرجع مستقل، همهٔ خانههای مثبت و منفی را با جمعِ مستقیم نماییهای مختلط در کتابخانهٔ استاندارد حساب میکند؛ rfft و برشِ دوبرابرکنندهٔ مسیر اصلی را فراخوانی نمیکند. سپس خانهٔ k و خانهٔ طولمنهایk را جفت میکند. بنابراین خطای انتخابِ انتهای زوج یا فرد، پشتِ استفادهٔ دوباره از همان برش پنهان نمیماند؛ هرچند هر دو مسیر به تعریفِ یک DFT وابستهاند.
مرجعِ زمانیِ جدا، مربعِ وزندار را با Fraction از مقدارهای float موجود حساب میکند. در هشتادویک fixture، نمونهها و وزنها کوچک و دودوییاند؛ مقایسه برای همین مجموعه انجام شده است. طولهای یک تا نه، طولِ تبدیلِ اصلی و دو انتخابِ افزوده، و سه بردارِ وزن پوشش دارند. این هشتادویک حالت فقط یک گروهِ آزموناند و اثباتِ همهٔ ورودیها یا خطای مشترکِ ناممکن نیستند.
شانزده گروهِ نامدار موفق شدند. بیشترین اختلافِ چگالی با DFT مستقیم حدود ۵٫۵۵ ضربدر ده به توان منفی شانزده ولتِ مربع بر هرتز و اختلافِ ترازِ زمانی حدود ۲٫۲۲ ضربدر ده به توان منفی شانزده ولتِ مربع شد. تلورانسِ نسبی دو ضربدر ده به توان منفی یازده و مطلق دو ضربدر ده به توان منفی دوازده در واحدِ کمیتِ مقایسهشده است؛ تلورانسِ ابزار اندازهگیری نیست.
{
"python": "3.14.5",
"numpy": "2.4.6",
"units": {
"samples": "V",
"frequency": "Hz",
"density": "V^2/Hz",
"integrated_density": "V^2"
},
"detrending": "off; supplied samples are not centered",
"hand": {
"frequency_hz": [
0.0,
2.0,
4.0
],
"density_v2_per_hz": [
2.0,
0.5,
0.5
],
"df_hz": 2.0,
"integrated_v2": 6.0,
"window_square_sum": 4.0,
"n_samples": 4,
"nfft": 4,
"detrend": "off"
},
"odd_example": {
"frequency_hz": [
0.0,
1.0
],
"density_v2_per_hz": [
0.1111111111111111,
0.2222222222222222
],
"df_hz": 1.0,
"integrated_v2": 0.3333333333333333,
"window_square_sum": 3.0,
"n_samples": 3,
"nfft": 3,
"detrend": "off"
},
"window_counterexample": {
"weighted_integral_v2": 10.0,
"ordinary_mean_square_v2": 6,
"windowed_mean_square_v2": 5
},
"zero_padded_integral_v2": 6.0,
"independent_oracle_fixtures_in_ONE_group": 81,
"max_density_difference_v2_per_hz": 5.551115123125783e-16,
"max_Parseval_difference_v2": 2.220446049250313e-16,
"comparison_relative_tolerance": 2e-11,
"comparison_absolute_tolerance_in_corresponding_units": 2e-12,
"checks": {
"literal_DC_interior_Nyquist_density": true,
"literal_bin_width_and_mean_square": true,
"odd_last_bin_is_paired_not_Nyquist": true,
"single_sample_has_only_DC": true,
"weighted_square_is_not_ordinary_or_windowed_mean": true,
"full_DFT_and_Fraction_time_oracles_81_fixtures_ONE_group": true,
"zero_padding_changes_grid_not_integrated_square": true,
"sampling_rate_scales_density_inverse_and_frequency_direct": true,
"signal_gain_squared_in_density": true,
"common_nonzero_window_gain_cancels": true,
"zero_signal_remains_zero_without_detrending": true,
"input_lists_are_not_mutated": true,
"container_shape_type_and_nfft_rejections": true,
"sampling_frequency_rejections": true,
"zero_and_nonfinite_window_rejections": true,
"finite_input_intermediate_overflow_is_explicit": true
}
}
افزودن صفر، فاصلهٔ خانهها را در مثال از دو به یک هرتز میرساند، ولی تراز شش ولتِ مربع میماند. در خانههای مشترک، چگالیِ ثبتشده سازگار است. این شاهد نشاندهندهٔ نمونهبرداریِ متراکمترِ نمایش طیفی است؛ از آن افزایشِ اطلاعاتِ رکورد، تفکیکِ تضمینیِ دو منبع یا بازیابیِ فرکانسِ آنالوگ را نتیجه نمیگیریم.
۵. چهار پرسش و مرز تفسیر PSD

پرسش اول: اگر نرخِ همان چهار عدد را دو برابر اعلام کنیم چه میشود؟ پاسخ: محور و فاصلهٔ خانهها دو برابر و چگالیِ هر خانه نصف میشود؛ ترازِ شش ثابت میماند. این تبدیلِ برچسبِ زمانی، مدرکِ نرخِ واقعیِ ثبت نیست. زمانبندیِ نامنظم نیز با عوضکردنِ همین یک عدد به نمونهبرداریِ یکنواخت تبدیل نمیشود.
پرسش دوم: چرا پنجرهٔ یک، صفر، یک، صفر عدد ده میدهد؟ پاسخ: جمعِ مربعِ نمونههای باقیمانده بیست و مجموع مربعِ وزنها دو است. تقسیم، ده ولتِ مربع میدهد. تقسیمِ همان بیست بر چهار، پنج است و به میانگین مربعِ نسخهٔ پنجرهخورده مربوط میشود؛ نامِ متفاوتِ مخرج، تفاوتِ نتیجه را توضیح میدهد.
پرسش سوم: ضربِ نمونهها در منفی دو چه اثری دارد؟ پاسخ: چگالی و جمعِ آن چهار برابر میشوند، چون تبدیل خطی و خروجی از مربعِ اندازه ساخته شده است. در مقابل، ضربِ همهٔ وزنهای پنجره در سه، صورت و مخرج را هر دو نه برابر میکند و چگالیِ مثال تغییر نمیکند. هر دو ویژگی در کد آزموده شدهاند.
پرسش چهارم: آیا شش ولتِ مربع یعنی شش وات یا PSD واقعیِ نویز؟ پاسخ: خیر؛ مدلِ بار و اندازهگیری نداریم، و یک رکوردِ قطعی، چگالیِ تصادفیِ فرایند را معلوم نمیکند. برای مقاومتِ معلوم، تفسیرِ توان به ولتاژِ واقعیِ دو سر همان بار وابسته است. منابع در ۲۶ مهر ۱۴۰۵ خوانده شدهاند؛ مطلب آموزش پایه است، تصاویر مفهومیاند و نمودارِ آزمایش معرفی نمیشوند.
منابع و مطالعهٔ بیشتر
- SciPy؛ قرارداد periodogram و مقیاس density
- NumPy؛ تعریف DFT و نرمالسازی backward
- SciPy؛ بخش کاملِ سیگنال نمونهبرداریشده و نرمالسازی طیف
بررسی منابع: ۲۶ مهر ۱۴۰۵. تحلیلها و پیشنهادهای اجرایی، جمعبندی نویسندهاند.

