diff --git a/().npy b/().npy index 60cc29d..af564e0 100644 Binary files a/().npy and b/().npy differ diff --git a/calib_envelope.npy b/calib_envelope.npy index 9e96279..fc87840 100644 Binary files a/calib_envelope.npy and b/calib_envelope.npy differ diff --git a/rfg_adc_plotter/gui/matplotlib_backend.py b/rfg_adc_plotter/gui/matplotlib_backend.py index 161a9a9..774b6b3 100644 --- a/rfg_adc_plotter/gui/matplotlib_backend.py +++ b/rfg_adc_plotter/gui/matplotlib_backend.py @@ -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, diff --git a/rfg_adc_plotter/gui/pyqtgraph_backend.py b/rfg_adc_plotter/gui/pyqtgraph_backend.py index c164d7f..58d4c12 100644 --- a/rfg_adc_plotter/gui/pyqtgraph_backend.py +++ b/rfg_adc_plotter/gui/pyqtgraph_backend.py @@ -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: diff --git a/rfg_adc_plotter/processing/fourier.py b/rfg_adc_plotter/processing/fourier.py index d99fe68..6d4c34a 100644 --- a/rfg_adc_plotter/processing/fourier.py +++ b/rfg_adc_plotter/processing/fourier.py @@ -156,7 +156,11 @@ 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) diff --git a/rfg_adc_plotter/state/ring_buffer.py b/rfg_adc_plotter/state/ring_buffer.py index 5769758..fc22f08 100644 --- a/rfg_adc_plotter/state/ring_buffer.py +++ b/rfg_adc_plotter/state/ring_buffer.py @@ -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: diff --git a/tests/test_ring_buffer_fft_axis.py b/tests/test_ring_buffer_fft_axis.py index 056e87e..499deec 100644 --- a/tests/test_ring_buffer_fft_axis.py +++ b/tests/test_ring_buffer_fft_axis.py @@ -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