calib fix
This commit is contained in:
@ -13,14 +13,17 @@ import numpy as np
|
|||||||
from rfg_adc_plotter.constants import FFT_LEN, SWEEP_FREQ_MAX_GHZ, SWEEP_FREQ_MIN_GHZ
|
from rfg_adc_plotter.constants import FFT_LEN, SWEEP_FREQ_MAX_GHZ, SWEEP_FREQ_MIN_GHZ
|
||||||
from rfg_adc_plotter.io.sweep_reader import SweepReader
|
from rfg_adc_plotter.io.sweep_reader import SweepReader
|
||||||
from rfg_adc_plotter.processing.calibration import (
|
from rfg_adc_plotter.processing.calibration import (
|
||||||
|
build_calib_envelope,
|
||||||
calibrate_freqs,
|
calibrate_freqs,
|
||||||
get_calibration_base,
|
get_calibration_base,
|
||||||
get_calibration_coeffs,
|
get_calibration_coeffs,
|
||||||
|
load_calib_envelope,
|
||||||
|
save_calib_envelope,
|
||||||
set_calibration_base_value,
|
set_calibration_base_value,
|
||||||
)
|
)
|
||||||
from rfg_adc_plotter.processing.fft import compute_fft_row, fft_mag_to_db
|
from rfg_adc_plotter.processing.fft import compute_fft_row, fft_mag_to_db
|
||||||
from rfg_adc_plotter.processing.formatting import compute_auto_ylim, format_status_kv, parse_spec_clip
|
from rfg_adc_plotter.processing.formatting import compute_auto_ylim, format_status_kv, parse_spec_clip
|
||||||
from rfg_adc_plotter.processing.normalization import normalize_by_calib
|
from rfg_adc_plotter.processing.normalization import normalize_by_envelope, resample_envelope
|
||||||
from rfg_adc_plotter.processing.peaks import (
|
from rfg_adc_plotter.processing.peaks import (
|
||||||
find_peak_width_markers,
|
find_peak_width_markers,
|
||||||
find_top_peaks_over_ref,
|
find_top_peaks_over_ref,
|
||||||
@ -95,7 +98,6 @@ def run_pyqtgraph(args) -> None:
|
|||||||
fft_bins = FFT_LEN // 2 + 1
|
fft_bins = FFT_LEN // 2 + 1
|
||||||
spec_clip = parse_spec_clip(getattr(args, "spec_clip", None))
|
spec_clip = parse_spec_clip(getattr(args, "spec_clip", None))
|
||||||
spec_mean_sec = float(getattr(args, "spec_mean_sec", 0.0))
|
spec_mean_sec = float(getattr(args, "spec_mean_sec", 0.0))
|
||||||
norm_type = str(getattr(args, "norm_type", "projector")).strip().lower()
|
|
||||||
runtime = RuntimeState(ring=RingBuffer(max_sweeps))
|
runtime = RuntimeState(ring=RingBuffer(max_sweeps))
|
||||||
|
|
||||||
pg.setConfigOptions(
|
pg.setConfigOptions(
|
||||||
@ -130,8 +132,8 @@ def run_pyqtgraph(args) -> None:
|
|||||||
settings_layout.setContentsMargins(6, 6, 6, 6)
|
settings_layout.setContentsMargins(6, 6, 6, 6)
|
||||||
settings_layout.setSpacing(8)
|
settings_layout.setSpacing(8)
|
||||||
try:
|
try:
|
||||||
settings_widget.setMinimumWidth(130)
|
settings_widget.setMinimumWidth(250)
|
||||||
settings_widget.setMaximumWidth(150)
|
settings_widget.setMaximumWidth(340)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
main_layout.addWidget(settings_widget)
|
main_layout.addWidget(settings_widget)
|
||||||
@ -201,16 +203,41 @@ def run_pyqtgraph(args) -> None:
|
|||||||
spec_left_line.setVisible(False)
|
spec_left_line.setVisible(False)
|
||||||
spec_right_line.setVisible(False)
|
spec_right_line.setVisible(False)
|
||||||
|
|
||||||
calib_cb = QtWidgets.QCheckBox("нормировка")
|
calib_cb = QtWidgets.QCheckBox("калибровка по огибающей")
|
||||||
bg_compute_cb = QtWidgets.QCheckBox("расчет фона")
|
bg_compute_cb = QtWidgets.QCheckBox("расчет фона")
|
||||||
bg_subtract_cb = QtWidgets.QCheckBox("вычет фона")
|
bg_subtract_cb = QtWidgets.QCheckBox("вычет фона")
|
||||||
fft_bg_subtract_cb = QtWidgets.QCheckBox("FFT вычет фона")
|
fft_bg_subtract_cb = QtWidgets.QCheckBox("FFT вычет фона")
|
||||||
peak_search_cb = QtWidgets.QCheckBox("поиск пиков")
|
peak_search_cb = QtWidgets.QCheckBox("поиск пиков")
|
||||||
|
calib_group = QtWidgets.QGroupBox("Калибровка")
|
||||||
|
calib_group_layout = QtWidgets.QVBoxLayout(calib_group)
|
||||||
|
calib_group_layout.setContentsMargins(6, 6, 6, 6)
|
||||||
|
calib_group_layout.setSpacing(6)
|
||||||
|
calib_group_layout.addWidget(calib_cb)
|
||||||
|
calib_path_edit = QtWidgets.QLineEdit("calibration_envelope.npy")
|
||||||
|
try:
|
||||||
|
calib_path_edit.setPlaceholderText("calibration_envelope.npy")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
calib_path_row = QtWidgets.QHBoxLayout()
|
||||||
|
calib_path_row.setContentsMargins(0, 0, 0, 0)
|
||||||
|
calib_path_row.setSpacing(4)
|
||||||
|
calib_path_row.addWidget(calib_path_edit)
|
||||||
|
calib_pick_btn = QtWidgets.QPushButton("Файл...")
|
||||||
|
calib_path_row.addWidget(calib_pick_btn)
|
||||||
|
calib_group_layout.addLayout(calib_path_row)
|
||||||
|
calib_buttons_row = QtWidgets.QHBoxLayout()
|
||||||
|
calib_buttons_row.setContentsMargins(0, 0, 0, 0)
|
||||||
|
calib_buttons_row.setSpacing(4)
|
||||||
|
calib_save_btn = QtWidgets.QPushButton("Сохранить текущую")
|
||||||
|
calib_load_btn = QtWidgets.QPushButton("Загрузить")
|
||||||
|
calib_buttons_row.addWidget(calib_save_btn)
|
||||||
|
calib_buttons_row.addWidget(calib_load_btn)
|
||||||
|
calib_group_layout.addLayout(calib_buttons_row)
|
||||||
try:
|
try:
|
||||||
settings_layout.addWidget(QtWidgets.QLabel("Настройки"))
|
settings_layout.addWidget(QtWidgets.QLabel("Настройки"))
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
settings_layout.addWidget(calib_cb)
|
settings_layout.addWidget(calib_group)
|
||||||
settings_layout.addWidget(bg_compute_cb)
|
settings_layout.addWidget(bg_compute_cb)
|
||||||
settings_layout.addWidget(bg_subtract_cb)
|
settings_layout.addWidget(bg_subtract_cb)
|
||||||
settings_layout.addWidget(fft_bg_subtract_cb)
|
settings_layout.addWidget(fft_bg_subtract_cb)
|
||||||
@ -223,6 +250,8 @@ def run_pyqtgraph(args) -> None:
|
|||||||
bg_compute_enabled = True
|
bg_compute_enabled = True
|
||||||
bg_subtract_enabled = False
|
bg_subtract_enabled = False
|
||||||
fft_bg_subtract_enabled = False
|
fft_bg_subtract_enabled = False
|
||||||
|
status_note = ""
|
||||||
|
status_dirty = True
|
||||||
fixed_ylim: Optional[Tuple[float, float]] = None
|
fixed_ylim: Optional[Tuple[float, float]] = None
|
||||||
if args.ylim:
|
if args.ylim:
|
||||||
try:
|
try:
|
||||||
@ -270,8 +299,49 @@ def run_pyqtgraph(args) -> None:
|
|||||||
img_fft.setRect(0, d_min, max_sweeps, d_max - d_min)
|
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)
|
p_spec.setRange(xRange=(0, max_sweeps - 1), yRange=(d_min, d_max), padding=0)
|
||||||
|
|
||||||
def normalize_sweep(raw: np.ndarray, calib: np.ndarray) -> np.ndarray:
|
def resolve_curve_xs(size: int) -> np.ndarray:
|
||||||
return normalize_by_calib(raw, calib, norm_type=norm_type)
|
if size <= 0:
|
||||||
|
return np.zeros((0,), dtype=np.float32)
|
||||||
|
if runtime.current_freqs is not None and runtime.current_freqs.size == size:
|
||||||
|
return runtime.current_freqs
|
||||||
|
if runtime.current_freqs is not None and runtime.current_freqs.size > 1:
|
||||||
|
return np.linspace(
|
||||||
|
float(runtime.current_freqs[0]),
|
||||||
|
float(runtime.current_freqs[-1]),
|
||||||
|
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)
|
||||||
|
|
||||||
|
def set_status_note(note: str) -> None:
|
||||||
|
nonlocal status_note, status_dirty
|
||||||
|
status_note = str(note).strip()
|
||||||
|
status_dirty = True
|
||||||
|
|
||||||
|
def get_calib_file_path() -> str:
|
||||||
|
try:
|
||||||
|
path = calib_path_edit.text().strip()
|
||||||
|
except Exception:
|
||||||
|
path = ""
|
||||||
|
return path or "calibration_envelope.npy"
|
||||||
|
|
||||||
|
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:
|
||||||
|
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:
|
||||||
|
return
|
||||||
|
|
||||||
|
sweep_for_processing = runtime.current_sweep_norm if runtime.current_sweep_norm is not None else runtime.current_sweep_raw
|
||||||
|
ensure_buffer(runtime.current_sweep_raw.size)
|
||||||
|
runtime.ring.push(sweep_for_processing, runtime.current_freqs)
|
||||||
|
runtime.current_distances = runtime.ring.distance_axis
|
||||||
|
runtime.current_fft_db = runtime.ring.last_fft_db
|
||||||
|
|
||||||
def set_calib_enabled() -> None:
|
def set_calib_enabled() -> None:
|
||||||
nonlocal calib_enabled
|
nonlocal calib_enabled
|
||||||
@ -279,10 +349,74 @@ def run_pyqtgraph(args) -> None:
|
|||||||
calib_enabled = bool(calib_cb.isChecked())
|
calib_enabled = bool(calib_cb.isChecked())
|
||||||
except Exception:
|
except Exception:
|
||||||
calib_enabled = False
|
calib_enabled = False
|
||||||
if calib_enabled and runtime.current_sweep_raw is not None and runtime.last_calib_sweep is not None:
|
if calib_enabled and runtime.calib_envelope is None:
|
||||||
runtime.current_sweep_norm = normalize_sweep(runtime.current_sweep_raw, runtime.last_calib_sweep)
|
set_status_note("калибровка: огибающая не загружена")
|
||||||
else:
|
recompute_current_processed_sweep(push_to_ring=False)
|
||||||
runtime.current_sweep_norm = None
|
runtime.mark_dirty()
|
||||||
|
|
||||||
|
def pick_calib_file() -> None:
|
||||||
|
start_path = get_calib_file_path()
|
||||||
|
try:
|
||||||
|
selected, _ = QtWidgets.QFileDialog.getSaveFileName(
|
||||||
|
main_window,
|
||||||
|
"Файл калибровки",
|
||||||
|
start_path,
|
||||||
|
"NumPy (*.npy);;All files (*)",
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
set_status_note(f"калибровка: выбор файла недоступен ({exc})")
|
||||||
|
runtime.mark_dirty()
|
||||||
|
return
|
||||||
|
if not selected:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
calib_path_edit.setText(selected)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def save_current_calibration() -> None:
|
||||||
|
if runtime.current_sweep_raw is None or runtime.current_sweep_raw.size == 0:
|
||||||
|
set_status_note("калибровка: нет текущего raw-свипа")
|
||||||
|
runtime.mark_dirty()
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
envelope = build_calib_envelope(runtime.current_sweep_raw)
|
||||||
|
saved_path = save_calib_envelope(get_calib_file_path(), envelope)
|
||||||
|
except Exception as exc:
|
||||||
|
set_status_note(f"калибровка: не удалось сохранить ({exc})")
|
||||||
|
runtime.mark_dirty()
|
||||||
|
return
|
||||||
|
|
||||||
|
runtime.calib_envelope = envelope
|
||||||
|
runtime.calib_file_path = saved_path
|
||||||
|
runtime.last_calib_sweep = None
|
||||||
|
try:
|
||||||
|
calib_path_edit.setText(saved_path)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
recompute_current_processed_sweep(push_to_ring=False)
|
||||||
|
set_status_note(f"калибровка сохранена: {saved_path}")
|
||||||
|
runtime.mark_dirty()
|
||||||
|
|
||||||
|
def load_calibration_file() -> None:
|
||||||
|
path = get_calib_file_path()
|
||||||
|
try:
|
||||||
|
envelope = load_calib_envelope(path)
|
||||||
|
except Exception as exc:
|
||||||
|
set_status_note(f"калибровка: не удалось загрузить ({exc})")
|
||||||
|
runtime.mark_dirty()
|
||||||
|
return
|
||||||
|
|
||||||
|
normalized_path = path if path.lower().endswith(".npy") else f"{path}.npy"
|
||||||
|
runtime.calib_envelope = envelope
|
||||||
|
runtime.calib_file_path = normalized_path
|
||||||
|
runtime.last_calib_sweep = None
|
||||||
|
try:
|
||||||
|
calib_path_edit.setText(normalized_path)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
recompute_current_processed_sweep(push_to_ring=False)
|
||||||
|
set_status_note(f"калибровка загружена: {normalized_path}")
|
||||||
runtime.mark_dirty()
|
runtime.mark_dirty()
|
||||||
|
|
||||||
def set_bg_compute_enabled() -> None:
|
def set_bg_compute_enabled() -> None:
|
||||||
@ -317,6 +451,9 @@ def run_pyqtgraph(args) -> None:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
calib_cb.stateChanged.connect(lambda _v: set_calib_enabled())
|
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())
|
||||||
|
calib_load_btn.clicked.connect(lambda _checked=False: load_calibration_file())
|
||||||
bg_compute_cb.stateChanged.connect(lambda _v: set_bg_compute_enabled())
|
bg_compute_cb.stateChanged.connect(lambda _v: set_bg_compute_enabled())
|
||||||
bg_subtract_cb.stateChanged.connect(lambda _v: set_bg_subtract_enabled())
|
bg_subtract_cb.stateChanged.connect(lambda _v: set_bg_subtract_enabled())
|
||||||
fft_bg_subtract_cb.stateChanged.connect(lambda _v: set_fft_bg_subtract_enabled())
|
fft_bg_subtract_cb.stateChanged.connect(lambda _v: set_fft_bg_subtract_enabled())
|
||||||
@ -531,29 +668,7 @@ def run_pyqtgraph(args) -> None:
|
|||||||
runtime.current_sweep_raw = calibrated["I"]
|
runtime.current_sweep_raw = calibrated["I"]
|
||||||
runtime.current_aux_curves = aux_curves
|
runtime.current_aux_curves = aux_curves
|
||||||
runtime.current_info = info
|
runtime.current_info = info
|
||||||
|
recompute_current_processed_sweep(push_to_ring=True)
|
||||||
channel = 0
|
|
||||||
try:
|
|
||||||
channel = int(info.get("ch", 0)) if isinstance(info, dict) else 0
|
|
||||||
except Exception:
|
|
||||||
channel = 0
|
|
||||||
|
|
||||||
if channel == 0:
|
|
||||||
runtime.last_calib_sweep = runtime.current_sweep_raw
|
|
||||||
runtime.current_sweep_norm = None
|
|
||||||
sweep_for_processing = runtime.current_sweep_raw
|
|
||||||
else:
|
|
||||||
if calib_enabled and runtime.last_calib_sweep is not None:
|
|
||||||
runtime.current_sweep_norm = normalize_sweep(runtime.current_sweep_raw, runtime.last_calib_sweep)
|
|
||||||
sweep_for_processing = runtime.current_sweep_norm
|
|
||||||
else:
|
|
||||||
runtime.current_sweep_norm = None
|
|
||||||
sweep_for_processing = runtime.current_sweep_raw
|
|
||||||
|
|
||||||
ensure_buffer(runtime.current_sweep_raw.size)
|
|
||||||
runtime.ring.push(sweep_for_processing, runtime.current_freqs)
|
|
||||||
runtime.current_distances = runtime.ring.distance_axis
|
|
||||||
runtime.current_fft_db = runtime.ring.last_fft_db
|
|
||||||
if drained > 0:
|
if drained > 0:
|
||||||
update_physical_axes()
|
update_physical_axes()
|
||||||
return drained
|
return drained
|
||||||
@ -569,7 +684,7 @@ def run_pyqtgraph(args) -> None:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def update() -> None:
|
def update() -> None:
|
||||||
nonlocal peak_ref_window
|
nonlocal peak_ref_window, status_dirty
|
||||||
if peak_calibrate_mode and any(edit.hasFocus() for edit in c_edits):
|
if peak_calibrate_mode and any(edit.hasFocus() for edit in c_edits):
|
||||||
return
|
return
|
||||||
if peak_search_enabled and peak_window_edit is not None and peak_window_edit.hasFocus():
|
if peak_search_enabled and peak_window_edit is not None and peak_window_edit.hasFocus():
|
||||||
@ -584,26 +699,34 @@ def run_pyqtgraph(args) -> None:
|
|||||||
except Exception:
|
except Exception:
|
||||||
bg_fft_for_line = None
|
bg_fft_for_line = None
|
||||||
|
|
||||||
if redraw_needed and runtime.current_sweep_raw is not None:
|
if redraw_needed and (runtime.current_sweep_raw is not None or runtime.calib_envelope is not None):
|
||||||
xs = None
|
xs = resolve_curve_xs(
|
||||||
if runtime.current_freqs is not None and runtime.current_freqs.size == runtime.current_sweep_raw.size:
|
runtime.current_sweep_raw.size if runtime.current_sweep_raw is not None else runtime.calib_envelope.size
|
||||||
xs = runtime.current_freqs
|
)
|
||||||
elif runtime.ring.x_shared is not None and runtime.current_sweep_raw.size <= runtime.ring.x_shared.size:
|
displayed_calib = None
|
||||||
xs = runtime.ring.x_shared[: runtime.current_sweep_raw.size]
|
|
||||||
else:
|
|
||||||
xs = np.arange(runtime.current_sweep_raw.size)
|
|
||||||
|
|
||||||
curve.setData(xs, runtime.current_sweep_raw, autoDownsample=True)
|
if runtime.current_sweep_raw is not None:
|
||||||
if runtime.current_aux_curves is not None:
|
curve.setData(xs[: runtime.current_sweep_raw.size], runtime.current_sweep_raw, autoDownsample=True)
|
||||||
avg_1_curve, avg_2_curve = runtime.current_aux_curves
|
if runtime.current_aux_curves is not None:
|
||||||
curve_avg1.setData(xs[: avg_1_curve.size], avg_1_curve, autoDownsample=True)
|
avg_1_curve, avg_2_curve = runtime.current_aux_curves
|
||||||
curve_avg2.setData(xs[: avg_2_curve.size], avg_2_curve, autoDownsample=True)
|
curve_avg1.setData(xs[: avg_1_curve.size], avg_1_curve, autoDownsample=True)
|
||||||
|
curve_avg2.setData(xs[: avg_2_curve.size], avg_2_curve, autoDownsample=True)
|
||||||
|
else:
|
||||||
|
curve_avg1.setData([], [])
|
||||||
|
curve_avg2.setData([], [])
|
||||||
else:
|
else:
|
||||||
|
curve.setData([], [])
|
||||||
curve_avg1.setData([], [])
|
curve_avg1.setData([], [])
|
||||||
curve_avg2.setData([], [])
|
curve_avg2.setData([], [])
|
||||||
|
|
||||||
if runtime.last_calib_sweep is not None:
|
if runtime.calib_envelope is not None:
|
||||||
curve_calib.setData(xs[: runtime.last_calib_sweep.size], runtime.last_calib_sweep, autoDownsample=True)
|
if runtime.current_sweep_raw is not None:
|
||||||
|
displayed_calib = resample_envelope(runtime.calib_envelope, runtime.current_sweep_raw.size)
|
||||||
|
xs_calib = xs[: displayed_calib.size]
|
||||||
|
else:
|
||||||
|
displayed_calib = runtime.calib_envelope
|
||||||
|
xs_calib = resolve_curve_xs(displayed_calib.size)
|
||||||
|
curve_calib.setData(xs_calib, displayed_calib, autoDownsample=True)
|
||||||
else:
|
else:
|
||||||
curve_calib.setData([], [])
|
curve_calib.setData([], [])
|
||||||
|
|
||||||
@ -613,8 +736,8 @@ def run_pyqtgraph(args) -> None:
|
|||||||
curve_norm.setData([], [])
|
curve_norm.setData([], [])
|
||||||
|
|
||||||
if fixed_ylim is None:
|
if fixed_ylim is None:
|
||||||
y_series = [runtime.current_sweep_raw, runtime.last_calib_sweep, runtime.current_sweep_norm]
|
y_series = [runtime.current_sweep_raw, displayed_calib, runtime.current_sweep_norm]
|
||||||
if runtime.current_aux_curves is not None:
|
if runtime.current_aux_curves is not None and runtime.current_sweep_raw is not None:
|
||||||
y_series.extend(runtime.current_aux_curves)
|
y_series.extend(runtime.current_aux_curves)
|
||||||
y_limits = compute_auto_ylim(*y_series)
|
y_limits = compute_auto_ylim(*y_series)
|
||||||
if y_limits is not None:
|
if y_limits is not None:
|
||||||
@ -627,7 +750,7 @@ def run_pyqtgraph(args) -> None:
|
|||||||
|
|
||||||
sweep_for_fft = runtime.current_sweep_norm if runtime.current_sweep_norm is not None else runtime.current_sweep_raw
|
sweep_for_fft = runtime.current_sweep_norm if runtime.current_sweep_norm is not None else runtime.current_sweep_raw
|
||||||
distance_axis = runtime.current_distances if runtime.current_distances is not None else runtime.ring.distance_axis
|
distance_axis = runtime.current_distances if runtime.current_distances is not None else runtime.ring.distance_axis
|
||||||
if sweep_for_fft.size > 0 and distance_axis is not None:
|
if sweep_for_fft is not None and sweep_for_fft.size > 0 and distance_axis is not None:
|
||||||
if runtime.current_fft_db is None or runtime.current_fft_db.size != distance_axis.size or runtime.plot_dirty:
|
if runtime.current_fft_db is None or runtime.current_fft_db.size != distance_axis.size or runtime.plot_dirty:
|
||||||
runtime.current_fft_db = compute_fft_row(sweep_for_fft, runtime.current_freqs, distance_axis.size)
|
runtime.current_fft_db = compute_fft_row(sweep_for_fft, runtime.current_freqs, distance_axis.size)
|
||||||
fft_vals = runtime.current_fft_db
|
fft_vals = runtime.current_fft_db
|
||||||
@ -735,16 +858,23 @@ def run_pyqtgraph(args) -> None:
|
|||||||
else:
|
else:
|
||||||
img.setImage(disp, autoLevels=False)
|
img.setImage(disp, autoLevels=False)
|
||||||
|
|
||||||
if changed and runtime.current_info:
|
if redraw_needed or status_dirty:
|
||||||
try:
|
try:
|
||||||
status_payload = dict(runtime.current_info)
|
status_payload = dict(runtime.current_info) if runtime.current_info else {}
|
||||||
if peak_calibrate_mode and runtime.current_peak_width is not None:
|
if peak_calibrate_mode and runtime.current_peak_width is not None:
|
||||||
status_payload["peak_w"] = runtime.current_peak_width
|
status_payload["peak_w"] = runtime.current_peak_width
|
||||||
if peak_calibrate_mode and runtime.current_peak_amplitude is not None:
|
if peak_calibrate_mode and runtime.current_peak_amplitude is not None:
|
||||||
status_payload["peak_a"] = runtime.current_peak_amplitude
|
status_payload["peak_a"] = runtime.current_peak_amplitude
|
||||||
status.setText(format_status_kv(status_payload))
|
base_status = format_status_kv(status_payload) if status_payload else ""
|
||||||
|
if status_note:
|
||||||
|
status.setText(f"{base_status} | {status_note}" if base_status else status_note)
|
||||||
|
else:
|
||||||
|
status.setText(base_status)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
status_dirty = False
|
||||||
|
|
||||||
|
if changed and runtime.current_info:
|
||||||
try:
|
try:
|
||||||
chs = runtime.current_info.get("chs") if isinstance(runtime.current_info, dict) else None
|
chs = runtime.current_info.get("chs") if isinstance(runtime.current_info, dict) else None
|
||||||
if chs is None:
|
if chs is None:
|
||||||
|
|||||||
@ -1,10 +1,13 @@
|
|||||||
"""Pure sweep-processing helpers."""
|
"""Pure sweep-processing helpers."""
|
||||||
|
|
||||||
from rfg_adc_plotter.processing.calibration import (
|
from rfg_adc_plotter.processing.calibration import (
|
||||||
|
build_calib_envelope,
|
||||||
calibrate_freqs,
|
calibrate_freqs,
|
||||||
get_calibration_base,
|
get_calibration_base,
|
||||||
get_calibration_coeffs,
|
get_calibration_coeffs,
|
||||||
|
load_calib_envelope,
|
||||||
recalculate_calibration_c,
|
recalculate_calibration_c,
|
||||||
|
save_calib_envelope,
|
||||||
set_calibration_base_value,
|
set_calibration_base_value,
|
||||||
)
|
)
|
||||||
from rfg_adc_plotter.processing.fft import (
|
from rfg_adc_plotter.processing.fft import (
|
||||||
@ -20,6 +23,7 @@ from rfg_adc_plotter.processing.formatting import (
|
|||||||
)
|
)
|
||||||
from rfg_adc_plotter.processing.normalization import (
|
from rfg_adc_plotter.processing.normalization import (
|
||||||
build_calib_envelopes,
|
build_calib_envelopes,
|
||||||
|
normalize_by_envelope,
|
||||||
normalize_by_calib,
|
normalize_by_calib,
|
||||||
)
|
)
|
||||||
from rfg_adc_plotter.processing.peaks import (
|
from rfg_adc_plotter.processing.peaks import (
|
||||||
@ -30,6 +34,7 @@ from rfg_adc_plotter.processing.peaks import (
|
|||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"build_calib_envelopes",
|
"build_calib_envelopes",
|
||||||
|
"build_calib_envelope",
|
||||||
"calibrate_freqs",
|
"calibrate_freqs",
|
||||||
"compute_auto_ylim",
|
"compute_auto_ylim",
|
||||||
"compute_distance_axis",
|
"compute_distance_axis",
|
||||||
@ -41,9 +46,12 @@ __all__ = [
|
|||||||
"format_status_kv",
|
"format_status_kv",
|
||||||
"get_calibration_base",
|
"get_calibration_base",
|
||||||
"get_calibration_coeffs",
|
"get_calibration_coeffs",
|
||||||
|
"load_calib_envelope",
|
||||||
|
"normalize_by_envelope",
|
||||||
"normalize_by_calib",
|
"normalize_by_calib",
|
||||||
"parse_spec_clip",
|
"parse_spec_clip",
|
||||||
"recalculate_calibration_c",
|
"recalculate_calibration_c",
|
||||||
"rolling_median_ref",
|
"rolling_median_ref",
|
||||||
|
"save_calib_envelope",
|
||||||
"set_calibration_base_value",
|
"set_calibration_base_value",
|
||||||
]
|
]
|
||||||
|
|||||||
@ -2,11 +2,13 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
from typing import Any, Mapping
|
from typing import Any, Mapping
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from rfg_adc_plotter.constants import SWEEP_FREQ_MAX_GHZ, SWEEP_FREQ_MIN_GHZ
|
from rfg_adc_plotter.constants import SWEEP_FREQ_MAX_GHZ, SWEEP_FREQ_MIN_GHZ
|
||||||
|
from rfg_adc_plotter.processing.normalization import build_calib_envelopes
|
||||||
from rfg_adc_plotter.types import SweepData
|
from rfg_adc_plotter.types import SweepData
|
||||||
|
|
||||||
|
|
||||||
@ -79,3 +81,44 @@ def calibrate_freqs(sweep: Mapping[str, Any]) -> SweepData:
|
|||||||
"F": freqs_cal,
|
"F": freqs_cal,
|
||||||
"I": values_cal,
|
"I": values_cal,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def build_calib_envelope(sweep: np.ndarray) -> np.ndarray:
|
||||||
|
"""Build the active calibration envelope from a raw sweep."""
|
||||||
|
values = np.asarray(sweep, dtype=np.float32).reshape(-1)
|
||||||
|
if values.size == 0:
|
||||||
|
raise ValueError("Calibration sweep is empty")
|
||||||
|
_, upper = build_calib_envelopes(values)
|
||||||
|
return np.asarray(upper, dtype=np.float32)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_calib_envelope(envelope: np.ndarray) -> np.ndarray:
|
||||||
|
"""Validate a saved calibration envelope payload."""
|
||||||
|
values = np.asarray(envelope, dtype=np.float32).reshape(-1)
|
||||||
|
if values.size == 0:
|
||||||
|
raise ValueError("Calibration envelope is empty")
|
||||||
|
if not np.issubdtype(values.dtype, np.number):
|
||||||
|
raise ValueError("Calibration envelope must be numeric")
|
||||||
|
return values
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_calib_path(path: str | Path) -> Path:
|
||||||
|
out = Path(path).expanduser()
|
||||||
|
if out.suffix.lower() != ".npy":
|
||||||
|
out = out.with_suffix(".npy")
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def save_calib_envelope(path: str | Path, envelope: np.ndarray) -> str:
|
||||||
|
"""Persist a calibration envelope as a .npy file and return the final path."""
|
||||||
|
normalized_path = _normalize_calib_path(path)
|
||||||
|
values = validate_calib_envelope(envelope)
|
||||||
|
np.save(normalized_path, values.astype(np.float32, copy=False))
|
||||||
|
return str(normalized_path)
|
||||||
|
|
||||||
|
|
||||||
|
def load_calib_envelope(path: str | Path) -> np.ndarray:
|
||||||
|
"""Load and validate a calibration envelope from a .npy file."""
|
||||||
|
normalized_path = _normalize_calib_path(path)
|
||||||
|
loaded = np.load(normalized_path, allow_pickle=False)
|
||||||
|
return validate_calib_envelope(loaded)
|
||||||
|
|||||||
@ -108,6 +108,52 @@ def normalize_sweep_projector(raw: np.ndarray, calib: np.ndarray) -> np.ndarray:
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def resample_envelope(envelope: np.ndarray, width: int) -> np.ndarray:
|
||||||
|
"""Resample an envelope to the target sweep width on the index axis."""
|
||||||
|
target_width = int(width)
|
||||||
|
if target_width <= 0:
|
||||||
|
return np.zeros((0,), dtype=np.float32)
|
||||||
|
|
||||||
|
values = np.asarray(envelope, dtype=np.float32).reshape(-1)
|
||||||
|
if values.size == 0:
|
||||||
|
return np.full((target_width,), np.nan, dtype=np.float32)
|
||||||
|
if values.size == target_width:
|
||||||
|
return values.astype(np.float32, copy=True)
|
||||||
|
|
||||||
|
x_src = np.arange(values.size, dtype=np.float32)
|
||||||
|
finite = np.isfinite(values)
|
||||||
|
if not np.any(finite):
|
||||||
|
return np.full((target_width,), np.nan, dtype=np.float32)
|
||||||
|
if int(np.count_nonzero(finite)) == 1:
|
||||||
|
fill = float(values[finite][0])
|
||||||
|
return np.full((target_width,), fill, dtype=np.float32)
|
||||||
|
|
||||||
|
x_dst = np.linspace(0.0, float(values.size - 1), target_width, dtype=np.float32)
|
||||||
|
return np.interp(x_dst, x_src[finite], values[finite]).astype(np.float32)
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_by_envelope(raw: np.ndarray, envelope: np.ndarray) -> np.ndarray:
|
||||||
|
"""Normalize a sweep by an envelope with safe resampling and zero protection."""
|
||||||
|
raw_arr = np.asarray(raw, dtype=np.float32).reshape(-1)
|
||||||
|
if raw_arr.size == 0:
|
||||||
|
return raw_arr.copy()
|
||||||
|
|
||||||
|
env = resample_envelope(envelope, raw_arr.size)
|
||||||
|
out = np.full_like(raw_arr, np.nan, dtype=np.float32)
|
||||||
|
|
||||||
|
finite_env = np.abs(env[np.isfinite(env)])
|
||||||
|
if finite_env.size > 0:
|
||||||
|
eps = max(float(np.median(finite_env)) * 1e-6, 1e-9)
|
||||||
|
else:
|
||||||
|
eps = 1e-9
|
||||||
|
|
||||||
|
valid = np.isfinite(raw_arr) & np.isfinite(env) & (np.abs(env) > eps)
|
||||||
|
if np.any(valid):
|
||||||
|
with np.errstate(divide="ignore", invalid="ignore"):
|
||||||
|
out[valid] = raw_arr[valid] / env[valid]
|
||||||
|
return np.nan_to_num(out, nan=np.nan, posinf=np.nan, neginf=np.nan)
|
||||||
|
|
||||||
|
|
||||||
def normalize_by_calib(raw: np.ndarray, calib: np.ndarray, norm_type: str) -> np.ndarray:
|
def normalize_by_calib(raw: np.ndarray, calib: np.ndarray, norm_type: str) -> np.ndarray:
|
||||||
"""Apply the selected normalization method."""
|
"""Apply the selected normalization method."""
|
||||||
norm = str(norm_type).strip().lower()
|
norm = str(norm_type).strip().lower()
|
||||||
|
|||||||
@ -21,6 +21,8 @@ class RuntimeState:
|
|||||||
current_sweep_norm: Optional[np.ndarray] = None
|
current_sweep_norm: Optional[np.ndarray] = None
|
||||||
current_fft_db: Optional[np.ndarray] = None
|
current_fft_db: Optional[np.ndarray] = None
|
||||||
last_calib_sweep: Optional[np.ndarray] = None
|
last_calib_sweep: Optional[np.ndarray] = None
|
||||||
|
calib_envelope: Optional[np.ndarray] = None
|
||||||
|
calib_file_path: Optional[str] = None
|
||||||
current_info: Optional[SweepInfo] = None
|
current_info: Optional[SweepInfo] = None
|
||||||
bg_spec_cache: Optional[np.ndarray] = None
|
bg_spec_cache: Optional[np.ndarray] = None
|
||||||
current_peak_width: Optional[float] = None
|
current_peak_width: Optional[float] = None
|
||||||
|
|||||||
@ -1,11 +1,24 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from rfg_adc_plotter.processing.calibration import calibrate_freqs, recalculate_calibration_c
|
from rfg_adc_plotter.processing.calibration import (
|
||||||
|
build_calib_envelope,
|
||||||
|
calibrate_freqs,
|
||||||
|
load_calib_envelope,
|
||||||
|
recalculate_calibration_c,
|
||||||
|
save_calib_envelope,
|
||||||
|
)
|
||||||
from rfg_adc_plotter.processing.fft import compute_distance_axis, compute_fft_mag_row, compute_fft_row
|
from rfg_adc_plotter.processing.fft import compute_distance_axis, compute_fft_mag_row, compute_fft_row
|
||||||
from rfg_adc_plotter.processing.normalization import build_calib_envelopes, normalize_by_calib
|
from rfg_adc_plotter.processing.normalization import (
|
||||||
|
build_calib_envelopes,
|
||||||
|
normalize_by_calib,
|
||||||
|
normalize_by_envelope,
|
||||||
|
resample_envelope,
|
||||||
|
)
|
||||||
from rfg_adc_plotter.processing.peaks import find_peak_width_markers, find_top_peaks_over_ref, rolling_median_ref
|
from rfg_adc_plotter.processing.peaks import find_peak_width_markers, find_top_peaks_over_ref, rolling_median_ref
|
||||||
|
|
||||||
|
|
||||||
@ -39,6 +52,32 @@ class ProcessingTests(unittest.TestCase):
|
|||||||
self.assertTrue(np.any(np.isfinite(simple)))
|
self.assertTrue(np.any(np.isfinite(simple)))
|
||||||
self.assertTrue(np.any(np.isfinite(projector)))
|
self.assertTrue(np.any(np.isfinite(projector)))
|
||||||
|
|
||||||
|
def test_file_calibration_envelope_roundtrip_and_division(self):
|
||||||
|
raw = (np.sin(np.linspace(0.0, 8.0 * np.pi, 128)) * 50.0 + 100.0).astype(np.float32)
|
||||||
|
envelope = build_calib_envelope(raw)
|
||||||
|
normalized = normalize_by_envelope(raw, envelope)
|
||||||
|
resampled = resample_envelope(envelope, 96)
|
||||||
|
|
||||||
|
self.assertEqual(envelope.shape, raw.shape)
|
||||||
|
self.assertEqual(normalized.shape, raw.shape)
|
||||||
|
self.assertEqual(resampled.shape, (96,))
|
||||||
|
self.assertTrue(np.any(np.isfinite(normalized)))
|
||||||
|
self.assertTrue(np.all(np.isfinite(envelope)))
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||||
|
path = os.path.join(tmp_dir, "calibration_envelope")
|
||||||
|
saved_path = save_calib_envelope(path, envelope)
|
||||||
|
loaded = load_calib_envelope(saved_path)
|
||||||
|
self.assertTrue(saved_path.endswith(".npy"))
|
||||||
|
self.assertTrue(np.allclose(loaded, envelope))
|
||||||
|
|
||||||
|
def test_load_calib_envelope_rejects_empty_payload(self):
|
||||||
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||||
|
path = os.path.join(tmp_dir, "empty.npy")
|
||||||
|
np.save(path, np.zeros((0,), dtype=np.float32))
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
load_calib_envelope(path)
|
||||||
|
|
||||||
def test_fft_helpers_return_expected_shapes(self):
|
def test_fft_helpers_return_expected_shapes(self):
|
||||||
sweep = np.sin(np.linspace(0.0, 4.0 * np.pi, 128)).astype(np.float32)
|
sweep = np.sin(np.linspace(0.0, 4.0 * np.pi, 128)).astype(np.float32)
|
||||||
freqs = np.linspace(3.3, 14.3, 128, dtype=np.float64)
|
freqs = np.linspace(3.3, 14.3, 128, dtype=np.float64)
|
||||||
|
|||||||
Reference in New Issue
Block a user