diff --git a/rfg_adc_plotter/gui/pyqtgraph_backend.py b/rfg_adc_plotter/gui/pyqtgraph_backend.py index 40cb62b..a2495c1 100644 --- a/rfg_adc_plotter/gui/pyqtgraph_backend.py +++ b/rfg_adc_plotter/gui/pyqtgraph_backend.py @@ -465,6 +465,23 @@ def resolve_visible_aux_curves(aux_curves: SweepAuxCurves, enabled: bool) -> Swe return aux_1_arr, aux_2_arr +def compute_aux_phase_curve(aux_curves: SweepAuxCurves) -> Optional[np.ndarray]: + """Compute phase-like curve atan2(aux_2, aux_1) for raw CH2/CH1 display.""" + if aux_curves is None: + return None + try: + aux_1, aux_2 = aux_curves + except Exception: + return None + aux_1_arr = np.asarray(aux_1, dtype=np.float32).reshape(-1) + aux_2_arr = np.asarray(aux_2, dtype=np.float32).reshape(-1) + width = min(aux_1_arr.size, aux_2_arr.size) + if width <= 0: + return None + phase = np.arctan2(aux_2_arr[:width], aux_1_arr[:width]).astype(np.float32, copy=False) + return phase + + def decimate_curve_for_display( xs: Optional[np.ndarray], ys: Optional[np.ndarray], @@ -717,6 +734,21 @@ def run_pyqtgraph(args) -> None: except Exception: pass + p_line_phase = win.addPlot(row=2, col=0, title="Raw phase: atan(CH2/CH1)") + p_line_phase.showGrid(x=True, y=True, alpha=0.3) + curve_phase = p_line_phase.plot(pen=pg.mkPen((230, 180, 40), width=1)) + p_line_phase.setLabel("bottom", "ГГц") + p_line_phase.setLabel("left", "рад") + try: + p_line_phase.setXLink(p_line) + except Exception: + pass + if not complex_sweep_mode: + try: + p_line_phase.hide() + except Exception: + pass + p_img = win.addPlot(row=0, col=1, title="Сырые данные водопад") p_img.invertY(False) p_img.showGrid(x=False, y=False) @@ -1820,6 +1852,7 @@ def run_pyqtgraph(args) -> None: ) displayed_calib = None displayed_aux = resolve_visible_aux_curves(runtime.current_aux_curves, parsed_data_enabled) + displayed_phase = compute_aux_phase_curve(displayed_aux) if runtime.current_sweep_raw is not None: raw_x, raw_y = decimate_curve_for_display(xs, runtime.current_sweep_raw) @@ -1841,6 +1874,14 @@ def run_pyqtgraph(args) -> None: curve_aux_1.setData([], []) curve_aux_2.setData([], []) + if displayed_phase is not None: + phase_width = min(xs.size, displayed_phase.size) + phase_x, phase_y = decimate_curve_for_display(xs[:phase_width], displayed_phase[:phase_width]) + phase_x, phase_y = sanitize_curve_data_for_display(phase_x, phase_y) + curve_phase.setData(phase_x, phase_y, autoDownsample=False) + else: + curve_phase.setData([], []) + if runtime.calib_envelope is not None: if runtime.current_sweep_raw is not None: displayed_calib = resample_envelope(runtime.calib_envelope, runtime.current_sweep_raw.size) @@ -1887,10 +1928,14 @@ def run_pyqtgraph(args) -> None: ) if aux_limits is not None: p_line_aux_vb.setYRange(aux_limits[0], aux_limits[1], padding=0) + phase_limits = compute_auto_ylim(displayed_phase) + if phase_limits is not None: + p_line_phase.setYRange(phase_limits[0], phase_limits[1], padding=0) line_x_bounds = resolve_axis_bounds(xs) if line_x_bounds is not None: p_line.setXRange(line_x_bounds[0], line_x_bounds[1], padding=0) + p_line_phase.setXRange(line_x_bounds[0], line_x_bounds[1], padding=0) sweep_for_fft = runtime.current_fft_input if sweep_for_fft is None: diff --git a/tests/test_processing.py b/tests/test_processing.py index 88ee0d7..9a87aae 100644 --- a/tests/test_processing.py +++ b/tests/test_processing.py @@ -13,6 +13,7 @@ from rfg_adc_plotter.gui.pyqtgraph_backend import ( build_main_window_layout, coalesce_packets_for_ui, compute_background_subtracted_bscan_levels, + compute_aux_phase_curve, decimate_curve_for_display, resolve_axis_bounds, resolve_heavy_refresh_stride, @@ -217,6 +218,19 @@ class ProcessingTests(unittest.TestCase): self.assertTrue(np.allclose(visible[0], aux[0])) self.assertTrue(np.allclose(visible[1], aux[1])) + def test_compute_aux_phase_curve_returns_atan2_of_aux_channels(self): + aux = ( + np.asarray([1.0, 1.0, -1.0, 0.0], dtype=np.float32), + np.asarray([0.0, 1.0, 1.0, 1.0], dtype=np.float32), + ) + + phase = compute_aux_phase_curve(aux) + + self.assertIsNotNone(phase) + expected = np.asarray([0.0, np.pi / 4.0, 3.0 * np.pi / 4.0, np.pi / 2.0], dtype=np.float32) + self.assertEqual(phase.shape, expected.shape) + self.assertTrue(np.allclose(phase, expected, atol=1e-6)) + def test_decimate_curve_for_display_preserves_small_series(self): xs = np.linspace(3.3, 14.3, 64, dtype=np.float64) ys = np.linspace(-1.0, 1.0, 64, dtype=np.float32)