2025, Nov 01 15:16
Как получить лог-нормальные сигналы с заданной PSD с помощью IAAFT
Как получить лог-нормальные сигналы с целевой PSD (синий и фиолетовый шум) без разрушения спектра: IAAFT, ранговое отображение и лог-лог интерполяция.
Генерация лог-нормальных сигналов с возрастающей плотностью спектральной мощности на словах кажется простой: сгенерировать гауссов шум, придать ему нужный спектр и возвести в степень. На практике последний шаг ломает спектральную структуру для высокочастотных вариантов вроде синего или фиолетового шума. В этом руководстве разберёмся, почему так происходит, и как получить сигналы, которые одновременно соответствуют целевому одномерному распределению (например, лог-нормальному) и целевой PSD с помощью IAAFT.
Постановка задачи
Цель — синтезировать случайные последовательности, чьи значения подчиняются длиннохвостому распределению на положительной полуоси (например, лог-нормальному), при этом плотность спектральной мощности мала на низких частотах и растёт с частотой (например, синий шум с наклоном +10 дБ/декаду или фиолетовый с +20 дБ/декаду). Взять выборку из лог-нормального распределения несложно. Формовать гауссов шум под заданную PSD через FFT — тоже стандартная практика. Трудность возникает, когда эти шаги сочетают, применяя нелинейное отображение (экспоненту) к уже сформованному процессу.
Демонстрация: сначала формование, затем экспонента
Следующий код генерирует цветной гауссов шум с несколькими каноническими PSD, затем применяет экспоненту, чтобы получить лог-нормальные выборки. После возведения в степень форма PSD сохраняется для белого и розового шума, но для синего и фиолетового спектр деградирует к белоподобному.
import numpy as np
import matplotlib.pyplot as plt
from scipy.signal import welch
# Спектральное формование через rFFT: умножаем на целевой амплитудный спектр
# и выполняем обратное преобразование.
def shape_spectrum(n_points, psd_fn=lambda f: 1.0):
freq_side = np.fft.rfft(np.random.randn(n_points))
spec = psd_fn(np.fft.rfftfreq(n_points))
spec = spec / np.sqrt(np.mean(spec ** 2))
colored = freq_side * spec
return np.fft.irfft(colored)
# Вспомогательная функция: «замораживает» функцию PSD в генератор вида длина->сигнал.
def with_psd(psd_fn):
return lambda n: shape_spectrum(n, psd_fn)
@with_psd
def make_white(f):
return 1.0
@with_psd
def make_blue(f):
return np.sqrt(f)
@with_psd
def make_violet(f):
return f
@with_psd
def make_brown(f):
return 1.0 / np.where(f == 0, float('inf'), f)
@with_psd
def make_pink(f):
return 1.0 / np.where(f == 0, float('inf'), np.sqrt(f))
def synthesize_colored(n_points, kind="white"):
if kind == "white":
return make_white(n_points)
elif kind == "pink":
return make_pink(n_points)
elif kind == "blue":
return make_blue(n_points)
elif kind == "violet":
return make_violet(n_points)
elif kind == "brown":
return make_brown(n_points)
# Демонстрация: сравниваем гауссовские и лог-нормализованные версии при разных типах PSD.
n_points = 100_000
kinds = ["white", "pink", "blue", "violet"]
colors = ["lightgray", "magenta", "blue", "violet"]
fig, axes = plt.subplots(2, 2, sharex=False, sharey="row")
bins_gauss = np.linspace(-5, 5, 101)
for nm, col in zip(kinds, colors):
x = synthesize_colored(n_points, nm)
axes[0, 0].hist(x, bins=bins_gauss, color=col, label=nm, histtype="step", density=True)
fx, px = welch(x)
axes[1, 0].loglog(fx, px, color=col, label=nm)
bins_logn = np.linspace(0, 10, 101)
for nm, col in zip(kinds, colors):
y = np.exp(synthesize_colored(n_points, nm))
axes[0, 1].hist(y, bins=bins_logn, color=col, label=nm, histtype="step", density=True)
fy, py = welch(y)
axes[1, 1].loglog(fy, py, color=col, label=nm)
axes[0, 0].set_title("Gaussian samples")
axes[0, 1].set_title("Exp(Gaussian) samples")
axes[0, 0].set_ylabel("Density")
axes[0, 0].set_xlabel("Value")
axes[0, 1].set_xlabel("Value")
axes[1, 0].set_ylabel("Power")
axes[1, 0].set_xlabel("Frequency [Hz]")
axes[1, 1].set_xlabel("Frequency [Hz]")
axes[0, 0].legend(loc="upper left")
fig.tight_layout()
plt.show()
Почему спектральная форма рушится после возведения в степень
Экспонента — нелинейная операция. В отличие от линейной фильтрации, она не сохраняет линейную корреляционную структуру, которую кодирует PSD. Для высокочастотных спектров, таких как синий и фиолетовый шум, применение экспоненты сильно искажает спектр, и итоговая PSD стремится к белому шуму. Это согласуется с наблюдаемым поведением.
Отдельный, независимый момент в том, что нормировка формующей кривой для сохранения средней энергии, как часто делают при генерации цветного шума в частотной области, не компенсирует последующую нелинейную дисторсию. Эффект «отбеливания» вызван именно нелинейным отображением, а не нормировкой.
IAAFT: одновременно соблюдаем PSD и маргинальное распределение
Надёжный способ выполнить оба ограничения — итеративно переходить от одного к другому. Алгоритм IAAFT (Iterative Amplitude Adjusted Fourier Transform) делает ровно это. Он чередует две проекции: на одном шаге навязывается требуемый модуль спектра; на другом восстанавливается целевое распределение значений через ранговое отображение. Такой подход описан для стохастических сигналов и хорошо задокументирован в профильной литературе.
Дополнительная деталь реализации, заметно улучшающая точность соответствия спектральной цели, — строить модуль ДПФ с использованием кусочно-линейной интерполяции в лог–лог шкале, когда PSD задана в лог–лог виде. Это соответствует тому, как обычно задают такие наклоны (например, «+10 дБ/декаду»), и даёт гораздо более плотный контроль над PSD.
import numpy as np
from scipy import signal as sig
from scipy.stats import rankdata
from numpy.fft import rfft, irfft
# Конфигурация
n_pts = 2 ** 16
fs_rate = 1.0
max_iter = 50
eps_tol = 1e-6 # порог относительной ошибки PSD
# Целевая маргиналь: лог-нормальное распределение через экспоненту от гауссовских выборок
target_seed = np.exp(np.random.randn(n_pts))
asc_template = np.sort(target_seed)
# Целевой модуль спектра: пример +10 дБ/декаду («синий шум»)
# Строим через кусочно-линейную интерполяцию на лог–лог осях.
brk_freqs = np.array([1e-5 * fs_rate, fs_rate / 2])
brk_psd_db = 10 * np.log10(brk_freqs)
freq_bins = np.fft.rfftfreq(n_pts, d=1 / fs_rate)
# Добавляем первую точку, чтобы избежать log10(0), и явно задаём DC.
brk_freqs = np.insert(brk_freqs, 0, fs_rate / n_pts)
brk_psd_db = np.insert(brk_psd_db, 0, brk_psd_db[0])
psd_interp_db = np.interp(
np.log10(freq_bins[1:]),
np.log10(brk_freqs[1:]),
brk_psd_db[1:]
)
# Преобразуем в амплитудный спектр; DC-бин ставим в ноль для ВЧ-форм.
target_amp = np.empty_like(freq_bins)
target_amp[0] = 0.0
target_amp[1:] = 10 ** (psd_interp_db / 20)
# Ранговое отображение: заменяем значения на значения целевой маргинали с теми же рангами.
def remap_by_rank(values, sorted_ref):
ranks = rankdata(values, method='ordinal') - 1
return sorted_ref[ranks]
# Цикл IAAFT: поочерёдно навязываем спектр и восстанавливаем маргиналь.
synth = target_seed.copy()
prev_err = np.inf
for i in range(max_iter):
# Навязываем целевой модуль спектра.
X = rfft(synth)
X = target_amp * np.exp(1j * np.angle(X))
shaped = irfft(X, n=n_pts)
# Восстанавливаем целевую маргиналь ранговым отображением.
synth = remap_by_rank(shaped, asc_template)
# Периодически проверяем сходимость.
if (i % 5 == 0) or (i == max_iter - 1):
cur_err = np.linalg.norm(np.abs(rfft(synth)) - target_amp) / np.linalg.norm(target_amp)
print(f"iter {i:2d}: PSD rel-err = {cur_err:.3e}")
if cur_err > prev_err - eps_tol:
print("converged.")
break
prev_err = cur_err
Сравнение с подходами на основе фильтрации
Линейная фильтрация лог-нормальных выборок фильтрами с сине-шумовым формованием (FIR или IIR) может обеспечить нужные наклоны PSD. Результат остаётся строго положительным и с правой асимметрией, но уже не является лог-нормальным из-за линейного смешивания отсчётов. В приведённых результатах подход IAAFT дал наилучшее одновременное соответствие PSD и маргинали среди протестированных методов, плюс бонусом — лог–лог интерполяция для точного отслеживания PSD, заданных по декадам.
Тем не менее, тесты показали, что для лог-нормальной целевой маргінали и небелой PSD эмпирическое распределение может отклоняться и иметь моду в нуле, а не около единицы, хотя хвост остаётся длинным. Это важное предупреждение при валидации результатов.
Зачем это важно
Многие последующие алгоритмы предполагают определённые особенности PSD или маргинальные свойства. Если что-то не совпадает — например, PSD разваливается после нелинейного отображения или распределение уплывает после фильтрации — бенчмарки, детекторы или имитационные анализы могут вводить в заблуждение. Совместное соблюдение обеих сторон защищает от таких расхождений.
Практические выводы
Если вам нужны и заданная PSD, и конкретное распределение значений, например лог-нормальное, итеративная схема вроде IAAFT — эффективный инструмент. Она чередует навязывание спектра и восстановление амплитуд по рангам, а при построении PSD в лог–лог шкале позволяет точно отслеживать наклоны вроде +10 дБ/декаду. При валидации смотрите и на PSD, и на маргиналь. Помните, что для небелых целей эмпирическое распределение всё же может отклоняться; проверьте положение моды и поведение хвоста относительно ожиданий. Если важна только PSD, прямая фильтрация может быть проще; если критичны обе стороны, уместнее итеративный подход.
Вкратце: формуйте спектр и восстанавливайте амплитуды совместно, проверяйте оба представления данных и отдавайте предпочтение лог–лог обработке, когда PSD задана по декадам.
Статья основана на вопросе с StackOverflow от Paul Brodersen и ответе от Dan Boschen.