new background remove algoritm
This commit is contained in:
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
WF_WIDTH = 1000
|
WF_WIDTH = 1000
|
||||||
FFT_LEN = 1024
|
FFT_LEN = 1024
|
||||||
|
BACKGROUND_MEDIAN_SWEEPS = 64
|
||||||
|
|
||||||
SWEEP_FREQ_MIN_GHZ = 3.3
|
SWEEP_FREQ_MIN_GHZ = 3.3
|
||||||
SWEEP_FREQ_MAX_GHZ = 14.3
|
SWEEP_FREQ_MAX_GHZ = 14.3
|
||||||
|
|||||||
@ -12,6 +12,11 @@ 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.background import (
|
||||||
|
load_fft_background,
|
||||||
|
save_fft_background,
|
||||||
|
subtract_fft_background,
|
||||||
|
)
|
||||||
from rfg_adc_plotter.processing.calibration import (
|
from rfg_adc_plotter.processing.calibration import (
|
||||||
build_calib_envelope,
|
build_calib_envelope,
|
||||||
calibrate_freqs,
|
calibrate_freqs,
|
||||||
@ -21,7 +26,7 @@ from rfg_adc_plotter.processing.calibration import (
|
|||||||
save_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_mag_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_envelope, resample_envelope
|
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 (
|
||||||
@ -248,9 +253,6 @@ def run_pyqtgraph(args) -> None:
|
|||||||
spec_right_line.setVisible(False)
|
spec_right_line.setVisible(False)
|
||||||
|
|
||||||
calib_cb = QtWidgets.QCheckBox("калибровка по огибающей")
|
calib_cb = QtWidgets.QCheckBox("калибровка по огибающей")
|
||||||
bg_compute_cb = QtWidgets.QCheckBox("расчет фона")
|
|
||||||
bg_subtract_cb = QtWidgets.QCheckBox("вычет фона")
|
|
||||||
fft_bg_subtract_cb = QtWidgets.QCheckBox("FFT вычет фона")
|
|
||||||
range_group = QtWidgets.QGroupBox("Рабочий диапазон")
|
range_group = QtWidgets.QGroupBox("Рабочий диапазон")
|
||||||
range_group_layout = QtWidgets.QFormLayout(range_group)
|
range_group_layout = QtWidgets.QFormLayout(range_group)
|
||||||
range_group_layout.setContentsMargins(6, 6, 6, 6)
|
range_group_layout.setContentsMargins(6, 6, 6, 6)
|
||||||
@ -300,15 +302,39 @@ def run_pyqtgraph(args) -> None:
|
|||||||
calib_buttons_row.addWidget(calib_save_btn)
|
calib_buttons_row.addWidget(calib_save_btn)
|
||||||
calib_buttons_row.addWidget(calib_load_btn)
|
calib_buttons_row.addWidget(calib_load_btn)
|
||||||
calib_group_layout.addLayout(calib_buttons_row)
|
calib_group_layout.addLayout(calib_buttons_row)
|
||||||
|
background_group = QtWidgets.QGroupBox("Фон")
|
||||||
|
background_group_layout = QtWidgets.QVBoxLayout(background_group)
|
||||||
|
background_group_layout.setContentsMargins(6, 6, 6, 6)
|
||||||
|
background_group_layout.setSpacing(6)
|
||||||
|
background_cb = QtWidgets.QCheckBox("вычитание фона")
|
||||||
|
background_group_layout.addWidget(background_cb)
|
||||||
|
background_path_edit = QtWidgets.QLineEdit("fft_background.npy")
|
||||||
|
try:
|
||||||
|
background_path_edit.setPlaceholderText("fft_background.npy")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
background_path_row = QtWidgets.QHBoxLayout()
|
||||||
|
background_path_row.setContentsMargins(0, 0, 0, 0)
|
||||||
|
background_path_row.setSpacing(4)
|
||||||
|
background_path_row.addWidget(background_path_edit)
|
||||||
|
background_pick_btn = QtWidgets.QPushButton("Файл...")
|
||||||
|
background_path_row.addWidget(background_pick_btn)
|
||||||
|
background_group_layout.addLayout(background_path_row)
|
||||||
|
background_buttons_row = QtWidgets.QHBoxLayout()
|
||||||
|
background_buttons_row.setContentsMargins(0, 0, 0, 0)
|
||||||
|
background_buttons_row.setSpacing(4)
|
||||||
|
background_save_btn = QtWidgets.QPushButton("Сохранить медиану")
|
||||||
|
background_load_btn = QtWidgets.QPushButton("Загрузить")
|
||||||
|
background_buttons_row.addWidget(background_save_btn)
|
||||||
|
background_buttons_row.addWidget(background_load_btn)
|
||||||
|
background_group_layout.addLayout(background_buttons_row)
|
||||||
try:
|
try:
|
||||||
settings_layout.addWidget(QtWidgets.QLabel("Настройки"))
|
settings_layout.addWidget(QtWidgets.QLabel("Настройки"))
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
settings_layout.addWidget(range_group)
|
settings_layout.addWidget(range_group)
|
||||||
settings_layout.addWidget(calib_group)
|
settings_layout.addWidget(calib_group)
|
||||||
settings_layout.addWidget(bg_compute_cb)
|
settings_layout.addWidget(background_group)
|
||||||
settings_layout.addWidget(bg_subtract_cb)
|
|
||||||
settings_layout.addWidget(fft_bg_subtract_cb)
|
|
||||||
settings_layout.addWidget(fft_mode_label)
|
settings_layout.addWidget(fft_mode_label)
|
||||||
settings_layout.addWidget(fft_mode_combo)
|
settings_layout.addWidget(fft_mode_combo)
|
||||||
settings_layout.addWidget(peak_search_cb)
|
settings_layout.addWidget(peak_search_cb)
|
||||||
@ -317,9 +343,7 @@ def run_pyqtgraph(args) -> None:
|
|||||||
win.addItem(status, row=3, col=0, colspan=2)
|
win.addItem(status, row=3, col=0, colspan=2)
|
||||||
|
|
||||||
calib_enabled = False
|
calib_enabled = False
|
||||||
bg_compute_enabled = True
|
background_enabled = False
|
||||||
bg_subtract_enabled = False
|
|
||||||
fft_bg_subtract_enabled = False
|
|
||||||
fft_mode = "symmetric"
|
fft_mode = "symmetric"
|
||||||
status_note = ""
|
status_note = ""
|
||||||
status_dirty = True
|
status_dirty = True
|
||||||
@ -422,11 +446,35 @@ def run_pyqtgraph(args) -> None:
|
|||||||
path = ""
|
path = ""
|
||||||
return path or "calibration_envelope.npy"
|
return path or "calibration_envelope.npy"
|
||||||
|
|
||||||
|
def get_background_file_path() -> str:
|
||||||
|
try:
|
||||||
|
path = background_path_edit.text().strip()
|
||||||
|
except Exception:
|
||||||
|
path = ""
|
||||||
|
return path or "fft_background.npy"
|
||||||
|
|
||||||
|
def reset_background_state(*, clear_profile: bool = True) -> None:
|
||||||
|
runtime.background_buffer.reset()
|
||||||
|
if clear_profile:
|
||||||
|
runtime.background_profile = None
|
||||||
|
runtime.current_fft_mag = None
|
||||||
|
runtime.current_fft_db = None
|
||||||
|
|
||||||
|
def resolve_active_background(expected_size: int) -> Optional[np.ndarray]:
|
||||||
|
if not background_enabled:
|
||||||
|
return None
|
||||||
|
if runtime.background_profile is None:
|
||||||
|
return None
|
||||||
|
if runtime.background_profile.size != int(expected_size):
|
||||||
|
set_status_note("фон: размер профиля не совпадает с FFT")
|
||||||
|
return None
|
||||||
|
return runtime.background_profile
|
||||||
|
|
||||||
def reset_ring_buffers() -> None:
|
def reset_ring_buffers() -> None:
|
||||||
runtime.ring.reset()
|
runtime.ring.reset()
|
||||||
runtime.current_distances = None
|
runtime.current_distances = None
|
||||||
|
runtime.current_fft_mag = None
|
||||||
runtime.current_fft_db = None
|
runtime.current_fft_db = None
|
||||||
runtime.bg_spec_cache = None
|
|
||||||
runtime.current_peak_width = None
|
runtime.current_peak_width = None
|
||||||
runtime.current_peak_amplitude = None
|
runtime.current_peak_amplitude = None
|
||||||
runtime.peak_candidates = []
|
runtime.peak_candidates = []
|
||||||
@ -442,6 +490,7 @@ def run_pyqtgraph(args) -> None:
|
|||||||
runtime.current_freqs = None
|
runtime.current_freqs = None
|
||||||
runtime.current_sweep_raw = None
|
runtime.current_sweep_raw = None
|
||||||
runtime.current_sweep_norm = None
|
runtime.current_sweep_norm = None
|
||||||
|
runtime.current_fft_mag = None
|
||||||
runtime.current_fft_db = None
|
runtime.current_fft_db = None
|
||||||
runtime.current_distances = runtime.ring.distance_axis
|
runtime.current_distances = runtime.ring.distance_axis
|
||||||
return
|
return
|
||||||
@ -461,6 +510,7 @@ def run_pyqtgraph(args) -> None:
|
|||||||
runtime.current_freqs = None
|
runtime.current_freqs = None
|
||||||
runtime.current_sweep_raw = None
|
runtime.current_sweep_raw = None
|
||||||
runtime.current_sweep_norm = None
|
runtime.current_sweep_norm = None
|
||||||
|
runtime.current_fft_mag = None
|
||||||
runtime.current_fft_db = None
|
runtime.current_fft_db = None
|
||||||
runtime.current_distances = None
|
runtime.current_distances = None
|
||||||
set_status_note("диапазон: нет точек в выбранном окне")
|
set_status_note("диапазон: нет точек в выбранном окне")
|
||||||
@ -479,6 +529,7 @@ def run_pyqtgraph(args) -> None:
|
|||||||
else:
|
else:
|
||||||
runtime.current_sweep_norm = None
|
runtime.current_sweep_norm = None
|
||||||
|
|
||||||
|
runtime.current_fft_mag = None
|
||||||
runtime.current_fft_db = None
|
runtime.current_fft_db = None
|
||||||
if (
|
if (
|
||||||
not push_to_ring
|
not push_to_ring
|
||||||
@ -491,7 +542,10 @@ def run_pyqtgraph(args) -> None:
|
|||||||
ensure_buffer(runtime.current_sweep_raw.size)
|
ensure_buffer(runtime.current_sweep_raw.size)
|
||||||
runtime.ring.push(sweep_for_processing, runtime.current_freqs)
|
runtime.ring.push(sweep_for_processing, runtime.current_freqs)
|
||||||
runtime.current_distances = runtime.ring.distance_axis
|
runtime.current_distances = runtime.ring.distance_axis
|
||||||
|
runtime.current_fft_mag = runtime.ring.get_last_fft_linear()
|
||||||
runtime.current_fft_db = runtime.ring.last_fft_db
|
runtime.current_fft_db = runtime.ring.last_fft_db
|
||||||
|
if runtime.current_fft_mag is not None:
|
||||||
|
runtime.background_buffer.push(runtime.current_fft_mag)
|
||||||
|
|
||||||
def set_calib_enabled() -> None:
|
def set_calib_enabled() -> None:
|
||||||
nonlocal calib_enabled
|
nonlocal calib_enabled
|
||||||
@ -501,6 +555,7 @@ def run_pyqtgraph(args) -> None:
|
|||||||
calib_enabled = False
|
calib_enabled = False
|
||||||
if calib_enabled and runtime.calib_envelope is None:
|
if calib_enabled and runtime.calib_envelope is None:
|
||||||
set_status_note("калибровка: огибающая не загружена")
|
set_status_note("калибровка: огибающая не загружена")
|
||||||
|
reset_background_state(clear_profile=True)
|
||||||
recompute_current_processed_sweep(push_to_ring=False)
|
recompute_current_processed_sweep(push_to_ring=False)
|
||||||
runtime.mark_dirty()
|
runtime.mark_dirty()
|
||||||
|
|
||||||
@ -530,6 +585,7 @@ def run_pyqtgraph(args) -> None:
|
|||||||
return
|
return
|
||||||
runtime.range_min_ghz = new_min
|
runtime.range_min_ghz = new_min
|
||||||
runtime.range_max_ghz = new_max
|
runtime.range_max_ghz = new_max
|
||||||
|
reset_background_state(clear_profile=True)
|
||||||
refresh_current_window(push_to_ring=True, reset_ring=True)
|
refresh_current_window(push_to_ring=True, reset_ring=True)
|
||||||
set_status_note(f"диапазон: {new_min:.6g}..{new_max:.6g} GHz")
|
set_status_note(f"диапазон: {new_min:.6g}..{new_max:.6g} GHz")
|
||||||
runtime.mark_dirty()
|
runtime.mark_dirty()
|
||||||
@ -554,6 +610,26 @@ def run_pyqtgraph(args) -> None:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def pick_background_file() -> None:
|
||||||
|
start_path = get_background_file_path()
|
||||||
|
try:
|
||||||
|
selected, _ = QtWidgets.QFileDialog.getSaveFileName(
|
||||||
|
main_window,
|
||||||
|
"Файл фона FFT",
|
||||||
|
start_path,
|
||||||
|
"NumPy (*.npy);;All files (*)",
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
set_status_note(f"фон: выбор файла недоступен ({exc})")
|
||||||
|
runtime.mark_dirty()
|
||||||
|
return
|
||||||
|
if not selected:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
background_path_edit.setText(selected)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def save_current_calibration() -> None:
|
def save_current_calibration() -> None:
|
||||||
if runtime.current_sweep_raw is None or runtime.current_sweep_raw.size == 0:
|
if runtime.current_sweep_raw is None or runtime.current_sweep_raw.size == 0:
|
||||||
set_status_note("калибровка: нет текущего raw-свипа")
|
set_status_note("калибровка: нет текущего raw-свипа")
|
||||||
@ -574,6 +650,7 @@ def run_pyqtgraph(args) -> None:
|
|||||||
calib_path_edit.setText(saved_path)
|
calib_path_edit.setText(saved_path)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
reset_background_state(clear_profile=True)
|
||||||
recompute_current_processed_sweep(push_to_ring=False)
|
recompute_current_processed_sweep(push_to_ring=False)
|
||||||
set_status_note(f"калибровка сохранена: {saved_path}")
|
set_status_note(f"калибровка сохранена: {saved_path}")
|
||||||
runtime.mark_dirty()
|
runtime.mark_dirty()
|
||||||
@ -595,32 +672,67 @@ def run_pyqtgraph(args) -> None:
|
|||||||
calib_path_edit.setText(normalized_path)
|
calib_path_edit.setText(normalized_path)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
reset_background_state(clear_profile=True)
|
||||||
recompute_current_processed_sweep(push_to_ring=False)
|
recompute_current_processed_sweep(push_to_ring=False)
|
||||||
set_status_note(f"калибровка загружена: {normalized_path}")
|
set_status_note(f"калибровка загружена: {normalized_path}")
|
||||||
runtime.mark_dirty()
|
runtime.mark_dirty()
|
||||||
|
|
||||||
def set_bg_compute_enabled() -> None:
|
def save_current_background() -> None:
|
||||||
nonlocal bg_compute_enabled
|
background = runtime.background_buffer.median()
|
||||||
|
if background is None or background.size == 0:
|
||||||
|
set_status_note("фон: буфер пуст, нечего сохранять")
|
||||||
|
runtime.mark_dirty()
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
bg_compute_enabled = bool(bg_compute_cb.isChecked())
|
saved_path = save_fft_background(get_background_file_path(), background)
|
||||||
|
except Exception as exc:
|
||||||
|
set_status_note(f"фон: не удалось сохранить ({exc})")
|
||||||
|
runtime.mark_dirty()
|
||||||
|
return
|
||||||
|
|
||||||
|
runtime.background_profile = np.asarray(background, dtype=np.float32).copy()
|
||||||
|
runtime.background_file_path = saved_path
|
||||||
|
try:
|
||||||
|
background_path_edit.setText(saved_path)
|
||||||
except Exception:
|
except Exception:
|
||||||
bg_compute_enabled = False
|
pass
|
||||||
|
runtime.current_fft_db = None
|
||||||
|
set_status_note(f"фон сохранен: {saved_path}")
|
||||||
runtime.mark_dirty()
|
runtime.mark_dirty()
|
||||||
|
|
||||||
def set_bg_subtract_enabled() -> None:
|
def load_background_file() -> None:
|
||||||
nonlocal bg_subtract_enabled
|
path = get_background_file_path()
|
||||||
try:
|
try:
|
||||||
bg_subtract_enabled = bool(bg_subtract_cb.isChecked())
|
background = load_fft_background(path)
|
||||||
|
except Exception as exc:
|
||||||
|
set_status_note(f"фон: не удалось загрузить ({exc})")
|
||||||
|
runtime.mark_dirty()
|
||||||
|
return
|
||||||
|
|
||||||
|
if background.size != fft_bins:
|
||||||
|
set_status_note(f"фон: ожидалось {fft_bins} FFT-бинов, получено {background.size}")
|
||||||
|
runtime.mark_dirty()
|
||||||
|
return
|
||||||
|
|
||||||
|
normalized_path = path if path.lower().endswith(".npy") else f"{path}.npy"
|
||||||
|
runtime.background_profile = np.asarray(background, dtype=np.float32).copy()
|
||||||
|
runtime.background_file_path = normalized_path
|
||||||
|
try:
|
||||||
|
background_path_edit.setText(normalized_path)
|
||||||
except Exception:
|
except Exception:
|
||||||
bg_subtract_enabled = False
|
pass
|
||||||
|
runtime.current_fft_db = None
|
||||||
|
set_status_note(f"фон загружен: {normalized_path}")
|
||||||
runtime.mark_dirty()
|
runtime.mark_dirty()
|
||||||
|
|
||||||
def set_fft_bg_subtract_enabled() -> None:
|
def set_background_enabled() -> None:
|
||||||
nonlocal fft_bg_subtract_enabled
|
nonlocal background_enabled
|
||||||
try:
|
try:
|
||||||
fft_bg_subtract_enabled = bool(fft_bg_subtract_cb.isChecked())
|
background_enabled = bool(background_cb.isChecked())
|
||||||
except Exception:
|
except Exception:
|
||||||
fft_bg_subtract_enabled = False
|
background_enabled = False
|
||||||
|
if background_enabled and runtime.background_profile is None:
|
||||||
|
set_status_note("фон: профиль не загружен")
|
||||||
runtime.mark_dirty()
|
runtime.mark_dirty()
|
||||||
|
|
||||||
def set_fft_mode() -> None:
|
def set_fft_mode() -> None:
|
||||||
@ -630,7 +742,9 @@ def run_pyqtgraph(args) -> None:
|
|||||||
except Exception:
|
except Exception:
|
||||||
fft_mode = "symmetric"
|
fft_mode = "symmetric"
|
||||||
runtime.ring.set_fft_mode(fft_mode)
|
runtime.ring.set_fft_mode(fft_mode)
|
||||||
|
reset_background_state(clear_profile=True)
|
||||||
runtime.current_distances = runtime.ring.distance_axis
|
runtime.current_distances = runtime.ring.distance_axis
|
||||||
|
runtime.current_fft_mag = None
|
||||||
runtime.current_fft_db = None
|
runtime.current_fft_db = None
|
||||||
mode_label = {
|
mode_label = {
|
||||||
"direct": "IFFT: обычный",
|
"direct": "IFFT: обычный",
|
||||||
@ -642,12 +756,11 @@ def run_pyqtgraph(args) -> None:
|
|||||||
runtime.mark_dirty()
|
runtime.mark_dirty()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
bg_compute_cb.setChecked(True)
|
|
||||||
fft_mode_combo.setCurrentIndex(1)
|
fft_mode_combo.setCurrentIndex(1)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
restore_range_controls()
|
restore_range_controls()
|
||||||
set_bg_compute_enabled()
|
set_background_enabled()
|
||||||
set_fft_mode()
|
set_fft_mode()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -657,9 +770,10 @@ def run_pyqtgraph(args) -> None:
|
|||||||
calib_pick_btn.clicked.connect(lambda _checked=False: pick_calib_file())
|
calib_pick_btn.clicked.connect(lambda _checked=False: pick_calib_file())
|
||||||
calib_save_btn.clicked.connect(lambda _checked=False: save_current_calibration())
|
calib_save_btn.clicked.connect(lambda _checked=False: save_current_calibration())
|
||||||
calib_load_btn.clicked.connect(lambda _checked=False: load_calibration_file())
|
calib_load_btn.clicked.connect(lambda _checked=False: load_calibration_file())
|
||||||
bg_compute_cb.stateChanged.connect(lambda _v: set_bg_compute_enabled())
|
background_cb.stateChanged.connect(lambda _v: set_background_enabled())
|
||||||
bg_subtract_cb.stateChanged.connect(lambda _v: set_bg_subtract_enabled())
|
background_pick_btn.clicked.connect(lambda _checked=False: pick_background_file())
|
||||||
fft_bg_subtract_cb.stateChanged.connect(lambda _v: set_fft_bg_subtract_enabled())
|
background_save_btn.clicked.connect(lambda _checked=False: save_current_background())
|
||||||
|
background_load_btn.clicked.connect(lambda _checked=False: load_background_file())
|
||||||
fft_mode_combo.currentIndexChanged.connect(lambda _v: set_fft_mode())
|
fft_mode_combo.currentIndexChanged.connect(lambda _v: set_fft_mode())
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
@ -776,6 +890,7 @@ def run_pyqtgraph(args) -> None:
|
|||||||
def apply_c_value(idx: int, edit) -> None:
|
def apply_c_value(idx: int, edit) -> None:
|
||||||
try:
|
try:
|
||||||
set_calibration_base_value(idx, float(edit.text().strip()))
|
set_calibration_base_value(idx, float(edit.text().strip()))
|
||||||
|
reset_background_state(clear_profile=True)
|
||||||
runtime.mark_dirty()
|
runtime.mark_dirty()
|
||||||
except Exception:
|
except Exception:
|
||||||
try:
|
try:
|
||||||
@ -821,38 +936,14 @@ def run_pyqtgraph(args) -> None:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def visible_bg_fft(disp_fft: np.ndarray, force: bool = False) -> Optional[np.ndarray]:
|
def refresh_current_fft_cache(sweep_for_fft: np.ndarray, bins: int) -> None:
|
||||||
nonlocal bg_compute_enabled, bg_subtract_enabled
|
runtime.current_fft_mag = compute_fft_mag_row(
|
||||||
need_bg = bool(bg_subtract_enabled or force)
|
sweep_for_fft,
|
||||||
if (not need_bg) or disp_fft.size == 0:
|
runtime.current_freqs,
|
||||||
return None
|
bins,
|
||||||
ny, nx = disp_fft.shape
|
mode=fft_mode,
|
||||||
if ny <= 0 or nx <= 0:
|
)
|
||||||
return runtime.bg_spec_cache
|
runtime.current_fft_db = fft_mag_to_db(runtime.current_fft_mag)
|
||||||
if runtime.bg_spec_cache is not None and runtime.bg_spec_cache.size != ny:
|
|
||||||
runtime.bg_spec_cache = None
|
|
||||||
if not bg_compute_enabled:
|
|
||||||
return runtime.bg_spec_cache
|
|
||||||
try:
|
|
||||||
(x0, x1), _ = p_spec.viewRange()
|
|
||||||
except Exception:
|
|
||||||
x0, x1 = 0.0, float(nx - 1)
|
|
||||||
xmin, xmax = sorted((float(x0), float(x1)))
|
|
||||||
ix0 = max(0, min(nx - 1, int(np.floor(xmin))))
|
|
||||||
ix1 = max(0, min(nx - 1, int(np.ceil(xmax))))
|
|
||||||
if ix1 < ix0:
|
|
||||||
ix1 = ix0
|
|
||||||
window = disp_fft[:, ix0 : ix1 + 1]
|
|
||||||
if window.size == 0:
|
|
||||||
return runtime.bg_spec_cache
|
|
||||||
try:
|
|
||||||
bg_spec = np.nanmedian(window, axis=1)
|
|
||||||
except Exception:
|
|
||||||
return runtime.bg_spec_cache
|
|
||||||
if not np.any(np.isfinite(bg_spec)):
|
|
||||||
return runtime.bg_spec_cache
|
|
||||||
runtime.bg_spec_cache = np.nan_to_num(bg_spec, nan=0.0).astype(np.float32, copy=False)
|
|
||||||
return runtime.bg_spec_cache
|
|
||||||
|
|
||||||
def drain_queue() -> int:
|
def drain_queue() -> int:
|
||||||
drained = 0
|
drained = 0
|
||||||
@ -897,12 +988,6 @@ def run_pyqtgraph(args) -> None:
|
|||||||
|
|
||||||
changed = drain_queue() > 0
|
changed = drain_queue() > 0
|
||||||
redraw_needed = changed or runtime.plot_dirty
|
redraw_needed = changed or runtime.plot_dirty
|
||||||
bg_fft_for_line = None
|
|
||||||
if redraw_needed and fft_bg_subtract_enabled and runtime.ring.ring_fft is not None:
|
|
||||||
try:
|
|
||||||
bg_fft_for_line = visible_bg_fft(runtime.ring.get_display_fft_linear(), force=True)
|
|
||||||
except Exception:
|
|
||||||
bg_fft_for_line = None
|
|
||||||
|
|
||||||
if redraw_needed:
|
if redraw_needed:
|
||||||
xs = resolve_curve_xs(
|
xs = resolve_curve_xs(
|
||||||
@ -952,25 +1037,24 @@ 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 is not None and 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_mag is None or runtime.current_fft_mag.size != distance_axis.size or runtime.plot_dirty:
|
||||||
runtime.current_fft_db = compute_fft_row(
|
refresh_current_fft_cache(sweep_for_fft, distance_axis.size)
|
||||||
sweep_for_fft,
|
fft_mag = runtime.current_fft_mag
|
||||||
runtime.current_freqs,
|
xs_fft = distance_axis[: fft_mag.size]
|
||||||
distance_axis.size,
|
active_background = None
|
||||||
mode=fft_mode,
|
try:
|
||||||
)
|
active_background = resolve_active_background(fft_mag.size)
|
||||||
fft_vals = runtime.current_fft_db
|
except Exception:
|
||||||
xs_fft = distance_axis[: fft_vals.size]
|
active_background = None
|
||||||
if fft_bg_subtract_enabled and bg_fft_for_line is not None:
|
display_fft_mag = fft_mag
|
||||||
n_bg = int(min(fft_vals.size, bg_fft_for_line.size))
|
if active_background is not None:
|
||||||
if n_bg > 0:
|
try:
|
||||||
num = np.maximum(
|
display_fft_mag = subtract_fft_background(fft_mag, active_background)
|
||||||
np.power(10.0, np.asarray(fft_vals[:n_bg], dtype=np.float64) / 20.0),
|
except Exception as exc:
|
||||||
0.0,
|
set_status_note(f"фон: не удалось применить ({exc})")
|
||||||
)
|
active_background = None
|
||||||
den = np.maximum(np.asarray(bg_fft_for_line[:n_bg], dtype=np.float64), 0.0)
|
display_fft_mag = fft_mag
|
||||||
fft_vals = (20.0 * np.log10((num + 1e-9) / (den + 1e-9))).astype(np.float32, copy=False)
|
fft_vals = fft_mag_to_db(display_fft_mag)
|
||||||
xs_fft = xs_fft[:n_bg]
|
|
||||||
curve_fft.setData(xs_fft, fft_vals)
|
curve_fft.setData(xs_fft, fft_vals)
|
||||||
finite_x = xs_fft[np.isfinite(xs_fft)]
|
finite_x = xs_fft[np.isfinite(xs_fft)]
|
||||||
if finite_x.size > 0:
|
if finite_x.size > 0:
|
||||||
@ -1006,7 +1090,7 @@ def run_pyqtgraph(args) -> None:
|
|||||||
for box in fft_peak_boxes:
|
for box in fft_peak_boxes:
|
||||||
box.setVisible(False)
|
box.setVisible(False)
|
||||||
|
|
||||||
if fft_bg_subtract_enabled and bg_fft_for_line is not None:
|
if active_background is not None:
|
||||||
p_fft.setYRange(-10.0, 30.0, padding=0)
|
p_fft.setYRange(-10.0, 30.0, padding=0)
|
||||||
else:
|
else:
|
||||||
finite_y = y_for_range[np.isfinite(y_for_range)]
|
finite_y = y_for_range[np.isfinite(y_for_range)]
|
||||||
@ -1109,7 +1193,7 @@ def run_pyqtgraph(args) -> None:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if changed and runtime.ring.ring_fft is not None:
|
if redraw_needed and runtime.ring.ring_fft is not None:
|
||||||
disp_fft_lin = runtime.ring.get_display_fft_linear()
|
disp_fft_lin = runtime.ring.get_display_fft_linear()
|
||||||
if spec_mean_sec > 0.0:
|
if spec_mean_sec > 0.0:
|
||||||
disp_times = runtime.ring.get_display_times()
|
disp_times = runtime.ring.get_display_times()
|
||||||
@ -1124,16 +1208,21 @@ def run_pyqtgraph(args) -> None:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
bg_spec = visible_bg_fft(disp_fft_lin)
|
active_background = None
|
||||||
if bg_spec is not None:
|
try:
|
||||||
num = np.maximum(disp_fft_lin, 0.0).astype(np.float32, copy=False) + 1e-9
|
active_background = resolve_active_background(disp_fft_lin.shape[0])
|
||||||
den = bg_spec[:, None] + 1e-9
|
except Exception:
|
||||||
disp_fft = (20.0 * np.log10(num / den)).astype(np.float32, copy=False)
|
active_background = None
|
||||||
else:
|
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)
|
disp_fft = fft_mag_to_db(disp_fft_lin)
|
||||||
|
|
||||||
levels = None
|
levels = None
|
||||||
if bg_spec is not None:
|
if active_background is not None:
|
||||||
try:
|
try:
|
||||||
p5 = float(np.nanpercentile(disp_fft, 5))
|
p5 = float(np.nanpercentile(disp_fft, 5))
|
||||||
p95 = float(np.nanpercentile(disp_fft, 95))
|
p95 = float(np.nanpercentile(disp_fft, 95))
|
||||||
|
|||||||
@ -1,5 +1,11 @@
|
|||||||
"""Pure sweep-processing helpers."""
|
"""Pure sweep-processing helpers."""
|
||||||
|
|
||||||
|
from rfg_adc_plotter.processing.background import (
|
||||||
|
load_fft_background,
|
||||||
|
save_fft_background,
|
||||||
|
subtract_fft_background,
|
||||||
|
validate_fft_background,
|
||||||
|
)
|
||||||
from rfg_adc_plotter.processing.calibration import (
|
from rfg_adc_plotter.processing.calibration import (
|
||||||
build_calib_envelope,
|
build_calib_envelope,
|
||||||
calibrate_freqs,
|
calibrate_freqs,
|
||||||
@ -47,11 +53,15 @@ __all__ = [
|
|||||||
"get_calibration_base",
|
"get_calibration_base",
|
||||||
"get_calibration_coeffs",
|
"get_calibration_coeffs",
|
||||||
"load_calib_envelope",
|
"load_calib_envelope",
|
||||||
|
"load_fft_background",
|
||||||
"normalize_by_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",
|
"save_calib_envelope",
|
||||||
|
"save_fft_background",
|
||||||
"set_calibration_base_value",
|
"set_calibration_base_value",
|
||||||
|
"subtract_fft_background",
|
||||||
|
"validate_fft_background",
|
||||||
]
|
]
|
||||||
|
|||||||
66
rfg_adc_plotter/processing/background.py
Normal file
66
rfg_adc_plotter/processing/background.py
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
"""Helpers for persisted FFT background profiles."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
def validate_fft_background(background: np.ndarray) -> np.ndarray:
|
||||||
|
"""Validate a saved FFT background payload."""
|
||||||
|
values = np.asarray(background)
|
||||||
|
if values.ndim != 1:
|
||||||
|
raise ValueError("FFT background must be a 1D array")
|
||||||
|
if not np.issubdtype(values.dtype, np.number):
|
||||||
|
raise ValueError("FFT background must be numeric")
|
||||||
|
values = np.asarray(values, dtype=np.float32).reshape(-1)
|
||||||
|
if values.size == 0:
|
||||||
|
raise ValueError("FFT background is empty")
|
||||||
|
return values
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_background_path(path: str | Path) -> Path:
|
||||||
|
out = Path(path).expanduser()
|
||||||
|
if out.suffix.lower() != ".npy":
|
||||||
|
out = out.with_suffix(".npy")
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def save_fft_background(path: str | Path, background: np.ndarray) -> str:
|
||||||
|
"""Persist an FFT background profile as a .npy file."""
|
||||||
|
normalized_path = _normalize_background_path(path)
|
||||||
|
values = validate_fft_background(background)
|
||||||
|
np.save(normalized_path, values.astype(np.float32, copy=False))
|
||||||
|
return str(normalized_path)
|
||||||
|
|
||||||
|
|
||||||
|
def load_fft_background(path: str | Path) -> np.ndarray:
|
||||||
|
"""Load and validate an FFT background profile from a .npy file."""
|
||||||
|
normalized_path = _normalize_background_path(path)
|
||||||
|
loaded = np.load(normalized_path, allow_pickle=False)
|
||||||
|
return validate_fft_background(loaded)
|
||||||
|
|
||||||
|
|
||||||
|
def subtract_fft_background(signal_mag: np.ndarray, background_mag: np.ndarray) -> np.ndarray:
|
||||||
|
"""Subtract a background profile from FFT magnitudes in linear amplitude."""
|
||||||
|
signal = np.asarray(signal_mag, dtype=np.float32)
|
||||||
|
background = validate_fft_background(background_mag)
|
||||||
|
if signal.ndim == 1:
|
||||||
|
if signal.size != background.size:
|
||||||
|
raise ValueError("FFT background size does not match signal size")
|
||||||
|
valid = np.isfinite(signal) & np.isfinite(background)
|
||||||
|
out = np.full_like(signal, np.nan, dtype=np.float32)
|
||||||
|
if np.any(valid):
|
||||||
|
out[valid] = np.maximum(signal[valid] - background[valid], 0.0)
|
||||||
|
return out
|
||||||
|
|
||||||
|
if signal.ndim == 2:
|
||||||
|
if signal.shape[0] != background.size:
|
||||||
|
raise ValueError("FFT background size does not match signal rows")
|
||||||
|
background_2d = background[:, None]
|
||||||
|
valid = np.isfinite(signal) & np.isfinite(background_2d)
|
||||||
|
diff = signal - background_2d
|
||||||
|
return np.where(valid, np.maximum(diff, 0.0), np.nan).astype(np.float32, copy=False)
|
||||||
|
|
||||||
|
raise ValueError("FFT background subtraction supports only 1D or 2D signals")
|
||||||
@ -1,6 +1,7 @@
|
|||||||
"""Runtime state helpers."""
|
"""Runtime state helpers."""
|
||||||
|
|
||||||
|
from rfg_adc_plotter.state.background_buffer import BackgroundMedianBuffer
|
||||||
from rfg_adc_plotter.state.ring_buffer import RingBuffer
|
from rfg_adc_plotter.state.ring_buffer import RingBuffer
|
||||||
from rfg_adc_plotter.state.runtime_state import RuntimeState
|
from rfg_adc_plotter.state.runtime_state import RuntimeState
|
||||||
|
|
||||||
__all__ = ["RingBuffer", "RuntimeState"]
|
__all__ = ["BackgroundMedianBuffer", "RingBuffer", "RuntimeState"]
|
||||||
|
|||||||
49
rfg_adc_plotter/state/background_buffer.py
Normal file
49
rfg_adc_plotter/state/background_buffer.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
"""Rolling median buffer for persisted FFT background capture."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
class BackgroundMedianBuffer:
|
||||||
|
"""Store recent FFT rows and expose their median profile."""
|
||||||
|
|
||||||
|
def __init__(self, max_rows: int):
|
||||||
|
self.max_rows = max(1, int(max_rows))
|
||||||
|
self.width = 0
|
||||||
|
self.head = 0
|
||||||
|
self.count = 0
|
||||||
|
self.rows: Optional[np.ndarray] = None
|
||||||
|
|
||||||
|
def reset(self) -> None:
|
||||||
|
self.width = 0
|
||||||
|
self.head = 0
|
||||||
|
self.count = 0
|
||||||
|
self.rows = None
|
||||||
|
|
||||||
|
def push(self, fft_mag: np.ndarray) -> None:
|
||||||
|
values = np.asarray(fft_mag, dtype=np.float32).reshape(-1)
|
||||||
|
if values.size == 0:
|
||||||
|
return
|
||||||
|
if self.rows is None or self.width != values.size:
|
||||||
|
self.width = values.size
|
||||||
|
self.rows = np.full((self.max_rows, self.width), np.nan, dtype=np.float32)
|
||||||
|
self.head = 0
|
||||||
|
self.count = 0
|
||||||
|
self.rows[self.head, :] = values
|
||||||
|
self.head = (self.head + 1) % self.max_rows
|
||||||
|
self.count = min(self.count + 1, self.max_rows)
|
||||||
|
|
||||||
|
def median(self) -> Optional[np.ndarray]:
|
||||||
|
if self.rows is None or self.count <= 0:
|
||||||
|
return None
|
||||||
|
rows = self.rows[: self.count] if self.count < self.max_rows else self.rows
|
||||||
|
valid_rows = np.any(np.isfinite(rows), axis=1)
|
||||||
|
if not np.any(valid_rows):
|
||||||
|
return None
|
||||||
|
median = np.nanmedian(rows[valid_rows], axis=0).astype(np.float32, copy=False)
|
||||||
|
if not np.any(np.isfinite(median)):
|
||||||
|
return None
|
||||||
|
return np.nan_to_num(median, nan=0.0).astype(np.float32, copy=False)
|
||||||
@ -25,6 +25,7 @@ class RingBuffer:
|
|||||||
self.ring_fft: Optional[np.ndarray] = None
|
self.ring_fft: Optional[np.ndarray] = None
|
||||||
self.x_shared: Optional[np.ndarray] = None
|
self.x_shared: Optional[np.ndarray] = None
|
||||||
self.distance_axis: Optional[np.ndarray] = None
|
self.distance_axis: Optional[np.ndarray] = None
|
||||||
|
self.last_fft_mag: Optional[np.ndarray] = None
|
||||||
self.last_fft_db: Optional[np.ndarray] = None
|
self.last_fft_db: Optional[np.ndarray] = None
|
||||||
self.last_freqs: Optional[np.ndarray] = None
|
self.last_freqs: Optional[np.ndarray] = None
|
||||||
self.y_min_fft: Optional[float] = None
|
self.y_min_fft: Optional[float] = None
|
||||||
@ -47,6 +48,7 @@ class RingBuffer:
|
|||||||
self.ring_fft = None
|
self.ring_fft = None
|
||||||
self.x_shared = None
|
self.x_shared = None
|
||||||
self.distance_axis = None
|
self.distance_axis = None
|
||||||
|
self.last_fft_mag = None
|
||||||
self.last_fft_db = None
|
self.last_fft_db = None
|
||||||
self.last_freqs = None
|
self.last_freqs = None
|
||||||
self.y_min_fft = None
|
self.y_min_fft = None
|
||||||
@ -125,6 +127,7 @@ class RingBuffer:
|
|||||||
last_idx = (self.head - 1) % self.max_sweeps
|
last_idx = (self.head - 1) % self.max_sweeps
|
||||||
if self.ring_fft.shape[0] > 0:
|
if self.ring_fft.shape[0] > 0:
|
||||||
last_fft = self.ring_fft[last_idx]
|
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.last_fft_db = fft_mag_to_db(last_fft)
|
||||||
finite = self.ring_fft[np.isfinite(self.ring_fft)]
|
finite = self.ring_fft[np.isfinite(self.ring_fft)]
|
||||||
if finite.size > 0:
|
if finite.size > 0:
|
||||||
@ -155,6 +158,7 @@ class RingBuffer:
|
|||||||
|
|
||||||
fft_mag = compute_fft_mag_row(sweep, freqs, self.fft_bins, mode=self.fft_mode)
|
fft_mag = compute_fft_mag_row(sweep, freqs, self.fft_bins, mode=self.fft_mode)
|
||||||
self.ring_fft[self.head, :] = fft_mag
|
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)
|
self.last_fft_db = fft_mag_to_db(fft_mag)
|
||||||
|
|
||||||
if self.last_fft_db.size > 0:
|
if self.last_fft_db.size > 0:
|
||||||
@ -178,6 +182,11 @@ class RingBuffer:
|
|||||||
base = self.ring_fft if self.head == 0 else np.roll(self.ring_fft, -self.head, axis=0)
|
base = self.ring_fft if self.head == 0 else np.roll(self.ring_fft, -self.head, axis=0)
|
||||||
return base.T
|
return base.T
|
||||||
|
|
||||||
|
def get_last_fft_linear(self) -> Optional[np.ndarray]:
|
||||||
|
if self.last_fft_mag is None:
|
||||||
|
return None
|
||||||
|
return np.asarray(self.last_fft_mag, dtype=np.float32).copy()
|
||||||
|
|
||||||
def get_display_times(self) -> Optional[np.ndarray]:
|
def get_display_times(self) -> Optional[np.ndarray]:
|
||||||
if self.ring_time is None:
|
if self.ring_time is None:
|
||||||
return None
|
return None
|
||||||
|
|||||||
@ -7,6 +7,8 @@ from typing import Dict, List, Optional
|
|||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
from rfg_adc_plotter.constants import BACKGROUND_MEDIAN_SWEEPS
|
||||||
|
from rfg_adc_plotter.state.background_buffer import BackgroundMedianBuffer
|
||||||
from rfg_adc_plotter.state.ring_buffer import RingBuffer
|
from rfg_adc_plotter.state.ring_buffer import RingBuffer
|
||||||
from rfg_adc_plotter.types import SweepAuxCurves, SweepInfo
|
from rfg_adc_plotter.types import SweepAuxCurves, SweepInfo
|
||||||
|
|
||||||
@ -23,12 +25,17 @@ class RuntimeState:
|
|||||||
current_sweep_raw: Optional[np.ndarray] = None
|
current_sweep_raw: Optional[np.ndarray] = None
|
||||||
current_aux_curves: SweepAuxCurves = None
|
current_aux_curves: SweepAuxCurves = None
|
||||||
current_sweep_norm: Optional[np.ndarray] = None
|
current_sweep_norm: Optional[np.ndarray] = None
|
||||||
|
current_fft_mag: 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_envelope: Optional[np.ndarray] = None
|
||||||
calib_file_path: Optional[str] = None
|
calib_file_path: Optional[str] = None
|
||||||
|
background_buffer: BackgroundMedianBuffer = field(
|
||||||
|
default_factory=lambda: BackgroundMedianBuffer(BACKGROUND_MEDIAN_SWEEPS)
|
||||||
|
)
|
||||||
|
background_profile: Optional[np.ndarray] = None
|
||||||
|
background_file_path: Optional[str] = None
|
||||||
current_info: Optional[SweepInfo] = None
|
current_info: Optional[SweepInfo] = None
|
||||||
bg_spec_cache: Optional[np.ndarray] = None
|
|
||||||
current_peak_width: Optional[float] = None
|
current_peak_width: Optional[float] = None
|
||||||
current_peak_amplitude: Optional[float] = None
|
current_peak_amplitude: Optional[float] = None
|
||||||
peak_candidates: List[Dict[str, float]] = field(default_factory=list)
|
peak_candidates: List[Dict[str, float]] = field(default_factory=list)
|
||||||
|
|||||||
44
tests/test_background_buffer.py
Normal file
44
tests/test_background_buffer.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from rfg_adc_plotter.state.background_buffer import BackgroundMedianBuffer
|
||||||
|
|
||||||
|
|
||||||
|
class BackgroundMedianBufferTests(unittest.TestCase):
|
||||||
|
def test_buffer_returns_median_for_partial_fill(self):
|
||||||
|
buffer = BackgroundMedianBuffer(max_rows=4)
|
||||||
|
buffer.push(np.asarray([1.0, 5.0, 9.0], dtype=np.float32))
|
||||||
|
buffer.push(np.asarray([3.0, 7.0, 11.0], dtype=np.float32))
|
||||||
|
|
||||||
|
median = buffer.median()
|
||||||
|
|
||||||
|
self.assertIsNotNone(median)
|
||||||
|
self.assertTrue(np.allclose(median, np.asarray([2.0, 6.0, 10.0], dtype=np.float32)))
|
||||||
|
|
||||||
|
def test_buffer_wraparound_keeps_latest_rows(self):
|
||||||
|
buffer = BackgroundMedianBuffer(max_rows=2)
|
||||||
|
buffer.push(np.asarray([1.0, 5.0], dtype=np.float32))
|
||||||
|
buffer.push(np.asarray([3.0, 7.0], dtype=np.float32))
|
||||||
|
buffer.push(np.asarray([9.0, 11.0], dtype=np.float32))
|
||||||
|
|
||||||
|
median = buffer.median()
|
||||||
|
|
||||||
|
self.assertIsNotNone(median)
|
||||||
|
self.assertTrue(np.allclose(median, np.asarray([6.0, 9.0], dtype=np.float32)))
|
||||||
|
|
||||||
|
def test_buffer_reset_clears_state(self):
|
||||||
|
buffer = BackgroundMedianBuffer(max_rows=2)
|
||||||
|
buffer.push(np.asarray([1.0, 2.0], dtype=np.float32))
|
||||||
|
|
||||||
|
buffer.reset()
|
||||||
|
|
||||||
|
self.assertIsNone(buffer.rows)
|
||||||
|
self.assertIsNone(buffer.median())
|
||||||
|
self.assertEqual(buffer.count, 0)
|
||||||
|
self.assertEqual(buffer.head, 0)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@ -14,6 +14,11 @@ from rfg_adc_plotter.processing.calibration import (
|
|||||||
recalculate_calibration_c,
|
recalculate_calibration_c,
|
||||||
save_calib_envelope,
|
save_calib_envelope,
|
||||||
)
|
)
|
||||||
|
from rfg_adc_plotter.processing.background import (
|
||||||
|
load_fft_background,
|
||||||
|
save_fft_background,
|
||||||
|
subtract_fft_background,
|
||||||
|
)
|
||||||
from rfg_adc_plotter.processing.fft import (
|
from rfg_adc_plotter.processing.fft import (
|
||||||
build_positive_only_centered_ifft_spectrum,
|
build_positive_only_centered_ifft_spectrum,
|
||||||
build_symmetric_ifft_spectrum,
|
build_symmetric_ifft_spectrum,
|
||||||
@ -101,6 +106,28 @@ class ProcessingTests(unittest.TestCase):
|
|||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
load_calib_envelope(path)
|
load_calib_envelope(path)
|
||||||
|
|
||||||
|
def test_fft_background_roundtrip_and_rejects_non_1d_payload(self):
|
||||||
|
background = np.asarray([0.5, 1.5, 2.5], dtype=np.float32)
|
||||||
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||||
|
path = os.path.join(tmp_dir, "fft_background")
|
||||||
|
saved_path = save_fft_background(path, background)
|
||||||
|
loaded = load_fft_background(saved_path)
|
||||||
|
self.assertTrue(saved_path.endswith(".npy"))
|
||||||
|
self.assertTrue(np.allclose(loaded, background))
|
||||||
|
|
||||||
|
invalid_path = os.path.join(tmp_dir, "fft_background_invalid.npy")
|
||||||
|
np.save(invalid_path, np.zeros((2, 2), dtype=np.float32))
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
load_fft_background(invalid_path)
|
||||||
|
|
||||||
|
def test_subtract_fft_background_clamps_negative_residuals_to_zero(self):
|
||||||
|
signal = np.asarray([1.0, 2.0, 3.0], dtype=np.float32)
|
||||||
|
background = np.asarray([1.0, 1.5, 5.0], dtype=np.float32)
|
||||||
|
subtracted = subtract_fft_background(signal, background)
|
||||||
|
|
||||||
|
self.assertTrue(np.allclose(subtracted, np.asarray([0.0, 0.5, 0.0], dtype=np.float32)))
|
||||||
|
self.assertTrue(np.allclose(subtract_fft_background(signal, signal), 0.0))
|
||||||
|
|
||||||
def test_apply_working_range_crops_sweep_to_selected_band(self):
|
def test_apply_working_range_crops_sweep_to_selected_band(self):
|
||||||
freqs = np.linspace(3.3, 14.3, 12, dtype=np.float64)
|
freqs = np.linspace(3.3, 14.3, 12, dtype=np.float64)
|
||||||
sweep = np.arange(12, dtype=np.float32)
|
sweep = np.arange(12, dtype=np.float32)
|
||||||
|
|||||||
@ -15,6 +15,7 @@ class RingBufferTests(unittest.TestCase):
|
|||||||
self.assertIsNotNone(ring.ring_fft)
|
self.assertIsNotNone(ring.ring_fft)
|
||||||
self.assertIsNotNone(ring.ring_time)
|
self.assertIsNotNone(ring.ring_time)
|
||||||
self.assertIsNotNone(ring.distance_axis)
|
self.assertIsNotNone(ring.distance_axis)
|
||||||
|
self.assertIsNotNone(ring.get_last_fft_linear())
|
||||||
self.assertIsNotNone(ring.last_fft_db)
|
self.assertIsNotNone(ring.last_fft_db)
|
||||||
self.assertEqual(ring.ring.shape[0], 4)
|
self.assertEqual(ring.ring.shape[0], 4)
|
||||||
self.assertEqual(ring.ring_fft.shape, (4, ring.fft_bins))
|
self.assertEqual(ring.ring_fft.shape, (4, ring.fft_bins))
|
||||||
@ -52,6 +53,7 @@ class RingBufferTests(unittest.TestCase):
|
|||||||
self.assertTrue(changed)
|
self.assertTrue(changed)
|
||||||
self.assertFalse(ring.fft_symmetric)
|
self.assertFalse(ring.fft_symmetric)
|
||||||
self.assertEqual(ring.get_display_raw().shape[1], 2)
|
self.assertEqual(ring.get_display_raw().shape[1], 2)
|
||||||
|
self.assertIsNotNone(ring.get_last_fft_linear())
|
||||||
self.assertEqual(ring.last_fft_db.shape, fft_before.shape)
|
self.assertEqual(ring.last_fft_db.shape, fft_before.shape)
|
||||||
self.assertFalse(np.allclose(ring.last_fft_db, fft_before))
|
self.assertFalse(np.allclose(ring.last_fft_db, fft_before))
|
||||||
self.assertFalse(np.allclose(ring.distance_axis, axis_before))
|
self.assertFalse(np.allclose(ring.distance_axis, axis_before))
|
||||||
|
|||||||
Reference in New Issue
Block a user