right ifft implementation

This commit is contained in:
awe
2026-02-11 18:43:43 +03:00
parent ea57f87920
commit 66b9eee230
5 changed files with 66 additions and 1579 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,13 @@
WF_WIDTH = 1000 # максимальное число точек в ряду водопада WF_WIDTH = 1000 # максимальное число точек в ряду водопада
FFT_LEN = 1024 # длина БПФ для спектра/водопада спектров FFT_LEN = 2048 # длина БПФ для спектра/водопада спектров
# Порог для инверсии сырых данных: если среднее значение свипа ниже порога — # Порог для инверсии сырых данных: если среднее значение свипа ниже порога —
# считаем, что сигнал «меньше нуля» и домножаем свип на -1 # считаем, что сигнал «меньше нуля» и домножаем свип на -1
DATA_INVERSION_THRESHOLD = 10.0 DATA_INVERSION_THRESHOLD = 10.0
# Параметры IFFT-спектра (временной профиль из спектра 3.2..14.3 ГГц)
# Двусторонний спектр формируется как: [нули -14.3..-3.2 | нули -3.2..+3.2 | данные +3.2..+14.3]
ZEROS_LOW = 758 # нули от -14.3 до -3.2 ГГц
ZEROS_MID = 437 # нули от -3.2 до +3.2 ГГц
SWEEP_LEN = 758 # ожидаемая длина свипа (3.2 → 14.3 ГГц)
FREQ_SPAN_GHZ = 28.6 # полная двусторонняя полоса (-14.3 .. +14.3 ГГц)
IFFT_LEN = ZEROS_LOW + ZEROS_MID + SWEEP_LEN # = 1953

View File

@ -243,15 +243,14 @@ def run_matplotlib(args):
ax_line.set_ylim(-1.05, 1.05) ax_line.set_ylim(-1.05, 1.05)
ax_line.set_ylabel("/ max") ax_line.set_ylabel("/ max")
# Спектр — используем уже вычисленный в ring FFT # Спектр — используем уже вычисленный в ring IFFT (временной профиль)
if ring.last_fft_vals is not None and ring.freq_shared is not None: if ring.last_fft_vals is not None and ring.fft_time_axis is not None:
fft_vals = ring.last_fft_vals fft_vals = ring.last_fft_vals
xs_fft = ring.freq_shared xs_fft = ring.fft_time_axis
if fft_vals.size > xs_fft.size: n = min(fft_vals.size, xs_fft.size)
fft_vals = fft_vals[: xs_fft.size] fft_line_obj.set_data(xs_fft[:n], fft_vals[:n])
fft_line_obj.set_data(xs_fft[: fft_vals.size], fft_vals)
if np.isfinite(np.nanmin(fft_vals)) and np.isfinite(np.nanmax(fft_vals)): 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(0, float(xs_fft[n - 1]))
ax_fft.set_ylim(float(np.nanmin(fft_vals)), float(np.nanmax(fft_vals))) ax_fft.set_ylim(float(np.nanmin(fft_vals)), float(np.nanmax(fft_vals)))
# Водопад сырых данных # Водопад сырых данных

View File

@ -7,12 +7,16 @@ from typing import Optional, Tuple
import numpy as np import numpy as np
from rfg_adc_plotter.constants import FREQ_SPAN_GHZ, IFFT_LEN
from rfg_adc_plotter.io.sweep_reader import SweepReader from rfg_adc_plotter.io.sweep_reader import SweepReader
from rfg_adc_plotter.processing.normalizer import build_calib_envelopes from rfg_adc_plotter.processing.normalizer import build_calib_envelopes
from rfg_adc_plotter.state.app_state import AppState, format_status from rfg_adc_plotter.state.app_state import AppState, format_status
from rfg_adc_plotter.state.ring_buffer import RingBuffer from rfg_adc_plotter.state.ring_buffer import RingBuffer
from rfg_adc_plotter.types import SweepPacket from rfg_adc_plotter.types import SweepPacket
# Максимальное значение временной оси IFFT в нс
_IFFT_T_MAX_NS = float((IFFT_LEN - 1) / (FREQ_SPAN_GHZ * 1e9) * 1e9)
def _parse_ylim(ylim_str: Optional[str]) -> Optional[Tuple[float, float]]: def _parse_ylim(ylim_str: Optional[str]) -> Optional[Tuple[float, float]]:
if not ylim_str: if not ylim_str:
@ -164,8 +168,8 @@ def run_pyqtgraph(args):
p_fft = win.addPlot(row=1, col=0, title="FFT") p_fft = win.addPlot(row=1, col=0, title="FFT")
p_fft.showGrid(x=True, y=True, alpha=0.3) p_fft.showGrid(x=True, y=True, alpha=0.3)
curve_fft = p_fft.plot(pen=pg.mkPen((255, 120, 80), width=1)) 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", "Амплитуда, дБ") p_fft.setLabel("left", "Мощность, дБ")
# Водопад спектров (справа-снизу) # Водопад спектров (справа-снизу)
p_spec = win.addPlot(row=1, col=1, title="B-scan (дБ)") p_spec = win.addPlot(row=1, col=1, title="B-scan (дБ)")
@ -176,7 +180,7 @@ def run_pyqtgraph(args):
p_spec.getAxis("bottom").setStyle(showValues=False) p_spec.getAxis("bottom").setStyle(showValues=False)
except Exception: except Exception:
pass pass
p_spec.setLabel("left", "Бин (0 снизу)") p_spec.setLabel("left", "Время, нс")
img_fft = pg.ImageItem() img_fft = pg.ImageItem()
p_spec.addItem(img_fft) p_spec.addItem(img_fft)
@ -197,7 +201,6 @@ def run_pyqtgraph(args):
FREQ_MAX = 14.323 FREQ_MAX = 14.323
def _init_imshow_extents(): def _init_imshow_extents():
w = ring.width
ms = ring.max_sweeps ms = ring.max_sweeps
fb = ring.fft_bins fb = ring.fft_bins
img.setImage(ring.ring.T, autoLevels=False) img.setImage(ring.ring.T, autoLevels=False)
@ -205,8 +208,9 @@ def run_pyqtgraph(args):
p_img.setRange(xRange=(0, ms - 1), yRange=(FREQ_MIN, FREQ_MAX), padding=0) p_img.setRange(xRange=(0, ms - 1), yRange=(FREQ_MIN, FREQ_MAX), padding=0)
p_line.setXRange(FREQ_MIN, FREQ_MAX, padding=0) p_line.setXRange(FREQ_MIN, FREQ_MAX, padding=0)
img_fft.setImage(ring.ring_fft.T, autoLevels=False) img_fft.setImage(ring.ring_fft.T, autoLevels=False)
p_spec.setRange(xRange=(0, ms - 1), yRange=(0, max(1, fb - 1)), padding=0) img_fft.setRect(pg.QtCore.QRectF(0.0, 0.0, float(ms), _IFFT_T_MAX_NS))
p_fft.setXRange(0, max(1, fb - 1), padding=0) p_spec.setRange(xRange=(0, ms - 1), yRange=(0.0, _IFFT_T_MAX_NS), padding=0)
p_fft.setXRange(0.0, _IFFT_T_MAX_NS, padding=0)
def _img_rect(ms: int) -> "pg.QtCore.QRectF": def _img_rect(ms: int) -> "pg.QtCore.QRectF":
return pg.QtCore.QRectF(0.0, FREQ_MIN, float(ms), FREQ_MAX - FREQ_MIN) return pg.QtCore.QRectF(0.0, FREQ_MIN, float(ms), FREQ_MAX - FREQ_MIN)
@ -245,13 +249,12 @@ def run_pyqtgraph(args):
p_line.setYRange(-1.05, 1.05, padding=0) p_line.setYRange(-1.05, 1.05, padding=0)
p_line.setLabel("left", "/ max") p_line.setLabel("left", "/ max")
# Спектр — используем уже вычисленный в ring FFT # Спектр — используем уже вычисленный в ring IFFT (временной профиль)
if ring.last_fft_vals is not None and ring.freq_shared is not None: if ring.last_fft_vals is not None and ring.fft_time_axis is not None:
fft_vals = ring.last_fft_vals fft_vals = ring.last_fft_vals
xs_fft = ring.freq_shared xs_fft = ring.fft_time_axis
if fft_vals.size > xs_fft.size: n = min(fft_vals.size, xs_fft.size)
fft_vals = fft_vals[: xs_fft.size] curve_fft.setData(xs_fft[:n], fft_vals[:n])
curve_fft.setData(xs_fft[: fft_vals.size], fft_vals)
p_fft.setYRange(float(np.nanmin(fft_vals)), float(np.nanmax(fft_vals)), padding=0) p_fft.setYRange(float(np.nanmin(fft_vals)), float(np.nanmax(fft_vals)), padding=0)
# Позиция подписи канала # Позиция подписи канала
@ -290,6 +293,7 @@ def run_pyqtgraph(args):
img_fft.setImage(disp_fft, autoLevels=False, levels=levels) img_fft.setImage(disp_fft, autoLevels=False, levels=levels)
else: else:
img_fft.setImage(disp_fft, autoLevels=False) img_fft.setImage(disp_fft, autoLevels=False)
img_fft.setRect(pg.QtCore.QRectF(0.0, 0.0, float(ring.max_sweeps), _IFFT_T_MAX_NS))
timer = pg.QtCore.QTimer() timer = pg.QtCore.QTimer()
timer.timeout.connect(update) timer.timeout.connect(update)

View File

@ -5,7 +5,15 @@ from typing import Optional, Tuple
import numpy as np import numpy as np
from rfg_adc_plotter.constants import FFT_LEN, WF_WIDTH from rfg_adc_plotter.constants import (
FFT_LEN,
FREQ_SPAN_GHZ,
IFFT_LEN,
SWEEP_LEN,
WF_WIDTH,
ZEROS_LOW,
ZEROS_MID,
)
class RingBuffer: class RingBuffer:
@ -17,7 +25,7 @@ class RingBuffer:
def __init__(self, max_sweeps: int): def __init__(self, max_sweeps: int):
self.max_sweeps = max_sweeps self.max_sweeps = max_sweeps
self.fft_bins = FFT_LEN // 2 + 1 self.fft_bins = IFFT_LEN # = 1953 (полная длина IFFT-результата)
# Инициализируются при первом свипе (ensure_init) # Инициализируются при первом свипе (ensure_init)
self.ring: Optional[np.ndarray] = None # (max_sweeps, WF_WIDTH) self.ring: Optional[np.ndarray] = None # (max_sweeps, WF_WIDTH)
@ -26,7 +34,7 @@ class RingBuffer:
self.head: int = 0 self.head: int = 0
self.width: Optional[int] = None self.width: Optional[int] = None
self.x_shared: Optional[np.ndarray] = None self.x_shared: Optional[np.ndarray] = None
self.freq_shared: Optional[np.ndarray] = None self.fft_time_axis: Optional[np.ndarray] = None # временная ось IFFT в нс
self.y_min_fft: Optional[float] = None self.y_min_fft: Optional[float] = None
self.y_max_fft: Optional[float] = None self.y_max_fft: Optional[float] = None
# FFT последнего свипа (для отображения без повторного вычисления) # FFT последнего свипа (для отображения без повторного вычисления)
@ -43,7 +51,10 @@ class RingBuffer:
self.ring = np.full((self.max_sweeps, self.width), np.nan, dtype=np.float32) self.ring = np.full((self.max_sweeps, self.width), np.nan, dtype=np.float32)
self.ring_time = np.full((self.max_sweeps,), np.nan, dtype=np.float64) self.ring_time = np.full((self.max_sweeps,), np.nan, dtype=np.float64)
self.ring_fft = np.full((self.max_sweeps, self.fft_bins), np.nan, dtype=np.float32) self.ring_fft = np.full((self.max_sweeps, self.fft_bins), np.nan, dtype=np.float32)
self.freq_shared = np.arange(self.fft_bins, dtype=np.int32) # Временная ось IFFT: шаг dt = 1/(FREQ_SPAN_GHZ*1e9), переведём в нс
self.fft_time_axis = (
np.arange(IFFT_LEN, dtype=np.float64) / (FREQ_SPAN_GHZ * 1e9) * 1e9
).astype(np.float32)
self.head = 0 self.head = 0
# Обновляем x_shared если пришёл свип большего размера # Обновляем x_shared если пришёл свип большего размера
if self.x_shared is None or sweep_width > self.x_shared.size: if self.x_shared is None or sweep_width > self.x_shared.size:
@ -64,20 +75,29 @@ class RingBuffer:
self._push_fft(s) self._push_fft(s)
def _push_fft(self, s: np.ndarray): def _push_fft(self, s: np.ndarray):
bins = self.ring_fft.shape[1] bins = self.ring_fft.shape[1] # = IFFT_LEN = 1953
take_fft = min(int(s.size), FFT_LEN) if s is None or s.size == 0:
if take_fft <= 0:
fft_row = np.full((bins,), np.nan, dtype=np.float32) fft_row = np.full((bins,), np.nan, dtype=np.float32)
else: else:
fft_in = np.zeros((FFT_LEN,), dtype=np.float32) # 1. Взять первые SWEEP_LEN отсчётов (остаток — нули если свип короче)
seg = np.nan_to_num(s[:take_fft], nan=0.0).astype(np.float32, copy=False) sig = np.zeros(SWEEP_LEN, dtype=np.float32)
win = np.hanning(take_fft).astype(np.float32) take = min(int(s.size), SWEEP_LEN)
fft_in[:take_fft] = seg * win seg = np.nan_to_num(s[:take], nan=0.0).astype(np.float32, copy=False)
spec = np.fft.rfft(fft_in) sig[:take] = seg
mag = np.abs(spec).astype(np.float32)
# 2. Собрать двусторонний спектр:
# [ZEROS_LOW нулей | ZEROS_MID нулей | SWEEP_LEN данных]
# = [-14.3..-3.2 ГГц | -3.2..+3.2 ГГц | +3.2..+14.3 ГГц]
data = np.zeros(IFFT_LEN, dtype=np.complex64)
data[ZEROS_LOW + ZEROS_MID:] = sig
# 3. ifftshift + ifft → временной профиль
spec = np.fft.ifftshift(data)
result = np.fft.ifft(spec)
# 4. Амплитуда в дБ
mag = np.abs(result).astype(np.float32)
fft_row = (20.0 * np.log10(mag + 1e-9)).astype(np.float32) fft_row = (20.0 * np.log10(mag + 1e-9)).astype(np.float32)
if fft_row.shape[0] != bins:
fft_row = fft_row[:bins]
prev_head = (self.head - 1) % self.ring_fft.shape[0] prev_head = (self.head - 1) % self.ring_fft.shape[0]
self.ring_fft[prev_head, :] = fft_row self.ring_fft[prev_head, :] = fft_row