diff --git a/calib_envelope.npy b/calib_envelope.npy index bb4541f..7805967 100644 Binary files a/calib_envelope.npy and b/calib_envelope.npy differ diff --git a/rfg_adc_plotter/constants.py b/rfg_adc_plotter/constants.py index 1ccfef8..6ba9cba 100644 --- a/rfg_adc_plotter/constants.py +++ b/rfg_adc_plotter/constants.py @@ -1,5 +1,6 @@ WF_WIDTH = 1000 # максимальное число точек в ряду водопада FFT_LEN = 2048 # длина БПФ для спектра/водопада спектров +LOG_EXP = 2.0 # основание экспоненты для опции --logscale # Порог для инверсии сырых данных: если среднее значение свипа ниже порога — # считаем, что сигнал «меньше нуля» и домножаем свип на -1 DATA_INVERSION_THRESHOLD = 10.0 diff --git a/rfg_adc_plotter/gui/matplotlib_backend.py b/rfg_adc_plotter/gui/matplotlib_backend.py index b768553..972bb57 100644 --- a/rfg_adc_plotter/gui/matplotlib_backend.py +++ b/rfg_adc_plotter/gui/matplotlib_backend.py @@ -96,6 +96,7 @@ def run_matplotlib(args): stop_event, fancy=bool(args.fancy), bin_mode=bool(getattr(args, "bin_mode", False)), + logscale=bool(getattr(args, "logscale", False)), ) reader.start() @@ -106,6 +107,7 @@ def run_matplotlib(args): spec_mean_sec = float(getattr(args, "spec_mean_sec", 0.0)) fixed_ylim = _parse_ylim(getattr(args, "ylim", None)) norm_type = str(getattr(args, "norm_type", "projector")).strip().lower() + logscale_enabled = bool(getattr(args, "logscale", False)) state = AppState(norm_type=norm_type) ring = RingBuffer(max_sweeps) @@ -123,6 +125,8 @@ def run_matplotlib(args): # График последнего свипа line_obj, = ax_line.plot([], [], lw=1, color="tab:blue") line_norm_obj, = ax_line.plot([], [], lw=1, color="tab:green") + line_pre_exp_obj, = ax_line.plot([], [], lw=1, color="tab:red") + line_post_exp_obj, = ax_line.plot([], [], lw=1, color="tab:green") line_env_lo, = ax_line.plot([], [], lw=1, color="tab:orange", linestyle="--", alpha=0.7) line_env_hi, = ax_line.plot([], [], lw=1, color="tab:orange", linestyle="--", alpha=0.7) ax_line.set_title("Сырые данные", pad=1) @@ -271,10 +275,7 @@ def run_matplotlib(args): xs = ring.x_shared[: raw.size] else: xs = np.arange(raw.size, dtype=np.int32) - def _norm_to_max(data): - m = float(np.nanmax(np.abs(data))) - return data / m if m > 0.0 else data - line_obj.set_data(xs, _norm_to_max(raw)) + line_obj.set_data(xs, raw) if state.calib_mode == "file" and state.calib_file_envelope is not None: upper = state.calib_file_envelope lower = -upper @@ -294,14 +295,38 @@ def run_matplotlib(args): else: line_env_lo.set_data([], []) line_env_hi.set_data([], []) - if state.current_sweep_norm is not None: - line_norm_obj.set_data(xs[: state.current_sweep_norm.size], _norm_to_max(state.current_sweep_norm)) - else: + if logscale_enabled: + if state.current_sweep_pre_exp is not None: + pre = state.current_sweep_pre_exp + line_pre_exp_obj.set_data(xs[: pre.size], pre) + else: + line_pre_exp_obj.set_data([], []) + + post = state.current_sweep_post_exp if state.current_sweep_post_exp is not None else raw + line_post_exp_obj.set_data(xs[: post.size], post) + + if state.current_sweep_processed is not None: + proc = state.current_sweep_processed + line_obj.set_data(xs[: proc.size], proc) + else: + line_obj.set_data([], []) line_norm_obj.set_data([], []) + else: + line_pre_exp_obj.set_data([], []) + line_post_exp_obj.set_data([], []) + if state.current_sweep_norm is not None: + line_norm_obj.set_data( + xs[: state.current_sweep_norm.size], state.current_sweep_norm + ) + else: + line_norm_obj.set_data([], []) ax_line.set_xlim(FREQ_MIN, FREQ_MAX) - if fixed_ylim is None: - ax_line.set_ylim(-1.05, 1.05) - ax_line.set_ylabel("/ max") + if fixed_ylim is not None: + ax_line.set_ylim(fixed_ylim) + else: + ax_line.relim() + 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_time_axis is not None: @@ -345,7 +370,19 @@ def run_matplotlib(args): status_text.set_text(format_status(state.current_info)) channel_text.set_text(state.format_channel_label()) - return (line_obj, line_norm_obj, line_env_lo, line_env_hi, img_obj, fft_line_obj, img_fft_obj, status_text, channel_text) + return ( + line_obj, + line_norm_obj, + line_pre_exp_obj, + line_post_exp_obj, + line_env_lo, + line_env_hi, + img_obj, + fft_line_obj, + img_fft_obj, + status_text, + channel_text, + ) ani = FuncAnimation(fig, update, interval=interval_ms, blit=False) plt.show() diff --git a/rfg_adc_plotter/gui/pyqtgraph_backend.py b/rfg_adc_plotter/gui/pyqtgraph_backend.py index a9069b5..90741e1 100644 --- a/rfg_adc_plotter/gui/pyqtgraph_backend.py +++ b/rfg_adc_plotter/gui/pyqtgraph_backend.py @@ -113,6 +113,7 @@ def run_pyqtgraph(args): stop_event, fancy=bool(args.fancy), bin_mode=bool(getattr(args, "bin_mode", False)), + logscale=bool(getattr(args, "logscale", False)), ) reader.start() @@ -123,6 +124,7 @@ def run_pyqtgraph(args): spec_mean_sec = float(getattr(args, "spec_mean_sec", 0.0)) fixed_ylim = _parse_ylim(getattr(args, "ylim", None)) norm_type = str(getattr(args, "norm_type", "projector")).strip().lower() + logscale_enabled = bool(getattr(args, "logscale", False)) state = AppState(norm_type=norm_type) ring = RingBuffer(max_sweeps) @@ -138,6 +140,8 @@ def run_pyqtgraph(args): p_line.showGrid(x=True, y=True, alpha=0.3) curve = p_line.plot(pen=pg.mkPen((80, 120, 255), width=1)) curve_norm = p_line.plot(pen=pg.mkPen((60, 180, 90), width=1)) + curve_pre_exp = p_line.plot(pen=pg.mkPen((220, 60, 60), width=1)) + curve_post_exp = p_line.plot(pen=pg.mkPen((60, 180, 90), width=1)) curve_env_lo = p_line.plot(pen=pg.mkPen((255, 165, 0), width=1, style=QtCore.Qt.DashLine)) curve_env_hi = p_line.plot(pen=pg.mkPen((255, 165, 0), width=1, style=QtCore.Qt.DashLine)) p_line.setLabel("bottom", "Частота, ГГц") @@ -293,10 +297,7 @@ def run_pyqtgraph(args): if state.current_sweep_raw is not None and ring.x_shared is not None: raw = state.current_sweep_raw xs = ring.x_shared[: raw.size] if raw.size <= ring.x_shared.size else np.arange(raw.size) - def _norm_to_max(data): - m = float(np.nanmax(np.abs(data))) - return data / m if m > 0.0 else data - curve.setData(xs, _norm_to_max(raw), autoDownsample=True) + curve.setData(xs, raw, autoDownsample=True) if state.calib_mode == "file" and state.calib_file_envelope is not None: upper = state.calib_file_envelope lower = -upper @@ -316,13 +317,38 @@ def run_pyqtgraph(args): else: curve_env_lo.setData([], []) curve_env_hi.setData([], []) - if state.current_sweep_norm is not None: - curve_norm.setData(xs[: state.current_sweep_norm.size], _norm_to_max(state.current_sweep_norm), autoDownsample=True) - else: + if logscale_enabled: + if state.current_sweep_pre_exp is not None: + pre = state.current_sweep_pre_exp + curve_pre_exp.setData(xs[: pre.size], pre, autoDownsample=True) + else: + curve_pre_exp.setData([], []) + + post = state.current_sweep_post_exp if state.current_sweep_post_exp is not None else raw + curve_post_exp.setData(xs[: post.size], post, autoDownsample=True) + + if state.current_sweep_processed is not None: + proc = state.current_sweep_processed + curve.setData(xs[: proc.size], proc, autoDownsample=True) + else: + curve.setData([], []) curve_norm.setData([], []) - if fixed_ylim is None: - p_line.setYRange(-1.05, 1.05, padding=0) - p_line.setLabel("left", "/ max") + else: + curve_pre_exp.setData([], []) + curve_post_exp.setData([], []) + if state.current_sweep_norm is not None: + curve_norm.setData( + xs[: state.current_sweep_norm.size], + state.current_sweep_norm, + autoDownsample=True, + ) + else: + curve_norm.setData([], []) + if fixed_ylim is not None: + p_line.setYRange(fixed_ylim[0], fixed_ylim[1], padding=0) + else: + 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_time_axis is not None: diff --git a/rfg_adc_plotter/io/sweep_reader.py b/rfg_adc_plotter/io/sweep_reader.py index 705983c..4c3598a 100644 --- a/rfg_adc_plotter/io/sweep_reader.py +++ b/rfg_adc_plotter/io/sweep_reader.py @@ -9,7 +9,7 @@ from typing import Optional import numpy as np -from rfg_adc_plotter.constants import DATA_INVERSION_THRESHOLD +from rfg_adc_plotter.constants import DATA_INVERSION_THRESHOLD, LOG_EXP from rfg_adc_plotter.io.serial_source import SerialChunkReader, SerialLineSource from rfg_adc_plotter.types import SweepInfo, SweepPacket @@ -25,6 +25,7 @@ class SweepReader(threading.Thread): stop_event: threading.Event, fancy: bool = False, bin_mode: bool = False, + logscale: bool = False, ): super().__init__(daemon=True) self._port_path = port_path @@ -34,6 +35,7 @@ class SweepReader(threading.Thread): self._src: Optional[SerialLineSource] = None self._fancy = bool(fancy) self._bin_mode = bool(bin_mode) + self._logscale = bool(logscale) self._max_width: int = 0 self._sweep_idx: int = 0 self._last_sweep_ts: Optional[float] = None @@ -92,6 +94,16 @@ class SweepReader(threading.Thread): except Exception: pass + pre_exp_sweep = None + if self._logscale: + try: + pre_exp_sweep = sweep.copy() + with np.errstate(over="ignore", invalid="ignore"): + sweep = np.power(LOG_EXP, np.asarray(sweep, dtype=np.float64)).astype(np.float32) + sweep[~np.isfinite(sweep)] = np.nan + except Exception: + pass + self._sweep_idx += 1 if len(ch_list) > 1: sys.stderr.write( @@ -129,6 +141,8 @@ class SweepReader(threading.Thread): "std": std, "dt_ms": dt_ms, } + if pre_exp_sweep is not None: + info["pre_exp_sweep"] = pre_exp_sweep try: self._q.put_nowait((sweep, info)) diff --git a/rfg_adc_plotter/main.py b/rfg_adc_plotter/main.py index ff1feb6..ca4e644 100755 --- a/rfg_adc_plotter/main.py +++ b/rfg_adc_plotter/main.py @@ -86,6 +86,11 @@ def build_parser() -> argparse.ArgumentParser: "точки step,uint32(hi16,lo16),0x000A" ), ) + parser.add_argument( + "--logscale", + action="store_true", + help="После поправки знака применять экспоненту LOG_EXP**x (LOG_EXP=2)", + ) return parser diff --git a/rfg_adc_plotter/state/app_state.py b/rfg_adc_plotter/state/app_state.py index 0530fb4..10c339a 100644 --- a/rfg_adc_plotter/state/app_state.py +++ b/rfg_adc_plotter/state/app_state.py @@ -34,7 +34,7 @@ def format_status(data: Mapping[str, Any]) -> str: return f"{fv:.3g}" return f"{fv:.3f}".rstrip("0").rstrip(".") - parts = [f"{k}:{_fmt(v)}" for k, v in data.items()] + parts = [f"{k}:{_fmt(v)}" for k, v in data.items() if k != "pre_exp_sweep"] return " ".join(parts) @@ -46,6 +46,9 @@ class AppState: """ def __init__(self, norm_type: str = "projector"): + self.current_sweep_pre_exp: Optional[np.ndarray] = None + self.current_sweep_post_exp: Optional[np.ndarray] = None + self.current_sweep_processed: Optional[np.ndarray] = None self.current_sweep_raw: Optional[np.ndarray] = None self.current_sweep_norm: Optional[np.ndarray] = None self.last_calib_sweep: Optional[np.ndarray] = None @@ -154,6 +157,9 @@ class AppState: self.current_sweep_norm = None else: self.current_sweep_norm = None + self.current_sweep_processed = ( + self.current_sweep_norm if self.current_sweep_norm is not None else self.current_sweep_raw + ) def drain_queue(self, q: "Queue[SweepPacket]", ring: RingBuffer) -> int: """Вытащить все ожидающие свипы из очереди, обновить state и ring. @@ -168,7 +174,10 @@ class AppState: break drained += 1 self.current_sweep_raw = s + self.current_sweep_post_exp = s self.current_info = info + pre_exp = info.get("pre_exp_sweep") if isinstance(info, dict) else None + self.current_sweep_pre_exp = pre_exp if isinstance(pre_exp, np.ndarray) else None ch = 0 try: @@ -204,6 +213,7 @@ class AppState: self.current_sweep_norm = sweep_for_ring self._last_sweep_for_ring = sweep_for_ring + self.current_sweep_processed = sweep_for_ring ring.ensure_init(s.size) ring.push(sweep_for_ring) return drained