giga fix
This commit is contained in:
@ -42,6 +42,151 @@ RAW_PLOT_MAX_POINTS = 4096
|
||||
RAW_WATERFALL_MAX_POINTS = 2048
|
||||
UI_MAX_PACKETS_PER_TICK = 8
|
||||
DEBUG_FRAME_LOG_EVERY = 10
|
||||
UI_BACKLOG_TAIL_THRESHOLD_MULTIPLIER = 2
|
||||
UI_BACKLOG_LATEST_ONLY_THRESHOLD_MULTIPLIER = 4
|
||||
UI_HEAVY_REFRESH_BACKLOG_MULTIPLIER = 2
|
||||
UI_HEAVY_REFRESH_MAX_STRIDE = 4
|
||||
DEFAULT_MAIN_WINDOW_WIDTH = 1200
|
||||
DEFAULT_MAIN_WINDOW_HEIGHT = 680
|
||||
MIN_MAIN_WINDOW_WIDTH = 640
|
||||
MIN_MAIN_WINDOW_HEIGHT = 420
|
||||
|
||||
|
||||
def sanitize_curve_data_for_display(
|
||||
xs: Optional[np.ndarray],
|
||||
ys: Optional[np.ndarray],
|
||||
) -> Tuple[np.ndarray, np.ndarray]:
|
||||
"""Drop non-finite points before passing a line series to pyqtgraph."""
|
||||
if xs is None or ys is None:
|
||||
return (
|
||||
np.zeros((0,), dtype=np.float64),
|
||||
np.zeros((0,), dtype=np.float32),
|
||||
)
|
||||
|
||||
x_arr = np.asarray(xs, dtype=np.float64).reshape(-1)
|
||||
y_arr = np.asarray(ys, dtype=np.float32).reshape(-1)
|
||||
width = min(x_arr.size, y_arr.size)
|
||||
if width <= 0:
|
||||
return (
|
||||
np.zeros((0,), dtype=np.float64),
|
||||
np.zeros((0,), dtype=np.float32),
|
||||
)
|
||||
|
||||
x_arr = x_arr[:width]
|
||||
y_arr = y_arr[:width]
|
||||
finite = np.isfinite(x_arr) & np.isfinite(y_arr)
|
||||
if not np.any(finite):
|
||||
return (
|
||||
np.zeros((0,), dtype=np.float64),
|
||||
np.zeros((0,), dtype=np.float32),
|
||||
)
|
||||
return x_arr[finite], y_arr[finite]
|
||||
|
||||
|
||||
def sanitize_image_for_display(data: Optional[np.ndarray]) -> Optional[np.ndarray]:
|
||||
"""Reject empty or fully non-finite images before calling ImageItem.setImage."""
|
||||
if data is None:
|
||||
return None
|
||||
arr = np.asarray(data, dtype=np.float32)
|
||||
if arr.ndim != 2 or arr.size <= 0 or not np.isfinite(arr).any():
|
||||
return None
|
||||
return arr
|
||||
|
||||
|
||||
def resolve_axis_bounds(
|
||||
values: Optional[np.ndarray],
|
||||
*,
|
||||
default_span: float = 1.0,
|
||||
) -> Optional[Tuple[float, float]]:
|
||||
"""Return a finite plotting span for a 1D axis."""
|
||||
if values is None:
|
||||
return None
|
||||
arr = np.asarray(values, dtype=np.float64).reshape(-1)
|
||||
finite = arr[np.isfinite(arr)]
|
||||
if finite.size <= 0:
|
||||
return None
|
||||
lower = float(np.min(finite))
|
||||
upper = float(np.max(finite))
|
||||
if upper <= lower:
|
||||
upper = lower + max(float(default_span), 1e-9)
|
||||
return (lower, upper)
|
||||
|
||||
|
||||
def resolve_heavy_refresh_stride(
|
||||
backlog_packets: int,
|
||||
*,
|
||||
max_packets: int = UI_MAX_PACKETS_PER_TICK,
|
||||
) -> int:
|
||||
"""Lower image refresh frequency when the UI is already behind."""
|
||||
base = max(1, int(max_packets))
|
||||
backlog = max(0, int(backlog_packets))
|
||||
if backlog >= (base * UI_BACKLOG_LATEST_ONLY_THRESHOLD_MULTIPLIER):
|
||||
return UI_HEAVY_REFRESH_MAX_STRIDE
|
||||
if backlog >= (base * UI_HEAVY_REFRESH_BACKLOG_MULTIPLIER):
|
||||
return 2
|
||||
return 1
|
||||
|
||||
|
||||
def resolve_initial_window_size(available_width: int, available_height: int) -> Tuple[int, int]:
|
||||
"""Fit the initial window to the current screen without assuming desktop-sized geometry."""
|
||||
width_in = int(max(0, available_width))
|
||||
height_in = int(max(0, available_height))
|
||||
if width_in <= 0 or height_in <= 0:
|
||||
return DEFAULT_MAIN_WINDOW_WIDTH, DEFAULT_MAIN_WINDOW_HEIGHT
|
||||
|
||||
width_floor = min(MIN_MAIN_WINDOW_WIDTH, width_in)
|
||||
height_floor = min(MIN_MAIN_WINDOW_HEIGHT, height_in)
|
||||
width = int(width_in * 0.94)
|
||||
height = int(height_in * 0.92)
|
||||
width = min(max(width, width_floor), width_in)
|
||||
height = min(max(height, height_floor), height_in)
|
||||
return width, height
|
||||
|
||||
|
||||
def build_main_window_layout(QtCore, QtWidgets, main_window):
|
||||
"""Build a splitter-based main layout so settings stay usable on small screens."""
|
||||
main_layout = QtWidgets.QVBoxLayout(main_window)
|
||||
main_layout.setContentsMargins(6, 6, 6, 6)
|
||||
main_layout.setSpacing(6)
|
||||
|
||||
splitter = QtWidgets.QSplitter(QtCore.Qt.Horizontal, main_window)
|
||||
try:
|
||||
splitter.setChildrenCollapsible(False)
|
||||
splitter.setHandleWidth(6)
|
||||
except Exception:
|
||||
pass
|
||||
main_layout.addWidget(splitter)
|
||||
|
||||
plot_host = QtWidgets.QWidget()
|
||||
plot_layout = QtWidgets.QVBoxLayout(plot_host)
|
||||
plot_layout.setContentsMargins(0, 0, 0, 0)
|
||||
plot_layout.setSpacing(0)
|
||||
splitter.addWidget(plot_host)
|
||||
|
||||
settings_widget = QtWidgets.QWidget()
|
||||
settings_layout = QtWidgets.QVBoxLayout(settings_widget)
|
||||
settings_layout.setContentsMargins(6, 6, 6, 6)
|
||||
settings_layout.setSpacing(8)
|
||||
|
||||
settings_scroll = QtWidgets.QScrollArea()
|
||||
settings_scroll.setWidgetResizable(True)
|
||||
settings_scroll.setFrameShape(QtWidgets.QFrame.NoFrame)
|
||||
settings_scroll.setWidget(settings_widget)
|
||||
try:
|
||||
settings_scroll.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
|
||||
settings_scroll.setMinimumWidth(250)
|
||||
settings_scroll.setMaximumWidth(360)
|
||||
except Exception:
|
||||
pass
|
||||
splitter.addWidget(settings_scroll)
|
||||
|
||||
try:
|
||||
splitter.setStretchFactor(0, 1)
|
||||
splitter.setStretchFactor(1, 0)
|
||||
splitter.setSizes([900, 300])
|
||||
except Exception:
|
||||
pass
|
||||
return main_layout, splitter, plot_layout, settings_widget, settings_layout, settings_scroll
|
||||
|
||||
|
||||
def _visible_levels_pyqtgraph(data: np.ndarray, plot_item) -> Optional[Tuple[float, float]]:
|
||||
@ -247,10 +392,18 @@ def coalesce_packets_for_ui(
|
||||
packets: Sequence[SweepPacket],
|
||||
*,
|
||||
max_packets: int = UI_MAX_PACKETS_PER_TICK,
|
||||
backlog_packets: Optional[int] = None,
|
||||
) -> Tuple[List[SweepPacket], int]:
|
||||
"""Keep only the newest packets so a burst cannot starve the Qt event loop."""
|
||||
packet_list = list(packets)
|
||||
limit = max(1, int(max_packets))
|
||||
base_limit = max(1, int(max_packets))
|
||||
backlog = max(len(packet_list), int(backlog_packets or 0))
|
||||
if backlog >= (base_limit * UI_BACKLOG_LATEST_ONLY_THRESHOLD_MULTIPLIER):
|
||||
limit = 1
|
||||
elif backlog >= (base_limit * UI_BACKLOG_TAIL_THRESHOLD_MULTIPLIER):
|
||||
limit = min(2, base_limit)
|
||||
else:
|
||||
limit = base_limit
|
||||
if len(packet_list) <= limit:
|
||||
return packet_list, 0
|
||||
skipped = len(packet_list) - limit
|
||||
@ -381,23 +534,16 @@ def run_pyqtgraph(args) -> None:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
main_layout = QtWidgets.QHBoxLayout(main_window)
|
||||
main_layout.setContentsMargins(6, 6, 6, 6)
|
||||
main_layout.setSpacing(6)
|
||||
|
||||
win = pg.GraphicsLayoutWidget(show=False, title=args.title)
|
||||
main_layout.addWidget(win)
|
||||
|
||||
settings_widget = QtWidgets.QWidget()
|
||||
settings_layout = QtWidgets.QVBoxLayout(settings_widget)
|
||||
settings_layout.setContentsMargins(6, 6, 6, 6)
|
||||
settings_layout.setSpacing(8)
|
||||
try:
|
||||
settings_widget.setMinimumWidth(250)
|
||||
settings_widget.setMaximumWidth(340)
|
||||
except Exception:
|
||||
pass
|
||||
main_layout.addWidget(settings_widget)
|
||||
(
|
||||
_main_layout,
|
||||
splitter,
|
||||
plot_layout,
|
||||
settings_widget,
|
||||
settings_layout,
|
||||
settings_scroll,
|
||||
) = build_main_window_layout(QtCore, QtWidgets, main_window)
|
||||
plot_layout.addWidget(win)
|
||||
|
||||
p_line = win.addPlot(row=0, col=0, title="Сырые данные")
|
||||
p_line.showGrid(x=True, y=True, alpha=0.3)
|
||||
@ -597,8 +743,13 @@ def run_pyqtgraph(args) -> None:
|
||||
fft_imag_enabled = True
|
||||
fft_mode = "symmetric"
|
||||
status_note = ""
|
||||
status_note_expires_at: Optional[float] = None
|
||||
status_dirty = True
|
||||
range_change_in_progress = False
|
||||
debug_event_counts: Dict[str, int] = {}
|
||||
last_queue_backlog = 0
|
||||
last_backlog_skipped = 0
|
||||
last_heavy_refresh_stride = 1
|
||||
fixed_ylim: Optional[Tuple[float, float]] = None
|
||||
if args.ylim:
|
||||
try:
|
||||
@ -615,50 +766,37 @@ def run_pyqtgraph(args) -> None:
|
||||
return
|
||||
f_min = float(runtime.range_min_ghz)
|
||||
f_max = float(runtime.range_max_ghz)
|
||||
if runtime.current_freqs is not None and runtime.current_freqs.size > 0:
|
||||
finite_f = runtime.current_freqs[np.isfinite(runtime.current_freqs)]
|
||||
if finite_f.size > 0:
|
||||
f_min = float(np.min(finite_f))
|
||||
f_max = float(np.max(finite_f))
|
||||
img.setImage(runtime.ring.get_display_raw_decimated(RAW_WATERFALL_MAX_POINTS), autoLevels=False)
|
||||
freq_bounds = resolve_axis_bounds(runtime.current_freqs)
|
||||
if freq_bounds is not None:
|
||||
f_min, f_max = freq_bounds
|
||||
disp_raw = sanitize_image_for_display(runtime.ring.get_display_raw_decimated(RAW_WATERFALL_MAX_POINTS))
|
||||
if disp_raw is not None:
|
||||
img.setImage(disp_raw, autoLevels=False)
|
||||
img.setRect(0, f_min, max_sweeps, max(1e-9, f_max - f_min))
|
||||
p_img.setRange(
|
||||
xRange=(0, max_sweeps - 1),
|
||||
yRange=(f_min, f_max),
|
||||
padding=0,
|
||||
)
|
||||
p_img.setRange(xRange=(0, max_sweeps - 1), yRange=(f_min, f_max), padding=0)
|
||||
p_line.setXRange(f_min, f_max, padding=0)
|
||||
img_fft.setImage(runtime.ring.get_display_fft_linear(), autoLevels=False)
|
||||
disp_fft = sanitize_image_for_display(runtime.ring.get_display_fft_linear())
|
||||
if disp_fft is not None:
|
||||
img_fft.setImage(disp_fft, autoLevels=False)
|
||||
img_fft.setRect(0, 0.0, max_sweeps, 1.0)
|
||||
p_spec.setRange(xRange=(0, max_sweeps - 1), yRange=(0.0, 1.0), padding=0)
|
||||
p_fft.setXRange(0.0, 1.0, padding=0)
|
||||
|
||||
def update_physical_axes() -> None:
|
||||
if runtime.current_freqs is not None and runtime.current_freqs.size > 0:
|
||||
finite_f = runtime.current_freqs[np.isfinite(runtime.current_freqs)]
|
||||
if finite_f.size > 0:
|
||||
f_min = float(np.min(finite_f))
|
||||
f_max = float(np.max(finite_f))
|
||||
if f_max <= f_min:
|
||||
f_max = f_min + 1.0
|
||||
img.setRect(0, f_min, max_sweeps, f_max - f_min)
|
||||
p_img.setRange(xRange=(0, max_sweeps - 1), yRange=(f_min, f_max), padding=0)
|
||||
p_line.setXRange(f_min, f_max, padding=0)
|
||||
else:
|
||||
f_min = float(runtime.range_min_ghz)
|
||||
f_max = float(runtime.range_max_ghz)
|
||||
if f_max <= f_min:
|
||||
f_max = f_min + 1.0
|
||||
freq_bounds = resolve_axis_bounds(runtime.current_freqs)
|
||||
if freq_bounds is None:
|
||||
freq_bounds = resolve_axis_bounds(
|
||||
np.asarray([runtime.range_min_ghz, runtime.range_max_ghz], dtype=np.float64)
|
||||
)
|
||||
if freq_bounds is not None:
|
||||
f_min, f_max = freq_bounds
|
||||
img.setRect(0, f_min, max_sweeps, f_max - f_min)
|
||||
p_img.setRange(xRange=(0, max_sweeps - 1), yRange=(f_min, f_max), padding=0)
|
||||
p_line.setXRange(f_min, f_max, padding=0)
|
||||
|
||||
distance_axis = runtime.ring.distance_axis
|
||||
if distance_axis is not None and distance_axis.size > 0:
|
||||
d_min = float(distance_axis[0])
|
||||
d_max = float(distance_axis[-1]) if distance_axis.size > 1 else float(distance_axis[0] + 1.0)
|
||||
if d_max <= d_min:
|
||||
d_max = d_min + 1.0
|
||||
distance_bounds = resolve_axis_bounds(runtime.ring.distance_axis)
|
||||
if distance_bounds is not None:
|
||||
d_min, d_max = distance_bounds
|
||||
img_fft.setRect(0, d_min, max_sweeps, d_max - d_min)
|
||||
p_spec.setRange(xRange=(0, max_sweeps - 1), yRange=(d_min, d_max), padding=0)
|
||||
|
||||
@ -685,11 +823,28 @@ def run_pyqtgraph(args) -> None:
|
||||
return runtime.ring.x_shared[:size]
|
||||
return np.arange(size, dtype=np.float32)
|
||||
|
||||
def set_status_note(note: str) -> None:
|
||||
nonlocal status_note, status_dirty
|
||||
def set_status_note(note: str, ttl_s: Optional[float] = None) -> None:
|
||||
nonlocal status_note, status_note_expires_at, status_dirty
|
||||
status_note = str(note).strip()
|
||||
status_note_expires_at = None if ttl_s is None else (time.time() + max(0.1, float(ttl_s)))
|
||||
status_dirty = True
|
||||
|
||||
def clear_expired_status_note() -> None:
|
||||
nonlocal status_note, status_note_expires_at, status_dirty
|
||||
if status_note_expires_at is None:
|
||||
return
|
||||
if time.time() < status_note_expires_at:
|
||||
return
|
||||
status_note = ""
|
||||
status_note_expires_at = None
|
||||
status_dirty = True
|
||||
|
||||
def log_debug_event(name: str, message: str, *, every: int = DEBUG_FRAME_LOG_EVERY) -> None:
|
||||
count = int(debug_event_counts.get(name, 0)) + 1
|
||||
debug_event_counts[name] = count
|
||||
if count == 1 or count % max(1, int(every)) == 0:
|
||||
sys.stderr.write(f"[debug] {message} (x{count})\n")
|
||||
|
||||
def get_calib_file_path() -> str:
|
||||
try:
|
||||
path = calib_path_edit.text().strip()
|
||||
@ -836,7 +991,13 @@ def run_pyqtgraph(args) -> None:
|
||||
runtime.current_fft_complex = None
|
||||
runtime.current_fft_mag = runtime.ring.get_last_fft_linear()
|
||||
runtime.current_fft_db = runtime.ring.last_fft_db
|
||||
if runtime.current_fft_mag is not None:
|
||||
if not runtime.ring.last_push_fft_valid:
|
||||
set_status_note("fft: пропущен невалидный кадр", ttl_s=2.0)
|
||||
log_debug_event("invalid_fft", "ui invalid FFT row suppressed")
|
||||
if not runtime.ring.last_push_axis_valid:
|
||||
set_status_note("fft: ось оставлена по последнему валидному кадру", ttl_s=2.0)
|
||||
log_debug_event("invalid_fft_axis", "ui invalid FFT axis suppressed")
|
||||
if runtime.ring.last_push_fft_valid and runtime.current_fft_mag is not None:
|
||||
runtime.background_buffer.push(runtime.current_fft_mag)
|
||||
|
||||
def set_calib_enabled() -> None:
|
||||
@ -1261,19 +1422,26 @@ def run_pyqtgraph(args) -> None:
|
||||
processed_frames = 0
|
||||
ui_frames_skipped = 0
|
||||
ui_started_at = time.perf_counter()
|
||||
update_ticks = 0
|
||||
|
||||
def refresh_current_fft_cache(sweep_for_fft: np.ndarray, bins: int) -> None:
|
||||
runtime.current_fft_complex = compute_fft_complex_row(
|
||||
fft_complex = compute_fft_complex_row(
|
||||
sweep_for_fft,
|
||||
runtime.current_freqs,
|
||||
bins,
|
||||
mode=fft_mode,
|
||||
)
|
||||
runtime.current_fft_mag = np.abs(runtime.current_fft_complex).astype(np.float32, copy=False)
|
||||
fft_mag = np.abs(fft_complex).astype(np.float32, copy=False)
|
||||
if not np.isfinite(fft_mag).any():
|
||||
set_status_note("fft: локальный пересчет пропущен", ttl_s=2.0)
|
||||
log_debug_event("invalid_fft_cache", "ui invalid FFT cache suppressed")
|
||||
return
|
||||
runtime.current_fft_complex = fft_complex
|
||||
runtime.current_fft_mag = fft_mag
|
||||
runtime.current_fft_db = fft_mag_to_db(runtime.current_fft_mag)
|
||||
|
||||
def drain_queue() -> int:
|
||||
nonlocal processed_frames, ui_frames_skipped
|
||||
nonlocal processed_frames, ui_frames_skipped, last_queue_backlog, last_backlog_skipped, last_heavy_refresh_stride
|
||||
pending_packets: List[SweepPacket] = []
|
||||
while True:
|
||||
try:
|
||||
@ -1281,12 +1449,31 @@ def run_pyqtgraph(args) -> None:
|
||||
except Empty:
|
||||
break
|
||||
drained = len(pending_packets)
|
||||
last_queue_backlog = drained
|
||||
if drained <= 0:
|
||||
last_backlog_skipped = 0
|
||||
last_heavy_refresh_stride = 1
|
||||
return 0
|
||||
|
||||
pending_packets, skipped_packets = coalesce_packets_for_ui(pending_packets)
|
||||
pending_packets, skipped_packets = coalesce_packets_for_ui(
|
||||
pending_packets,
|
||||
backlog_packets=drained,
|
||||
)
|
||||
last_backlog_skipped = skipped_packets
|
||||
last_heavy_refresh_stride = resolve_heavy_refresh_stride(drained)
|
||||
ui_frames_skipped += skipped_packets
|
||||
if skipped_packets > 0:
|
||||
log_debug_event(
|
||||
"ui_backlog",
|
||||
f"ui backlog:{drained} keep:{len(pending_packets)} skipped:{skipped_packets}",
|
||||
)
|
||||
for sweep, info, aux_curves in pending_packets:
|
||||
if runtime.ring.width > 0 and sweep.size < max(8, runtime.ring.width // 2):
|
||||
set_status_note(f"короткий свип: {int(sweep.size)} точек", ttl_s=2.0)
|
||||
log_debug_event(
|
||||
"short_sweep",
|
||||
f"ui short sweep width:{int(sweep.size)} expected:{int(runtime.ring.width)}",
|
||||
)
|
||||
base_freqs = np.linspace(SWEEP_FREQ_MIN_GHZ, SWEEP_FREQ_MAX_GHZ, sweep.size, dtype=np.float64)
|
||||
runtime.full_current_aux_curves = None
|
||||
runtime.full_current_fft_source = None
|
||||
@ -1364,15 +1551,22 @@ def run_pyqtgraph(args) -> None:
|
||||
pass
|
||||
|
||||
def update() -> None:
|
||||
nonlocal peak_ref_window, status_dirty
|
||||
nonlocal peak_ref_window, status_dirty, update_ticks
|
||||
norm_display_scale = 500.0
|
||||
if peak_calibrate_mode and any(edit.hasFocus() for edit in c_edits):
|
||||
return
|
||||
if peak_search_enabled and peak_window_edit is not None and peak_window_edit.hasFocus():
|
||||
return
|
||||
update_ticks += 1
|
||||
clear_expired_status_note()
|
||||
|
||||
changed = drain_queue() > 0
|
||||
redraw_needed = changed or runtime.plot_dirty
|
||||
refresh_heavy_views = (
|
||||
runtime.plot_dirty
|
||||
or last_heavy_refresh_stride <= 1
|
||||
or (update_ticks % last_heavy_refresh_stride) == 0
|
||||
)
|
||||
|
||||
if redraw_needed:
|
||||
xs = resolve_curve_xs(
|
||||
@ -1385,6 +1579,7 @@ def run_pyqtgraph(args) -> None:
|
||||
|
||||
if runtime.current_sweep_raw is not None:
|
||||
raw_x, raw_y = decimate_curve_for_display(xs, runtime.current_sweep_raw)
|
||||
raw_x, raw_y = sanitize_curve_data_for_display(raw_x, raw_y)
|
||||
curve.setData(raw_x, raw_y, autoDownsample=False)
|
||||
else:
|
||||
curve.setData([], [])
|
||||
@ -1394,6 +1589,8 @@ def run_pyqtgraph(args) -> None:
|
||||
aux_width = min(xs.size, aux_1.size, aux_2.size)
|
||||
aux_x_1, aux_y_1 = decimate_curve_for_display(xs[:aux_width], aux_1[:aux_width])
|
||||
aux_x_2, aux_y_2 = decimate_curve_for_display(xs[:aux_width], aux_2[:aux_width])
|
||||
aux_x_1, aux_y_1 = sanitize_curve_data_for_display(aux_x_1, aux_y_1)
|
||||
aux_x_2, aux_y_2 = sanitize_curve_data_for_display(aux_x_2, aux_y_2)
|
||||
curve_aux_1.setData(aux_x_1, aux_y_1, autoDownsample=False)
|
||||
curve_aux_2.setData(aux_x_2, aux_y_2, autoDownsample=False)
|
||||
else:
|
||||
@ -1408,6 +1605,7 @@ def run_pyqtgraph(args) -> None:
|
||||
displayed_calib = runtime.calib_envelope
|
||||
xs_calib = resolve_curve_xs(displayed_calib.size)
|
||||
calib_x, calib_y = decimate_curve_for_display(xs_calib, displayed_calib)
|
||||
calib_x, calib_y = sanitize_curve_data_for_display(calib_x, calib_y)
|
||||
curve_calib.setData(calib_x, calib_y, autoDownsample=False)
|
||||
else:
|
||||
curve_calib.setData([], [])
|
||||
@ -1415,6 +1613,7 @@ def run_pyqtgraph(args) -> None:
|
||||
if runtime.current_sweep_norm is not None:
|
||||
norm_display = runtime.current_sweep_norm * norm_display_scale
|
||||
norm_x, norm_y = decimate_curve_for_display(xs[: norm_display.size], norm_display)
|
||||
norm_x, norm_y = sanitize_curve_data_for_display(norm_x, norm_y)
|
||||
curve_norm.setData(norm_x, norm_y, autoDownsample=False)
|
||||
else:
|
||||
curve_norm.setData([], [])
|
||||
@ -1431,10 +1630,9 @@ def run_pyqtgraph(args) -> None:
|
||||
if y_limits is not None:
|
||||
p_line.setYRange(y_limits[0], y_limits[1], padding=0)
|
||||
|
||||
if isinstance(xs, np.ndarray) and xs.size > 0:
|
||||
finite_x = xs[np.isfinite(xs)]
|
||||
if finite_x.size > 0:
|
||||
p_line.setXRange(float(np.min(finite_x)), float(np.max(finite_x)), 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)
|
||||
|
||||
sweep_for_fft = runtime.current_fft_input
|
||||
if sweep_for_fft is None:
|
||||
@ -1470,9 +1668,9 @@ def run_pyqtgraph(args) -> None:
|
||||
set_status_note(f"фон: не удалось применить ({exc})")
|
||||
active_background = None
|
||||
display_fft_mag = fft_mag
|
||||
finite_x = xs_fft[np.isfinite(xs_fft)]
|
||||
if finite_x.size > 0:
|
||||
p_fft.setXRange(float(np.min(finite_x)), float(np.max(finite_x)), padding=0)
|
||||
fft_x_bounds = resolve_axis_bounds(xs_fft)
|
||||
if fft_x_bounds is not None:
|
||||
p_fft.setXRange(fft_x_bounds[0], fft_x_bounds[1], padding=0)
|
||||
|
||||
fft_vals_db = fft_mag_to_db(display_fft_mag)
|
||||
ref_curve_for_range = None
|
||||
@ -1487,15 +1685,18 @@ def run_pyqtgraph(args) -> None:
|
||||
show_imag=fft_imag_enabled,
|
||||
)
|
||||
if visible_abs is not None:
|
||||
curve_fft.setData(xs_fft[: visible_abs.size], visible_abs)
|
||||
abs_x, abs_y = sanitize_curve_data_for_display(xs_fft[: visible_abs.size], visible_abs)
|
||||
curve_fft.setData(abs_x, abs_y)
|
||||
else:
|
||||
curve_fft.setData([], [])
|
||||
if visible_real is not None:
|
||||
curve_fft_real.setData(xs_fft[: visible_real.size], visible_real)
|
||||
real_x, real_y = sanitize_curve_data_for_display(xs_fft[: visible_real.size], visible_real)
|
||||
curve_fft_real.setData(real_x, real_y)
|
||||
else:
|
||||
curve_fft_real.setData([], [])
|
||||
if visible_imag is not None:
|
||||
curve_fft_imag.setData(xs_fft[: visible_imag.size], visible_imag)
|
||||
imag_x, imag_y = sanitize_curve_data_for_display(xs_fft[: visible_imag.size], visible_imag)
|
||||
curve_fft_imag.setData(imag_x, imag_y)
|
||||
else:
|
||||
curve_fft_imag.setData([], [])
|
||||
|
||||
@ -1504,7 +1705,8 @@ 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])
|
||||
curve_fft_ref.setData(xs_fft[finite_ref], fft_ref_lin)
|
||||
ref_x, ref_y = sanitize_curve_data_for_display(xs_fft[finite_ref], fft_ref_lin)
|
||||
curve_fft_ref.setData(ref_x, ref_y)
|
||||
curve_fft_ref.setVisible(True)
|
||||
ref_curve_for_range = fft_ref_lin
|
||||
else:
|
||||
@ -1573,7 +1775,8 @@ def run_pyqtgraph(args) -> None:
|
||||
curve_fft_real.setData([], [])
|
||||
curve_fft_imag.setData([], [])
|
||||
if fft_abs_enabled:
|
||||
curve_fft.setData(xs_fft, fft_vals_db)
|
||||
fft_x, fft_y = sanitize_curve_data_for_display(xs_fft, fft_vals_db)
|
||||
curve_fft.setData(fft_x, fft_y)
|
||||
else:
|
||||
curve_fft.setData([], [])
|
||||
|
||||
@ -1583,7 +1786,8 @@ 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):
|
||||
curve_fft_ref.setData(xs_fft[finite_ref], fft_ref[finite_ref])
|
||||
ref_x, ref_y = sanitize_curve_data_for_display(xs_fft[finite_ref], fft_ref[finite_ref])
|
||||
curve_fft_ref.setData(ref_x, ref_y)
|
||||
curve_fft_ref.setVisible(True)
|
||||
y_for_range = np.concatenate((y_for_range, fft_ref[finite_ref]))
|
||||
else:
|
||||
@ -1668,12 +1872,22 @@ def run_pyqtgraph(args) -> None:
|
||||
runtime.plot_dirty = False
|
||||
|
||||
if changed and runtime.ring.ring is not None:
|
||||
disp = runtime.ring.get_display_raw_decimated(RAW_WATERFALL_MAX_POINTS)
|
||||
levels = _visible_levels_pyqtgraph(disp, p_img)
|
||||
if levels is not None:
|
||||
img.setImage(disp, autoLevels=False, levels=levels)
|
||||
if refresh_heavy_views:
|
||||
disp = sanitize_image_for_display(runtime.ring.get_display_raw_decimated(RAW_WATERFALL_MAX_POINTS))
|
||||
if disp is not None:
|
||||
levels = _visible_levels_pyqtgraph(disp, p_img)
|
||||
if levels is not None:
|
||||
img.setImage(disp, autoLevels=False, levels=levels)
|
||||
else:
|
||||
img.setImage(disp, autoLevels=False)
|
||||
else:
|
||||
set_status_note("raw waterfall: кадр пропущен", ttl_s=2.0)
|
||||
log_debug_event("invalid_raw_image", "ui invalid raw waterfall suppressed")
|
||||
else:
|
||||
img.setImage(disp, autoLevels=False)
|
||||
log_debug_event(
|
||||
"suppressed_image_refresh",
|
||||
f"ui waterfall refresh suppressed stride:{last_heavy_refresh_stride}",
|
||||
)
|
||||
|
||||
if redraw_needed or status_dirty:
|
||||
try:
|
||||
@ -1683,8 +1897,16 @@ def run_pyqtgraph(args) -> None:
|
||||
if peak_calibrate_mode and runtime.current_peak_amplitude is not None:
|
||||
status_payload["peak_a"] = runtime.current_peak_amplitude
|
||||
base_status = format_status_kv(status_payload) if status_payload else ""
|
||||
status_parts = []
|
||||
if status_note:
|
||||
status.setText(f"{base_status} | {status_note}" if base_status else status_note)
|
||||
status_parts.append(status_note)
|
||||
if last_backlog_skipped > 0:
|
||||
status_parts.append(f"ui backlog:{last_queue_backlog} skip:{last_backlog_skipped}")
|
||||
if last_heavy_refresh_stride > 1:
|
||||
status_parts.append(f"ui wf/{last_heavy_refresh_stride}")
|
||||
status_suffix = " | ".join(status_parts)
|
||||
if status_suffix:
|
||||
status.setText(f"{base_status} | {status_suffix}" if base_status else status_suffix)
|
||||
else:
|
||||
status.setText(base_status)
|
||||
except Exception:
|
||||
@ -1713,66 +1935,77 @@ def run_pyqtgraph(args) -> None:
|
||||
pass
|
||||
|
||||
if redraw_needed and runtime.ring.ring_fft is not None:
|
||||
disp_fft_lin = runtime.ring.get_display_fft_linear()
|
||||
if spec_mean_sec > 0.0:
|
||||
disp_times = runtime.ring.get_display_times()
|
||||
if disp_times is not None:
|
||||
now_t = time.time()
|
||||
mask = np.isfinite(disp_times) & (disp_times >= (now_t - spec_mean_sec))
|
||||
if np.any(mask):
|
||||
try:
|
||||
mean_spec = np.nanmean(disp_fft_lin[:, mask], axis=1)
|
||||
mean_spec = np.nan_to_num(mean_spec, nan=0.0)
|
||||
disp_fft_lin = disp_fft_lin - mean_spec[:, None]
|
||||
except Exception:
|
||||
pass
|
||||
if not refresh_heavy_views:
|
||||
log_debug_event(
|
||||
"suppressed_fft_image_refresh",
|
||||
f"ui FFT waterfall refresh suppressed stride:{last_heavy_refresh_stride}",
|
||||
)
|
||||
else:
|
||||
disp_fft_lin = runtime.ring.get_display_fft_linear()
|
||||
if spec_mean_sec > 0.0:
|
||||
disp_times = runtime.ring.get_display_times()
|
||||
if disp_times is not None:
|
||||
now_t = time.time()
|
||||
mask = np.isfinite(disp_times) & (disp_times >= (now_t - spec_mean_sec))
|
||||
if np.any(mask):
|
||||
try:
|
||||
mean_spec = np.nanmean(disp_fft_lin[:, mask], axis=1)
|
||||
mean_spec = np.nan_to_num(mean_spec, nan=0.0)
|
||||
disp_fft_lin = disp_fft_lin - mean_spec[:, None]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
active_background = None
|
||||
try:
|
||||
active_background = resolve_active_background(disp_fft_lin.shape[0])
|
||||
except Exception:
|
||||
active_background = None
|
||||
if active_background is not None:
|
||||
try:
|
||||
disp_fft_lin = subtract_fft_background(disp_fft_lin, active_background)
|
||||
except Exception as exc:
|
||||
set_status_note(f"фон: не удалось применить ({exc})")
|
||||
active_background = None
|
||||
disp_fft = fft_mag_to_db(disp_fft_lin)
|
||||
|
||||
levels = None
|
||||
if active_background is not None:
|
||||
levels = compute_background_subtracted_bscan_levels(disp_fft_lin, disp_fft)
|
||||
if levels is None:
|
||||
try:
|
||||
mean_spec = np.nanmean(disp_fft, axis=1)
|
||||
vmin_v = float(np.nanmin(mean_spec))
|
||||
vmax_v = float(np.nanmax(mean_spec))
|
||||
if np.isfinite(vmin_v) and np.isfinite(vmax_v) and vmin_v != vmax_v:
|
||||
levels = (vmin_v, vmax_v)
|
||||
active_background = resolve_active_background(disp_fft_lin.shape[0])
|
||||
except Exception:
|
||||
levels = None
|
||||
if levels is None and spec_clip is not None:
|
||||
active_background = None
|
||||
if active_background is not None:
|
||||
try:
|
||||
vmin_v = float(np.nanpercentile(disp_fft, spec_clip[0]))
|
||||
vmax_v = float(np.nanpercentile(disp_fft, spec_clip[1]))
|
||||
disp_fft_lin = subtract_fft_background(disp_fft_lin, active_background)
|
||||
except Exception as exc:
|
||||
set_status_note(f"фон: не удалось применить ({exc})")
|
||||
active_background = None
|
||||
disp_fft = fft_mag_to_db(disp_fft_lin)
|
||||
disp_fft = sanitize_image_for_display(disp_fft)
|
||||
if disp_fft is None:
|
||||
set_status_note("b-scan: кадр пропущен", ttl_s=2.0)
|
||||
log_debug_event("invalid_fft_image", "ui invalid FFT waterfall suppressed")
|
||||
return
|
||||
|
||||
levels = None
|
||||
if active_background is not None:
|
||||
levels = compute_background_subtracted_bscan_levels(disp_fft_lin, disp_fft)
|
||||
if levels is None:
|
||||
try:
|
||||
mean_spec = np.nanmean(disp_fft, axis=1)
|
||||
vmin_v = float(np.nanmin(mean_spec))
|
||||
vmax_v = float(np.nanmax(mean_spec))
|
||||
if np.isfinite(vmin_v) and np.isfinite(vmax_v) and vmin_v != vmax_v:
|
||||
levels = (vmin_v, vmax_v)
|
||||
except Exception:
|
||||
levels = None
|
||||
if (
|
||||
levels is None
|
||||
and runtime.ring.y_min_fft is not None
|
||||
and runtime.ring.y_max_fft is not None
|
||||
and np.isfinite(runtime.ring.y_min_fft)
|
||||
and np.isfinite(runtime.ring.y_max_fft)
|
||||
and runtime.ring.y_min_fft != runtime.ring.y_max_fft
|
||||
):
|
||||
levels = (runtime.ring.y_min_fft, runtime.ring.y_max_fft)
|
||||
if levels is not None:
|
||||
img_fft.setImage(disp_fft, autoLevels=False, levels=levels)
|
||||
else:
|
||||
img_fft.setImage(disp_fft, autoLevels=False)
|
||||
if levels is None and spec_clip is not None:
|
||||
try:
|
||||
vmin_v = float(np.nanpercentile(disp_fft, spec_clip[0]))
|
||||
vmax_v = float(np.nanpercentile(disp_fft, spec_clip[1]))
|
||||
if np.isfinite(vmin_v) and np.isfinite(vmax_v) and vmin_v != vmax_v:
|
||||
levels = (vmin_v, vmax_v)
|
||||
except Exception:
|
||||
levels = None
|
||||
if (
|
||||
levels is None
|
||||
and runtime.ring.y_min_fft is not None
|
||||
and runtime.ring.y_max_fft is not None
|
||||
and np.isfinite(runtime.ring.y_min_fft)
|
||||
and np.isfinite(runtime.ring.y_max_fft)
|
||||
and runtime.ring.y_min_fft != runtime.ring.y_max_fft
|
||||
):
|
||||
levels = (runtime.ring.y_min_fft, runtime.ring.y_max_fft)
|
||||
if levels is not None:
|
||||
img_fft.setImage(disp_fft, autoLevels=False, levels=levels)
|
||||
else:
|
||||
img_fft.setImage(disp_fft, autoLevels=False)
|
||||
|
||||
timer = pg.QtCore.QTimer()
|
||||
timer.timeout.connect(update)
|
||||
@ -1842,7 +2075,16 @@ def run_pyqtgraph(args) -> None:
|
||||
|
||||
app.aboutToQuit.connect(on_quit)
|
||||
try:
|
||||
main_window.resize(1200, 680)
|
||||
available = None
|
||||
screen = app.primaryScreen()
|
||||
if screen is not None:
|
||||
available = screen.availableGeometry()
|
||||
if available is not None:
|
||||
width, height = resolve_initial_window_size(available.width(), available.height())
|
||||
else:
|
||||
width, height = DEFAULT_MAIN_WINDOW_WIDTH, DEFAULT_MAIN_WINDOW_HEIGHT
|
||||
main_window.resize(width, height)
|
||||
splitter.setSizes([max(int(width * 0.68), width - 320), min(320, max(240, int(width * 0.32)))])
|
||||
except Exception:
|
||||
pass
|
||||
main_window.show()
|
||||
|
||||
@ -31,6 +31,9 @@ class RingBuffer:
|
||||
self.last_freqs: Optional[np.ndarray] = None
|
||||
self.y_min_fft: Optional[float] = None
|
||||
self.y_max_fft: Optional[float] = None
|
||||
self.last_push_valid_points = 0
|
||||
self.last_push_fft_valid = False
|
||||
self.last_push_axis_valid = False
|
||||
|
||||
@property
|
||||
def is_ready(self) -> bool:
|
||||
@ -55,6 +58,38 @@ class RingBuffer:
|
||||
self.last_freqs = None
|
||||
self.y_min_fft = None
|
||||
self.y_max_fft = None
|
||||
self.last_push_valid_points = 0
|
||||
self.last_push_fft_valid = False
|
||||
self.last_push_axis_valid = False
|
||||
|
||||
def _promote_fft_cache(self, fft_mag: np.ndarray) -> bool:
|
||||
fft_mag_arr = np.asarray(fft_mag, dtype=np.float32).reshape(-1)
|
||||
if fft_mag_arr.size <= 0:
|
||||
self.last_push_fft_valid = False
|
||||
return False
|
||||
fft_db = fft_mag_to_db(fft_mag_arr)
|
||||
finite_db = fft_db[np.isfinite(fft_db)]
|
||||
if finite_db.size <= 0:
|
||||
self.last_push_fft_valid = False
|
||||
return False
|
||||
|
||||
self.last_fft_mag = fft_mag_arr.copy()
|
||||
self.last_fft_db = fft_db
|
||||
fr_min = float(np.min(finite_db))
|
||||
fr_max = float(np.max(finite_db))
|
||||
self.y_min_fft = fr_min if self.y_min_fft is None else min(self.y_min_fft, fr_min)
|
||||
self.y_max_fft = fr_max if self.y_max_fft is None else max(self.y_max_fft, fr_max)
|
||||
self.last_push_fft_valid = True
|
||||
return True
|
||||
|
||||
def _promote_distance_axis(self, axis: np.ndarray) -> bool:
|
||||
axis_arr = np.asarray(axis, dtype=np.float64).reshape(-1)
|
||||
if axis_arr.size <= 0 or not np.all(np.isfinite(axis_arr)):
|
||||
self.last_push_axis_valid = False
|
||||
return False
|
||||
self.distance_axis = axis_arr.copy()
|
||||
self.last_push_axis_valid = True
|
||||
return True
|
||||
|
||||
def ensure_init(self, sweep_width: int) -> bool:
|
||||
"""Allocate or resize buffers. Returns True when geometry changed."""
|
||||
@ -107,6 +142,8 @@ class RingBuffer:
|
||||
self.fft_mode = normalized_mode
|
||||
self.y_min_fft = None
|
||||
self.y_max_fft = None
|
||||
self.last_push_fft_valid = False
|
||||
self.last_push_axis_valid = False
|
||||
|
||||
if self.ring is None or self.ring_fft is None:
|
||||
return True
|
||||
@ -131,17 +168,18 @@ class RingBuffer:
|
||||
self.ring_fft[row_idx, :] = fft_mag
|
||||
|
||||
if self.last_freqs is not None:
|
||||
self.distance_axis = compute_distance_axis(
|
||||
self.last_freqs,
|
||||
self.fft_bins,
|
||||
mode=self.fft_mode,
|
||||
self._promote_distance_axis(
|
||||
compute_distance_axis(
|
||||
self.last_freqs,
|
||||
self.fft_bins,
|
||||
mode=self.fft_mode,
|
||||
)
|
||||
)
|
||||
|
||||
last_idx = (self.head - 1) % self.max_sweeps
|
||||
if self.ring_fft.shape[0] > 0:
|
||||
last_fft = self.ring_fft[last_idx]
|
||||
self.last_fft_mag = np.asarray(last_fft, dtype=np.float32).copy()
|
||||
self.last_fft_db = fft_mag_to_db(last_fft)
|
||||
self._promote_fft_cache(last_fft)
|
||||
finite = self.ring_fft[np.isfinite(self.ring_fft)]
|
||||
if finite.size > 0:
|
||||
finite_db = fft_mag_to_db(finite.astype(np.float32, copy=False))
|
||||
@ -170,6 +208,7 @@ class RingBuffer:
|
||||
row = np.full((self.width,), np.nan, dtype=np.float32)
|
||||
take = min(self.width, int(sweep.size))
|
||||
row[:take] = np.asarray(sweep[:take], dtype=np.float32)
|
||||
self.last_push_valid_points = int(np.count_nonzero(np.isfinite(row[:take])))
|
||||
self.ring[self.head, :] = row
|
||||
self.ring_time[self.head] = time.time()
|
||||
if freqs is not None:
|
||||
@ -183,16 +222,8 @@ class RingBuffer:
|
||||
|
||||
fft_mag = compute_fft_mag_row(fft_source, freqs, self.fft_bins, mode=self.fft_mode)
|
||||
self.ring_fft[self.head, :] = fft_mag
|
||||
self.last_fft_mag = np.asarray(fft_mag, dtype=np.float32).copy()
|
||||
self.last_fft_db = fft_mag_to_db(fft_mag)
|
||||
|
||||
if self.last_fft_db.size > 0:
|
||||
fr_min = float(np.nanmin(self.last_fft_db))
|
||||
fr_max = float(np.nanmax(self.last_fft_db))
|
||||
self.y_min_fft = fr_min if self.y_min_fft is None else min(self.y_min_fft, fr_min)
|
||||
self.y_max_fft = fr_max if self.y_max_fft is None else max(self.y_max_fft, fr_max)
|
||||
|
||||
self.distance_axis = compute_distance_axis(freqs, self.fft_bins, mode=self.fft_mode)
|
||||
self._promote_fft_cache(fft_mag)
|
||||
self._promote_distance_axis(compute_distance_axis(freqs, self.fft_bins, mode=self.fft_mode))
|
||||
self.head = (self.head + 1) % self.max_sweeps
|
||||
|
||||
def get_display_raw(self) -> np.ndarray:
|
||||
|
||||
Reference in New Issue
Block a user