diff --git a/rfg_adc_plotter/gui/pyqtgraph_backend.py b/rfg_adc_plotter/gui/pyqtgraph_backend.py index ce027ee..cc25e6f 100644 --- a/rfg_adc_plotter/gui/pyqtgraph_backend.py +++ b/rfg_adc_plotter/gui/pyqtgraph_backend.py @@ -74,6 +74,32 @@ TTY_RANGE_MIN_V = 1e-6 TTY_RANGE_MAX_V = 1_000_000.0 LOGDET_EXP_INPUT_LIMIT = 80.0 DO1_TAGGED_INFO_KEY = "_do1_tagged_payload" +DISPLAY_DISTANCE_ZERO_M = 9.0 + + +def display_distance_value(distance_m: float, *, zero_at_m: float = DISPLAY_DISTANCE_ZERO_M) -> float: + """Map physical distance to the displayed reversed distance axis.""" + try: + distance_val = float(distance_m) + except Exception: + return float("nan") + if not np.isfinite(distance_val): + return float("nan") + return float(float(zero_at_m) - distance_val) + + +def display_distance_axis( + distance_axis: Optional[np.ndarray], + *, + zero_at_m: float = DISPLAY_DISTANCE_ZERO_M, +) -> np.ndarray: + """Return display coordinates where physical ``zero_at_m`` is shown as 0 m.""" + if distance_axis is None: + return np.zeros((0,), dtype=np.float64) + axis_arr = np.asarray(distance_axis, dtype=np.float64).reshape(-1) + if axis_arr.size <= 0: + return np.zeros((0,), dtype=np.float64) + return (float(zero_at_m) - axis_arr).astype(np.float64, copy=False) def sanitize_curve_data_for_display( @@ -996,7 +1022,7 @@ def run_pyqtgraph(args) -> None: fft_bg_line.setVisible(False) fft_left_line.setVisible(False) fft_right_line.setVisible(False) - p_fft.setLabel("bottom", "Расстояние, м") + p_fft.setLabel("bottom", "Расстояние, м (0 = 9 м)") p_fft.setLabel("left", "Амплитуда" if complex_sweep_mode else "дБ") if complex_sweep_mode: try: @@ -1017,7 +1043,7 @@ def run_pyqtgraph(args) -> None: p_spec.getAxis("bottom").setStyle(showValues=False) except Exception: pass - p_spec.setLabel("left", "Расстояние, м") + p_spec.setLabel("left", "Расстояние, м (0 = 9 м)") img_fft = pg.ImageItem() p_spec.addItem(img_fft) spec_left_line = pg.InfiniteLine(angle=0, movable=False, pen=peak_pen) @@ -1472,7 +1498,7 @@ def run_pyqtgraph(args) -> None: text = f"{fft_low_cut_percent:.1f}%" cut_start = _active_distance_cut_start() if cut_start is not None and np.isfinite(cut_start): - text = f"{text} (~{cut_start:.4g} м)" + text = f"{text} (~{display_distance_value(cut_start):.4g} м)" if text == last_fft_low_cut_label_text: return try: @@ -1501,22 +1527,31 @@ def run_pyqtgraph(args) -> None: distance_bounds = resolve_axis_bounds(runtime.ring.distance_axis) if distance_bounds is not None: - d_min_full, d_max = distance_bounds - span = max(1e-9, float(d_max - d_min_full)) - set_image_rect_if_changed("fft_waterfall_rect", img_fft, 0.0, d_min_full, float(max_sweeps), span) - set_xy_range_if_changed( - "fft_waterfall_range", - p_spec, - x_bounds=(0, max_sweeps - 1), - y_bounds=(d_min_full, d_max), - padding=0, - ) + display_axis_full = display_distance_axis(runtime.ring.distance_axis) + display_bounds = resolve_axis_bounds(display_axis_full) + if display_bounds is not None: + d_min_display, d_max_display = display_bounds + set_image_rect_if_changed( + "fft_waterfall_rect", + img_fft, + 0.0, + d_min_display, + float(max_sweeps), + max(1e-9, float(d_max_display - d_min_display)), + ) + set_xy_range_if_changed( + "fft_waterfall_range", + p_spec, + x_bounds=(0, max_sweeps - 1), + y_bounds=(d_min_display, d_max_display), + padding=0, + ) - d_min_fft = d_min_full d_cut = _active_distance_cut_start() - if d_cut is not None and np.isfinite(d_cut): - d_min_fft = max(float(d_min_fft), float(d_cut)) - set_x_range_if_changed("fft_x", p_fft, d_min_fft, d_max, padding=0) + fft_axis_physical, _fft_keep = apply_distance_cut_to_axis(runtime.ring.distance_axis, d_cut) + fft_display_bounds = resolve_axis_bounds(display_distance_axis(fft_axis_physical)) + if fft_display_bounds is not None: + set_x_range_if_changed("fft_x", p_fft, fft_display_bounds[0], fft_display_bounds[1], padding=0) refresh_fft_low_cut_label() def resolve_curve_xs(size: int) -> np.ndarray: @@ -2422,7 +2457,7 @@ def run_pyqtgraph(args) -> None: if idx < len(peaks): peak = peaks[idx] lines.append(f"peak {idx + 1}:") - lines.append(f" X: {peak['x']:.4g} m") + lines.append(f" X: {display_distance_value(peak['x']):.4g} m") lines.append(f" H: {peak['height']:.4g} dB") lines.append(f" W: {peak['width']:.4g} m") else: @@ -3151,7 +3186,8 @@ def run_pyqtgraph(args) -> None: fft_complex_plot = fft_complex_plot[fft_keep_mask] elif fft_complex_plot is not None: fft_complex_plot = None - fft_x_bounds = resolve_axis_bounds(xs_fft) + xs_fft_display = display_distance_axis(xs_fft) + fft_x_bounds = resolve_axis_bounds(xs_fft_display) if fft_x_bounds is not None: set_x_range_if_changed("fft_x", p_fft, fft_x_bounds[0], fft_x_bounds[1], padding=0) @@ -3168,17 +3204,17 @@ def run_pyqtgraph(args) -> None: show_imag=fft_imag_enabled, ) if visible_abs is not None: - abs_x, abs_y = sanitize_curve_data_for_display(xs_fft[: visible_abs.size], visible_abs) + abs_x, abs_y = sanitize_curve_data_for_display(xs_fft_display[: visible_abs.size], visible_abs) set_curve_data("fft_abs", curve_fft, abs_x, abs_y) else: clear_curve_if_needed("fft_abs", curve_fft) if visible_real is not None: - real_x, real_y = sanitize_curve_data_for_display(xs_fft[: visible_real.size], visible_real) + real_x, real_y = sanitize_curve_data_for_display(xs_fft_display[: visible_real.size], visible_real) set_curve_data("fft_real", curve_fft_real, real_x, real_y) else: clear_curve_if_needed("fft_real", curve_fft_real) if visible_imag is not None: - imag_x, imag_y = sanitize_curve_data_for_display(xs_fft[: visible_imag.size], visible_imag) + imag_x, imag_y = sanitize_curve_data_for_display(xs_fft_display[: visible_imag.size], visible_imag) set_curve_data("fft_imag", curve_fft_imag, imag_x, imag_y) else: clear_curve_if_needed("fft_imag", curve_fft_imag) @@ -3188,7 +3224,7 @@ def run_pyqtgraph(args) -> None: finite_ref = np.isfinite(xs_fft) & np.isfinite(fft_ref_db) if np.any(finite_ref): fft_ref_lin = _db_to_linear_amplitude(fft_ref_db[finite_ref]) - ref_x, ref_y = sanitize_curve_data_for_display(xs_fft[finite_ref], fft_ref_lin) + ref_x, ref_y = sanitize_curve_data_for_display(xs_fft_display[finite_ref], fft_ref_lin) set_curve_data("fft_ref", curve_fft_ref, ref_x, ref_y) set_item_visible_if_changed("fft_ref", curve_fft_ref, True) ref_curve_for_range = fft_ref_lin @@ -3208,7 +3244,12 @@ def run_pyqtgraph(args) -> None: set_curve_data( f"fft_peak_box_{idx}", box, - [peak["left"], peak["left"], peak["right"], peak["right"], peak["left"]], + display_distance_axis( + np.asarray( + [peak["left"], peak["left"], peak["right"], peak["right"], peak["left"]], + dtype=np.float64, + ) + ), y_box, ) set_item_visible_if_changed(f"fft_peak_box_{idx}", box, True) @@ -3229,10 +3270,10 @@ def run_pyqtgraph(args) -> None: markers = find_peak_width_markers(xs_fft, fft_vals_db) if markers is not None: fft_bg_line.setValue(float(_db_to_linear_amplitude(np.asarray([markers["background"]]))[0])) - fft_left_line.setValue(markers["left"]) - fft_right_line.setValue(markers["right"]) - spec_left_line.setValue(markers["left"]) - spec_right_line.setValue(markers["right"]) + fft_left_line.setValue(display_distance_value(markers["left"])) + fft_right_line.setValue(display_distance_value(markers["right"])) + spec_left_line.setValue(display_distance_value(markers["left"])) + spec_right_line.setValue(display_distance_value(markers["right"])) set_peak_marker_visibility(True) runtime.current_peak_width = markers["width"] runtime.current_peak_amplitude = markers["amplitude"] @@ -3248,7 +3289,7 @@ def run_pyqtgraph(args) -> None: clear_curve_if_needed("fft_real", curve_fft_real) clear_curve_if_needed("fft_imag", curve_fft_imag) if fft_abs_enabled: - fft_x, fft_y = sanitize_curve_data_for_display(xs_fft, fft_vals_db) + fft_x, fft_y = sanitize_curve_data_for_display(xs_fft_display, fft_vals_db) set_curve_data("fft_abs", curve_fft, fft_x, fft_y) else: clear_curve_if_needed("fft_abs", curve_fft) @@ -3259,7 +3300,7 @@ def run_pyqtgraph(args) -> None: fft_ref = rolling_median_ref(xs_fft, fft_vals_db, peak_ref_window) finite_ref = np.isfinite(xs_fft) & np.isfinite(fft_ref) if np.any(finite_ref): - ref_x, ref_y = sanitize_curve_data_for_display(xs_fft[finite_ref], fft_ref[finite_ref]) + ref_x, ref_y = sanitize_curve_data_for_display(xs_fft_display[finite_ref], fft_ref[finite_ref]) set_curve_data("fft_ref", curve_fft_ref, ref_x, ref_y) set_item_visible_if_changed("fft_ref", curve_fft_ref, True) y_for_range = np.concatenate((y_for_range, fft_ref[finite_ref])) @@ -3273,7 +3314,12 @@ def run_pyqtgraph(args) -> None: set_curve_data( f"fft_peak_box_{idx}", box, - [peak["left"], peak["left"], peak["right"], peak["right"], peak["left"]], + display_distance_axis( + np.asarray( + [peak["left"], peak["left"], peak["right"], peak["right"], peak["left"]], + dtype=np.float64, + ) + ), [peak["ref"], peak["peak_y"], peak["peak_y"], peak["ref"], peak["ref"]], ) set_item_visible_if_changed(f"fft_peak_box_{idx}", box, True) @@ -3303,10 +3349,10 @@ def run_pyqtgraph(args) -> None: markers = find_peak_width_markers(xs_fft, fft_vals_db) if markers is not None: fft_bg_line.setValue(markers["background"]) - fft_left_line.setValue(markers["left"]) - fft_right_line.setValue(markers["right"]) - spec_left_line.setValue(markers["left"]) - spec_right_line.setValue(markers["right"]) + fft_left_line.setValue(display_distance_value(markers["left"])) + fft_right_line.setValue(display_distance_value(markers["right"])) + spec_left_line.setValue(display_distance_value(markers["left"])) + spec_right_line.setValue(display_distance_value(markers["right"])) set_peak_marker_visibility(True) runtime.current_peak_width = markers["width"] runtime.current_peak_amplitude = markers["amplitude"] @@ -3494,7 +3540,11 @@ def run_pyqtgraph(args) -> None: and runtime.ring.y_min_fft != runtime.ring.y_max_fft ): levels = (runtime.ring.y_min_fft, runtime.ring.y_max_fft) - distance_bounds = resolve_axis_bounds(disp_fft_axis) + disp_fft_display_axis = display_distance_axis(disp_fft_axis) + if disp_fft_display_axis.size == disp_fft.shape[0]: + disp_fft = disp_fft[::-1, :] + disp_fft_display_axis = disp_fft_display_axis[::-1] + distance_bounds = resolve_axis_bounds(disp_fft_display_axis) if distance_bounds is not None: d_min, d_max = distance_bounds set_image_rect_if_changed("fft_waterfall_rect", img_fft, 0.0, d_min, float(max_sweeps), max(1e-9, d_max - d_min)) diff --git a/tests/test_processing.py b/tests/test_processing.py index 11fc46b..b3063ef 100644 --- a/tests/test_processing.py +++ b/tests/test_processing.py @@ -20,6 +20,8 @@ from rfg_adc_plotter.gui.pyqtgraph_backend import ( convert_tty_i16_to_voltage, decimate_bscan_rows_for_display, decimate_curve_for_display, + display_distance_axis, + display_distance_value, fft_bscan_image_to_db, is_short_sweep, resolve_axis_bounds, @@ -546,6 +548,13 @@ class ProcessingTests(unittest.TestCase): self.assertTrue(bool(keep_mask[-1])) self.assertAlmostEqual(float(cut_axis[0]), 3.0, places=6) + def test_display_distance_axis_zero_is_at_nine_meters(self): + axis = np.asarray([0.0, 4.5, 9.0], dtype=np.float64) + display_axis = display_distance_axis(axis) + + np.testing.assert_allclose(display_axis, np.asarray([9.0, 4.5, 0.0], dtype=np.float64)) + self.assertAlmostEqual(display_distance_value(9.0), 0.0, places=12) + def test_resolve_initial_window_size_stays_within_small_screen(self): width, height = resolve_initial_window_size(800, 480)