Compare commits
2 Commits
c199ab7f28
...
1e05b1f3fd
| Author | SHA1 | Date | |
|---|---|---|---|
| 1e05b1f3fd | |||
| 8cc21316e7 |
Binary file not shown.
@ -146,10 +146,13 @@ def run_matplotlib(args):
|
||||
ax_line.set_ylim(fixed_ylim)
|
||||
|
||||
# График спектра
|
||||
fft_line_obj, = ax_fft.plot([], [], lw=1)
|
||||
fft_line_t1, = ax_fft.plot([], [], lw=1, color="tab:blue", label="1/3 (low f)")
|
||||
fft_line_t2, = ax_fft.plot([], [], lw=1, color="tab:orange", label="2/3 (mid f)")
|
||||
fft_line_t3, = ax_fft.plot([], [], lw=1, color="tab:green", label="3/3 (high f)")
|
||||
ax_fft.set_title("FFT", pad=1)
|
||||
ax_fft.set_xlabel("Глубина, м")
|
||||
ax_fft.set_ylabel("Амплитуда")
|
||||
ax_fft.legend(loc="upper right", fontsize=8)
|
||||
|
||||
# Водопад сырых данных
|
||||
img_obj = ax_img.imshow(
|
||||
@ -435,6 +438,9 @@ def run_matplotlib(args):
|
||||
ring.set_fft_complex_mode(str(label))
|
||||
except Exception:
|
||||
pass
|
||||
fft_line_t1.set_data([], [])
|
||||
fft_line_t2.set_data([], [])
|
||||
fft_line_t3.set_data([], [])
|
||||
_refresh_status_texts()
|
||||
try:
|
||||
fig.canvas.draw_idle()
|
||||
@ -584,18 +590,31 @@ def run_matplotlib(args):
|
||||
ax_line.autoscale_view(scalex=False, scaley=True)
|
||||
ax_line.set_ylabel("Y")
|
||||
|
||||
# Профиль по глубине — используем уже вычисленный в ring IFFT.
|
||||
if ring.last_fft_vals is not None and ring.fft_depth_axis_m is not None:
|
||||
fft_vals = ring.last_fft_vals
|
||||
xs_fft = ring.fft_depth_axis_m
|
||||
n = min(fft_vals.size, xs_fft.size)
|
||||
if n > 0:
|
||||
fft_line_obj.set_data(xs_fft[:n], fft_vals[:n])
|
||||
else:
|
||||
fft_line_obj.set_data([], [])
|
||||
if n > 0 and np.isfinite(np.nanmin(fft_vals)) and np.isfinite(np.nanmax(fft_vals)):
|
||||
ax_fft.set_xlim(0, float(xs_fft[n - 1]))
|
||||
ax_fft.set_ylim(float(np.nanmin(fft_vals)), float(np.nanmax(fft_vals)))
|
||||
# Профиль по глубине: три линии для 1/3, 2/3, 3/3 частотного диапазона.
|
||||
third_axes = ring.last_fft_third_axes_m
|
||||
third_vals = ring.last_fft_third_vals
|
||||
lines = (fft_line_t1, fft_line_t2, fft_line_t3)
|
||||
xs_max = []
|
||||
ys_min = []
|
||||
ys_max = []
|
||||
for line_fft, xs_fft, fft_vals in zip(lines, third_axes, third_vals):
|
||||
if xs_fft is None or fft_vals is None:
|
||||
line_fft.set_data([], [])
|
||||
continue
|
||||
n = min(int(xs_fft.size), int(fft_vals.size))
|
||||
if n <= 0:
|
||||
line_fft.set_data([], [])
|
||||
continue
|
||||
x_seg = xs_fft[:n]
|
||||
y_seg = fft_vals[:n]
|
||||
line_fft.set_data(x_seg, y_seg)
|
||||
xs_max.append(float(x_seg[n - 1]))
|
||||
ys_min.append(float(np.nanmin(y_seg)))
|
||||
ys_max.append(float(np.nanmax(y_seg)))
|
||||
|
||||
if xs_max and ys_min and ys_max:
|
||||
ax_fft.set_xlim(0, float(max(xs_max)))
|
||||
ax_fft.set_ylim(float(min(ys_min)), float(max(ys_max)))
|
||||
|
||||
# Водопад сырых данных
|
||||
if changed and ring.is_ready:
|
||||
@ -645,7 +664,9 @@ def run_matplotlib(args):
|
||||
line_env_lo,
|
||||
line_env_hi,
|
||||
img_obj,
|
||||
fft_line_obj,
|
||||
fft_line_t1,
|
||||
fft_line_t2,
|
||||
fft_line_t3,
|
||||
img_fft_obj,
|
||||
status_text,
|
||||
pipeline_text,
|
||||
|
||||
@ -202,7 +202,9 @@ def run_pyqtgraph(args):
|
||||
# FFT (слева-снизу)
|
||||
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))
|
||||
curve_fft_t1 = p_fft.plot(pen=pg.mkPen((80, 120, 255), width=1))
|
||||
curve_fft_t2 = p_fft.plot(pen=pg.mkPen((255, 140, 70), width=1))
|
||||
curve_fft_t3 = p_fft.plot(pen=pg.mkPen((60, 180, 90), width=1))
|
||||
p_fft.setLabel("bottom", "Глубина, м")
|
||||
p_fft.setLabel("left", "Амплитуда")
|
||||
|
||||
@ -492,7 +494,9 @@ def run_pyqtgraph(args):
|
||||
changed = False
|
||||
if changed:
|
||||
try:
|
||||
curve_fft.setData([], [])
|
||||
curve_fft_t1.setData([], [])
|
||||
curve_fft_t2.setData([], [])
|
||||
curve_fft_t3.setData([], [])
|
||||
except Exception:
|
||||
pass
|
||||
_refresh_pipeline_label()
|
||||
@ -626,15 +630,31 @@ def run_pyqtgraph(args):
|
||||
p_line.enableAutoRange(axis="y", enable=True)
|
||||
p_line.setLabel("left", "Y")
|
||||
|
||||
# Профиль по глубине — используем уже вычисленный в ring IFFT.
|
||||
if ring.last_fft_vals is not None and ring.fft_depth_axis_m is not None:
|
||||
fft_vals = ring.last_fft_vals
|
||||
xs_fft = ring.fft_depth_axis_m
|
||||
n = min(fft_vals.size, xs_fft.size)
|
||||
if n > 0:
|
||||
curve_fft.setData(xs_fft[:n], fft_vals[:n])
|
||||
p_fft.setXRange(0.0, float(xs_fft[n - 1]), padding=0)
|
||||
p_fft.setYRange(float(np.nanmin(fft_vals)), float(np.nanmax(fft_vals)), padding=0)
|
||||
# Профиль по глубине: три линии для 1/3, 2/3, 3/3 частотного диапазона.
|
||||
third_axes = ring.last_fft_third_axes_m
|
||||
third_vals = ring.last_fft_third_vals
|
||||
curves = (curve_fft_t1, curve_fft_t2, curve_fft_t3)
|
||||
xs_max = []
|
||||
ys_min = []
|
||||
ys_max = []
|
||||
for curve_fft, xs_fft, fft_vals in zip(curves, third_axes, third_vals):
|
||||
if xs_fft is None or fft_vals is None:
|
||||
curve_fft.setData([], [])
|
||||
continue
|
||||
n = min(int(xs_fft.size), int(fft_vals.size))
|
||||
if n <= 0:
|
||||
curve_fft.setData([], [])
|
||||
continue
|
||||
x_seg = xs_fft[:n]
|
||||
y_seg = fft_vals[:n]
|
||||
curve_fft.setData(x_seg, y_seg)
|
||||
xs_max.append(float(x_seg[n - 1]))
|
||||
ys_min.append(float(np.nanmin(y_seg)))
|
||||
ys_max.append(float(np.nanmax(y_seg)))
|
||||
|
||||
if xs_max and ys_min and ys_max:
|
||||
p_fft.setXRange(0.0, float(max(xs_max)), padding=0)
|
||||
p_fft.setYRange(float(min(ys_min)), float(max(ys_max)), padding=0)
|
||||
|
||||
# Позиция подписи канала
|
||||
try:
|
||||
|
||||
@ -156,14 +156,18 @@ def reconstruct_complex_spectrum_diff(sweep: np.ndarray) -> np.ndarray:
|
||||
d = np.gradient(cos_phi)
|
||||
sin_est = normalize_trace_unit_range(d)
|
||||
sin_est = np.clip(sin_est, -1.0, 1.0)
|
||||
|
||||
sin_est = normalize_trace_unit_range(d)
|
||||
# mag = np.abs(sin_est)
|
||||
# mask = mag > _EPS
|
||||
# if np.any(mask):
|
||||
# sin_est[mask] = sin_est[mask] / mag[mask]
|
||||
z = cos_phi.astype(np.complex128, copy=False) + 1j * sin_est.astype(np.complex128, copy=False)
|
||||
mag = np.abs(z)
|
||||
z_unit = np.ones_like(z, dtype=np.complex128)
|
||||
mask = mag > _EPS
|
||||
if np.any(mask):
|
||||
z_unit[mask] = z[mask] / mag[mask]
|
||||
return mag
|
||||
return z_unit
|
||||
|
||||
|
||||
def reconstruct_complex_spectrum_from_real_trace(
|
||||
@ -284,7 +288,7 @@ def compute_ifft_profile_from_sweep(
|
||||
n = min(depth_m.size, y.size)
|
||||
if n <= 0:
|
||||
return _fallback_depth_response(s.size, s)
|
||||
return depth_m[:n].astype(np.float32, copy=False), y[:n].astype(np.float32, copy=False) # log10 для лучшей визуализации в водопаде
|
||||
return depth_m[:n].astype(np.float32, copy=False), np.maximum(y[:n], 1e-12).astype(np.float32, copy=False) # log10 для лучшей визуализации в водопаде
|
||||
except Exception as exc: # noqa: BLE001
|
||||
logger.error("compute_ifft_profile_from_sweep failed: %r", exc)
|
||||
return _fallback_depth_response(np.asarray(sweep).size if sweep is not None else 1, sweep)
|
||||
@ -294,4 +298,3 @@ def compute_ifft_db_profile(sweep: Optional[np.ndarray]) -> np.ndarray:
|
||||
"""Legacy wrapper (deprecated name): возвращает линейный |IFFT| профиль."""
|
||||
_depth_m, y = compute_ifft_profile_from_sweep(sweep, complex_mode="arccos")
|
||||
return y
|
||||
|
||||
|
||||
@ -10,7 +10,12 @@ from rfg_adc_plotter.constants import (
|
||||
FREQ_MIN_GHZ,
|
||||
WF_WIDTH,
|
||||
)
|
||||
from rfg_adc_plotter.processing.fourier import compute_ifft_profile_from_sweep
|
||||
from rfg_adc_plotter.processing.fourier import (
|
||||
build_frequency_axis_hz,
|
||||
compute_ifft_profile_from_sweep,
|
||||
perform_ifft_depth_response,
|
||||
reconstruct_complex_spectrum_from_real_trace,
|
||||
)
|
||||
|
||||
|
||||
class RingBuffer:
|
||||
@ -38,6 +43,17 @@ class RingBuffer:
|
||||
self.y_max_fft: Optional[float] = None
|
||||
# FFT последнего свипа (для отображения без повторного вычисления)
|
||||
self.last_fft_vals: Optional[np.ndarray] = None
|
||||
# FFT-профили по третям входного частотного диапазона (для line-plot).
|
||||
self.last_fft_third_axes_m: tuple[Optional[np.ndarray], Optional[np.ndarray], Optional[np.ndarray]] = (
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
self.last_fft_third_vals: tuple[Optional[np.ndarray], Optional[np.ndarray], Optional[np.ndarray]] = (
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
|
||||
@property
|
||||
def is_ready(self) -> bool:
|
||||
@ -64,6 +80,8 @@ class RingBuffer:
|
||||
self.fft_depth_axis_m = None
|
||||
self.fft_bins = 0
|
||||
self.last_fft_vals = None
|
||||
self.last_fft_third_axes_m = (None, None, None)
|
||||
self.last_fft_third_vals = (None, None, None)
|
||||
self.y_min_fft = None
|
||||
self.y_max_fft = None
|
||||
return True
|
||||
@ -94,6 +112,11 @@ class RingBuffer:
|
||||
self._push_fft(s)
|
||||
|
||||
def _push_fft(self, s: np.ndarray):
|
||||
empty_thirds = (
|
||||
np.zeros((0,), dtype=np.float32),
|
||||
np.zeros((0,), dtype=np.float32),
|
||||
np.zeros((0,), dtype=np.float32),
|
||||
)
|
||||
depth_axis_m, fft_row = compute_ifft_profile_from_sweep(
|
||||
s,
|
||||
complex_mode=self.fft_complex_mode,
|
||||
@ -103,6 +126,8 @@ class RingBuffer:
|
||||
|
||||
n = min(int(fft_row.size), int(depth_axis_m.size))
|
||||
if n <= 0:
|
||||
self.last_fft_third_axes_m = empty_thirds
|
||||
self.last_fft_third_vals = empty_thirds
|
||||
return
|
||||
if n != fft_row.size:
|
||||
fft_row = fft_row[:n]
|
||||
@ -144,6 +169,7 @@ class RingBuffer:
|
||||
prev_head = (self.head - 1) % self.ring_fft.shape[0]
|
||||
self.ring_fft[prev_head, :] = fft_row
|
||||
self.last_fft_vals = fft_row
|
||||
self.last_fft_third_axes_m, self.last_fft_third_vals = self._compute_fft_thirds(s)
|
||||
|
||||
fr_min = np.nanmin(fft_row)
|
||||
fr_max = float(np.nanpercentile(fft_row, 90))
|
||||
@ -152,6 +178,65 @@ class RingBuffer:
|
||||
if self.y_max_fft is None or (not np.isnan(fr_max) and fr_max > self.y_max_fft):
|
||||
self.y_max_fft = float(fr_max)
|
||||
|
||||
def _compute_fft_thirds(
|
||||
self, s: np.ndarray
|
||||
) -> tuple[tuple[np.ndarray, np.ndarray, np.ndarray], tuple[np.ndarray, np.ndarray, np.ndarray]]:
|
||||
sweep = np.asarray(s, dtype=np.float64).ravel()
|
||||
total = int(sweep.size)
|
||||
|
||||
def _empty() -> np.ndarray:
|
||||
return np.zeros((0,), dtype=np.float32)
|
||||
|
||||
if total <= 0:
|
||||
return (_empty(), _empty(), _empty()), (_empty(), _empty(), _empty())
|
||||
|
||||
freq_hz = build_frequency_axis_hz(total)
|
||||
edges = np.linspace(0, total, 4, dtype=np.int64)
|
||||
|
||||
axes: list[np.ndarray] = []
|
||||
vals: list[np.ndarray] = []
|
||||
|
||||
for idx in range(3):
|
||||
i0 = int(edges[idx])
|
||||
i1 = int(edges[idx + 1])
|
||||
if i1 - i0 < 2:
|
||||
axes.append(_empty())
|
||||
vals.append(_empty())
|
||||
continue
|
||||
|
||||
seg = sweep[i0:i1]
|
||||
seg_freq = freq_hz[i0:i1]
|
||||
seg_complex = reconstruct_complex_spectrum_from_real_trace(
|
||||
seg,
|
||||
complex_mode=self.fft_complex_mode,
|
||||
)
|
||||
depth_m, seg_fft = perform_ifft_depth_response(seg_complex, seg_freq, axis="abs")
|
||||
|
||||
depth_m = np.asarray(depth_m, dtype=np.float32).ravel()
|
||||
seg_fft = np.asarray(seg_fft, dtype=np.float32).ravel()
|
||||
n = min(int(depth_m.size), int(seg_fft.size))
|
||||
if n <= 0:
|
||||
axes.append(_empty())
|
||||
vals.append(_empty())
|
||||
continue
|
||||
|
||||
depth_m = depth_m[:n]
|
||||
seg_fft = seg_fft[:n]
|
||||
|
||||
n_keep = max(1, (n + 1) // 2)
|
||||
axes.append(depth_m[:n_keep])
|
||||
vals.append(seg_fft[:n_keep])
|
||||
|
||||
return (
|
||||
axes[0],
|
||||
axes[1],
|
||||
axes[2],
|
||||
), (
|
||||
vals[0],
|
||||
vals[1],
|
||||
vals[2],
|
||||
)
|
||||
|
||||
def get_display_ring(self) -> np.ndarray:
|
||||
"""Кольцо в порядке от старого к новому, ось времени по X. Форма: (width, time)."""
|
||||
if self.ring is None:
|
||||
|
||||
@ -16,6 +16,14 @@ def test_ring_buffer_allocates_fft_buffers_from_first_push():
|
||||
assert ring.fft_bins == ring.ring_fft.shape[1]
|
||||
assert ring.fft_bins == ring.fft_depth_axis_m.size
|
||||
assert ring.fft_bins == ring.last_fft_vals.size
|
||||
assert ring.last_fft_third_axes_m != (None, None, None)
|
||||
assert ring.last_fft_third_vals != (None, None, None)
|
||||
for axis, vals in zip(ring.last_fft_third_axes_m, ring.last_fft_third_vals):
|
||||
assert axis is not None
|
||||
assert vals is not None
|
||||
assert axis.dtype == np.float32
|
||||
assert vals.dtype == np.float32
|
||||
assert axis.size == vals.size
|
||||
# Legacy alias kept for compatibility with existing GUI code paths.
|
||||
assert ring.fft_time_axis is ring.fft_depth_axis_m
|
||||
|
||||
@ -48,6 +56,8 @@ def test_ring_buffer_mode_switch_resets_fft_buffers_only():
|
||||
assert ring.ring is not None
|
||||
assert ring.ring_fft is not None
|
||||
raw_before = ring.ring.copy()
|
||||
assert ring.last_fft_third_axes_m != (None, None, None)
|
||||
assert ring.last_fft_third_vals != (None, None, None)
|
||||
|
||||
changed = ring.set_fft_complex_mode("diff")
|
||||
assert changed is True
|
||||
@ -57,9 +67,35 @@ def test_ring_buffer_mode_switch_resets_fft_buffers_only():
|
||||
assert ring.ring_fft is None
|
||||
assert ring.fft_depth_axis_m is None
|
||||
assert ring.last_fft_vals is None
|
||||
assert ring.last_fft_third_axes_m == (None, None, None)
|
||||
assert ring.last_fft_third_vals == (None, None, None)
|
||||
assert ring.fft_bins == 0
|
||||
|
||||
ring.push(np.linspace(-1.0, 1.0, 128, dtype=np.float32))
|
||||
assert ring.ring_fft is not None
|
||||
assert ring.fft_depth_axis_m is not None
|
||||
assert ring.last_fft_vals is not None
|
||||
assert ring.last_fft_third_axes_m != (None, None, None)
|
||||
assert ring.last_fft_third_vals != (None, None, None)
|
||||
for axis, vals in zip(ring.last_fft_third_axes_m, ring.last_fft_third_vals):
|
||||
assert axis is not None
|
||||
assert vals is not None
|
||||
assert axis.dtype == np.float32
|
||||
assert vals.dtype == np.float32
|
||||
assert axis.size == vals.size
|
||||
|
||||
|
||||
def test_ring_buffer_short_sweeps_keep_third_profiles_well_formed():
|
||||
for n in (1, 2, 3):
|
||||
ring = RingBuffer(max_sweeps=4)
|
||||
ring.ensure_init(n)
|
||||
ring.push(np.linspace(-1.0, 1.0, n, dtype=np.float32))
|
||||
|
||||
assert ring.last_fft_third_axes_m != (None, None, None)
|
||||
assert ring.last_fft_third_vals != (None, None, None)
|
||||
for axis, vals in zip(ring.last_fft_third_axes_m, ring.last_fft_third_vals):
|
||||
assert axis is not None
|
||||
assert vals is not None
|
||||
assert axis.dtype == np.float32
|
||||
assert vals.dtype == np.float32
|
||||
assert axis.size == vals.size
|
||||
|
||||
Reference in New Issue
Block a user