last implement diff
This commit is contained in:
Binary file not shown.
@ -112,6 +112,10 @@ def run_matplotlib(args):
|
||||
state = AppState(norm_type=norm_type)
|
||||
state.configure_capture_import(fancy=bool(args.fancy), logscale=bool(getattr(args, "logscale", False)))
|
||||
ring = RingBuffer(max_sweeps)
|
||||
try:
|
||||
ring.set_fft_complex_mode(str(getattr(args, "ifft_complex_mode", "arccos")))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# --- Создание фигуры ---
|
||||
fig, axs = plt.subplots(2, 2, figsize=(12, 8))
|
||||
@ -181,17 +185,28 @@ def run_matplotlib(args):
|
||||
ax_cb = fig.add_axes([0.92, 0.45, 0.08, 0.08])
|
||||
ax_cb_file = fig.add_axes([0.92, 0.36, 0.08, 0.08])
|
||||
ax_line_mode = fig.add_axes([0.92, 0.10, 0.08, 0.08])
|
||||
ax_ifft_mode = fig.add_axes([0.92, 0.01, 0.08, 0.08])
|
||||
ymin_slider = Slider(ax_smin, "Y min", 0, max(1, fft_bins - 1), valinit=0, valstep=1, orientation="vertical")
|
||||
ymax_slider = Slider(ax_smax, "Y max", 0, max(1, fft_bins - 1), valinit=max(1, fft_bins - 1), valstep=1, orientation="vertical")
|
||||
contrast_slider = Slider(ax_sctr, "Int max", 0, 100, valinit=100, valstep=1, orientation="vertical")
|
||||
calib_cb = CheckButtons(ax_cb, ["калибровка"], [False])
|
||||
calib_file_cb = CheckButtons(ax_cb_file, ["из файла"], [False])
|
||||
line_mode_rb = RadioButtons(ax_line_mode, ("raw", "processed"), active=0)
|
||||
ifft_mode_rb = RadioButtons(
|
||||
ax_ifft_mode,
|
||||
("arccos", "diff"),
|
||||
active=(1 if ring.fft_complex_mode == "diff" else 0),
|
||||
)
|
||||
try:
|
||||
ax_line_mode.set_title("Линия", fontsize=8, pad=2)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
ax_ifft_mode.set_title("IFFT", fontsize=8, pad=2)
|
||||
except Exception:
|
||||
pass
|
||||
line_mode_state = {"value": "raw"}
|
||||
ifft_mode_state = {"value": str(ring.fft_complex_mode)}
|
||||
|
||||
import os as _os
|
||||
try:
|
||||
@ -280,7 +295,7 @@ def run_matplotlib(args):
|
||||
pass
|
||||
|
||||
def _refresh_status_texts():
|
||||
pipeline_text.set_text(state.format_pipeline_status())
|
||||
pipeline_text.set_text(f"{state.format_pipeline_status()} | cplx:{ring.fft_complex_mode}")
|
||||
ref_text.set_text(state.format_reference_status())
|
||||
try:
|
||||
fig.canvas.draw_idle()
|
||||
@ -414,6 +429,18 @@ def run_matplotlib(args):
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _on_ifft_mode_clicked(label):
|
||||
ifft_mode_state["value"] = str(label)
|
||||
try:
|
||||
ring.set_fft_complex_mode(str(label))
|
||||
except Exception:
|
||||
pass
|
||||
_refresh_status_texts()
|
||||
try:
|
||||
fig.canvas.draw_idle()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
save_bg_btn.on_clicked(_on_save_bg)
|
||||
bg_cb.on_clicked(_on_bg_clicked)
|
||||
calib_load_btn2.on_clicked(_on_calib_load_btn)
|
||||
@ -425,6 +452,7 @@ def run_matplotlib(args):
|
||||
bg_sample_btn2.on_clicked(_on_bg_sample_btn)
|
||||
bg_save_btn2.on_clicked(_on_bg_save_btn2)
|
||||
line_mode_rb.on_clicked(_on_line_mode_clicked)
|
||||
ifft_mode_rb.on_clicked(_on_ifft_mode_clicked)
|
||||
|
||||
ymin_slider.on_changed(_on_ylim_change)
|
||||
ymax_slider.on_changed(_on_ylim_change)
|
||||
@ -437,6 +465,7 @@ def run_matplotlib(args):
|
||||
except Exception:
|
||||
calib_cb = None
|
||||
line_mode_state = {"value": "raw"}
|
||||
ifft_mode_state = {"value": str(getattr(ring, "fft_complex_mode", "arccos"))}
|
||||
|
||||
FREQ_MIN = float(FREQ_MIN_GHZ)
|
||||
FREQ_MAX = float(FREQ_MAX_GHZ)
|
||||
@ -602,10 +631,10 @@ def run_matplotlib(args):
|
||||
if changed and state.current_info:
|
||||
status_text.set_text(format_status(state.current_info))
|
||||
channel_text.set_text(state.format_channel_label())
|
||||
pipeline_text.set_text(state.format_pipeline_status())
|
||||
pipeline_text.set_text(f"{state.format_pipeline_status()} | cplx:{ring.fft_complex_mode}")
|
||||
ref_text.set_text(state.format_reference_status())
|
||||
elif changed:
|
||||
pipeline_text.set_text(state.format_pipeline_status())
|
||||
pipeline_text.set_text(f"{state.format_pipeline_status()} | cplx:{ring.fft_complex_mode}")
|
||||
ref_text.set_text(state.format_reference_status())
|
||||
|
||||
return (
|
||||
|
||||
@ -140,6 +140,10 @@ def run_pyqtgraph(args):
|
||||
state = AppState(norm_type=norm_type)
|
||||
state.configure_capture_import(fancy=bool(args.fancy), logscale=bool(getattr(args, "logscale", False)))
|
||||
ring = RingBuffer(max_sweeps)
|
||||
try:
|
||||
ring.set_fft_complex_mode(str(getattr(args, "ifft_complex_mode", "arccos")))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
_qt_text_selectable = QtCore.Qt.TextSelectableByMouse
|
||||
@ -445,6 +449,25 @@ def run_pyqtgraph(args):
|
||||
def _line_mode() -> str:
|
||||
return "processed" if line_mode_proc_rb.isChecked() else "raw"
|
||||
|
||||
# Переключатель режима реконструкции комплексного спектра перед IFFT
|
||||
ifft_mode_widget = QtWidgets.QWidget()
|
||||
ifft_mode_layout = QtWidgets.QHBoxLayout(ifft_mode_widget)
|
||||
ifft_mode_layout.setContentsMargins(2, 2, 2, 2)
|
||||
ifft_mode_layout.setSpacing(8)
|
||||
ifft_mode_layout.addWidget(QtWidgets.QLabel("IFFT mode:"))
|
||||
ifft_mode_arccos_rb = QtWidgets.QRadioButton("arccos")
|
||||
ifft_mode_diff_rb = QtWidgets.QRadioButton("diff")
|
||||
if ring.fft_complex_mode == "diff":
|
||||
ifft_mode_diff_rb.setChecked(True)
|
||||
else:
|
||||
ifft_mode_arccos_rb.setChecked(True)
|
||||
ifft_mode_layout.addWidget(ifft_mode_arccos_rb)
|
||||
ifft_mode_layout.addWidget(ifft_mode_diff_rb)
|
||||
ifft_mode_layout.addStretch(1)
|
||||
ifft_mode_proxy = QtWidgets.QGraphicsProxyWidget()
|
||||
ifft_mode_proxy.setWidget(ifft_mode_widget)
|
||||
win.addItem(ifft_mode_proxy, row=7, col=0, colspan=2)
|
||||
|
||||
# Статусная строка
|
||||
status = pg.LabelItem(justify="left")
|
||||
win.addItem(status, row=3, col=0, colspan=2)
|
||||
@ -455,12 +478,32 @@ def run_pyqtgraph(args):
|
||||
|
||||
def _refresh_pipeline_label():
|
||||
txt = state.format_pipeline_status()
|
||||
txt = f"{txt} | cplx:{ring.fft_complex_mode}"
|
||||
trace = state.format_stage_trace()
|
||||
if trace:
|
||||
txt = f"{txt} | trace: {trace}"
|
||||
pipeline_status.setText(txt)
|
||||
ref_status.setText(state.format_reference_status())
|
||||
|
||||
def _apply_ifft_complex_mode(mode: str):
|
||||
try:
|
||||
changed = ring.set_fft_complex_mode(mode)
|
||||
except Exception:
|
||||
changed = False
|
||||
if changed:
|
||||
try:
|
||||
curve_fft.setData([], [])
|
||||
except Exception:
|
||||
pass
|
||||
_refresh_pipeline_label()
|
||||
|
||||
ifft_mode_arccos_rb.toggled.connect(
|
||||
lambda checked: _apply_ifft_complex_mode("arccos") if checked else None
|
||||
)
|
||||
ifft_mode_diff_rb.toggled.connect(
|
||||
lambda checked: _apply_ifft_complex_mode("diff") if checked else None
|
||||
)
|
||||
|
||||
_refresh_calib_controls()
|
||||
_refresh_bg_controls()
|
||||
_refresh_pipeline_label()
|
||||
|
||||
@ -77,6 +77,15 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
default="projector",
|
||||
help="Тип нормировки: projector (по огибающим в [-1000,+1000]) или simple (raw/calib)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ifft-complex-mode",
|
||||
choices=["arccos", "diff"],
|
||||
default="arccos",
|
||||
help=(
|
||||
"Режим реконструкции комплексного спектра перед IFFT: "
|
||||
"arccos (phi=arccos(x), unwrap) или diff (sin(phi) через численную производную)"
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--bin",
|
||||
dest="bin_mode",
|
||||
|
||||
@ -1,12 +1,8 @@
|
||||
"""Преобразование свипа в IFFT-профиль по глубине (м).
|
||||
|
||||
Новый pipeline перед IFFT:
|
||||
1) нормировка по max(abs(sweep))
|
||||
2) clip в [-1, 1]
|
||||
3) phi = arccos(x)
|
||||
4) непрерывная развёртка фазы (nearest-continuous)
|
||||
5) s_complex = exp(1j * phi)
|
||||
6) IFFT с учётом смещения частотной сетки
|
||||
Поддерживает несколько режимов восстановления комплексного спектра перед IFFT:
|
||||
- ``arccos``: phi = arccos(x), continuous unwrap, z = exp(1j*phi)
|
||||
- ``diff``: x ~= cos(phi), diff(x) -> sin(phi), z = cos + 1j*sin (с проекцией на единичную окружность)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@ -29,6 +25,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
_EPS = 1e-12
|
||||
_TWO_PI = float(2.0 * np.pi)
|
||||
_VALID_COMPLEX_MODES = {"arccos", "diff"}
|
||||
|
||||
|
||||
def _fallback_depth_response(
|
||||
@ -58,6 +55,13 @@ def _fallback_depth_response(
|
||||
return depth, out
|
||||
|
||||
|
||||
def _normalize_complex_mode(mode: str) -> str:
|
||||
m = str(mode).strip().lower()
|
||||
if m not in _VALID_COMPLEX_MODES:
|
||||
raise ValueError(f"Invalid complex reconstruction mode: {mode!r}")
|
||||
return m
|
||||
|
||||
|
||||
def build_ifft_time_axis_ns() -> np.ndarray:
|
||||
"""Legacy helper: старая временная ось IFFT в наносекундах (фиксированная длина)."""
|
||||
return (
|
||||
@ -69,22 +73,27 @@ def build_frequency_axis_hz(sweep_width: int) -> np.ndarray:
|
||||
"""Построить частотную сетку (Гц) для текущей длины свипа."""
|
||||
n = int(sweep_width)
|
||||
if n <= 0:
|
||||
return np.zeros((0,), dtype=np.float128)
|
||||
return np.zeros((0,), dtype=np.float64)
|
||||
if n == 1:
|
||||
return np.array([FREQ_MIN_GHZ * 1e9], dtype=np.float64)
|
||||
return np.linspace(FREQ_MIN_GHZ * 1e9, FREQ_MAX_GHZ * 1e9, n, dtype=np.float64)
|
||||
|
||||
|
||||
def normalize_sweep_for_phase(sweep: np.ndarray) -> np.ndarray:
|
||||
"""Нормировать свип на max(abs(.)) и вернуть float64."""
|
||||
x = np.asarray(sweep, dtype=np.float64).ravel()
|
||||
if x.size == 0:
|
||||
return x
|
||||
x = np.nan_to_num(x, nan=0.0, posinf=0.0, neginf=0.0)
|
||||
amax = float(np.max(np.abs(x)))
|
||||
def normalize_trace_unit_range(x: np.ndarray) -> np.ndarray:
|
||||
"""Signed-нормировка массива по max(abs(.)) в диапазон около [-1, 1]."""
|
||||
arr = np.asarray(x, dtype=np.float64).ravel()
|
||||
if arr.size == 0:
|
||||
return arr
|
||||
arr = np.nan_to_num(arr, nan=0.0, posinf=0.0, neginf=0.0)
|
||||
amax = float(np.max(np.abs(arr)))
|
||||
if (not np.isfinite(amax)) or amax <= _EPS:
|
||||
return np.zeros_like(x, dtype=np.float64)
|
||||
return x / amax
|
||||
return np.zeros_like(arr, dtype=np.float64)
|
||||
return arr / amax
|
||||
|
||||
|
||||
def normalize_sweep_for_phase(sweep: np.ndarray) -> np.ndarray:
|
||||
"""Совместимый alias: нормировка свипа перед восстановлением фазы."""
|
||||
return normalize_trace_unit_range(sweep)
|
||||
|
||||
|
||||
def unwrap_arccos_phase_continuous(x_norm: np.ndarray) -> np.ndarray:
|
||||
@ -101,6 +110,7 @@ def unwrap_arccos_phase_continuous(x_norm: np.ndarray) -> np.ndarray:
|
||||
phi0 = np.arccos(x)
|
||||
|
||||
out = np.empty_like(phi0, dtype=np.float64)
|
||||
out[0] = float(phi0[0])
|
||||
for i in range(1, phi0.size):
|
||||
base_phi = float(phi0[i])
|
||||
prev = float(out[i - 1])
|
||||
@ -110,7 +120,6 @@ def unwrap_arccos_phase_continuous(x_norm: np.ndarray) -> np.ndarray:
|
||||
|
||||
for sign in (1.0, -1.0):
|
||||
base = sign * base_phi
|
||||
# Ищем ближайший сдвиг 2πk относительно prev именно для этой ветви.
|
||||
k_center = int(np.round((prev - base) / _TWO_PI))
|
||||
for k in (k_center - 1, k_center, k_center + 1):
|
||||
cand = base + _TWO_PI * float(k)
|
||||
@ -122,20 +131,55 @@ def unwrap_arccos_phase_continuous(x_norm: np.ndarray) -> np.ndarray:
|
||||
best_cand = cand
|
||||
|
||||
out[i] = prev if best_cand is None else float(best_cand)
|
||||
|
||||
return out
|
||||
return phi0
|
||||
|
||||
def reconstruct_complex_spectrum_from_real_trace(sweep: np.ndarray) -> np.ndarray:
|
||||
"""Восстановить комплексный спектр из вещественного следа через arccos+Euler."""
|
||||
x_norm = normalize_sweep_for_phase(sweep)
|
||||
|
||||
def reconstruct_complex_spectrum_arccos(sweep: np.ndarray) -> np.ndarray:
|
||||
"""Режим arccos: cos(phi) -> phi -> exp(i*phi)."""
|
||||
x_norm = normalize_trace_unit_range(sweep)
|
||||
if x_norm.size == 0:
|
||||
return np.zeros((0,), dtype=np.complex128)
|
||||
x_norm = np.clip(x_norm, -1.0, 1.0)
|
||||
phi = unwrap_arccos_phase_continuous(x_norm)
|
||||
phi = unwrap_arccos_phase_continuous(np.clip(x_norm, -1.0, 1.0))
|
||||
return np.exp(1j * phi).astype(np.complex128, copy=False)
|
||||
|
||||
|
||||
def reconstruct_complex_spectrum_diff(sweep: np.ndarray) -> np.ndarray:
|
||||
"""Режим diff: x~=cos(phi), diff(x)->sin(phi), z=cos+i*sin с проекцией на единичную окружность."""
|
||||
cos_phi = normalize_trace_unit_range(sweep)
|
||||
if cos_phi.size == 0:
|
||||
return np.zeros((0,), dtype=np.complex128)
|
||||
cos_phi = np.clip(cos_phi, -1.0, 1.0)
|
||||
|
||||
if cos_phi.size < 2:
|
||||
sin_est = np.zeros_like(cos_phi, dtype=np.float64)
|
||||
else:
|
||||
d = np.gradient(cos_phi)
|
||||
sin_est = normalize_trace_unit_range(d)
|
||||
sin_est = np.clip(sin_est, -1.0, 1.0)
|
||||
|
||||
z = cos_phi.astype(np.complex128, copy=False) + 1j * sin_est.astype(np.complex128, copy=False)
|
||||
mag = np.abs(z)
|
||||
z_unit = np.ones_like(z, dtype=np.complex128)
|
||||
mask = mag > _EPS
|
||||
if np.any(mask):
|
||||
z_unit[mask] = z[mask] / mag[mask]
|
||||
return mag
|
||||
|
||||
|
||||
def reconstruct_complex_spectrum_from_real_trace(
|
||||
sweep: np.ndarray,
|
||||
*,
|
||||
complex_mode: str = "arccos",
|
||||
) -> np.ndarray:
|
||||
"""Восстановить комплексный спектр из вещественного свипа в выбранном режиме."""
|
||||
mode = _normalize_complex_mode(complex_mode)
|
||||
if mode == "arccos":
|
||||
return reconstruct_complex_spectrum_arccos(sweep)
|
||||
if mode == "diff":
|
||||
return reconstruct_complex_spectrum_diff(sweep)
|
||||
raise ValueError(f"Unsupported complex reconstruction mode: {complex_mode!r}")
|
||||
|
||||
|
||||
def perform_ifft_depth_response(
|
||||
s_array: np.ndarray,
|
||||
frequencies_hz: np.ndarray,
|
||||
@ -173,7 +217,6 @@ def perform_ifft_depth_response(
|
||||
n = int(f.size)
|
||||
if n < 2:
|
||||
raise ValueError("Not enough frequency points after filtering")
|
||||
|
||||
if np.any(np.diff(f) <= 0.0):
|
||||
raise ValueError("Non-increasing frequency grid")
|
||||
|
||||
@ -221,7 +264,11 @@ def perform_ifft_depth_response(
|
||||
return _fallback_depth_response(np.asarray(s_array).size, np.asarray(s_array))
|
||||
|
||||
|
||||
def compute_ifft_profile_from_sweep(sweep: Optional[np.ndarray]) -> tuple[np.ndarray, np.ndarray]:
|
||||
def compute_ifft_profile_from_sweep(
|
||||
sweep: Optional[np.ndarray],
|
||||
*,
|
||||
complex_mode: str = "arccos",
|
||||
) -> tuple[np.ndarray, np.ndarray]:
|
||||
"""Высокоуровневый pipeline: sweep -> complex spectrum -> IFFT(abs) depth profile."""
|
||||
if sweep is None:
|
||||
return _fallback_depth_response(1, None)
|
||||
@ -232,12 +279,12 @@ def compute_ifft_profile_from_sweep(sweep: Optional[np.ndarray]) -> tuple[np.nda
|
||||
return _fallback_depth_response(1, None)
|
||||
|
||||
freqs_hz = build_frequency_axis_hz(s.size)
|
||||
s_complex = reconstruct_complex_spectrum_from_real_trace(s)
|
||||
s_complex = reconstruct_complex_spectrum_from_real_trace(s, complex_mode=complex_mode)
|
||||
depth_m, y = perform_ifft_depth_response(s_complex, freqs_hz, axis="abs")
|
||||
n = min(depth_m.size, y.size)
|
||||
if n <= 0:
|
||||
return _fallback_depth_response(s.size, s)
|
||||
return depth_m[:n].astype(np.float128, copy=False), y[:n].astype(np.float128, copy=False) *20
|
||||
return depth_m[:n].astype(np.float32, copy=False), y[:n].astype(np.float32, copy=False) # log10 для лучшей визуализации в водопаде
|
||||
except Exception as exc: # noqa: BLE001
|
||||
logger.error("compute_ifft_profile_from_sweep failed: %r", exc)
|
||||
return _fallback_depth_response(np.asarray(sweep).size if sweep is not None else 1, sweep)
|
||||
@ -245,6 +292,6 @@ def compute_ifft_profile_from_sweep(sweep: Optional[np.ndarray]) -> tuple[np.nda
|
||||
|
||||
def compute_ifft_db_profile(sweep: Optional[np.ndarray]) -> np.ndarray:
|
||||
"""Legacy wrapper (deprecated name): возвращает линейный |IFFT| профиль."""
|
||||
_depth_m, y = compute_ifft_profile_from_sweep(sweep)
|
||||
_depth_m, y = compute_ifft_profile_from_sweep(sweep, complex_mode="arccos")
|
||||
return y
|
||||
|
||||
|
||||
@ -24,6 +24,7 @@ class RingBuffer:
|
||||
self.max_sweeps = max_sweeps
|
||||
# Размер IFFT-профиля теперь динамический и определяется по первому успешному свипу.
|
||||
self.fft_bins = 0
|
||||
self.fft_complex_mode: str = "arccos"
|
||||
|
||||
# Инициализируются при первом свипе (ensure_init)
|
||||
self.ring: Optional[np.ndarray] = None # (max_sweeps, WF_WIDTH)
|
||||
@ -47,6 +48,26 @@ class RingBuffer:
|
||||
"""Legacy alias: старое имя поля (раньше было время в нс, теперь глубина в м)."""
|
||||
return self.fft_depth_axis_m
|
||||
|
||||
def set_fft_complex_mode(self, mode: str) -> bool:
|
||||
"""Выбрать режим реконструкции комплексного спектра для IFFT.
|
||||
|
||||
Возвращает True, если режим изменился (и FFT-буфер был сброшен).
|
||||
"""
|
||||
m = str(mode).strip().lower()
|
||||
if m not in ("arccos", "diff"):
|
||||
raise ValueError(f"Unsupported IFFT complex mode: {mode!r}")
|
||||
if m == self.fft_complex_mode:
|
||||
return False
|
||||
self.fft_complex_mode = m
|
||||
# Сбрасываем только FFT-зависимые структуры. Сырые ряды сохраняем.
|
||||
self.ring_fft = None
|
||||
self.fft_depth_axis_m = None
|
||||
self.fft_bins = 0
|
||||
self.last_fft_vals = None
|
||||
self.y_min_fft = None
|
||||
self.y_max_fft = None
|
||||
return True
|
||||
|
||||
def ensure_init(self, sweep_width: int):
|
||||
"""Инициализировать буферы при первом свипе. Повторные вызовы — no-op (кроме x_shared)."""
|
||||
if self.ring is None:
|
||||
@ -73,7 +94,10 @@ class RingBuffer:
|
||||
self._push_fft(s)
|
||||
|
||||
def _push_fft(self, s: np.ndarray):
|
||||
depth_axis_m, fft_row = compute_ifft_profile_from_sweep(s)
|
||||
depth_axis_m, fft_row = compute_ifft_profile_from_sweep(
|
||||
s,
|
||||
complex_mode=self.fft_complex_mode,
|
||||
)
|
||||
fft_row = np.asarray(fft_row, dtype=np.float32).ravel()
|
||||
depth_axis_m = np.asarray(depth_axis_m, dtype=np.float32).ravel()
|
||||
|
||||
|
||||
54
tests/test_fourier_complex_modes.py
Normal file
54
tests/test_fourier_complex_modes.py
Normal file
@ -0,0 +1,54 @@
|
||||
import numpy as np
|
||||
|
||||
from rfg_adc_plotter.processing.fourier import (
|
||||
compute_ifft_profile_from_sweep,
|
||||
reconstruct_complex_spectrum_from_real_trace,
|
||||
)
|
||||
|
||||
|
||||
def test_reconstruct_complex_spectrum_arccos_mode_returns_complex128():
|
||||
sweep = np.linspace(-3.0, 7.0, 128, dtype=np.float32)
|
||||
z = reconstruct_complex_spectrum_from_real_trace(sweep, complex_mode="arccos")
|
||||
assert z.dtype == np.complex128
|
||||
assert z.shape == sweep.shape
|
||||
assert np.all(np.isfinite(np.real(z)))
|
||||
assert np.all(np.isfinite(np.imag(z)))
|
||||
|
||||
|
||||
def test_reconstruct_complex_spectrum_diff_mode_returns_complex128():
|
||||
sweep = np.linspace(-1.0, 1.0, 128, dtype=np.float32)
|
||||
z = reconstruct_complex_spectrum_from_real_trace(sweep, complex_mode="diff")
|
||||
assert z.dtype == np.complex128
|
||||
assert z.shape == sweep.shape
|
||||
assert np.all(np.isfinite(np.real(z)))
|
||||
assert np.all(np.isfinite(np.imag(z)))
|
||||
|
||||
|
||||
def test_reconstruct_complex_spectrum_diff_mode_projects_to_unit_circle():
|
||||
sweep = np.sin(np.linspace(0.0, 6.0 * np.pi, 256)).astype(np.float32)
|
||||
z = reconstruct_complex_spectrum_from_real_trace(sweep, complex_mode="diff")
|
||||
mag = np.abs(z)
|
||||
assert np.all(np.isfinite(mag))
|
||||
assert np.allclose(mag, np.ones_like(mag), atol=1e-5, rtol=0.0)
|
||||
|
||||
|
||||
def test_compute_ifft_profile_from_sweep_accepts_both_modes():
|
||||
sweep = np.linspace(-5.0, 5.0, 257, dtype=np.float32)
|
||||
d1, y1 = compute_ifft_profile_from_sweep(sweep, complex_mode="arccos")
|
||||
d2, y2 = compute_ifft_profile_from_sweep(sweep, complex_mode="diff")
|
||||
|
||||
assert d1.dtype == np.float32 and y1.dtype == np.float32
|
||||
assert d2.dtype == np.float32 and y2.dtype == np.float32
|
||||
assert d1.size == y1.size and d2.size == y2.size
|
||||
assert d1.size > 0 and d2.size > 0
|
||||
assert np.all(np.diff(d1) >= 0.0)
|
||||
assert np.all(np.diff(d2) >= 0.0)
|
||||
|
||||
|
||||
def test_invalid_complex_mode_falls_back_deterministically_in_outer_wrapper():
|
||||
sweep = np.linspace(-1.0, 1.0, 64, dtype=np.float32)
|
||||
depth, y = compute_ifft_profile_from_sweep(sweep, complex_mode="unknown")
|
||||
assert depth.dtype == np.float32
|
||||
assert y.dtype == np.float32
|
||||
assert depth.size == y.size
|
||||
assert depth.size > 0
|
||||
@ -38,3 +38,28 @@ def test_ring_buffer_reallocates_fft_buffers_when_ifft_length_changes():
|
||||
assert second_shape == (ring.max_sweeps, second_bins)
|
||||
assert ring.fft_depth_axis_m is not None
|
||||
assert ring.fft_depth_axis_m.size == second_bins
|
||||
|
||||
|
||||
def test_ring_buffer_mode_switch_resets_fft_buffers_only():
|
||||
ring = RingBuffer(max_sweeps=4)
|
||||
ring.ensure_init(128)
|
||||
|
||||
ring.push(np.linspace(-1.0, 1.0, 128, dtype=np.float32))
|
||||
assert ring.ring is not None
|
||||
assert ring.ring_fft is not None
|
||||
raw_before = ring.ring.copy()
|
||||
|
||||
changed = ring.set_fft_complex_mode("diff")
|
||||
assert changed is True
|
||||
assert ring.fft_complex_mode == "diff"
|
||||
assert ring.ring is not None
|
||||
assert np.array_equal(ring.ring, raw_before, equal_nan=True)
|
||||
assert ring.ring_fft is None
|
||||
assert ring.fft_depth_axis_m is None
|
||||
assert ring.last_fft_vals is None
|
||||
assert ring.fft_bins == 0
|
||||
|
||||
ring.push(np.linspace(-1.0, 1.0, 128, dtype=np.float32))
|
||||
assert ring.ring_fft is not None
|
||||
assert ring.fft_depth_axis_m is not None
|
||||
assert ring.last_fft_vals is not None
|
||||
|
||||
Reference in New Issue
Block a user