2 Commits

7 changed files with 116 additions and 23 deletions

Binary file not shown.

View File

@ -1,5 +1,6 @@
WF_WIDTH = 1000 # максимальное число точек в ряду водопада WF_WIDTH = 1000 # максимальное число точек в ряду водопада
FFT_LEN = 2048 # длина БПФ для спектра/водопада спектров FFT_LEN = 2048 # длина БПФ для спектра/водопада спектров
LOG_EXP = 2.0 # основание экспоненты для опции --logscale
# Порог для инверсии сырых данных: если среднее значение свипа ниже порога — # Порог для инверсии сырых данных: если среднее значение свипа ниже порога —
# считаем, что сигнал «меньше нуля» и домножаем свип на -1 # считаем, что сигнал «меньше нуля» и домножаем свип на -1
DATA_INVERSION_THRESHOLD = 10.0 DATA_INVERSION_THRESHOLD = 10.0

View File

@ -96,6 +96,7 @@ def run_matplotlib(args):
stop_event, stop_event,
fancy=bool(args.fancy), fancy=bool(args.fancy),
bin_mode=bool(getattr(args, "bin_mode", False)), bin_mode=bool(getattr(args, "bin_mode", False)),
logscale=bool(getattr(args, "logscale", False)),
) )
reader.start() reader.start()
@ -106,6 +107,7 @@ def run_matplotlib(args):
spec_mean_sec = float(getattr(args, "spec_mean_sec", 0.0)) spec_mean_sec = float(getattr(args, "spec_mean_sec", 0.0))
fixed_ylim = _parse_ylim(getattr(args, "ylim", None)) fixed_ylim = _parse_ylim(getattr(args, "ylim", None))
norm_type = str(getattr(args, "norm_type", "projector")).strip().lower() norm_type = str(getattr(args, "norm_type", "projector")).strip().lower()
logscale_enabled = bool(getattr(args, "logscale", False))
state = AppState(norm_type=norm_type) state = AppState(norm_type=norm_type)
ring = RingBuffer(max_sweeps) ring = RingBuffer(max_sweeps)
@ -123,6 +125,8 @@ def run_matplotlib(args):
# График последнего свипа # График последнего свипа
line_obj, = ax_line.plot([], [], lw=1, color="tab:blue") line_obj, = ax_line.plot([], [], lw=1, color="tab:blue")
line_norm_obj, = ax_line.plot([], [], lw=1, color="tab:green") 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_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) line_env_hi, = ax_line.plot([], [], lw=1, color="tab:orange", linestyle="--", alpha=0.7)
ax_line.set_title("Сырые данные", pad=1) ax_line.set_title("Сырые данные", pad=1)
@ -271,10 +275,7 @@ def run_matplotlib(args):
xs = ring.x_shared[: raw.size] xs = ring.x_shared[: raw.size]
else: else:
xs = np.arange(raw.size, dtype=np.int32) xs = np.arange(raw.size, dtype=np.int32)
def _norm_to_max(data): line_obj.set_data(xs, raw)
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))
if state.calib_mode == "file" and state.calib_file_envelope is not None: if state.calib_mode == "file" and state.calib_file_envelope is not None:
upper = state.calib_file_envelope upper = state.calib_file_envelope
lower = -upper lower = -upper
@ -294,14 +295,38 @@ def run_matplotlib(args):
else: else:
line_env_lo.set_data([], []) line_env_lo.set_data([], [])
line_env_hi.set_data([], []) line_env_hi.set_data([], [])
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: 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)) line_norm_obj.set_data(
xs[: state.current_sweep_norm.size], state.current_sweep_norm
)
else: else:
line_norm_obj.set_data([], []) line_norm_obj.set_data([], [])
ax_line.set_xlim(FREQ_MIN, FREQ_MAX) ax_line.set_xlim(FREQ_MIN, FREQ_MAX)
if fixed_ylim is None: if fixed_ylim is not None:
ax_line.set_ylim(-1.05, 1.05) ax_line.set_ylim(fixed_ylim)
ax_line.set_ylabel("/ max") else:
ax_line.relim()
ax_line.autoscale_view(scalex=False, scaley=True)
ax_line.set_ylabel("Y")
# Спектр — используем уже вычисленный в ring IFFT (временной профиль) # Спектр — используем уже вычисленный в ring IFFT (временной профиль)
if ring.last_fft_vals is not None and ring.fft_time_axis is not None: 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)) status_text.set_text(format_status(state.current_info))
channel_text.set_text(state.format_channel_label()) 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) ani = FuncAnimation(fig, update, interval=interval_ms, blit=False)
plt.show() plt.show()

View File

@ -113,6 +113,7 @@ def run_pyqtgraph(args):
stop_event, stop_event,
fancy=bool(args.fancy), fancy=bool(args.fancy),
bin_mode=bool(getattr(args, "bin_mode", False)), bin_mode=bool(getattr(args, "bin_mode", False)),
logscale=bool(getattr(args, "logscale", False)),
) )
reader.start() reader.start()
@ -123,6 +124,7 @@ def run_pyqtgraph(args):
spec_mean_sec = float(getattr(args, "spec_mean_sec", 0.0)) spec_mean_sec = float(getattr(args, "spec_mean_sec", 0.0))
fixed_ylim = _parse_ylim(getattr(args, "ylim", None)) fixed_ylim = _parse_ylim(getattr(args, "ylim", None))
norm_type = str(getattr(args, "norm_type", "projector")).strip().lower() norm_type = str(getattr(args, "norm_type", "projector")).strip().lower()
logscale_enabled = bool(getattr(args, "logscale", False))
state = AppState(norm_type=norm_type) state = AppState(norm_type=norm_type)
ring = RingBuffer(max_sweeps) ring = RingBuffer(max_sweeps)
@ -138,6 +140,8 @@ def run_pyqtgraph(args):
p_line.showGrid(x=True, y=True, alpha=0.3) p_line.showGrid(x=True, y=True, alpha=0.3)
curve = p_line.plot(pen=pg.mkPen((80, 120, 255), width=1)) 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_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_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)) curve_env_hi = p_line.plot(pen=pg.mkPen((255, 165, 0), width=1, style=QtCore.Qt.DashLine))
p_line.setLabel("bottom", "Частота, ГГц") 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: if state.current_sweep_raw is not None and ring.x_shared is not None:
raw = state.current_sweep_raw raw = state.current_sweep_raw
xs = ring.x_shared[: raw.size] if raw.size <= ring.x_shared.size else np.arange(raw.size) xs = ring.x_shared[: raw.size] if raw.size <= ring.x_shared.size else np.arange(raw.size)
def _norm_to_max(data): curve.setData(xs, raw, autoDownsample=True)
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)
if state.calib_mode == "file" and state.calib_file_envelope is not None: if state.calib_mode == "file" and state.calib_file_envelope is not None:
upper = state.calib_file_envelope upper = state.calib_file_envelope
lower = -upper lower = -upper
@ -316,13 +317,38 @@ def run_pyqtgraph(args):
else: else:
curve_env_lo.setData([], []) curve_env_lo.setData([], [])
curve_env_hi.setData([], []) curve_env_hi.setData([], [])
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([], [])
else:
curve_pre_exp.setData([], [])
curve_post_exp.setData([], [])
if state.current_sweep_norm is not None: 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) curve_norm.setData(
xs[: state.current_sweep_norm.size],
state.current_sweep_norm,
autoDownsample=True,
)
else: else:
curve_norm.setData([], []) curve_norm.setData([], [])
if fixed_ylim is None: if fixed_ylim is not None:
p_line.setYRange(-1.05, 1.05, padding=0) p_line.setYRange(fixed_ylim[0], fixed_ylim[1], padding=0)
p_line.setLabel("left", "/ max") else:
p_line.enableAutoRange(axis="y", enable=True)
p_line.setLabel("left", "Y")
# Спектр — используем уже вычисленный в ring IFFT (временной профиль) # Спектр — используем уже вычисленный в ring IFFT (временной профиль)
if ring.last_fft_vals is not None and ring.fft_time_axis is not None: if ring.last_fft_vals is not None and ring.fft_time_axis is not None:

View File

@ -9,7 +9,7 @@ from typing import Optional
import numpy as np 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.io.serial_source import SerialChunkReader, SerialLineSource
from rfg_adc_plotter.types import SweepInfo, SweepPacket from rfg_adc_plotter.types import SweepInfo, SweepPacket
@ -25,6 +25,7 @@ class SweepReader(threading.Thread):
stop_event: threading.Event, stop_event: threading.Event,
fancy: bool = False, fancy: bool = False,
bin_mode: bool = False, bin_mode: bool = False,
logscale: bool = False,
): ):
super().__init__(daemon=True) super().__init__(daemon=True)
self._port_path = port_path self._port_path = port_path
@ -34,6 +35,7 @@ class SweepReader(threading.Thread):
self._src: Optional[SerialLineSource] = None self._src: Optional[SerialLineSource] = None
self._fancy = bool(fancy) self._fancy = bool(fancy)
self._bin_mode = bool(bin_mode) self._bin_mode = bool(bin_mode)
self._logscale = bool(logscale)
self._max_width: int = 0 self._max_width: int = 0
self._sweep_idx: int = 0 self._sweep_idx: int = 0
self._last_sweep_ts: Optional[float] = None self._last_sweep_ts: Optional[float] = None
@ -92,6 +94,16 @@ class SweepReader(threading.Thread):
except Exception: except Exception:
pass 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 self._sweep_idx += 1
if len(ch_list) > 1: if len(ch_list) > 1:
sys.stderr.write( sys.stderr.write(
@ -129,6 +141,8 @@ class SweepReader(threading.Thread):
"std": std, "std": std,
"dt_ms": dt_ms, "dt_ms": dt_ms,
} }
if pre_exp_sweep is not None:
info["pre_exp_sweep"] = pre_exp_sweep
try: try:
self._q.put_nowait((sweep, info)) self._q.put_nowait((sweep, info))

View File

@ -86,6 +86,11 @@ def build_parser() -> argparse.ArgumentParser:
"точки step,uint32(hi16,lo16),0x000A" "точки step,uint32(hi16,lo16),0x000A"
), ),
) )
parser.add_argument(
"--logscale",
action="store_true",
help="После поправки знака применять экспоненту LOG_EXP**x (LOG_EXP=2)",
)
return parser return parser

View File

@ -34,7 +34,7 @@ def format_status(data: Mapping[str, Any]) -> str:
return f"{fv:.3g}" return f"{fv:.3g}"
return f"{fv:.3f}".rstrip("0").rstrip(".") 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) return " ".join(parts)
@ -46,6 +46,9 @@ class AppState:
""" """
def __init__(self, norm_type: str = "projector"): 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_raw: Optional[np.ndarray] = None
self.current_sweep_norm: Optional[np.ndarray] = None self.current_sweep_norm: Optional[np.ndarray] = None
self.last_calib_sweep: Optional[np.ndarray] = None self.last_calib_sweep: Optional[np.ndarray] = None
@ -154,6 +157,9 @@ class AppState:
self.current_sweep_norm = None self.current_sweep_norm = None
else: else:
self.current_sweep_norm = None 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: def drain_queue(self, q: "Queue[SweepPacket]", ring: RingBuffer) -> int:
"""Вытащить все ожидающие свипы из очереди, обновить state и ring. """Вытащить все ожидающие свипы из очереди, обновить state и ring.
@ -168,7 +174,10 @@ class AppState:
break break
drained += 1 drained += 1
self.current_sweep_raw = s self.current_sweep_raw = s
self.current_sweep_post_exp = s
self.current_info = info 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 ch = 0
try: try:
@ -204,6 +213,7 @@ class AppState:
self.current_sweep_norm = sweep_for_ring self.current_sweep_norm = sweep_for_ring
self._last_sweep_for_ring = sweep_for_ring self._last_sweep_for_ring = sweep_for_ring
self.current_sweep_processed = sweep_for_ring
ring.ensure_init(s.size) ring.ensure_init(s.size)
ring.push(sweep_for_ring) ring.push(sweep_for_ring)
return drained return drained