cut the range feature

This commit is contained in:
awe
2026-03-12 18:50:26 +03:00
parent 5054f8d3d7
commit b70df8c1bd
6 changed files with 307 additions and 26 deletions

View File

@ -65,6 +65,48 @@ def _visible_levels_pyqtgraph(data: np.ndarray, plot_item) -> Optional[Tuple[flo
return (vmin, vmax)
def apply_working_range(
freqs: Optional[np.ndarray],
sweep: Optional[np.ndarray],
range_min_ghz: float,
range_max_ghz: float,
) -> Tuple[np.ndarray, np.ndarray]:
"""Crop sweep data to the active working frequency range."""
if freqs is None or sweep is None:
return (
np.zeros((0,), dtype=np.float64),
np.zeros((0,), dtype=np.float32),
)
freq_arr = np.asarray(freqs, dtype=np.float64).reshape(-1)
sweep_arr = np.asarray(sweep, dtype=np.float32).reshape(-1)
width = min(freq_arr.size, sweep_arr.size)
if width <= 0:
return (
np.zeros((0,), dtype=np.float64),
np.zeros((0,), dtype=np.float32),
)
freq_arr = freq_arr[:width]
sweep_arr = sweep_arr[:width]
valid = (
np.isfinite(freq_arr)
& np.isfinite(sweep_arr)
& (freq_arr >= float(range_min_ghz))
& (freq_arr <= float(range_max_ghz))
)
if not np.any(valid):
return (
np.zeros((0,), dtype=np.float64),
np.zeros((0,), dtype=np.float32),
)
return (
freq_arr[valid].astype(np.float64, copy=False),
sweep_arr[valid].astype(np.float32, copy=False),
)
def run_pyqtgraph(args) -> None:
"""Start the PyQtGraph GUI."""
peak_calibrate_mode = bool(getattr(args, "calibrate", False))
@ -98,7 +140,11 @@ def run_pyqtgraph(args) -> None:
fft_bins = FFT_LEN // 2 + 1
spec_clip = parse_spec_clip(getattr(args, "spec_clip", None))
spec_mean_sec = float(getattr(args, "spec_mean_sec", 0.0))
runtime = RuntimeState(ring=RingBuffer(max_sweeps))
runtime = RuntimeState(
ring=RingBuffer(max_sweeps),
range_min_ghz=float(SWEEP_FREQ_MIN_GHZ),
range_max_ghz=float(SWEEP_FREQ_MAX_GHZ),
)
pg.setConfigOptions(
useOpenGL=not peak_calibrate_mode,
@ -205,6 +251,24 @@ def run_pyqtgraph(args) -> None:
bg_compute_cb = QtWidgets.QCheckBox("расчет фона")
bg_subtract_cb = QtWidgets.QCheckBox("вычет фона")
fft_bg_subtract_cb = QtWidgets.QCheckBox("FFT вычет фона")
range_group = QtWidgets.QGroupBox("Рабочий диапазон")
range_group_layout = QtWidgets.QFormLayout(range_group)
range_group_layout.setContentsMargins(6, 6, 6, 6)
range_group_layout.setSpacing(6)
range_min_spin = QtWidgets.QDoubleSpinBox()
range_max_spin = QtWidgets.QDoubleSpinBox()
for spin in (range_min_spin, range_max_spin):
spin.setDecimals(6)
spin.setRange(0.0, 100.0)
spin.setSingleStep(0.1)
try:
spin.setSuffix(" GHz")
except Exception:
pass
range_min_spin.setValue(runtime.range_min_ghz)
range_max_spin.setValue(runtime.range_max_ghz)
range_group_layout.addRow("f min", range_min_spin)
range_group_layout.addRow("f max", range_max_spin)
fft_mode_label = QtWidgets.QLabel("IFFT режим")
fft_mode_combo = QtWidgets.QComboBox()
fft_mode_combo.addItem("Обычный", "direct")
@ -240,6 +304,7 @@ def run_pyqtgraph(args) -> None:
settings_layout.addWidget(QtWidgets.QLabel("Настройки"))
except Exception:
pass
settings_layout.addWidget(range_group)
settings_layout.addWidget(calib_group)
settings_layout.addWidget(bg_compute_cb)
settings_layout.addWidget(bg_subtract_cb)
@ -258,6 +323,7 @@ def run_pyqtgraph(args) -> None:
fft_mode = "symmetric"
status_note = ""
status_dirty = True
range_change_in_progress = False
fixed_ylim: Optional[Tuple[float, float]] = None
if args.ylim:
try:
@ -272,14 +338,21 @@ def run_pyqtgraph(args) -> None:
changed = runtime.ring.ensure_init(sweep_width)
if not changed:
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(), autoLevels=False)
img.setRect(0, SWEEP_FREQ_MIN_GHZ, max_sweeps, SWEEP_FREQ_MAX_GHZ - SWEEP_FREQ_MIN_GHZ)
img.setRect(0, f_min, max_sweeps, max(1e-9, f_max - f_min))
p_img.setRange(
xRange=(0, max_sweeps - 1),
yRange=(SWEEP_FREQ_MIN_GHZ, SWEEP_FREQ_MAX_GHZ),
yRange=(f_min, f_max),
padding=0,
)
p_line.setXRange(SWEEP_FREQ_MIN_GHZ, SWEEP_FREQ_MAX_GHZ, padding=0)
p_line.setXRange(f_min, f_max, padding=0)
img_fft.setImage(runtime.ring.get_display_fft_linear(), 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)
@ -295,6 +368,15 @@ def run_pyqtgraph(args) -> None:
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
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:
@ -317,6 +399,13 @@ def run_pyqtgraph(args) -> None:
size,
dtype=np.float64,
)
if runtime.range_max_ghz > runtime.range_min_ghz:
return np.linspace(
float(runtime.range_min_ghz),
float(runtime.range_max_ghz),
size,
dtype=np.float64,
)
if runtime.ring.x_shared is not None and size <= runtime.ring.x_shared.size:
return runtime.ring.x_shared[:size]
return np.arange(size, dtype=np.float32)
@ -333,14 +422,69 @@ def run_pyqtgraph(args) -> None:
path = ""
return path or "calibration_envelope.npy"
def reset_ring_buffers() -> None:
runtime.ring.reset()
runtime.current_distances = None
runtime.current_fft_db = None
runtime.bg_spec_cache = None
runtime.current_peak_width = None
runtime.current_peak_amplitude = None
runtime.peak_candidates = []
img.setImage(runtime.ring.get_display_raw(), autoLevels=False)
img_fft.setImage(runtime.ring.get_display_fft_linear(), autoLevels=False)
update_physical_axes()
def refresh_current_window(push_to_ring: bool = False, *, reset_ring: bool = False) -> None:
if reset_ring:
reset_ring_buffers()
if runtime.full_current_freqs is None or runtime.full_current_sweep_raw is None:
runtime.current_freqs = None
runtime.current_sweep_raw = None
runtime.current_sweep_norm = None
runtime.current_fft_db = None
runtime.current_distances = runtime.ring.distance_axis
return
current_freqs, current_sweep = apply_working_range(
runtime.full_current_freqs,
runtime.full_current_sweep_raw,
runtime.range_min_ghz,
runtime.range_max_ghz,
)
runtime.current_freqs = current_freqs
runtime.current_sweep_raw = current_sweep
if runtime.current_sweep_raw.size == 0:
if push_to_ring:
reset_ring_buffers()
runtime.current_freqs = None
runtime.current_sweep_raw = None
runtime.current_sweep_norm = None
runtime.current_fft_db = None
runtime.current_distances = None
set_status_note("диапазон: нет точек в выбранном окне")
return
recompute_current_processed_sweep(push_to_ring=push_to_ring)
def recompute_current_processed_sweep(push_to_ring: bool = False) -> None:
if runtime.current_sweep_raw is not None and calib_enabled and runtime.calib_envelope is not None:
if (
runtime.current_sweep_raw is not None
and runtime.current_sweep_raw.size > 0
and calib_enabled
and runtime.calib_envelope is not None
):
runtime.current_sweep_norm = normalize_by_envelope(runtime.current_sweep_raw, runtime.calib_envelope)
else:
runtime.current_sweep_norm = None
runtime.current_fft_db = None
if not push_to_ring or runtime.current_sweep_raw is None:
if (
not push_to_ring
or runtime.current_sweep_raw is None
or runtime.current_sweep_raw.size == 0
):
return
sweep_for_processing = runtime.current_sweep_norm if runtime.current_sweep_norm is not None else runtime.current_sweep_raw
@ -360,6 +504,36 @@ def run_pyqtgraph(args) -> None:
recompute_current_processed_sweep(push_to_ring=False)
runtime.mark_dirty()
def restore_range_controls() -> None:
nonlocal range_change_in_progress
range_change_in_progress = True
try:
range_min_spin.setValue(float(runtime.range_min_ghz))
range_max_spin.setValue(float(runtime.range_max_ghz))
finally:
range_change_in_progress = False
def set_working_range() -> None:
nonlocal range_change_in_progress
if range_change_in_progress:
return
try:
new_min = float(range_min_spin.value())
new_max = float(range_max_spin.value())
except Exception:
restore_range_controls()
return
if (not np.isfinite(new_min)) or (not np.isfinite(new_max)) or new_min >= new_max:
set_status_note("диапазон: f_min должен быть меньше f_max")
restore_range_controls()
runtime.mark_dirty()
return
runtime.range_min_ghz = new_min
runtime.range_max_ghz = new_max
refresh_current_window(push_to_ring=True, reset_ring=True)
set_status_note(f"диапазон: {new_min:.6g}..{new_max:.6g} GHz")
runtime.mark_dirty()
def pick_calib_file() -> None:
start_path = get_calib_file_path()
try:
@ -472,10 +646,13 @@ def run_pyqtgraph(args) -> None:
fft_mode_combo.setCurrentIndex(1)
except Exception:
pass
restore_range_controls()
set_bg_compute_enabled()
set_fft_mode()
try:
range_min_spin.valueChanged.connect(lambda _v: set_working_range())
range_max_spin.valueChanged.connect(lambda _v: set_working_range())
calib_cb.stateChanged.connect(lambda _v: set_calib_enabled())
calib_pick_btn.clicked.connect(lambda _checked=False: pick_calib_file())
calib_save_btn.clicked.connect(lambda _checked=False: save_current_calibration())
@ -691,11 +868,11 @@ def run_pyqtgraph(args) -> None:
"I": sweep,
}
)
runtime.current_freqs = calibrated["F"]
runtime.current_sweep_raw = calibrated["I"]
runtime.full_current_freqs = np.asarray(calibrated["F"], dtype=np.float64)
runtime.full_current_sweep_raw = np.asarray(calibrated["I"], dtype=np.float32)
runtime.current_aux_curves = aux_curves
runtime.current_info = info
recompute_current_processed_sweep(push_to_ring=True)
refresh_current_window(push_to_ring=True)
if drained > 0:
update_physical_axes()
return drained
@ -727,9 +904,11 @@ def run_pyqtgraph(args) -> None:
except Exception:
bg_fft_for_line = None
if redraw_needed and (runtime.current_sweep_raw is not None or runtime.calib_envelope is not None):
if redraw_needed:
xs = resolve_curve_xs(
runtime.current_sweep_raw.size if runtime.current_sweep_raw is not None else runtime.calib_envelope.size
runtime.current_sweep_raw.size
if runtime.current_sweep_raw is not None
else (runtime.calib_envelope.size if runtime.calib_envelope is not None else 0)
)
displayed_calib = None
@ -871,8 +1050,16 @@ def run_pyqtgraph(args) -> None:
runtime.current_peak_amplitude = None
else:
curve_fft_ref.setVisible(False)
curve_fft.setData([], [])
for box in fft_peak_boxes:
box.setVisible(False)
fft_bg_line.setVisible(False)
fft_left_line.setVisible(False)
fft_right_line.setVisible(False)
spec_left_line.setVisible(False)
spec_right_line.setVisible(False)
runtime.current_peak_width = None
runtime.current_peak_amplitude = None
runtime.peak_candidates = []
refresh_peak_params_label([])
runtime.plot_dirty = False