From 3bc2382bd058bbb33c6bfbf9db3d0084020e2548 Mon Sep 17 00:00:00 2001 From: awe Date: Tue, 3 Feb 2026 14:40:39 +0300 Subject: [PATCH] new fft --- rfg_adc_plotter/config.py | 8 +- .../visualization/matplotlib_backend.py | 103 +++++++++++++----- .../visualization/pyqtgraph_backend.py | 85 ++++++++++++--- 3 files changed, 151 insertions(+), 45 deletions(-) diff --git a/rfg_adc_plotter/config.py b/rfg_adc_plotter/config.py index 71ca39c..34733aa 100644 --- a/rfg_adc_plotter/config.py +++ b/rfg_adc_plotter/config.py @@ -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 diff --git a/rfg_adc_plotter/visualization/matplotlib_backend.py b/rfg_adc_plotter/visualization/matplotlib_backend.py index 2031b62..2abcd57 100644 --- a/rfg_adc_plotter/visualization/matplotlib_backend.py +++ b/rfg_adc_plotter/visualization/matplotlib_backend.py @@ -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) diff --git a/rfg_adc_plotter/visualization/pyqtgraph_backend.py b/rfg_adc_plotter/visualization/pyqtgraph_backend.py index 110745f..3437be1 100644 --- a/rfg_adc_plotter/visualization/pyqtgraph_backend.py +++ b/rfg_adc_plotter/visualization/pyqtgraph_backend.py @@ -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