Create your own
Lesson illustration

Генерация и визуализация дискретных синусоидальных сигналов

Здравствуйте. В предыдущем уроке вы складывали синусоиды одной частоты через комплексные амплитуды и получали результат в непрерывном времени:

Теперь перейдём к тому, как такой сигнал представляется в программе. Компьютер не хранит непрерывную функцию : он хранит конечный массив значений, взятых в строго определённые моменты времени. В этой форме синусоиды будут использоваться далее для моделирования цепей, спектров, фильтров, модуляции и SDR-трактов.

К концу урока вы сможете задать частоту дискретизации, сгенерировать массив отсчётов синусоиды в NumPy и корректно показать его в Matplotlib как дискретный сигнал.


От непрерывного времени к отсчётам

Пусть исходный сигнал задан так:

где — амплитуда, — частота в герцах, а — начальная фаза в радианах.

При дискретизации мы измеряем этот сигнал не во все моменты времени, а через равные интервалы:

где:

  • — период дискретизации, секунды;
  • — частота дискретизации, отсчётов в секунду или Гц.

Моменты взятия отсчётов имеют номера :

Подставляя в исходную формулу, получаем дискретный синусоидальный сигнал:

Важно различать:

ВеличинаСмыслЕдиницы
частота самого сигналаГц
частота, с которой берутся отсчётыГц
расстояние по времени между отсчётамис
номер отсчётабезразмерный
число отсчётов в массивебезразмерный

Например, если синусоида имеет частоту , а частота дискретизации равна , то на один период приходится:

отсчётов. Это достаточно плотное представление для наглядного графика и простых моделей.

Строгое условие отсутствия алиасинга будет подробно разобрано в модуле о дискретизации. Пока достаточно придерживаться практического правила: выбирать заметно выше удвоенной максимальной частоты моделируемого сигнала.


Два массива: время и амплитуда

В NumPy сигнал обычно хранится в двух связанных массивах:

  1. t содержит моменты времени ;
  2. x содержит амплитуды , измеренные или вычисленные в эти моменты.

Для сигналов с известной частотой дискретизации надёжнее сначала создать индексы отсчётов, а время получить делением на :

n = np.arange(N)
t = n / fs

Такое построение прямо повторяет формулу:

Функция np.arange(N) создаёт последовательность целых номеров:

[0, 1, 2, ..., N - 1]

Затем NumPy применяет арифметические операции сразу ко всему массиву. Это фундаментальный стиль работы с численными массивами: не нужно вручную вычислять синус в цикле для каждого отсчёта.

Посмотрите, как эти понятия связываются в коротком фрагменте видео Representing Signals in Python (Sampling) от канала Signal Processing with Paul.

Representing Signals in Python (Sampling)

Видео показывает практическую связь между частотой дискретизации, интервалом между отсчётами и визуализацией дискретного сигнала.

Начните с частоты дискретизации: обратите внимание на различие между f_s и T_s, а также на смысл критерия Найквиста. Затем посмотрите моменты отсчётов, где временная сетка строится через numpy.arange. Завершите фрагментом дискретного графика: он особенно важен для понимания, почему в stem нужно передавать реальные значения времени по оси x, а не только массив амплитуд.


Первая модель в NumPy

Создадим синусоиду с параметрами:

Возьмём длительность наблюдения . За это время синусоида совершит пять периодов, а массив будет содержать сто отсчётов.

import numpy as np
import matplotlib.pyplot as plt

# Параметры синусоиды
A = 1.5
f0 = 50.0
phi = np.pi / 6

# Параметры дискретизации
fs = 1000.0
duration = 0.10
N = int(fs * duration)

# Временная сетка и дискретный сигнал
n = np.arange(N)
t = n / fs

x = A * np.sin(2 * np.pi * f0 * t + phi)

print(f"Число отсчётов: {N}")
print(f"Период дискретизации: {1 / fs:.6f} с")
print(f"Первый отсчёт: {x[0]:.3f}")

Первое значение можно проверить вручную. При время равно нулю, поэтому:

Если программа вывела значение, близкое к 0.750, массив сформирован в соответствии с заданной фазой.

В выражении для x:

np.sin(2 * np.pi * f0 * t + phi)

f0 — скаляр, phi — скаляр, а t — массив из ста временных точек. NumPy выполняет умножение, сложение и вычисление sin поэлементно и возвращает массив той же длины.

Функции np.sin, np.cos и другие тригонометрические функции принимают аргумент в радианах. Поэтому множитель обязателен, когда частота задана в герцах:

— это фаза в радианах.


Как правильно показать отсчёты

Для непрерывного графика обычно используют plot, а для дискретного сигнала — stem.

fig, ax = plt.subplots(figsize=(9, 4))

ax.stem(t, x, linefmt="C0-", markerfmt="C0o", basefmt="k-")

ax.set_xlabel("Время, с")
ax.set_ylabel("Амплитуда")
ax.set_title("Дискретная синусоида: 50 Гц, fs = 1000 Гц")
ax.grid(True)

plt.show()

stem рисует отдельный маркер для каждого отсчёта и вертикальную линию до базовой линии. Такая визуализация не утверждает, что между соседними значениями существуют измеренные промежуточные точки.

Столбиковый график Matplotlib: каждый ромб обозначает отдельный отсчёт дискретного сигнала, а вертикальная линия подчёркивает его значение относительно красной базовой линии. Базовая линия в этом примере выбрана на уровне 1 и служит элементом оформления, а не обязательным нулевым уровнем сигнала.

Напротив, команда:

ax.plot(t, x)

соединяет соседние точки отрезками. Это часто удобно для обзора формы сигнала, но визуально создаёт впечатление непрерывной кривой. Сам массив x от этого не становится непрерывным.

Для изучения графических основ просмотрите пример Line plot из документации Matplotlib. Он показывает минимальную структуру программы: создание временного массива, вызов np.sin, настройку осей и вывод графика.

Line plot — Matplotlib 3.10.8 documentation

Прочитайте короткий пример из официальной документации Matplotlib. Он поможет соотнести уже написанный код с базовым шаблоном построения линейного графика.

На странице Line plot разберите единственный кодовый пример. Начните с создания данных и графика. Сопоставьте t с временной осью, s с массивом значений сигнала, а ax.plot(t, s) — с отображением пар координат. Обратите особое внимание на xlabel, ylabel, title и grid: в моделировании сигналов подписи с единицами измерения являются частью корректного результата.


Дискретный сигнал и «гладкая» опорная кривая

Иногда полезно показать одновременно:

  • дискретные отсчёты, которые реально представлены в массиве;
  • плотную опорную кривую той же математической синусоиды.

Для опорной кривой можно вычислить сигнал в гораздо большем числе точек. Это не восстановление измеренного аналогового сигнала, а лишь удобная визуализация известной формулы.

t_ref = np.linspace(0, duration, 2000, endpoint=False)
x_ref = A * np.sin(2 * np.pi * f0 * t_ref + phi)

fig, ax = plt.subplots(figsize=(9, 4))

ax.plot(
    t_ref,
    x_ref,
    color="C1",
    linewidth=1.5,
    label="Плотная опорная кривая"
)

ax.stem(
    t,
    x,
    linefmt="C0-",
    markerfmt="C0o",
    basefmt="k-",
    label="Отсчёты"
)

ax.set_xlabel("Время, с")
ax.set_ylabel("Амплитуда")
ax.set_title("Синусоида и её отсчёты")
ax.grid(True)
ax.legend()

plt.show()

Здесь np.linspace создаёт 2000 равномерно расположенных точек. Параметр endpoint=False исключает точку duration, поэтому временная сетка занимает интервал:

Это согласуется с построением дискретного массива из отсчётов:

График показывает редкие исходные отсчёты синусоиды синими кругами и более плотное представление между ними оранжевыми крестами. Он иллюстрирует, почему гладкая линия на графике не означает появления новых измеренных значений: исходными данными остаются дискретные отсчёты.

В реальной системе между отсчётами нет сохранённых значений сигнала. Если они нужны, система должна либо вычислить их по известной модели, как в нашем примере, либо восстановить приближённое представление интерполяцией. Позже эта граница между дискретными данными, цифровой обработкой и восстановлением аналогового сигнала станет важной при изучении АЦП, ЦАП и SDR.


Выбор arange и linspace

Обе функции полезны, но отвечают на немного разные вопросы.

np.arange

Если известны количество отсчётов и частота дискретизации, наиболее прозрачна конструкция:

N = 100
n = np.arange(N)
t = n / fs

Она гарантирует ровно отсчётов, нумеруемых от до .

Можно встретить и такую форму:

Ts = 1 / fs
t = np.arange(0, duration, Ts)

Она хорошо передаёт физический смысл «брать отсчёт каждые секунд». Однако при дробных числах сравнение с конечной границей может зависеть от накопления ошибок представления чисел с плавающей точкой. Для воспроизводимой модели с фиксированным числом отсчётов удобнее вариант через индексы n.

np.linspace

linspace удобна, когда в первую очередь задан диапазон и нужное число точек:

t_ref = np.linspace(0, duration, 2000, endpoint=False)

Но следующий вариант — типичная ловушка:

t = np.linspace(0, duration, N)

По умолчанию linspace включает конечную точку. Тогда шаг составляет:

а не:

Если нужен именно шаг , либо используйте endpoint=False, либо стройте время через индексы:

t = np.arange(N) / fs

Частота в герцах и частота в радианах на отсчёт

В формуле непрерывного сигнала аргумент синуса выглядит так:

После подстановки его можно записать иначе:

где

— дискретная угловая частота, выраженная в радианах на отсчёт.

В нашем примере:

радиан на отсчёт.

Это одна и та же синусоида, записанная в двух системах координат:

  • описывает физический сигнал во времени;
  • рад/отсчёт описывает, насколько меняется фаза при переходе от одного элемента массива к следующему.

Для большей части прикладных расчётов удобнее задавать и в герцах, а NumPy уже вычислит нужную фазу.


Частые ошибки в первых моделях

Передача в sin частоты без множителя .

Неверно:

x = A * np.sin(f0 * t + phi)

Здесь f0 ошибочно интерпретируется как радианы в секунду, хотя была задана в герцах.

Верно:

x = A * np.sin(2 * np.pi * f0 * t + phi)

Построение графика только по x.

plt.stem(x)

Matplotlib в таком случае использует по оси индексы , а не секунды. Для отладки массива это допустимо, но не годится для физического графика сигнала. Передавайте временную ось явно:

plt.stem(t, x)

Смешение частоты сигнала и частоты дискретизации.

f0 определяет форму синусоиды, а fs определяет плотность её представления. Если изменить fs, но оставить неизменными f0 и duration, физическая частота сигнала не изменится; изменится число и расположение отсчётов.

Интерпретация соединяющей линии как сигнала между отсчётами.

plot проводит линию между точками для удобства просмотра. Дискретная последовательность состоит только из элементов массива:

Для подчёркивания этого факта используйте stem, особенно при невысокой частоте дискретизации.


Итоги

Дискретный синусоидальный сигнал строится по формуле:

Рабочий шаблон в Python выглядит так:

  1. Задать амплитуду , частоту , фазу , частоту дискретизации и число отсчётов .
  2. Создать индексы n = np.arange(N).
  3. Получить время t = n / fs.
  4. Вычислить массив x через np.sin.
  5. Показать дискретный сигнал командой ax.stem(t, x) и подписать оси с единицами измерения.

Вы уже умеете строить цифровое представление синусоиды. В следующем модуле этот навык станет основой для анализа спектра: мы начнём разбирать гармоники периодических сигналов и увидим, почему форма сигнала во времени определяет его составляющие в частотной области.

Can't find a good explanation? Sign up and we'll make it for you

Sign up