This commit is contained in:
awe
2026-02-03 14:40:39 +03:00
parent 2af6c8a486
commit 3bc2382bd0
3 changed files with 151 additions and 45 deletions

View File

@ -10,7 +10,13 @@ import numpy as np
WF_WIDTH = 1000
# Длина БПФ для спектра/водопада спектров
FFT_LEN = 1024
FFT_LEN = 2048
# Частотный диапазон для FFT (в ГГц)
FREQ_MIN_GHZ = -10.0 # Начало частотной оси
FREQ_MAX_GHZ = 10.0 # Конец частотной оси
DATA_FREQ_START_GHZ = 1.0 # Начало реальных данных
DATA_FREQ_END_GHZ = 10.0 # Конец реальных данных
# Порог для инверсии сырых данных: если среднее значение свипа ниже порога —
# считаем, что сигнал «меньше нуля» и домножаем свип на -1

View File

@ -18,7 +18,16 @@ try:
except Exception as e:
raise RuntimeError(f"Нужны matplotlib и ее зависимости: {e}")
from ..config import FFT_LEN, WF_WIDTH, SweepInfo, SweepPacket
from ..config import (
FFT_LEN,
WF_WIDTH,
SweepInfo,
SweepPacket,
FREQ_MIN_GHZ,
FREQ_MAX_GHZ,
DATA_FREQ_START_GHZ,
DATA_FREQ_END_GHZ,
)
from ..data_acquisition.sweep_reader import SweepReader
from ..signal_processing.phase_analysis import apply_temporal_unwrap, phase_to_distance
from ..utils.formatting import format_status_kv, parse_spec_clip
@ -49,8 +58,8 @@ def run_matplotlib(args):
ring_time = None # type: Optional[np.ndarray]
head = 0
# Авто-уровни цветовой шкалы водопада сырых данных пересчитываются по видимой области.
# FFT состояние
fft_bins = FFT_LEN // 2 + 1
# FFT состояние (полное FFT для отрицательных частот)
fft_bins = FFT_LEN
ring_fft = None # type: Optional[np.ndarray]
y_min_fft, y_max_fft = None, None
freq_shared: Optional[np.ndarray] = None
@ -86,7 +95,7 @@ def run_matplotlib(args):
# Линейный график спектра текущего свипа
fft_line_obj, = ax_fft.plot([], [], lw=1)
ax_fft.set_title("FFT", pad=1)
ax_fft.set_xlabel("X")
ax_fft.set_xlabel("Частота, ГГц")
ax_fft.set_ylabel("Амплитуда, дБ")
# Диапазон по Y для последнего свипа: авто по умолчанию (поддерживает отрицательные значения)
@ -128,7 +137,7 @@ def run_matplotlib(args):
)
ax_spec.set_title("B-scan (дБ)", pad=12)
ax_spec.set_xlabel("")
ax_spec.set_ylabel("расстояние")
ax_spec.set_ylabel("Частота, ГГц")
# Не показываем численные значения по времени на B-scan
try:
ax_spec.tick_params(axis="x", labelbottom=False)
@ -138,7 +147,7 @@ def run_matplotlib(args):
# График фазы текущего свипа
phase_line_obj, = ax_phase.plot([], [], lw=1)
ax_phase.set_title("Фаза спектра (развернутая)", pad=1)
ax_phase.set_xlabel("Бин")
ax_phase.set_xlabel("Частота, ГГц")
ax_phase.set_ylabel("Фаза, радианы")
# Добавим второй Y axis для расстояния
@ -155,7 +164,7 @@ def run_matplotlib(args):
)
ax_phase_wf.set_title("Водопад фазы", pad=12)
ax_phase_wf.set_xlabel("")
ax_phase_wf.set_ylabel("Бин")
ax_phase_wf.set_ylabel("Частота, ГГц")
# Не показываем численные значения по времени
try:
ax_phase_wf.tick_params(axis="x", labelbottom=False)
@ -166,14 +175,14 @@ def run_matplotlib(args):
ax_smin = fig.add_axes([0.92, 0.55, 0.02, 0.35])
ax_smax = fig.add_axes([0.95, 0.55, 0.02, 0.35])
ax_sctr = fig.add_axes([0.98, 0.55, 0.02, 0.35])
ymin_slider = Slider(ax_smin, "Y min", 0, max(1, fft_bins - 1), valinit=0, valstep=1, orientation="vertical")
ymax_slider = Slider(ax_smax, "Y max", 0, max(1, fft_bins - 1), valinit=max(1, fft_bins - 1), valstep=1, orientation="vertical")
ymin_slider = Slider(ax_smin, "Y min", FREQ_MIN_GHZ, FREQ_MAX_GHZ, valinit=FREQ_MIN_GHZ, valstep=0.1, orientation="vertical")
ymax_slider = Slider(ax_smax, "Y max", FREQ_MIN_GHZ, FREQ_MAX_GHZ, valinit=FREQ_MAX_GHZ, valstep=0.1, orientation="vertical")
contrast_slider = Slider(ax_sctr, "Int max", 0, 100, valinit=100, valstep=1, orientation="vertical")
def _on_ylim_change(_val):
try:
y0 = int(min(ymin_slider.val, ymax_slider.val))
y1 = int(max(ymin_slider.val, ymax_slider.val))
y0 = float(min(ymin_slider.val, ymax_slider.val))
y1 = float(max(ymin_slider.val, ymax_slider.val))
ax_spec.set_ylim(y0, y1)
fig.canvas.draw_idle()
except Exception:
@ -209,18 +218,18 @@ def run_matplotlib(args):
# FFT буферы: время по X, бин по Y
ring_fft = np.full((max_sweeps, fft_bins), np.nan, dtype=np.float32)
img_fft_obj.set_data(np.zeros((fft_bins, max_sweeps), dtype=np.float32))
img_fft_obj.set_extent((0, max_sweeps - 1, 0, fft_bins - 1))
img_fft_obj.set_extent((0, max_sweeps - 1, FREQ_MIN_GHZ, FREQ_MAX_GHZ))
ax_spec.set_xlim(0, max_sweeps - 1)
ax_spec.set_ylim(0, max(1, fft_bins - 1))
freq_shared = np.arange(fft_bins, dtype=np.int32)
ax_spec.set_ylim(FREQ_MIN_GHZ, FREQ_MAX_GHZ)
freq_shared = np.linspace(FREQ_MIN_GHZ, FREQ_MAX_GHZ, fft_bins, dtype=np.float32)
# Phase буферы: время по X, бин по Y
ring_phase = np.full((max_sweeps, fft_bins), np.nan, dtype=np.float32)
prev_phase_per_bin = np.zeros(fft_bins, dtype=np.float32)
phase_offset_per_bin = np.zeros(fft_bins, dtype=np.float32)
img_phase_obj.set_data(np.zeros((fft_bins, max_sweeps), dtype=np.float32))
img_phase_obj.set_extent((0, max_sweeps - 1, 0, fft_bins - 1))
img_phase_obj.set_extent((0, max_sweeps - 1, FREQ_MIN_GHZ, FREQ_MAX_GHZ))
ax_phase_wf.set_xlim(0, max_sweeps - 1)
ax_phase_wf.set_ylim(0, max(1, fft_bins - 1))
ax_phase_wf.set_ylim(FREQ_MIN_GHZ, FREQ_MAX_GHZ)
def _visible_levels_matplotlib(data: np.ndarray, axis) -> Optional[Tuple[float, float]]:
"""(vmin, vmax) по текущей видимой области imshow (без накопления по времени)."""
@ -277,21 +286,41 @@ def run_matplotlib(args):
fft_row = np.full((bins,), np.nan, dtype=np.float32)
phase_row = np.full((bins,), np.nan, dtype=np.float32)
else:
# Создаем буфер для полного FFT (с отрицательными частотами)
fft_in = np.zeros((FFT_LEN,), dtype=np.float32)
seg = s[:take_fft]
# Вычисляем индексы для размещения данных (1-10 ГГц в диапазоне -10 до +10 ГГц)
freq_range_total = FREQ_MAX_GHZ - FREQ_MIN_GHZ # 20 ГГц
freq_range_data = DATA_FREQ_END_GHZ - DATA_FREQ_START_GHZ # 9 ГГц
# Начальный индекс для данных в FFT буфере
start_idx = int((DATA_FREQ_START_GHZ - FREQ_MIN_GHZ) / freq_range_total * FFT_LEN)
# Количество точек для данных
data_points = int(freq_range_data / freq_range_total * FFT_LEN)
data_points = min(data_points, take_fft, FFT_LEN - start_idx)
# Подготовка данных
seg = s[:data_points]
if isinstance(seg, np.ndarray):
seg = np.nan_to_num(seg, nan=0.0).astype(np.float32, copy=False)
else:
seg = np.asarray(seg, dtype=np.float32)
seg = np.nan_to_num(seg, nan=0.0)
# Окно Хэннинга
win = np.hanning(take_fft).astype(np.float32)
fft_in[:take_fft] = seg * win
spec = np.fft.rfft(fft_in)
win = np.hanning(data_points).astype(np.float32)
# Размещаем данные в правильной позиции
fft_in[start_idx:start_idx + data_points] = seg * win
# Полное FFT (включая отрицательные частоты)
spec = np.fft.fft(fft_in)
# Сдвигаем для центрирования нулевой частоты
spec = np.fft.fftshift(spec)
mag = np.abs(spec).astype(np.float32)
fft_row = 20.0 * np.log10(mag + 1e-9)
if fft_row.shape[0] != bins:
# rfft длиной FFT_LEN даёт bins == FFT_LEN//2+1
fft_row = fft_row[:bins]
# Расчет фазы
@ -398,11 +427,31 @@ def run_matplotlib(args):
# Обновление спектра и фазы текущего свипа
take_fft = min(int(current_sweep.size), FFT_LEN)
if take_fft > 0 and freq_shared is not None:
# Создаем буфер для полного FFT (с отрицательными частотами)
fft_in = np.zeros((FFT_LEN,), dtype=np.float32)
seg = np.nan_to_num(current_sweep[:take_fft], nan=0.0).astype(np.float32, copy=False)
win = np.hanning(take_fft).astype(np.float32)
fft_in[:take_fft] = seg * win
spec = np.fft.rfft(fft_in)
# Вычисляем индексы для размещения данных (1-10 ГГц в диапазоне -10 до +10 ГГц)
freq_range_total = FREQ_MAX_GHZ - FREQ_MIN_GHZ # 20 ГГц
freq_range_data = DATA_FREQ_END_GHZ - DATA_FREQ_START_GHZ # 9 ГГц
# Начальный индекс для данных в FFT буфере
start_idx = int((DATA_FREQ_START_GHZ - FREQ_MIN_GHZ) / freq_range_total * FFT_LEN)
# Количество точек для данных
data_points = int(freq_range_data / freq_range_total * FFT_LEN)
data_points = min(data_points, take_fft, FFT_LEN - start_idx)
# Подготовка данных с окном Хэннинга
seg = np.nan_to_num(current_sweep[:data_points], nan=0.0).astype(np.float32, copy=False)
win = np.hanning(data_points).astype(np.float32)
# Размещаем данные в правильной позиции
fft_in[start_idx:start_idx + data_points] = seg * win
# Полное FFT (включая отрицательные частоты)
spec = np.fft.fft(fft_in)
# Сдвигаем для центрирования нулевой частоты
spec = np.fft.fftshift(spec)
mag = np.abs(spec).astype(np.float32)
fft_vals = 20.0 * np.log10(mag + 1e-9)
xs_fft = freq_shared
@ -411,7 +460,7 @@ def run_matplotlib(args):
fft_line_obj.set_data(xs_fft[: fft_vals.size], fft_vals)
# Авто-диапазон по Y для спектра
if np.isfinite(np.nanmin(fft_vals)) and np.isfinite(np.nanmax(fft_vals)):
ax_fft.set_xlim(0, max(1, xs_fft.size - 1))
ax_fft.set_xlim(FREQ_MIN_GHZ, FREQ_MAX_GHZ)
ax_fft.set_ylim(float(np.nanmin(fft_vals)), float(np.nanmax(fft_vals)))
# Расчет и отображение фазы текущего свипа
@ -423,7 +472,7 @@ def run_matplotlib(args):
phase_line_obj.set_data(xs_fft[: phase_unwrapped.size], phase_unwrapped)
# Авто-диапазон по Y для фазы
if np.isfinite(np.nanmin(phase_unwrapped)) and np.isfinite(np.nanmax(phase_unwrapped)):
ax_phase.set_xlim(0, max(1, xs_fft.size - 1))
ax_phase.set_xlim(FREQ_MIN_GHZ, FREQ_MAX_GHZ)
phase_min = float(np.nanmin(phase_unwrapped))
phase_max = float(np.nanmax(phase_unwrapped))
ax_phase.set_ylim(phase_min, phase_max)

View File

@ -23,7 +23,16 @@ except Exception:
"pyqtgraph/PyQt5(Pyside6) не найдены. Установите: pip install pyqtgraph PyQt5"
) from e
from ..config import FFT_LEN, WF_WIDTH, SweepInfo, SweepPacket
from ..config import (
FFT_LEN,
WF_WIDTH,
SweepInfo,
SweepPacket,
FREQ_MIN_GHZ,
FREQ_MAX_GHZ,
DATA_FREQ_START_GHZ,
DATA_FREQ_END_GHZ,
)
from ..data_acquisition.sweep_reader import SweepReader
from ..signal_processing.phase_analysis import apply_temporal_unwrap, phase_to_distance
from ..utils.formatting import format_status_kv, parse_spec_clip
@ -72,7 +81,7 @@ def run_pyqtgraph(args):
p_fft = win.addPlot(row=1, col=0, title="FFT")
p_fft.showGrid(x=True, y=True, alpha=0.3)
curve_fft = p_fft.plot(pen=pg.mkPen((255, 120, 80), width=1))
p_fft.setLabel("bottom", "Бин")
p_fft.setLabel("bottom", "Частота, ГГц")
p_fft.setLabel("left", "Амплитуда, дБ")
# Водопад спектров (справа-средний ряд)
@ -84,7 +93,7 @@ def run_pyqtgraph(args):
p_spec.getAxis("bottom").setStyle(showValues=False)
except Exception:
pass
p_spec.setLabel("left", "Бин (0 снизу)")
p_spec.setLabel("left", "Частота, ГГц (0 снизу)")
img_fft = pg.ImageItem()
p_spec.addItem(img_fft)
@ -92,7 +101,7 @@ def run_pyqtgraph(args):
p_phase = win.addPlot(row=2, col=0, title="Фаза спектра (развернутая)")
p_phase.showGrid(x=True, y=True, alpha=0.3)
curve_phase = p_phase.plot(pen=pg.mkPen((120, 255, 80), width=1))
p_phase.setLabel("bottom", "Бин")
p_phase.setLabel("bottom", "Частота, ГГц")
p_phase.setLabel("left", "Фаза, радианы")
# Добавим вторую ось Y для расстояния
p_phase_dist_axis = pg.ViewBox()
@ -121,7 +130,7 @@ def run_pyqtgraph(args):
p_phase_wf.getAxis("bottom").setStyle(showValues=False)
except Exception:
pass
p_phase_wf.setLabel("left", "Бин (0 снизу)")
p_phase_wf.setLabel("left", "Частота, ГГц (0 снизу)")
img_phase = pg.ImageItem()
p_phase_wf.addItem(img_phase)
@ -137,8 +146,8 @@ def run_pyqtgraph(args):
current_sweep: Optional[np.ndarray] = None
current_info: Optional[SweepInfo] = None
# Авто-уровни цветовой шкалы водопада сырых данных пересчитываются по видимой области.
# Для спектров
fft_bins = FFT_LEN // 2 + 1
# Для спектров (полное FFT для отрицательных частот)
fft_bins = FFT_LEN
ring_fft: Optional[np.ndarray] = None
freq_shared: Optional[np.ndarray] = None
y_min_fft, y_max_fft = None, None
@ -177,8 +186,8 @@ def run_pyqtgraph(args):
ring_fft = np.full((max_sweeps, fft_bins), np.nan, dtype=np.float32)
img_fft.setImage(ring_fft.T, autoLevels=False)
p_spec.setRange(xRange=(0, max_sweeps - 1), yRange=(0, max(1, fft_bins - 1)), padding=0)
p_fft.setXRange(0, max(1, fft_bins - 1), padding=0)
freq_shared = np.arange(fft_bins, dtype=np.int32)
p_fft.setXRange(FREQ_MIN_GHZ, FREQ_MAX_GHZ, padding=0)
freq_shared = np.linspace(FREQ_MIN_GHZ, FREQ_MAX_GHZ, fft_bins, dtype=np.float32)
# Phase: время по оси X, бин по оси Y
ring_phase = np.full((max_sweeps, fft_bins), np.nan, dtype=np.float32)
prev_phase_per_bin = np.zeros(fft_bins, dtype=np.float32)
@ -234,11 +243,33 @@ def run_pyqtgraph(args):
bins = ring_fft.shape[1]
take_fft = min(int(s.size), FFT_LEN)
if take_fft > 0:
# Создаем буфер для полного FFT (с отрицательными частотами)
fft_in = np.zeros((FFT_LEN,), dtype=np.float32)
seg = np.nan_to_num(s[:take_fft], nan=0.0).astype(np.float32, copy=False)
win = np.hanning(take_fft).astype(np.float32)
fft_in[:take_fft] = seg * win
spec = np.fft.rfft(fft_in)
# Вычисляем индексы для размещения данных (1-10 ГГц в диапазоне -10 до +10 ГГц)
# Диапазон данных: от DATA_FREQ_START_GHZ (1) до DATA_FREQ_END_GHZ (10)
# Полный диапазон: от FREQ_MIN_GHZ (-10) до FREQ_MAX_GHZ (10)
freq_range_total = FREQ_MAX_GHZ - FREQ_MIN_GHZ # 20 ГГц
freq_range_data = DATA_FREQ_END_GHZ - DATA_FREQ_START_GHZ # 9 ГГц
# Начальный индекс для данных в FFT буфере
start_idx = int((DATA_FREQ_START_GHZ - FREQ_MIN_GHZ) / freq_range_total * FFT_LEN)
# Количество точек для данных
data_points = int(freq_range_data / freq_range_total * FFT_LEN)
data_points = min(data_points, take_fft, FFT_LEN - start_idx)
# Подготовка данных с окном Хэннинга
seg = np.nan_to_num(s[:data_points], nan=0.0).astype(np.float32, copy=False)
win = np.hanning(data_points).astype(np.float32)
# Размещаем данные в правильной позиции (от -10 до 1 ГГц - нули, от 1 до 10 ГГц - данные)
fft_in[start_idx:start_idx + data_points] = seg * win
# Полное FFT (включая отрицательные частоты)
spec = np.fft.fft(fft_in)
# Сдвигаем для центрирования нулевой частоты
spec = np.fft.fftshift(spec)
mag = np.abs(spec).astype(np.float32)
fft_row = 20.0 * np.log10(mag + 1e-9)
if fft_row.shape[0] != bins:
@ -320,11 +351,31 @@ def run_pyqtgraph(args):
# Обновим спектр и фазу
take_fft = min(int(current_sweep.size), FFT_LEN)
if take_fft > 0 and freq_shared is not None:
# Создаем буфер для полного FFT (с отрицательными частотами)
fft_in = np.zeros((FFT_LEN,), dtype=np.float32)
seg = np.nan_to_num(current_sweep[:take_fft], nan=0.0).astype(np.float32, copy=False)
win = np.hanning(take_fft).astype(np.float32)
fft_in[:take_fft] = seg * win
spec = np.fft.rfft(fft_in)
# Вычисляем индексы для размещения данных (1-10 ГГц в диапазоне -10 до +10 ГГц)
freq_range_total = FREQ_MAX_GHZ - FREQ_MIN_GHZ # 20 ГГц
freq_range_data = DATA_FREQ_END_GHZ - DATA_FREQ_START_GHZ # 9 ГГц
# Начальный индекс для данных в FFT буфере
start_idx = int((DATA_FREQ_START_GHZ - FREQ_MIN_GHZ) / freq_range_total * FFT_LEN)
# Количество точек для данных
data_points = int(freq_range_data / freq_range_total * FFT_LEN)
data_points = min(data_points, take_fft, FFT_LEN - start_idx)
# Подготовка данных с окном Хэннинга
seg = np.nan_to_num(current_sweep[:data_points], nan=0.0).astype(np.float32, copy=False)
win = np.hanning(data_points).astype(np.float32)
# Размещаем данные в правильной позиции
fft_in[start_idx:start_idx + data_points] = seg * win
# Полное FFT (включая отрицательные частоты)
spec = np.fft.fft(fft_in)
# Сдвигаем для центрирования нулевой частоты
spec = np.fft.fftshift(spec)
mag = np.abs(spec).astype(np.float32)
fft_vals = 20.0 * np.log10(mag + 1e-9)
xs_fft = freq_shared