From c199ab7f281cb3b550148356b044e0ef6fbe36f3 Mon Sep 17 00:00:00 2001 From: awe Date: Fri, 27 Feb 2026 17:43:32 +0300 Subject: [PATCH] last implement diff --- calib_envelope.npy | Bin 3164 -> 3164 bytes rfg_adc_plotter/gui/matplotlib_backend.py | 35 ++++++- rfg_adc_plotter/gui/pyqtgraph_backend.py | 43 +++++++++ rfg_adc_plotter/main.py | 9 ++ rfg_adc_plotter/processing/fourier.py | 107 ++++++++++++++++------ rfg_adc_plotter/state/ring_buffer.py | 26 +++++- tests/test_fourier_complex_modes.py | 54 +++++++++++ tests/test_ring_buffer_fft_axis.py | 25 +++++ 8 files changed, 265 insertions(+), 34 deletions(-) create mode 100644 tests/test_fourier_complex_modes.py diff --git a/calib_envelope.npy b/calib_envelope.npy index 394f3f936a61f988a874964ec6d9aee01b5ff1ca..9e96279f26c94c48b5d49c737fb8b9bf0dad4790 100644 GIT binary patch literal 3164 zcmbW1Yiv|i6h<#d5eg_E5QC@#)=a^w$N&NsnTKE{hz)vCG|^6IM=(5Uc}NiSQfMvE zC_;lqn#VM+hiX0uAaEh$sO9Sg~NORRM(*RO0&X9e>!wKTfi;zq8NY`|R_$ zvueEgWcs8w#$4ldcV^bC$vN&3E_Z5Zusg-&4$aJ&ofDk#(#)L9EcwR-r_IX3KWj=b zJBxAh!$XFr^iOdubp7vNqG9~vH;lx9VcZG9fMKl+nBQy&R1MgJJ_5DyDQsu{1ME@= zp+^Kv^RYlxbUbE&fK{Ts@C8paVj9o|CIlU0dTAJj9-vFor! z*u~V#L({v(9*h^LH!Z6PVQjzKzdDKoD8=@v)`~fVf!-o z5Y~^KfSpY3snpI!vp>d$@l-K$_W+Jt8 zZ(64h+z)p{H@F!T_YarRO>h);QY#E|(W(AONe~wML!s4vk7pykPv9?5t$V-?_rZhE z1&%Y<5sSZub*=8~y#wPvso6}gXP7^XK8!ws)|$sac_-mC$aerv!A0V(K^tnc2krR_ zI1B1~7c8K+H(`;#E|yELOYr5hMmc$V(5ih2$hU5zhR$6*4}~P4M?>{>emc)%|@NdJv z1s#PlSOJScXR12918Xz(Gpx?Ck(@K+e1)ctwHLbstGm}->Wow)o@?(zG4-JP?9Ts1 z7ru49({|`Th}VC%&b&3(@#)*_Nqi?_+oCTMe*+TG`cKikzO$P^cdi=j(b8MuTrIu9 z-SnmZoD=9q>;=Z>(G8$A)Qi3|{qNih53rV-HTnNGdSiPq*8iaXqY}{C$8~C4qgD&~ zE$B<=Ca0#Qy41I{#)bHLde1sL-Lc}D;VS6cOrUNeb-SW_p>IL!KTmU)iBY}dKkg@ulhn!u)>1w7SB-x& zTCqDA??)en8rT}Q8rL(oo0wvBMl0qqF0kjSqFPsZhoBx>$UDbe9a`%}(Piir;Pl{} zS2#Xa?g2Ovw;Gp&;w^IXsIkl*JA0dhRzFK1Y|lC8oq^7^dvfZR+BwQqo^xL5SQpfL zBPA!j9?vmvT`cAeg=)R*A388M5R&6@vKkKqFQh@bFBBT%tBa-jJRXlPQqmuvYIX$! u>b<;YuW4SPp2p>}-~umeg&?2xOIUjoz8d1A#22G2$gtxKJI5M@HU0)WWnEMN literal 3164 zcmbW3X>3$g6o9W5B26N-hDrRTLH`&G5>5Qhn=uf7+T_dG&pqef*TTWY z_M-BY_o>Zln>Sd~xVFJt=<&`8P4nh^yrGQ^O$}8W)@*DD)`-8fs;;pHe`9S`eGT^b z83nWQC**sc_5AOjbfpekN_SdHby=}XA6fR{uPqh0Va4)(wCr2oTgv&)vaesWV!vD_ zCvK_4SNJYjD)J?H*DU3^f$wKab^Sn}xMeqAwqon968n}OpV8wS`6r1ZBcIaebIUHE zUU%c;ti%=ci}Z<-$1E8UYNJ-H{fuRwJ5Bu5U1Uzr3G$9odyIY^%=WSsOK-Q7(L(JZ zayiR$gdT02znAk5k{{-b1LTkw*n(WmE+PCI@o%Ni4(gs`?ncYbUW>niyjpVb>pJ8r z%eGfov8au{4Bt{@Id#?aYUa+W*=q^|3ovg6?-$e zLE^R4KSeF`MAqX&k8Hwzn%qiqr&3>pe<892+aNZZoEi9h*kh1c#0H_~&@Y=^`jgX_ zJ^JB8chHS=Y^2U0SHTTaslQC6`=AfSPraf~Un^6tPLfkWO~$Oy){( zwwxQu#Yap_TyjTag9hp2Fo)xTDal;@0KJ&U=u7{B%*&a2JTc-Y4bx z*aJBaOVog3+LcG_6Nv|$d66sTtI&ZqR`2VXYhBy z+wd0gH}Qi$m5i%)WDvU+xgH5>0|cPLj72L@(W}v`(4|JC z_}9XV^ozg=2xGs5d>+|~1a%A?JBsWipL(_L zF2*kWE%=eHpIWsYnxKx@3S^+yuaLwkwa`Su|e3Nhau(L$V2iz254~KO1=Z$ zPUS*>V%fx~ccg}Q6)Cp(iQ9HM=cJ+E@hh#6J%~Sme2|>pN!{t0^eb%j5DWn3QPLw5 z`hk3F;**)>+n5Brq5D1Z9m#vUF%LZ@X998}l6ejG&@vmly1ixgu|U219a7^kvm2AM z??v{KedP}1JkHYec71ZD=+d9KnuX-f-S?O8R_>lVcXznN{@fS8ceXv9JCR<}SL*q_ zaQBd%#(|uf1+uTq%6zh$Gm`!zm|5aO=`oyM?B&+nkNl7`xL>#5zcb37fBX4=2Hb*S z)MTXI^Fd;M zx2IAwvE`0szI^;DHU>CH4ChrCE+R-Ie50alz?DM4+$GdMDKHpU%+;PF+ hOy~l~h 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() diff --git a/rfg_adc_plotter/main.py b/rfg_adc_plotter/main.py index 059b3d4..14c888a 100755 --- a/rfg_adc_plotter/main.py +++ b/rfg_adc_plotter/main.py @@ -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", diff --git a/rfg_adc_plotter/processing/fourier.py b/rfg_adc_plotter/processing/fourier.py index 48b37b4..8dc3a2c 100644 --- a/rfg_adc_plotter/processing/fourier.py +++ b/rfg_adc_plotter/processing/fourier.py @@ -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 diff --git a/rfg_adc_plotter/state/ring_buffer.py b/rfg_adc_plotter/state/ring_buffer.py index 52675e9..5769758 100644 --- a/rfg_adc_plotter/state/ring_buffer.py +++ b/rfg_adc_plotter/state/ring_buffer.py @@ -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() diff --git a/tests/test_fourier_complex_modes.py b/tests/test_fourier_complex_modes.py new file mode 100644 index 0000000..79d6b7e --- /dev/null +++ b/tests/test_fourier_complex_modes.py @@ -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 diff --git a/tests/test_ring_buffer_fft_axis.py b/tests/test_ring_buffer_fft_axis.py index d145868..056e87e 100644 --- a/tests/test_ring_buffer_fft_axis.py +++ b/tests/test_ring_buffer_fft_axis.py @@ -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