From 1e05b1f3fda3a9060bbf25f086d7a9e760985c4a Mon Sep 17 00:00:00 2001 From: awe Date: Mon, 2 Mar 2026 15:43:41 +0300 Subject: [PATCH] 3 freq diversion --- ().npy | Bin 3164 -> 3164 bytes calib_envelope.npy | Bin 3164 -> 3164 bytes rfg_adc_plotter/gui/matplotlib_backend.py | 49 ++++++++---- rfg_adc_plotter/gui/pyqtgraph_backend.py | 42 ++++++++--- rfg_adc_plotter/processing/fourier.py | 6 +- rfg_adc_plotter/state/ring_buffer.py | 87 +++++++++++++++++++++- tests/test_ring_buffer_fft_axis.py | 36 +++++++++ 7 files changed, 193 insertions(+), 27 deletions(-) diff --git a/().npy b/().npy index 60cc29d56073f46a1140488d6ffaa3d58e017ac6..af564e01aa55de4c0e48867e98102607a622da93 100644 GIT binary patch literal 3164 zcmbW2eN2^g6vrQ5Q*uR{W?Q=9!l6Y*(uDUvTYB)cOuG9V;1d_fVdk?Ce;i(GSkzR&a6+n=$0cFymd^E>DK&hxuJ z3a9y|PJgABvD|noE+uv8>zQ#AV&W#IJrWlm6PLCmb6Mux#jh>NOi7h}(%gkhQ?V~i zpPP}&deq~Oj*lN5AG0dvfB!`p<}t6~`q66`H{m*5f=)Q$H6nYwhFR?O`DS@T!P~KA zc<@0idNTV8@P~;z$oeQT$B8|KehzyF@(jKJ@;Pr==|kSc#KFj)JP?*$cGk?ZzO1j_rVh|gSmN_pUzJ{b0|{%Um^YgbJ~Oa7+Hbz!*@VE z^G;&!hT%XyGYYBtZJ@ffpxObL4Lxqdh(^Xj7W~uS%v$}QhI&wrZcixK$t+Jg>&wVK z)QAT4r|+kl1K>KocBm(QEBn@>r-14dz!oTi9MC;Vf@Pq%GN^c5yDUn z3!pDdgLO~^5ztu=Qga8jOVP8D^I1%xa6YAhy zNPu~uUYA2M`4_=E=;}lET+rFFVKL}@D?w+^0`>4T#Cp=w#_$^$$363U%1WnVONOy9 z2yTPkaD$$%xG^{06!GnY(I zuSN8bM?X2px9Q~#Y|5kl-vkq~VFI*MzdvUlfj$wUp^-jjBCD})a&j(2e*yh5?889$ zR9~~K0DXrH{4WvrBzml+hk~l3o)xPY^{4r%RuAVMZN*u$GA(Ul~5li92ZOj?{o=r{7Hp^YQb&0?eqA8R_@5 zgBn3jY!J8_;o(l4U_gnkYE2Hb>R{HFVmqaSab z-V6Q4`-1$Jp%au>IeS2Hiq}5Fsr^0nc5*uGY5Emn;EW*tP= zO!pJB7rhoF>p(N!58?kH|8A&d-!As)9@aRz_RDAE^L>h6dF(p&Im6t~Pq>3SxsO5a zV-@}_#Oa>wL)Uk1K>xzor(6|K1iB|BPzJg~`ai0>C4V)!0^}?q_XcX{Zf4=jA%;83 zJ@@(YSl@aF^}9-kBy5xLjYp4%iSWE-x8JH1{4e5nV@p5|XB~xp)q3yv?%4V{wrESY z-@;D(Hlbh}Z^I$hVdNg(s9N+I2tYoVPz2SEzX81!c0&pASuhhl0lAi#&(KxB9U@Q- z%BMG1dI{tbx7ylk=U->pRoi}_yIHG0f&921a!rwJ9ha*5@=63gxv+PyWAru2i^M?ZFNsB@Vig%Ep{(Hy&j)(X25hv zppR6@L)Uy(!!ziHyR3AyJ27#FySZs4ve5F|^VHsaXC2`TI&&p!_2h>DXx^&xZ|qsG zz^)wXe-7w;)2&(ZhNJ5{RROarm{Ug5vcS?n`(GOKaeP2v_{ z^P#&eskNP3{V9Kj<>P-EQazOrr&**rc9$bZA)}eiKxWbh`MayR=`wXx>na%d2Usz- z)LM4=_5C6)-X&KkcnRI0uPAB^AtnZf!96e-)Q9#2ARkPa11^XG!_iG-fSkqTT#K&t IEnKcY0W&*aDF6Tf literal 3164 zcmbW1drXyO9LFCk6`3ZQc$*qI5>zr9SY|E{oU^jEk}cmYyNLyO$y-3?a@M1X8fo6r zl(dL0({s>Jw9HGsb82KzgtExYofMQDQ^GXTtUlkD<7WLC+h@Pa^SeF2=RK@?)OjR* zTpKgX%t^@1%6%>;VMJWQ$n2Daq_~9a898}56Q+-!k&~Gv{{s`I=4Ro~oit%)7Ixx2 z!|qKQm=rfR?tlM98~d%_6kYY3aBRRdB?XKL1dL5bK8>7=9FH6uaAMN~q2R*-<9iTN z;XW7!BVbg(g#E;g4mhhv5;rtp+<1Jqk$-C-bhdZEsp*cdYrtd>Z=U6BBK9 zz7*`9s8Ack-*B847sOEXUQ*+868dU2ywgPrT5I%zg;KCli@m2YasY9s`#9Z-LR$TBG_G|Y%Z}r(!G8oj%VI6q|_-3KcKu*TaN3MzZ=i;Zf zU5{Oee=T|lxexnYcFv?aJMNO<(gipv!N@arP>bT%YEC z9l02L5xVYUK{h?iO5i^3dz>+YPj{n-I}dvyy858I$^q$y8g>D1XZ?<&dDxHTZnn%H z3YOt7!?zuK3vv^=8_~rw>}m8+jU`CtQV$oYT^sFe5#K{Tb=5=*pu*~PXHs@^oFW{Sk%!Dy80*1rgkiyv=$OL!@o(JuncXD-adinH5?8oJO z4Eu2lvAL(wyf2~8LJHo;^RZ7ampVTAv~OlJPja70?B+#m-JRN&ck$kCGxjF<2=u;s zOZBI&eGGXQxkJbuj6M*$!Cz^{wnxTrb_-|S(R&j=0GSM9fnMI7l`{nJ+ju0gUT$-m z<6MQC@I}MvG-Gzc5jcnc=d@7pD0(?m!*Nhd6O_SJ*aggDPQoF$05R0+05`xT>^(3a zR>MZ-UXNUX%tz+Y=MDPEw*`GKdIh?AmB4I}eKWN*=WpoYR-gS2`8{3M(3Tkb z+bCok&^&ecYpGp{z8^I6HV8s7{vDj_j;eDC&cd(I!dZKE+m$-~*mGxegI-6^^PFEv zGrk+KJK%2vnoavr-_Fn%29wvHvtH<3_$GKe+k zsU}C?f!_Nd=zG`)d%%SQP)^L3#3~nTB*yN>=J#Q@Bex9p`-*NIev*@bv1dN1!U?2wKU??O(BE%EloxBd{EvbAT z5Khg`KAl=w(U_W?d?B@P-5>Z2F&9&LSMQv+KvqFHD6dq14n{udL1Kvy=Ro?bDX~DiIEuXQ2W139qmjr=v`EQCqCV$+Uun|w%%X!RC893U%8rd zFM1Jr5M6V+$j#V==-To1OvN4z@n9edVj_Cj$M=Xn6P^Oar9d3`AOjwUbkLc6lCp<` n;#5y{O2NWR$bfY4U59+u7Ya`CIgaD`L%~7lvFJu;qmbqw!i#r} diff --git a/calib_envelope.npy b/calib_envelope.npy index 9e96279f26c94c48b5d49c737fb8b9bf0dad4790..fc87840309c2c89886cdf68e634b66a2b54ba7bd 100644 GIT binary patch literal 3164 zcmbW3eRPgx9LFyjcD(G&A5;!mbk3GWj(HhL_w#I|!gALe4^3UZ$u%-^X1io$8OA^Etoo^}Bv=*LB^`j%7pa z!M;%q%nb8FY--xniCM9IVq*KIcaM#aiA|r9H7zS;^0+BkscG^LNXeL*hJWfaDVb@k z<9hYz9e;0p%&eII{jaGpWnN027#D0J#&w7t_J~OOj?xpB#{Ic|wc3uWQEWtksdO$b0 zpPF83>)zy_1~0-2P)+W|6fiq|oTK#F$9mh%8J%2uEg=6nd_&nEh_#>(bZ6Zj+YG+t zy^6hgxre+72|sw#)0-qEB;1Za*3F@>!<#t^f%%-})JJpo#?oCei^cqA^amPi>9|jZ8I9n%!c{H-;(Yz3(|qv%mf$@x~Hz7H_%;q;ZYby z-Y9H}dtTnp()+D;=i+;CRO12qCUK_6(2t{sqsdi%xW3Lc1FheI4cvp%1>23d_Qbb9 zU*Szo!d`w)$N62=p&Q}v01pwbJEWI02KxlogN-M*2Q~Vkbq3vU6nhP^7pU_KM4+1y z*OEBB@m-+%ZA)BxY)Aa9*>8%zPEY-ouaI|@8S2qLdyUhKy*9)v?{4S?gURudE8Q7% zhHBz>z-o93BA_7{=m{CH0Zvf+7(MhiA#5Uvn&mi!as z>kamy%g`U9cfwv!?<1g?lk9(q{_nr{wjD|B=JaVv|47hzF5H5A+EYw05JimQ_3i1K zy+rIO&H&~#7R;Gj2nNoO{~f-wa31tKxeC|uUt><4O?7Ia29Di=ZGTQo6+XS&XK0;s zAH6EE$K9S6!?kR?0W&pbMgw{i)zoad3)MIQ>T!xX%DI8oZ&qjd6)5uw7tOBwJqLB{YaaD3fdJ?`<+~04*51Z$BxV~iA@qLiZnu6B=L7wg z9Q~e@tN21F0M%Gbyr1|v=$Y7gth3R}VL{l(-({>1{|NTSqEoS>SPwxb|Am3*L7?BH zv;~Qv82QFPUuX;U9^M5Spd+Yv*%J(WWUmU;Q!%Qq^M=A}{&rF;AMFSAP_H$hJMcj!Xzvey5xO{> z6AUa2@9WI`UlgsG6|Xx=!#BxYkHlIK2hk7-20|7&Am4c6hmqGGorvy)ZikNcTP1*jmfBKc>zax1NQK0Rxc`4R?TianKbKL2>_tv3>_| C@NQ55 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 diff --git a/rfg_adc_plotter/gui/matplotlib_backend.py b/rfg_adc_plotter/gui/matplotlib_backend.py index 161a9a9..774b6b3 100644 --- a/rfg_adc_plotter/gui/matplotlib_backend.py +++ b/rfg_adc_plotter/gui/matplotlib_backend.py @@ -146,10 +146,13 @@ def run_matplotlib(args): ax_line.set_ylim(fixed_ylim) # График спектра - fft_line_obj, = ax_fft.plot([], [], lw=1) + fft_line_t1, = ax_fft.plot([], [], lw=1, color="tab:blue", label="1/3 (low f)") + fft_line_t2, = ax_fft.plot([], [], lw=1, color="tab:orange", label="2/3 (mid f)") + fft_line_t3, = ax_fft.plot([], [], lw=1, color="tab:green", label="3/3 (high f)") ax_fft.set_title("FFT", pad=1) ax_fft.set_xlabel("Глубина, м") ax_fft.set_ylabel("Амплитуда") + ax_fft.legend(loc="upper right", fontsize=8) # Водопад сырых данных img_obj = ax_img.imshow( @@ -435,6 +438,9 @@ def run_matplotlib(args): ring.set_fft_complex_mode(str(label)) except Exception: pass + fft_line_t1.set_data([], []) + fft_line_t2.set_data([], []) + fft_line_t3.set_data([], []) _refresh_status_texts() try: fig.canvas.draw_idle() @@ -584,18 +590,31 @@ def run_matplotlib(args): ax_line.autoscale_view(scalex=False, scaley=True) ax_line.set_ylabel("Y") - # Профиль по глубине — используем уже вычисленный в ring IFFT. - if ring.last_fft_vals is not None and ring.fft_depth_axis_m is not None: - fft_vals = ring.last_fft_vals - xs_fft = ring.fft_depth_axis_m - n = min(fft_vals.size, xs_fft.size) - if n > 0: - fft_line_obj.set_data(xs_fft[:n], fft_vals[:n]) - else: - fft_line_obj.set_data([], []) - if n > 0 and np.isfinite(np.nanmin(fft_vals)) and np.isfinite(np.nanmax(fft_vals)): - ax_fft.set_xlim(0, float(xs_fft[n - 1])) - ax_fft.set_ylim(float(np.nanmin(fft_vals)), float(np.nanmax(fft_vals))) + # Профиль по глубине: три линии для 1/3, 2/3, 3/3 частотного диапазона. + third_axes = ring.last_fft_third_axes_m + third_vals = ring.last_fft_third_vals + lines = (fft_line_t1, fft_line_t2, fft_line_t3) + xs_max = [] + ys_min = [] + ys_max = [] + for line_fft, xs_fft, fft_vals in zip(lines, third_axes, third_vals): + if xs_fft is None or fft_vals is None: + line_fft.set_data([], []) + continue + n = min(int(xs_fft.size), int(fft_vals.size)) + if n <= 0: + line_fft.set_data([], []) + continue + x_seg = xs_fft[:n] + y_seg = fft_vals[:n] + line_fft.set_data(x_seg, y_seg) + xs_max.append(float(x_seg[n - 1])) + ys_min.append(float(np.nanmin(y_seg))) + ys_max.append(float(np.nanmax(y_seg))) + + if xs_max and ys_min and ys_max: + ax_fft.set_xlim(0, float(max(xs_max))) + ax_fft.set_ylim(float(min(ys_min)), float(max(ys_max))) # Водопад сырых данных if changed and ring.is_ready: @@ -645,7 +664,9 @@ def run_matplotlib(args): line_env_lo, line_env_hi, img_obj, - fft_line_obj, + fft_line_t1, + fft_line_t2, + fft_line_t3, img_fft_obj, status_text, pipeline_text, diff --git a/rfg_adc_plotter/gui/pyqtgraph_backend.py b/rfg_adc_plotter/gui/pyqtgraph_backend.py index c164d7f..58d4c12 100644 --- a/rfg_adc_plotter/gui/pyqtgraph_backend.py +++ b/rfg_adc_plotter/gui/pyqtgraph_backend.py @@ -202,7 +202,9 @@ def run_pyqtgraph(args): # FFT (слева-снизу) p_fft = win.addPlot(row=1, col=0, title="FFT") p_fft.showGrid(x=True, y=True, alpha=0.3) - curve_fft = p_fft.plot(pen=pg.mkPen((255, 120, 80), width=1)) + curve_fft_t1 = p_fft.plot(pen=pg.mkPen((80, 120, 255), width=1)) + curve_fft_t2 = p_fft.plot(pen=pg.mkPen((255, 140, 70), width=1)) + curve_fft_t3 = p_fft.plot(pen=pg.mkPen((60, 180, 90), width=1)) p_fft.setLabel("bottom", "Глубина, м") p_fft.setLabel("left", "Амплитуда") @@ -492,7 +494,9 @@ def run_pyqtgraph(args): changed = False if changed: try: - curve_fft.setData([], []) + curve_fft_t1.setData([], []) + curve_fft_t2.setData([], []) + curve_fft_t3.setData([], []) except Exception: pass _refresh_pipeline_label() @@ -626,15 +630,31 @@ def run_pyqtgraph(args): p_line.enableAutoRange(axis="y", enable=True) p_line.setLabel("left", "Y") - # Профиль по глубине — используем уже вычисленный в ring IFFT. - if ring.last_fft_vals is not None and ring.fft_depth_axis_m is not None: - fft_vals = ring.last_fft_vals - xs_fft = ring.fft_depth_axis_m - n = min(fft_vals.size, xs_fft.size) - if n > 0: - curve_fft.setData(xs_fft[:n], fft_vals[:n]) - p_fft.setXRange(0.0, float(xs_fft[n - 1]), padding=0) - p_fft.setYRange(float(np.nanmin(fft_vals)), float(np.nanmax(fft_vals)), padding=0) + # Профиль по глубине: три линии для 1/3, 2/3, 3/3 частотного диапазона. + third_axes = ring.last_fft_third_axes_m + third_vals = ring.last_fft_third_vals + curves = (curve_fft_t1, curve_fft_t2, curve_fft_t3) + xs_max = [] + ys_min = [] + ys_max = [] + for curve_fft, xs_fft, fft_vals in zip(curves, third_axes, third_vals): + if xs_fft is None or fft_vals is None: + curve_fft.setData([], []) + continue + n = min(int(xs_fft.size), int(fft_vals.size)) + if n <= 0: + curve_fft.setData([], []) + continue + x_seg = xs_fft[:n] + y_seg = fft_vals[:n] + curve_fft.setData(x_seg, y_seg) + xs_max.append(float(x_seg[n - 1])) + ys_min.append(float(np.nanmin(y_seg))) + ys_max.append(float(np.nanmax(y_seg))) + + if xs_max and ys_min and ys_max: + p_fft.setXRange(0.0, float(max(xs_max)), padding=0) + p_fft.setYRange(float(min(ys_min)), float(max(ys_max)), padding=0) # Позиция подписи канала try: diff --git a/rfg_adc_plotter/processing/fourier.py b/rfg_adc_plotter/processing/fourier.py index d99fe68..6d4c34a 100644 --- a/rfg_adc_plotter/processing/fourier.py +++ b/rfg_adc_plotter/processing/fourier.py @@ -156,7 +156,11 @@ def reconstruct_complex_spectrum_diff(sweep: np.ndarray) -> np.ndarray: d = np.gradient(cos_phi) sin_est = normalize_trace_unit_range(d) sin_est = np.clip(sin_est, -1.0, 1.0) - + sin_est = normalize_trace_unit_range(d) + # mag = np.abs(sin_est) + # mask = mag > _EPS + # if np.any(mask): + # sin_est[mask] = sin_est[mask] / mag[mask] 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) diff --git a/rfg_adc_plotter/state/ring_buffer.py b/rfg_adc_plotter/state/ring_buffer.py index 5769758..fc22f08 100644 --- a/rfg_adc_plotter/state/ring_buffer.py +++ b/rfg_adc_plotter/state/ring_buffer.py @@ -10,7 +10,12 @@ from rfg_adc_plotter.constants import ( FREQ_MIN_GHZ, WF_WIDTH, ) -from rfg_adc_plotter.processing.fourier import compute_ifft_profile_from_sweep +from rfg_adc_plotter.processing.fourier import ( + build_frequency_axis_hz, + compute_ifft_profile_from_sweep, + perform_ifft_depth_response, + reconstruct_complex_spectrum_from_real_trace, +) class RingBuffer: @@ -38,6 +43,17 @@ class RingBuffer: self.y_max_fft: Optional[float] = None # FFT последнего свипа (для отображения без повторного вычисления) self.last_fft_vals: Optional[np.ndarray] = None + # FFT-профили по третям входного частотного диапазона (для line-plot). + self.last_fft_third_axes_m: tuple[Optional[np.ndarray], Optional[np.ndarray], Optional[np.ndarray]] = ( + None, + None, + None, + ) + self.last_fft_third_vals: tuple[Optional[np.ndarray], Optional[np.ndarray], Optional[np.ndarray]] = ( + None, + None, + None, + ) @property def is_ready(self) -> bool: @@ -64,6 +80,8 @@ class RingBuffer: self.fft_depth_axis_m = None self.fft_bins = 0 self.last_fft_vals = None + self.last_fft_third_axes_m = (None, None, None) + self.last_fft_third_vals = (None, None, None) self.y_min_fft = None self.y_max_fft = None return True @@ -94,6 +112,11 @@ class RingBuffer: self._push_fft(s) def _push_fft(self, s: np.ndarray): + empty_thirds = ( + np.zeros((0,), dtype=np.float32), + np.zeros((0,), dtype=np.float32), + np.zeros((0,), dtype=np.float32), + ) depth_axis_m, fft_row = compute_ifft_profile_from_sweep( s, complex_mode=self.fft_complex_mode, @@ -103,6 +126,8 @@ class RingBuffer: n = min(int(fft_row.size), int(depth_axis_m.size)) if n <= 0: + self.last_fft_third_axes_m = empty_thirds + self.last_fft_third_vals = empty_thirds return if n != fft_row.size: fft_row = fft_row[:n] @@ -144,6 +169,7 @@ class RingBuffer: prev_head = (self.head - 1) % self.ring_fft.shape[0] self.ring_fft[prev_head, :] = fft_row self.last_fft_vals = fft_row + self.last_fft_third_axes_m, self.last_fft_third_vals = self._compute_fft_thirds(s) fr_min = np.nanmin(fft_row) fr_max = float(np.nanpercentile(fft_row, 90)) @@ -152,6 +178,65 @@ class RingBuffer: if self.y_max_fft is None or (not np.isnan(fr_max) and fr_max > self.y_max_fft): self.y_max_fft = float(fr_max) + def _compute_fft_thirds( + self, s: np.ndarray + ) -> tuple[tuple[np.ndarray, np.ndarray, np.ndarray], tuple[np.ndarray, np.ndarray, np.ndarray]]: + sweep = np.asarray(s, dtype=np.float64).ravel() + total = int(sweep.size) + + def _empty() -> np.ndarray: + return np.zeros((0,), dtype=np.float32) + + if total <= 0: + return (_empty(), _empty(), _empty()), (_empty(), _empty(), _empty()) + + freq_hz = build_frequency_axis_hz(total) + edges = np.linspace(0, total, 4, dtype=np.int64) + + axes: list[np.ndarray] = [] + vals: list[np.ndarray] = [] + + for idx in range(3): + i0 = int(edges[idx]) + i1 = int(edges[idx + 1]) + if i1 - i0 < 2: + axes.append(_empty()) + vals.append(_empty()) + continue + + seg = sweep[i0:i1] + seg_freq = freq_hz[i0:i1] + seg_complex = reconstruct_complex_spectrum_from_real_trace( + seg, + complex_mode=self.fft_complex_mode, + ) + depth_m, seg_fft = perform_ifft_depth_response(seg_complex, seg_freq, axis="abs") + + depth_m = np.asarray(depth_m, dtype=np.float32).ravel() + seg_fft = np.asarray(seg_fft, dtype=np.float32).ravel() + n = min(int(depth_m.size), int(seg_fft.size)) + if n <= 0: + axes.append(_empty()) + vals.append(_empty()) + continue + + depth_m = depth_m[:n] + seg_fft = seg_fft[:n] + + n_keep = max(1, (n + 1) // 2) + axes.append(depth_m[:n_keep]) + vals.append(seg_fft[:n_keep]) + + return ( + axes[0], + axes[1], + axes[2], + ), ( + vals[0], + vals[1], + vals[2], + ) + def get_display_ring(self) -> np.ndarray: """Кольцо в порядке от старого к новому, ось времени по X. Форма: (width, time).""" if self.ring is None: diff --git a/tests/test_ring_buffer_fft_axis.py b/tests/test_ring_buffer_fft_axis.py index 056e87e..499deec 100644 --- a/tests/test_ring_buffer_fft_axis.py +++ b/tests/test_ring_buffer_fft_axis.py @@ -16,6 +16,14 @@ def test_ring_buffer_allocates_fft_buffers_from_first_push(): assert ring.fft_bins == ring.ring_fft.shape[1] assert ring.fft_bins == ring.fft_depth_axis_m.size assert ring.fft_bins == ring.last_fft_vals.size + assert ring.last_fft_third_axes_m != (None, None, None) + assert ring.last_fft_third_vals != (None, None, None) + for axis, vals in zip(ring.last_fft_third_axes_m, ring.last_fft_third_vals): + assert axis is not None + assert vals is not None + assert axis.dtype == np.float32 + assert vals.dtype == np.float32 + assert axis.size == vals.size # Legacy alias kept for compatibility with existing GUI code paths. assert ring.fft_time_axis is ring.fft_depth_axis_m @@ -48,6 +56,8 @@ def test_ring_buffer_mode_switch_resets_fft_buffers_only(): assert ring.ring is not None assert ring.ring_fft is not None raw_before = ring.ring.copy() + assert ring.last_fft_third_axes_m != (None, None, None) + assert ring.last_fft_third_vals != (None, None, None) changed = ring.set_fft_complex_mode("diff") assert changed is True @@ -57,9 +67,35 @@ def test_ring_buffer_mode_switch_resets_fft_buffers_only(): assert ring.ring_fft is None assert ring.fft_depth_axis_m is None assert ring.last_fft_vals is None + assert ring.last_fft_third_axes_m == (None, None, None) + assert ring.last_fft_third_vals == (None, None, 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 + assert ring.last_fft_third_axes_m != (None, None, None) + assert ring.last_fft_third_vals != (None, None, None) + for axis, vals in zip(ring.last_fft_third_axes_m, ring.last_fft_third_vals): + assert axis is not None + assert vals is not None + assert axis.dtype == np.float32 + assert vals.dtype == np.float32 + assert axis.size == vals.size + + +def test_ring_buffer_short_sweeps_keep_third_profiles_well_formed(): + for n in (1, 2, 3): + ring = RingBuffer(max_sweeps=4) + ring.ensure_init(n) + ring.push(np.linspace(-1.0, 1.0, n, dtype=np.float32)) + + assert ring.last_fft_third_axes_m != (None, None, None) + assert ring.last_fft_third_vals != (None, None, None) + for axis, vals in zip(ring.last_fft_third_axes_m, ring.last_fft_third_vals): + assert axis is not None + assert vals is not None + assert axis.dtype == np.float32 + assert vals.dtype == np.float32 + assert axis.size == vals.size