test_spectral.py 81 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188
  1. import sys
  2. import warnings
  3. import numpy as np
  4. from numpy.testing import (assert_,
  5. assert_allclose, assert_array_equal, assert_equal,
  6. assert_array_almost_equal_nulp)
  7. import pytest
  8. from pytest import raises as assert_raises
  9. from scipy import signal
  10. from scipy._lib._array_api import xp_assert_close
  11. from scipy.fft import fftfreq, rfftfreq, fft, irfft
  12. from scipy.integrate import trapezoid
  13. from scipy.signal import (periodogram, welch, lombscargle, coherence, csd,
  14. spectrogram, check_COLA, check_NOLA)
  15. from scipy.signal.windows import hann
  16. from scipy.signal._spectral_py import _spectral_helper
  17. # Compare ShortTimeFFT.stft() / ShortTimeFFT.istft() with stft() / istft():
  18. from scipy.signal.tests._scipy_spectral_test_shim import stft_compare as stft
  19. from scipy.signal.tests._scipy_spectral_test_shim import istft_compare as istft
  20. class TestPeriodogram:
  21. def test_real_onesided_even(self):
  22. x = np.zeros(16)
  23. x[0] = 1
  24. f, p = periodogram(x)
  25. assert_allclose(f, np.linspace(0, 0.5, 9))
  26. q = np.ones(9)
  27. q[0] = 0
  28. q[-1] /= 2.0
  29. q /= 8
  30. assert_allclose(p, q)
  31. def test_real_onesided_odd(self):
  32. x = np.zeros(15)
  33. x[0] = 1
  34. f, p = periodogram(x)
  35. assert_allclose(f, np.arange(8.0)/15.0)
  36. q = np.ones(8)
  37. q[0] = 0
  38. q *= 2.0/15.0
  39. assert_allclose(p, q, atol=1e-15)
  40. def test_real_twosided(self):
  41. x = np.zeros(16)
  42. x[0] = 1
  43. f, p = periodogram(x, return_onesided=False)
  44. assert_allclose(f, fftfreq(16, 1.0))
  45. q = np.full(16, 1/16.0)
  46. q[0] = 0
  47. assert_allclose(p, q)
  48. def test_real_spectrum(self):
  49. x = np.zeros(16)
  50. x[0] = 1
  51. f, p = periodogram(x, scaling='spectrum')
  52. g, q = periodogram(x, scaling='density')
  53. assert_allclose(f, np.linspace(0, 0.5, 9))
  54. assert_allclose(p, q/16.0)
  55. def test_integer_even(self):
  56. x = np.zeros(16, dtype=int)
  57. x[0] = 1
  58. f, p = periodogram(x)
  59. assert_allclose(f, np.linspace(0, 0.5, 9))
  60. q = np.ones(9)
  61. q[0] = 0
  62. q[-1] /= 2.0
  63. q /= 8
  64. assert_allclose(p, q)
  65. def test_integer_odd(self):
  66. x = np.zeros(15, dtype=int)
  67. x[0] = 1
  68. f, p = periodogram(x)
  69. assert_allclose(f, np.arange(8.0)/15.0)
  70. q = np.ones(8)
  71. q[0] = 0
  72. q *= 2.0/15.0
  73. assert_allclose(p, q, atol=1e-15)
  74. def test_integer_twosided(self):
  75. x = np.zeros(16, dtype=int)
  76. x[0] = 1
  77. f, p = periodogram(x, return_onesided=False)
  78. assert_allclose(f, fftfreq(16, 1.0))
  79. q = np.full(16, 1/16.0)
  80. q[0] = 0
  81. assert_allclose(p, q)
  82. def test_complex(self):
  83. x = np.zeros(16, np.complex128)
  84. x[0] = 1.0 + 2.0j
  85. f, p = periodogram(x, return_onesided=False)
  86. assert_allclose(f, fftfreq(16, 1.0))
  87. q = np.full(16, 5.0/16.0)
  88. q[0] = 0
  89. assert_allclose(p, q)
  90. def test_unk_scaling(self):
  91. assert_raises(ValueError, periodogram, np.zeros(4, np.complex128),
  92. scaling='foo')
  93. @pytest.mark.skipif(
  94. sys.maxsize <= 2**32,
  95. reason="On some 32-bit tolerance issue"
  96. )
  97. def test_nd_axis_m1(self):
  98. x = np.zeros(20, dtype=np.float64)
  99. x = x.reshape((2,1,10))
  100. x[:,:,0] = 1.0
  101. f, p = periodogram(x)
  102. assert_array_equal(p.shape, (2, 1, 6))
  103. assert_array_almost_equal_nulp(p[0,0,:], p[1,0,:], 60)
  104. f0, p0 = periodogram(x[0,0,:])
  105. assert_array_almost_equal_nulp(p0[np.newaxis,:], p[1,:], 60)
  106. @pytest.mark.skipif(
  107. sys.maxsize <= 2**32,
  108. reason="On some 32-bit tolerance issue"
  109. )
  110. def test_nd_axis_0(self):
  111. x = np.zeros(20, dtype=np.float64)
  112. x = x.reshape((10,2,1))
  113. x[0,:,:] = 1.0
  114. f, p = periodogram(x, axis=0)
  115. assert_array_equal(p.shape, (6,2,1))
  116. assert_array_almost_equal_nulp(p[:,0,0], p[:,1,0], 60)
  117. f0, p0 = periodogram(x[:,0,0])
  118. assert_array_almost_equal_nulp(p0, p[:,1,0])
  119. def test_window_external(self):
  120. x = np.zeros(16)
  121. x[0] = 1
  122. f, p = periodogram(x, 10, 'hann')
  123. win = signal.get_window('hann', 16)
  124. fe, pe = periodogram(x, 10, win)
  125. assert_array_almost_equal_nulp(p, pe)
  126. assert_array_almost_equal_nulp(f, fe)
  127. win_err = signal.get_window('hann', 32)
  128. assert_raises(ValueError, periodogram, x,
  129. 10, win_err) # win longer than signal
  130. def test_padded_fft(self):
  131. x = np.zeros(16)
  132. x[0] = 1
  133. f, p = periodogram(x)
  134. fp, pp = periodogram(x, nfft=32)
  135. assert_allclose(f, fp[::2])
  136. assert_allclose(p, pp[::2])
  137. assert_array_equal(pp.shape, (17,))
  138. def test_empty_input(self):
  139. f, p = periodogram([])
  140. assert_array_equal(f.shape, (0,))
  141. assert_array_equal(p.shape, (0,))
  142. for shape in [(0,), (3,0), (0,5,2)]:
  143. f, p = periodogram(np.empty(shape))
  144. assert_array_equal(f.shape, shape)
  145. assert_array_equal(p.shape, shape)
  146. def test_empty_input_other_axis(self):
  147. for shape in [(3,0), (0,5,2)]:
  148. f, p = periodogram(np.empty(shape), axis=1)
  149. assert_array_equal(f.shape, shape)
  150. assert_array_equal(p.shape, shape)
  151. def test_short_nfft(self):
  152. x = np.zeros(18)
  153. x[0] = 1
  154. f, p = periodogram(x, nfft=16)
  155. assert_allclose(f, np.linspace(0, 0.5, 9))
  156. q = np.ones(9)
  157. q[0] = 0
  158. q[-1] /= 2.0
  159. q /= 8
  160. assert_allclose(p, q)
  161. def test_nfft_is_xshape(self):
  162. x = np.zeros(16)
  163. x[0] = 1
  164. f, p = periodogram(x, nfft=16)
  165. assert_allclose(f, np.linspace(0, 0.5, 9))
  166. q = np.ones(9)
  167. q[0] = 0
  168. q[-1] /= 2.0
  169. q /= 8
  170. assert_allclose(p, q)
  171. def test_real_onesided_even_32(self):
  172. x = np.zeros(16, 'f')
  173. x[0] = 1
  174. f, p = periodogram(x)
  175. assert_allclose(f, np.linspace(0, 0.5, 9))
  176. q = np.ones(9, 'f')
  177. q[0] = 0
  178. q[-1] /= 2.0
  179. q /= 8
  180. assert_allclose(p, q)
  181. assert_(p.dtype == q.dtype)
  182. def test_real_onesided_odd_32(self):
  183. x = np.zeros(15, 'f')
  184. x[0] = 1
  185. f, p = periodogram(x)
  186. assert_allclose(f, np.arange(8.0)/15.0)
  187. q = np.ones(8, 'f')
  188. q[0] = 0
  189. q *= 2.0/15.0
  190. assert_allclose(p, q, atol=1e-7)
  191. assert_(p.dtype == q.dtype)
  192. def test_real_twosided_32(self):
  193. x = np.zeros(16, 'f')
  194. x[0] = 1
  195. f, p = periodogram(x, return_onesided=False)
  196. assert_allclose(f, fftfreq(16, 1.0))
  197. q = np.full(16, 1/16.0, 'f')
  198. q[0] = 0
  199. assert_allclose(p, q)
  200. assert_(p.dtype == q.dtype)
  201. def test_complex_32(self):
  202. x = np.zeros(16, 'F')
  203. x[0] = 1.0 + 2.0j
  204. f, p = periodogram(x, return_onesided=False)
  205. assert_allclose(f, fftfreq(16, 1.0))
  206. q = np.full(16, 5.0/16.0, 'f')
  207. q[0] = 0
  208. assert_allclose(p, q)
  209. assert_(p.dtype == q.dtype)
  210. def test_shorter_window_error(self):
  211. x = np.zeros(16)
  212. x[0] = 1
  213. win = signal.get_window('hann', 10)
  214. expected_msg = ('the size of the window must be the same size '
  215. 'of the input on the specified axis')
  216. with assert_raises(ValueError, match=expected_msg):
  217. periodogram(x, window=win)
  218. class TestWelch:
  219. def test_real_onesided_even(self):
  220. x = np.zeros(16)
  221. x[0] = 1
  222. x[8] = 1
  223. f, p = welch(x, nperseg=8)
  224. assert_allclose(f, np.linspace(0, 0.5, 5))
  225. q = np.array([0.08333333, 0.15277778, 0.22222222, 0.22222222,
  226. 0.11111111])
  227. assert_allclose(p, q, atol=1e-7, rtol=1e-7)
  228. def test_real_onesided_odd(self):
  229. x = np.zeros(16)
  230. x[0] = 1
  231. x[8] = 1
  232. f, p = welch(x, nperseg=9)
  233. assert_allclose(f, np.arange(5.0)/9.0)
  234. q = np.array([0.12477455, 0.23430933, 0.17072113, 0.17072113,
  235. 0.17072113])
  236. assert_allclose(p, q, atol=1e-7, rtol=1e-7)
  237. def test_real_twosided(self):
  238. x = np.zeros(16)
  239. x[0] = 1
  240. x[8] = 1
  241. f, p = welch(x, nperseg=8, return_onesided=False)
  242. assert_allclose(f, fftfreq(8, 1.0))
  243. q = np.array([0.08333333, 0.07638889, 0.11111111, 0.11111111,
  244. 0.11111111, 0.11111111, 0.11111111, 0.07638889])
  245. assert_allclose(p, q, atol=1e-7, rtol=1e-7)
  246. def test_real_spectrum(self):
  247. x = np.zeros(16)
  248. x[0] = 1
  249. x[8] = 1
  250. f, p = welch(x, nperseg=8, scaling='spectrum')
  251. assert_allclose(f, np.linspace(0, 0.5, 5))
  252. q = np.array([0.015625, 0.02864583, 0.04166667, 0.04166667,
  253. 0.02083333])
  254. assert_allclose(p, q, atol=1e-7, rtol=1e-7)
  255. def test_integer_onesided_even(self):
  256. x = np.zeros(16, dtype=int)
  257. x[0] = 1
  258. x[8] = 1
  259. f, p = welch(x, nperseg=8)
  260. assert_allclose(f, np.linspace(0, 0.5, 5))
  261. q = np.array([0.08333333, 0.15277778, 0.22222222, 0.22222222,
  262. 0.11111111])
  263. assert_allclose(p, q, atol=1e-7, rtol=1e-7)
  264. def test_integer_onesided_odd(self):
  265. x = np.zeros(16, dtype=int)
  266. x[0] = 1
  267. x[8] = 1
  268. f, p = welch(x, nperseg=9)
  269. assert_allclose(f, np.arange(5.0)/9.0)
  270. q = np.array([0.12477455, 0.23430933, 0.17072113, 0.17072113,
  271. 0.17072113])
  272. assert_allclose(p, q, atol=1e-7, rtol=1e-7)
  273. def test_integer_twosided(self):
  274. x = np.zeros(16, dtype=int)
  275. x[0] = 1
  276. x[8] = 1
  277. f, p = welch(x, nperseg=8, return_onesided=False)
  278. assert_allclose(f, fftfreq(8, 1.0))
  279. q = np.array([0.08333333, 0.07638889, 0.11111111, 0.11111111,
  280. 0.11111111, 0.11111111, 0.11111111, 0.07638889])
  281. assert_allclose(p, q, atol=1e-7, rtol=1e-7)
  282. def test_complex(self):
  283. x = np.zeros(16, np.complex128)
  284. x[0] = 1.0 + 2.0j
  285. x[8] = 1.0 + 2.0j
  286. f, p = welch(x, nperseg=8, return_onesided=False)
  287. assert_allclose(f, fftfreq(8, 1.0))
  288. q = np.array([0.41666667, 0.38194444, 0.55555556, 0.55555556,
  289. 0.55555556, 0.55555556, 0.55555556, 0.38194444])
  290. assert_allclose(p, q, atol=1e-7, rtol=1e-7)
  291. def test_unk_scaling(self):
  292. assert_raises(ValueError, welch, np.zeros(4, np.complex128),
  293. scaling='foo', nperseg=4)
  294. def test_detrend_linear(self):
  295. x = np.arange(10, dtype=np.float64) + 0.04
  296. f, p = welch(x, nperseg=10, detrend='linear')
  297. assert_allclose(p, np.zeros_like(p), atol=1e-15)
  298. def test_no_detrending(self):
  299. x = np.arange(10, dtype=np.float64) + 0.04
  300. f1, p1 = welch(x, nperseg=10, detrend=False)
  301. f2, p2 = welch(x, nperseg=10, detrend=lambda x: x)
  302. assert_allclose(f1, f2, atol=1e-15)
  303. assert_allclose(p1, p2, atol=1e-15)
  304. def test_detrend_external(self):
  305. x = np.arange(10, dtype=np.float64) + 0.04
  306. f, p = welch(x, nperseg=10,
  307. detrend=lambda seg: signal.detrend(seg, type='l'))
  308. assert_allclose(p, np.zeros_like(p), atol=1e-15)
  309. def test_detrend_external_nd_m1(self):
  310. x = np.arange(40, dtype=np.float64) + 0.04
  311. x = x.reshape((2,2,10))
  312. f, p = welch(x, nperseg=10,
  313. detrend=lambda seg: signal.detrend(seg, type='l'))
  314. assert_allclose(p, np.zeros_like(p), atol=1e-15)
  315. def test_detrend_external_nd_0(self):
  316. x = np.arange(20, dtype=np.float64) + 0.04
  317. x = x.reshape((2,1,10))
  318. x = np.moveaxis(x, 2, 0)
  319. f, p = welch(x, nperseg=10, axis=0,
  320. detrend=lambda seg: signal.detrend(seg, axis=0, type='l'))
  321. assert_allclose(p, np.zeros_like(p), atol=1e-15)
  322. def test_nd_axis_m1(self):
  323. x = np.arange(20, dtype=np.float64) + 0.04
  324. x = x.reshape((2,1,10))
  325. f, p = welch(x, nperseg=10)
  326. assert_array_equal(p.shape, (2, 1, 6))
  327. assert_allclose(p[0,0,:], p[1,0,:], atol=1e-13, rtol=1e-13)
  328. f0, p0 = welch(x[0,0,:], nperseg=10)
  329. assert_allclose(p0[np.newaxis,:], p[1,:], atol=1e-13, rtol=1e-13)
  330. def test_nd_axis_0(self):
  331. x = np.arange(20, dtype=np.float64) + 0.04
  332. x = x.reshape((10,2,1))
  333. f, p = welch(x, nperseg=10, axis=0)
  334. assert_array_equal(p.shape, (6,2,1))
  335. assert_allclose(p[:,0,0], p[:,1,0], atol=1e-13, rtol=1e-13)
  336. f0, p0 = welch(x[:,0,0], nperseg=10)
  337. assert_allclose(p0, p[:,1,0], atol=1e-13, rtol=1e-13)
  338. def test_window_external(self):
  339. x = np.zeros(16)
  340. x[0] = 1
  341. x[8] = 1
  342. f, p = welch(x, 10, 'hann', nperseg=8)
  343. win = signal.get_window('hann', 8)
  344. fe, pe = welch(x, 10, win, nperseg=None)
  345. assert_array_almost_equal_nulp(p, pe)
  346. assert_array_almost_equal_nulp(f, fe)
  347. assert_array_equal(fe.shape, (5,)) # because win length used as nperseg
  348. assert_array_equal(pe.shape, (5,))
  349. assert_raises(ValueError, welch, x,
  350. 10, win, nperseg=4) # because nperseg != win.shape[-1]
  351. win_err = signal.get_window('hann', 32)
  352. assert_raises(ValueError, welch, x,
  353. 10, win_err, nperseg=None) # win longer than signal
  354. def test_empty_input(self):
  355. f, p = welch([])
  356. assert_array_equal(f.shape, (0,))
  357. assert_array_equal(p.shape, (0,))
  358. for shape in [(0,), (3,0), (0,5,2)]:
  359. f, p = welch(np.empty(shape))
  360. assert_array_equal(f.shape, shape)
  361. assert_array_equal(p.shape, shape)
  362. def test_empty_input_other_axis(self):
  363. for shape in [(3,0), (0,5,2)]:
  364. f, p = welch(np.empty(shape), axis=1)
  365. assert_array_equal(f.shape, shape)
  366. assert_array_equal(p.shape, shape)
  367. def test_short_data(self):
  368. x = np.zeros(8)
  369. x[0] = 1
  370. #for string-like window, input signal length < nperseg value gives
  371. #UserWarning, sets nperseg to x.shape[-1]
  372. with warnings.catch_warnings():
  373. warnings.filterwarnings(
  374. "ignore", "nperseg=256 is greater than signal.*", UserWarning)
  375. f, p = welch(x,window='hann') # default nperseg
  376. f1, p1 = welch(x,window='hann', nperseg=256) # user-specified nperseg
  377. f2, p2 = welch(x, nperseg=8) # valid nperseg, doesn't give warning
  378. assert_allclose(f, f2)
  379. assert_allclose(p, p2)
  380. assert_allclose(f1, f2)
  381. assert_allclose(p1, p2)
  382. def test_window_long_or_nd(self):
  383. assert_raises(ValueError, welch, np.zeros(4), 1, np.array([1,1,1,1,1]))
  384. assert_raises(ValueError, welch, np.zeros(4), 1,
  385. np.arange(6).reshape((2,3)))
  386. def test_nondefault_noverlap(self):
  387. x = np.zeros(64)
  388. x[::8] = 1
  389. f, p = welch(x, nperseg=16, noverlap=4)
  390. q = np.array([0, 1./12., 1./3., 1./5., 1./3., 1./5., 1./3., 1./5.,
  391. 1./6.])
  392. assert_allclose(p, q, atol=1e-12)
  393. def test_bad_noverlap(self):
  394. assert_raises(ValueError, welch, np.zeros(4), 1, 'hann', 2, 7)
  395. def test_nfft_too_short(self):
  396. assert_raises(ValueError, welch, np.ones(12), nfft=3, nperseg=4)
  397. def test_real_onesided_even_32(self):
  398. x = np.zeros(16, 'f')
  399. x[0] = 1
  400. x[8] = 1
  401. f, p = welch(x, nperseg=8)
  402. assert_allclose(f, np.linspace(0, 0.5, 5))
  403. q = np.array([0.08333333, 0.15277778, 0.22222222, 0.22222222,
  404. 0.11111111], 'f')
  405. assert_allclose(p, q, atol=1e-7, rtol=1e-7)
  406. assert_(p.dtype == q.dtype)
  407. def test_real_onesided_odd_32(self):
  408. x = np.zeros(16, 'f')
  409. x[0] = 1
  410. x[8] = 1
  411. f, p = welch(x, nperseg=9)
  412. assert_allclose(f, np.arange(5.0)/9.0)
  413. q = np.array([0.12477458, 0.23430935, 0.17072113, 0.17072116,
  414. 0.17072113], 'f')
  415. assert_allclose(p, q, atol=1e-7, rtol=1e-7)
  416. assert_(p.dtype == q.dtype)
  417. def test_real_twosided_32(self):
  418. x = np.zeros(16, 'f')
  419. x[0] = 1
  420. x[8] = 1
  421. f, p = welch(x, nperseg=8, return_onesided=False)
  422. assert_allclose(f, fftfreq(8, 1.0))
  423. q = np.array([0.08333333, 0.07638889, 0.11111111,
  424. 0.11111111, 0.11111111, 0.11111111, 0.11111111,
  425. 0.07638889], 'f')
  426. assert_allclose(p, q, atol=1e-7, rtol=1e-7)
  427. assert_(p.dtype == q.dtype)
  428. def test_complex_32(self):
  429. x = np.zeros(16, 'F')
  430. x[0] = 1.0 + 2.0j
  431. x[8] = 1.0 + 2.0j
  432. f, p = welch(x, nperseg=8, return_onesided=False)
  433. assert_allclose(f, fftfreq(8, 1.0))
  434. q = np.array([0.41666666, 0.38194442, 0.55555552, 0.55555552,
  435. 0.55555558, 0.55555552, 0.55555552, 0.38194442], 'f')
  436. assert_allclose(p, q, atol=1e-7, rtol=1e-7)
  437. assert_(p.dtype == q.dtype,
  438. f'dtype mismatch, {p.dtype}, {q.dtype}')
  439. def test_padded_freqs(self):
  440. x = np.zeros(12)
  441. nfft = 24
  442. f = fftfreq(nfft, 1.0)[:nfft//2+1]
  443. f[-1] *= -1
  444. fodd, _ = welch(x, nperseg=5, nfft=nfft)
  445. feven, _ = welch(x, nperseg=6, nfft=nfft)
  446. assert_allclose(f, fodd)
  447. assert_allclose(f, feven)
  448. nfft = 25
  449. f = fftfreq(nfft, 1.0)[:(nfft + 1)//2]
  450. fodd, _ = welch(x, nperseg=5, nfft=nfft)
  451. feven, _ = welch(x, nperseg=6, nfft=nfft)
  452. assert_allclose(f, fodd)
  453. assert_allclose(f, feven)
  454. def test_window_correction(self):
  455. A = 20
  456. fs = 1e4
  457. nperseg = int(fs//10)
  458. fsig = 300
  459. ii = int(fsig*nperseg//fs) # Freq index of fsig
  460. tt = np.arange(fs)/fs
  461. x = A*np.sin(2*np.pi*fsig*tt)
  462. for window in ['hann', 'bartlett', ('tukey', 0.1), 'flattop']:
  463. _, p_spec = welch(x, fs=fs, nperseg=nperseg, window=window,
  464. scaling='spectrum')
  465. freq, p_dens = welch(x, fs=fs, nperseg=nperseg, window=window,
  466. scaling='density')
  467. # Check peak height at signal frequency for 'spectrum'
  468. assert_allclose(p_spec[ii], A**2/2.0)
  469. # Check integrated spectrum RMS for 'density'
  470. assert_allclose(np.sqrt(trapezoid(p_dens, freq)), A*np.sqrt(2)/2,
  471. rtol=1e-3)
  472. def test_axis_rolling(self):
  473. np.random.seed(1234)
  474. x_flat = np.random.randn(1024)
  475. _, p_flat = welch(x_flat)
  476. for a in range(3):
  477. newshape = [1,]*3
  478. newshape[a] = -1
  479. x = x_flat.reshape(newshape)
  480. _, p_plus = welch(x, axis=a) # Positive axis index
  481. _, p_minus = welch(x, axis=a-x.ndim) # Negative axis index
  482. assert_equal(p_flat, p_plus.squeeze(), err_msg=a)
  483. assert_equal(p_flat, p_minus.squeeze(), err_msg=a-x.ndim)
  484. def test_average(self):
  485. x = np.zeros(16)
  486. x[0] = 1
  487. x[8] = 1
  488. f, p = welch(x, nperseg=8, average='median')
  489. assert_allclose(f, np.linspace(0, 0.5, 5))
  490. q = np.array([.1, .05, 0., 1.54074396e-33, 0.])
  491. assert_allclose(p, q, atol=1e-7, rtol=1e-7)
  492. assert_raises(ValueError, welch, x, nperseg=8,
  493. average='unrecognised-average')
  494. def test_ratio_scale_to(self):
  495. """Verify the factor of ``sum(abs(window)**2)*fs / abs(sum(window))**2``
  496. used in the `welch` and `csd` docstrs. """
  497. x, win, fs = np.array([1., 0, 0, 0]), np.ones(4), 12
  498. params = dict(fs=fs, window=win, return_onesided=False, detrend=None)
  499. p_dens = welch(x, scaling='density', **params)[1]
  500. p_spec = welch(x, scaling='spectrum', **params)[1]
  501. p_fac = sum(win**2)*fs / abs(sum(win))**2
  502. assert_allclose(p_spec / p_dens, p_fac)
  503. class TestCSD:
  504. def test_pad_shorter_x(self):
  505. x = np.zeros(8)
  506. y = np.zeros(12)
  507. f = np.linspace(0, 0.5, 7)
  508. c = np.zeros(7,dtype=np.complex128)
  509. f1, c1 = csd(x, y, nperseg=12)
  510. assert_allclose(f, f1)
  511. assert_allclose(c, c1)
  512. def test_pad_shorter_y(self):
  513. x = np.zeros(12)
  514. y = np.zeros(8)
  515. f = np.linspace(0, 0.5, 7)
  516. c = np.zeros(7,dtype=np.complex128)
  517. f1, c1 = csd(x, y, nperseg=12)
  518. assert_allclose(f, f1)
  519. assert_allclose(c, c1)
  520. def test_unequal_length_input_1D(self):
  521. """Test zero-padding for input `x.shape[axis] != y.shape[axis]` for 1d arrays.
  522. This test ensures that issue 23036 is fixed.
  523. """
  524. x = np.tile([4, 0, -4, 0], 4)
  525. kw = dict(fs=len(x), window='boxcar', nperseg=4)
  526. X0 = signal.csd(x, np.copy(x), **kw)[1] # `x is x` must be False
  527. X1 = signal.csd(x, x[:8], **kw)[1]
  528. X2 = signal.csd(x[:8], x, **kw)[1]
  529. xp_assert_close(X1, X0 / 2)
  530. xp_assert_close(X2, X0 / 2)
  531. def test_unequal_length_input_3D(self):
  532. """Test zero-padding for input `x.shape[axis] != y.shape[axis]` for 3d arrays.
  533. This test ensures that issue 23036 is fixed.
  534. """
  535. n = 8
  536. x = np.zeros(2 * 3 * n).reshape(2, n, 3)
  537. x[:, 0, :] = n
  538. kw = dict(fs=n, window='boxcar', nperseg=n, detrend=None, axis=1)
  539. X0 = signal.csd(x, x.copy(), **kw)[1] # `x is x` must be False
  540. X1 = signal.csd(x, x[:, :2, :], **kw)[1]
  541. X2 = signal.csd(x[:, :2, :], x, **kw)[1]
  542. xp_assert_close(X1, X0)
  543. xp_assert_close(X2, X0)
  544. def test_real_onesided_even(self):
  545. x = np.zeros(16)
  546. x[0] = 1
  547. x[8] = 1
  548. f, p = csd(x, x, nperseg=8)
  549. assert_allclose(f, np.linspace(0, 0.5, 5))
  550. q = np.array([0.08333333, 0.15277778, 0.22222222, 0.22222222,
  551. 0.11111111])
  552. assert_allclose(p, q, atol=1e-7, rtol=1e-7)
  553. def test_real_onesided_odd(self):
  554. x = np.zeros(16)
  555. x[0] = 1
  556. x[8] = 1
  557. f, p = csd(x, x, nperseg=9)
  558. assert_allclose(f, np.arange(5.0)/9.0)
  559. q = np.array([0.12477455, 0.23430933, 0.17072113, 0.17072113,
  560. 0.17072113])
  561. assert_allclose(p, q, atol=1e-7, rtol=1e-7)
  562. def test_real_twosided(self):
  563. x = np.zeros(16)
  564. x[0] = 1
  565. x[8] = 1
  566. f, p = csd(x, x, nperseg=8, return_onesided=False)
  567. assert_allclose(f, fftfreq(8, 1.0))
  568. q = np.array([0.08333333, 0.07638889, 0.11111111, 0.11111111,
  569. 0.11111111, 0.11111111, 0.11111111, 0.07638889])
  570. assert_allclose(p, q, atol=1e-7, rtol=1e-7)
  571. def test_real_spectrum(self):
  572. x = np.zeros(16)
  573. x[0] = 1
  574. x[8] = 1
  575. f, p = csd(x, x, nperseg=8, scaling='spectrum')
  576. assert_allclose(f, np.linspace(0, 0.5, 5))
  577. q = np.array([0.015625, 0.02864583, 0.04166667, 0.04166667,
  578. 0.02083333])
  579. assert_allclose(p, q, atol=1e-7, rtol=1e-7)
  580. def test_integer_onesided_even(self):
  581. x = np.zeros(16, dtype=int)
  582. x[0] = 1
  583. x[8] = 1
  584. f, p = csd(x, x, nperseg=8)
  585. assert_allclose(f, np.linspace(0, 0.5, 5))
  586. q = np.array([0.08333333, 0.15277778, 0.22222222, 0.22222222,
  587. 0.11111111])
  588. assert_allclose(p, q, atol=1e-7, rtol=1e-7)
  589. def test_integer_onesided_odd(self):
  590. x = np.zeros(16, dtype=int)
  591. x[0] = 1
  592. x[8] = 1
  593. f, p = csd(x, x, nperseg=9)
  594. assert_allclose(f, np.arange(5.0)/9.0)
  595. q = np.array([0.12477455, 0.23430933, 0.17072113, 0.17072113,
  596. 0.17072113])
  597. assert_allclose(p, q, atol=1e-7, rtol=1e-7)
  598. def test_integer_twosided(self):
  599. x = np.zeros(16, dtype=int)
  600. x[0] = 1
  601. x[8] = 1
  602. f, p = csd(x, x, nperseg=8, return_onesided=False)
  603. assert_allclose(f, fftfreq(8, 1.0))
  604. q = np.array([0.08333333, 0.07638889, 0.11111111, 0.11111111,
  605. 0.11111111, 0.11111111, 0.11111111, 0.07638889])
  606. assert_allclose(p, q, atol=1e-7, rtol=1e-7)
  607. def test_complex(self):
  608. x = np.zeros(16, np.complex128)
  609. x[0] = 1.0 + 2.0j
  610. x[8] = 1.0 + 2.0j
  611. f, p = csd(x, x, nperseg=8, return_onesided=False)
  612. assert_allclose(f, fftfreq(8, 1.0))
  613. q = np.array([0.41666667, 0.38194444, 0.55555556, 0.55555556,
  614. 0.55555556, 0.55555556, 0.55555556, 0.38194444])
  615. assert_allclose(p, q, atol=1e-7, rtol=1e-7)
  616. def test_unk_scaling(self):
  617. assert_raises(ValueError, csd, np.zeros(4, np.complex128),
  618. np.ones(4, np.complex128), scaling='foo', nperseg=4)
  619. def test_detrend_linear(self):
  620. x = np.arange(10, dtype=np.float64) + 0.04
  621. f, p = csd(x, x, nperseg=10, detrend='linear')
  622. assert_allclose(p, np.zeros_like(p), atol=1e-15)
  623. def test_no_detrending(self):
  624. x = np.arange(10, dtype=np.float64) + 0.04
  625. f1, p1 = csd(x, x, nperseg=10, detrend=False)
  626. f2, p2 = csd(x, x, nperseg=10, detrend=lambda x: x)
  627. assert_allclose(f1, f2, atol=1e-15)
  628. assert_allclose(p1, p2, atol=1e-15)
  629. def test_detrend_external(self):
  630. x = np.arange(10, dtype=np.float64) + 0.04
  631. f, p = csd(x, x, nperseg=10,
  632. detrend=lambda seg: signal.detrend(seg, type='l'))
  633. assert_allclose(p, np.zeros_like(p), atol=1e-15)
  634. def test_detrend_external_nd_m1(self):
  635. x = np.arange(40, dtype=np.float64) + 0.04
  636. x = x.reshape((2,2,10))
  637. f, p = csd(x, x, nperseg=10,
  638. detrend=lambda seg: signal.detrend(seg, type='l'))
  639. assert_allclose(p, np.zeros_like(p), atol=1e-15)
  640. def test_detrend_external_nd_0(self):
  641. x = np.arange(20, dtype=np.float64) + 0.04
  642. x = x.reshape((2,1,10))
  643. x = np.moveaxis(x, 2, 0)
  644. f, p = csd(x, x, nperseg=10, axis=0,
  645. detrend=lambda seg: signal.detrend(seg, axis=0, type='l'))
  646. assert_allclose(p, np.zeros_like(p), atol=1e-15)
  647. def test_nd_axis_m1(self):
  648. x = np.arange(20, dtype=np.float64) + 0.04
  649. x = x.reshape((2,1,10))
  650. f, p = csd(x, x, nperseg=10)
  651. assert_array_equal(p.shape, (2, 1, 6))
  652. assert_allclose(p[0,0,:], p[1,0,:], atol=1e-13, rtol=1e-13)
  653. f0, p0 = csd(x[0,0,:], x[0,0,:], nperseg=10)
  654. assert_allclose(p0[np.newaxis,:], p[1,:], atol=1e-13, rtol=1e-13)
  655. def test_nd_axis_0(self):
  656. x = np.arange(20, dtype=np.float64) + 0.04
  657. x = x.reshape((10,2,1))
  658. f, p = csd(x, x, nperseg=10, axis=0)
  659. assert_array_equal(p.shape, (6,2,1))
  660. assert_allclose(p[:,0,0], p[:,1,0], atol=1e-13, rtol=1e-13)
  661. f0, p0 = csd(x[:,0,0], x[:,0,0], nperseg=10)
  662. assert_allclose(p0, p[:,1,0], atol=1e-13, rtol=1e-13)
  663. def test_window_external(self):
  664. x = np.zeros(16)
  665. x[0] = 1
  666. x[8] = 1
  667. f, p = csd(x, x, 10, 'hann', 8)
  668. win = signal.get_window('hann', 8)
  669. fe, pe = csd(x, x, 10, win, nperseg=None)
  670. assert_array_almost_equal_nulp(p, pe)
  671. assert_array_almost_equal_nulp(f, fe)
  672. assert_array_equal(fe.shape, (5,)) # because win length used as nperseg
  673. assert_array_equal(pe.shape, (5,))
  674. assert_raises(ValueError, csd, x, x,
  675. 10, win, nperseg=256) # because nperseg != win.shape[-1]
  676. win_err = signal.get_window('hann', 32)
  677. assert_raises(ValueError, csd, x, x,
  678. 10, win_err, nperseg=None) # because win longer than signal
  679. with pytest.raises(ValueError, match="Parameter nperseg=0.*"):
  680. csd(x, x, 0, nperseg=0)
  681. def test_empty_input(self):
  682. f, p = csd([],np.zeros(10))
  683. assert_array_equal(f.shape, (0,))
  684. assert_array_equal(p.shape, (0,))
  685. f, p = csd(np.zeros(10),[])
  686. assert_array_equal(f.shape, (0,))
  687. assert_array_equal(p.shape, (0,))
  688. for shape in [(0,), (3,0), (0,5,2)]:
  689. f, p = csd(np.empty(shape), np.empty(shape))
  690. assert_array_equal(f.shape, shape)
  691. assert_array_equal(p.shape, shape)
  692. f, p = csd(np.ones(10), np.empty((5,0)))
  693. assert_array_equal(f.shape, (5,0))
  694. assert_array_equal(p.shape, (5,0))
  695. f, p = csd(np.empty((5,0)), np.ones(10))
  696. assert_array_equal(f.shape, (5,0))
  697. assert_array_equal(p.shape, (5,0))
  698. def test_empty_input_other_axis(self):
  699. for shape in [(3,0), (0,5,2)]:
  700. f, p = csd(np.empty(shape), np.empty(shape), axis=1)
  701. assert_array_equal(f.shape, shape)
  702. assert_array_equal(p.shape, shape)
  703. f, p = csd(np.empty((10,10,3)), np.zeros((10,0,1)), axis=1)
  704. assert_array_equal(f.shape, (10,0,3))
  705. assert_array_equal(p.shape, (10,0,3))
  706. f, p = csd(np.empty((10,0,1)), np.zeros((10,10,3)), axis=1)
  707. assert_array_equal(f.shape, (10,0,3))
  708. assert_array_equal(p.shape, (10,0,3))
  709. def test_short_data(self):
  710. x = np.zeros(8)
  711. x[0] = 1
  712. #for string-like window, input signal length < nperseg value gives
  713. #UserWarning, sets nperseg to x.shape[-1]
  714. with warnings.catch_warnings():
  715. warnings.filterwarnings(
  716. "ignore", "nperseg=256 is greater than signal length.*", UserWarning)
  717. f, p = csd(x, x, window='hann') # default nperseg
  718. f1, p1 = csd(x, x, window='hann', nperseg=256) # user-specified nperseg
  719. f2, p2 = csd(x, x, nperseg=8) # valid nperseg, doesn't give warning
  720. assert_allclose(f, f2)
  721. assert_allclose(p, p2)
  722. assert_allclose(f1, f2)
  723. assert_allclose(p1, p2)
  724. def test_window_long_or_nd(self):
  725. assert_raises(ValueError, csd, np.zeros(4), np.ones(4), 1,
  726. np.array([1,1,1,1,1]))
  727. assert_raises(ValueError, csd, np.zeros(4), np.ones(4), 1,
  728. np.arange(6).reshape((2,3)))
  729. def test_nondefault_noverlap(self):
  730. x = np.zeros(64)
  731. x[::8] = 1
  732. f, p = csd(x, x, nperseg=16, noverlap=4)
  733. q = np.array([0, 1./12., 1./3., 1./5., 1./3., 1./5., 1./3., 1./5.,
  734. 1./6.])
  735. assert_allclose(p, q, atol=1e-12)
  736. def test_bad_noverlap(self):
  737. assert_raises(ValueError, csd, np.zeros(4), np.ones(4), 1, 'hann',
  738. 2, 7)
  739. def test_nfft_too_short(self):
  740. assert_raises(ValueError, csd, np.ones(12), np.zeros(12), nfft=3,
  741. nperseg=4)
  742. def test_incompatible_inputs(self):
  743. with pytest.raises(ValueError, match='x and y cannot be broadcast.*'):
  744. csd(np.ones((1, 8, 1)), np.ones((2, 8)), nperseg=4)
  745. def test_real_onesided_even_32(self):
  746. x = np.zeros(16, 'f')
  747. x[0] = 1
  748. x[8] = 1
  749. f, p = csd(x, x, nperseg=8)
  750. assert_allclose(f, np.linspace(0, 0.5, 5))
  751. q = np.array([0.08333333, 0.15277778, 0.22222222, 0.22222222,
  752. 0.11111111], 'f')
  753. assert_allclose(p, q, atol=1e-7, rtol=1e-7)
  754. assert_(p.dtype == q.dtype)
  755. def test_real_onesided_odd_32(self):
  756. x = np.zeros(16, 'f')
  757. x[0] = 1
  758. x[8] = 1
  759. f, p = csd(x, x, nperseg=9)
  760. assert_allclose(f, np.arange(5.0)/9.0)
  761. q = np.array([0.12477458, 0.23430935, 0.17072113, 0.17072116,
  762. 0.17072113], 'f')
  763. assert_allclose(p, q, atol=1e-7, rtol=1e-7)
  764. assert_(p.dtype == q.dtype)
  765. def test_real_twosided_32(self):
  766. x = np.zeros(16, 'f')
  767. x[0] = 1
  768. x[8] = 1
  769. f, p = csd(x, x, nperseg=8, return_onesided=False)
  770. assert_allclose(f, fftfreq(8, 1.0))
  771. q = np.array([0.08333333, 0.07638889, 0.11111111,
  772. 0.11111111, 0.11111111, 0.11111111, 0.11111111,
  773. 0.07638889], 'f')
  774. assert_allclose(p, q, atol=1e-7, rtol=1e-7)
  775. assert_(p.dtype == q.dtype)
  776. def test_complex_32(self):
  777. x = np.zeros(16, 'F')
  778. x[0] = 1.0 + 2.0j
  779. x[8] = 1.0 + 2.0j
  780. f, p = csd(x, x, nperseg=8, return_onesided=False)
  781. assert_allclose(f, fftfreq(8, 1.0))
  782. q = np.array([0.41666666, 0.38194442, 0.55555552, 0.55555552,
  783. 0.55555558, 0.55555552, 0.55555552, 0.38194442], 'f')
  784. assert_allclose(p, q, atol=1e-7, rtol=1e-7)
  785. assert_(p.dtype == q.dtype,
  786. f'dtype mismatch, {p.dtype}, {q.dtype}')
  787. def test_padded_freqs(self):
  788. x = np.zeros(12)
  789. y = np.ones(12)
  790. nfft = 24
  791. f = fftfreq(nfft, 1.0)[:nfft//2+1]
  792. f[-1] *= -1
  793. fodd, _ = csd(x, y, nperseg=5, nfft=nfft)
  794. feven, _ = csd(x, y, nperseg=6, nfft=nfft)
  795. assert_allclose(f, fodd)
  796. assert_allclose(f, feven)
  797. nfft = 25
  798. f = fftfreq(nfft, 1.0)[:(nfft + 1)//2]
  799. fodd, _ = csd(x, y, nperseg=5, nfft=nfft)
  800. feven, _ = csd(x, y, nperseg=6, nfft=nfft)
  801. assert_allclose(f, fodd)
  802. assert_allclose(f, feven)
  803. def test_copied_data(self):
  804. x = np.random.randn(64)
  805. y = x.copy()
  806. _, p_same = csd(x, x, nperseg=8, average='mean',
  807. return_onesided=False)
  808. _, p_copied = csd(x, y, nperseg=8, average='mean',
  809. return_onesided=False)
  810. assert_allclose(p_same, p_copied)
  811. _, p_same = csd(x, x, nperseg=8, average='median',
  812. return_onesided=False)
  813. _, p_copied = csd(x, y, nperseg=8, average='median',
  814. return_onesided=False)
  815. assert_allclose(p_same, p_copied)
  816. class TestCoherence:
  817. def test_identical_input(self):
  818. x = np.random.randn(20)
  819. y = np.copy(x) # So `y is x` -> False
  820. f = np.linspace(0, 0.5, 6)
  821. C = np.ones(6)
  822. f1, C1 = coherence(x, y, nperseg=10)
  823. assert_allclose(f, f1)
  824. assert_allclose(C, C1)
  825. def test_phase_shifted_input(self):
  826. x = np.random.randn(20)
  827. y = -x
  828. f = np.linspace(0, 0.5, 6)
  829. C = np.ones(6)
  830. f1, C1 = coherence(x, y, nperseg=10)
  831. assert_allclose(f, f1)
  832. assert_allclose(C, C1)
  833. class TestSpectrogram:
  834. def test_average_all_segments(self):
  835. x = np.random.randn(1024)
  836. fs = 1.0
  837. window = ('tukey', 0.25)
  838. nperseg = 16
  839. noverlap = 2
  840. f, _, P = spectrogram(x, fs, window, nperseg, noverlap)
  841. fw, Pw = welch(x, fs, window, nperseg, noverlap)
  842. assert_allclose(f, fw)
  843. assert_allclose(np.mean(P, axis=-1), Pw)
  844. def test_window_external(self):
  845. x = np.random.randn(1024)
  846. fs = 1.0
  847. window = ('tukey', 0.25)
  848. nperseg = 16
  849. noverlap = 2
  850. f, _, P = spectrogram(x, fs, window, nperseg, noverlap)
  851. win = signal.get_window(('tukey', 0.25), 16)
  852. fe, _, Pe = spectrogram(x, fs, win, nperseg=None, noverlap=2)
  853. assert_array_equal(fe.shape, (9,)) # because win length used as nperseg
  854. assert_array_equal(Pe.shape, (9,73))
  855. assert_raises(ValueError, spectrogram, x,
  856. fs, win, nperseg=8) # because nperseg != win.shape[-1]
  857. win_err = signal.get_window(('tukey', 0.25), 2048)
  858. assert_raises(ValueError, spectrogram, x,
  859. fs, win_err, nperseg=None) # win longer than signal
  860. def test_short_data(self):
  861. x = np.random.randn(1024)
  862. fs = 1.0
  863. #for string-like window, input signal length < nperseg value gives
  864. #UserWarning, sets nperseg to x.shape[-1]
  865. f, _, p = spectrogram(x, fs, window=('tukey',0.25)) # default nperseg
  866. with warnings.catch_warnings():
  867. warnings.filterwarnings(
  868. "ignore",
  869. "nperseg = 1025 is greater than input length = 1024, "
  870. "using nperseg = 1024",
  871. UserWarning,
  872. )
  873. f1, _, p1 = spectrogram(x, fs, window=('tukey',0.25),
  874. nperseg=1025) # user-specified nperseg
  875. f2, _, p2 = spectrogram(x, fs, nperseg=256) # to compare w/default
  876. f3, _, p3 = spectrogram(x, fs, nperseg=1024) # compare w/user-spec'd
  877. assert_allclose(f, f2)
  878. assert_allclose(p, p2)
  879. assert_allclose(f1, f3)
  880. assert_allclose(p1, p3)
  881. class TestLombscargle:
  882. def test_frequency(self):
  883. """Test if frequency location of peak corresponds to frequency of
  884. generated input signal.
  885. """
  886. # Input parameters
  887. ampl = 2.
  888. w = 1.
  889. phi = 0.5 * np.pi
  890. nin = 100
  891. nout = 1000
  892. p = 0.7 # Fraction of points to select
  893. # Randomly select a fraction of an array with timesteps
  894. rng = np.random.RandomState(2353425)
  895. r = rng.rand(nin)
  896. t = np.linspace(0.01*np.pi, 10.*np.pi, nin)[r >= p]
  897. # Plot a sine wave for the selected times
  898. y = ampl * np.sin(w*t + phi)
  899. # Define the array of frequencies for which to compute the periodogram
  900. f = np.linspace(0.01, 10., nout)
  901. # Calculate Lomb-Scargle periodogram
  902. P = lombscargle(t, y, f)
  903. # Check if difference between found frequency maximum and input
  904. # frequency is less than accuracy
  905. delta = f[1] - f[0]
  906. assert(w - f[np.argmax(P)] < (delta/2.))
  907. # also, check that it works with weights
  908. P = lombscargle(t, y, f, weights=np.ones_like(t, dtype=f.dtype))
  909. # Check if difference between found frequency maximum and input
  910. # frequency is less than accuracy
  911. delta = f[1] - f[0]
  912. assert(w - f[np.argmax(P)] < (delta/2.))
  913. def test_amplitude(self):
  914. # Test if height of peak in unnormalized Lomb-Scargle periodogram
  915. # corresponds to amplitude of the generated input signal.
  916. # Input parameters
  917. ampl = 2.
  918. w = 1.
  919. phi = 0.5 * np.pi
  920. nin = 1000
  921. nout = 1000
  922. p = 0.7 # Fraction of points to select
  923. # Randomly select a fraction of an array with timesteps
  924. rng = np.random.RandomState(2353425)
  925. r = rng.rand(nin)
  926. t = np.linspace(0.01*np.pi, 10.*np.pi, nin)[r >= p]
  927. # Plot a sine wave for the selected times
  928. y = ampl * np.sin(w*t + phi)
  929. # Define the array of frequencies for which to compute the periodogram
  930. f = np.linspace(0.01, 10., nout)
  931. # Calculate Lomb-Scargle periodogram
  932. pgram = lombscargle(t, y, f)
  933. # convert to the amplitude
  934. pgram = np.sqrt(4.0 * pgram / t.shape[0])
  935. # Check if amplitude is correct (this will not exactly match, due to
  936. # numerical differences when data is removed)
  937. assert_allclose(pgram[f==w], ampl, rtol=5e-2)
  938. @pytest.mark.filterwarnings("ignore::DeprecationWarning")
  939. def test_precenter(self):
  940. # Test if precenter gives the same result as manually precentering
  941. # (for a very simple offset)
  942. # Input parameters
  943. ampl = 2.
  944. w = 1.
  945. phi = 0.5 * np.pi
  946. nin = 100
  947. nout = 1000
  948. p = 0.7 # Fraction of points to select
  949. offset = 0.15 # Offset to be subtracted in pre-centering
  950. # Randomly select a fraction of an array with timesteps
  951. rng = np.random.RandomState(2353425)
  952. r = rng.rand(nin)
  953. t = np.linspace(0.01*np.pi, 10.*np.pi, nin)[r >= p]
  954. # Plot a sine wave for the selected times
  955. y = ampl * np.sin(w*t + phi) + offset
  956. # Define the array of frequencies for which to compute the periodogram
  957. f = np.linspace(0.01, 10., nout)
  958. # Calculate Lomb-Scargle periodogram
  959. pgram = lombscargle(t, y, f, precenter=True)
  960. pgram2 = lombscargle(t, y - y.mean(), f, precenter=False)
  961. # check if centering worked
  962. assert_allclose(pgram, pgram2)
  963. # do this again, but with floating_mean=True
  964. # Calculate Lomb-Scargle periodogram
  965. pgram = lombscargle(t, y, f, precenter=True, floating_mean=True)
  966. pgram2 = lombscargle(t, y - y.mean(), f, precenter=False, floating_mean=True)
  967. # check if centering worked
  968. assert_allclose(pgram, pgram2)
  969. def test_normalize(self):
  970. # Test normalize option of Lomb-Scarge.
  971. # Input parameters
  972. ampl = 2.
  973. w = 1.
  974. phi = 0.5 * np.pi
  975. nin = 100
  976. nout = 1000
  977. p = 0.7 # Fraction of points to select
  978. # Randomly select a fraction of an array with timesteps
  979. rng = np.random.RandomState(2353425)
  980. r = rng.rand(nin)
  981. t = np.linspace(0.01*np.pi, 10.*np.pi, nin)[r >= p]
  982. # Plot a sine wave for the selected times
  983. y = ampl * np.sin(w*t + phi)
  984. # Define the array of frequencies for which to compute the periodogram
  985. f = np.linspace(0.01, 10., nout)
  986. # Calculate Lomb-Scargle periodogram
  987. pgram = lombscargle(t, y, f)
  988. pgram2 = lombscargle(t, y, f, normalize=True)
  989. # Calculate the scale to convert from unnormalized to normalized
  990. weights = np.ones_like(t)/float(t.shape[0])
  991. YY_hat = (weights * y * y).sum()
  992. YY = YY_hat # correct formula for floating_mean=False
  993. scale_to_use = 2/(YY*t.shape[0])
  994. # check if normalization works as expected
  995. assert_allclose(pgram * scale_to_use, pgram2)
  996. assert_allclose(np.max(pgram2), 1.0)
  997. def test_wrong_shape(self):
  998. # different length t and y
  999. t = np.linspace(0, 1, 1)
  1000. y = np.linspace(0, 1, 2)
  1001. f = np.linspace(0, 1, 3) + 0.1
  1002. assert_raises(ValueError, lombscargle, t, y, f)
  1003. # t is 2D, with both axes length > 1
  1004. t = np.repeat(np.expand_dims(np.linspace(0, 1, 2), 1), 2, axis=1)
  1005. y = np.linspace(0, 1, 2)
  1006. f = np.linspace(0, 1, 3) + 0.1
  1007. assert_raises(ValueError, lombscargle, t, y, f)
  1008. # y is 2D, with both axes length > 1
  1009. t = np.linspace(0, 1, 2)
  1010. y = np.repeat(np.expand_dims(np.linspace(0, 1, 2), 1), 2, axis=1)
  1011. f = np.linspace(0, 1, 3) + 0.1
  1012. assert_raises(ValueError, lombscargle, t, y, f)
  1013. # f is 2D, with both axes length > 1
  1014. t = np.linspace(0, 1, 2)
  1015. y = np.linspace(0, 1, 2)
  1016. f = np.repeat(np.expand_dims(np.linspace(0, 1, 3), 1) + 0.1, 2, axis=1)
  1017. assert_raises(ValueError, lombscargle, t, y, f)
  1018. # weights is 2D, with both axes length > 1
  1019. t = np.linspace(0, 1, 2)
  1020. y = np.linspace(0, 1, 2)
  1021. f = np.linspace(0, 1, 3) + 0.1
  1022. weights = np.repeat(np.expand_dims(np.linspace(0, 1, 2), 1), 2, axis=1)
  1023. assert_raises(ValueError, lombscargle, t, y, f, weights=weights)
  1024. def test_lombscargle_atan_vs_atan2(self):
  1025. # https://github.com/scipy/scipy/issues/3787
  1026. # This raised a ZeroDivisionError.
  1027. t = np.linspace(0, 10, 1000, endpoint=False)
  1028. y = np.sin(4*t)
  1029. f = np.linspace(0, 50, 500, endpoint=False) + 0.1
  1030. lombscargle(t, y, f*2*np.pi)
  1031. def test_wrong_shape_weights(self):
  1032. # Weights must be the same shape as t
  1033. t = np.linspace(0, 1, 1)
  1034. y = np.linspace(0, 1, 1)
  1035. f = np.linspace(0, 1, 3) + 0.1
  1036. weights = np.linspace(1, 2, 2)
  1037. assert_raises(ValueError, lombscargle, t, y, f, weights=weights)
  1038. def test_zero_division_weights(self):
  1039. # Weights cannot sum to 0
  1040. t = np.zeros(1)
  1041. y = np.zeros(1)
  1042. f = np.ones(1)
  1043. weights = np.zeros(1)
  1044. assert_raises(ValueError, lombscargle, t, y, f, weights=weights)
  1045. def test_normalize_parameter(self):
  1046. # Test the validity of the normalize parameter input
  1047. # Input parameters
  1048. ampl = 2.
  1049. w = 1.
  1050. phi = 0
  1051. nin = 100
  1052. nout = 1000
  1053. p = 0.7 # Fraction of points to select
  1054. # Randomly select a fraction of an array with timesteps
  1055. rng = np.random.RandomState(2353425)
  1056. r = rng.rand(nin)
  1057. t = np.linspace(0.01*np.pi, 10.*np.pi, nin)[r >= p]
  1058. # Plot a sine wave for the selected times
  1059. y = ampl * np.sin(w*t + phi)
  1060. # Define the array of frequencies for which to compute the periodogram
  1061. f = np.linspace(0.01, 10., nout)
  1062. # check each of the valid inputs
  1063. pgram_false = lombscargle(t, y, f, normalize=False)
  1064. pgram_true = lombscargle(t, y, f, normalize=True)
  1065. pgram_power = lombscargle(t, y, f, normalize='power')
  1066. pgram_norm = lombscargle(t, y, f, normalize='normalize')
  1067. pgram_amp = lombscargle(t, y, f, normalize='amplitude')
  1068. # validate the results that should be the same
  1069. assert_allclose(pgram_false, pgram_power)
  1070. assert_allclose(pgram_true, pgram_norm)
  1071. # validate that the power and norm outputs are proper wrt each other
  1072. weights = np.ones_like(y)/float(y.shape[0])
  1073. YY_hat = (weights * y * y).sum()
  1074. YY = YY_hat # correct formula for floating_mean=False
  1075. assert_allclose(pgram_power * 2.0 / (float(t.shape[0]) * YY), pgram_norm)
  1076. # validate that the amp output is correct for the given input
  1077. f_i = np.where(f==w)[0][0]
  1078. assert_allclose(np.abs(pgram_amp[f_i]), ampl)
  1079. # check invalid inputs
  1080. # 1) a string that is not allowed
  1081. assert_raises(ValueError, lombscargle, t, y, f, normalize='lomb')
  1082. # 2) something besides a bool or str
  1083. assert_raises(ValueError, lombscargle, t, y, f, normalize=2)
  1084. def test_offset_removal(self):
  1085. # Verify that the amplitude is the same, even with an offset
  1086. # must use floating_mean=True, otherwise it will not remove an offset
  1087. # Input parameters
  1088. ampl = 2.
  1089. w = 1.
  1090. phi = 0.5 * np.pi
  1091. nin = 100
  1092. nout = 1000
  1093. p = 0.7 # Fraction of points to select
  1094. offset = 2.15 # Large offset
  1095. # Randomly select a fraction of an array with timesteps
  1096. rng = np.random.RandomState(2353425)
  1097. r = rng.rand(nin)
  1098. t = np.linspace(0.01*np.pi, 10.*np.pi, nin)[r >= p]
  1099. # Plot a sine wave for the selected times
  1100. y = ampl * np.sin(w*t + phi)
  1101. # Define the array of frequencies for which to compute the periodogram
  1102. f = np.linspace(0.01, 10., nout)
  1103. # Calculate Lomb-Scargle periodogram
  1104. pgram = lombscargle(t, y, f, floating_mean=True)
  1105. pgram_offset = lombscargle(t, y + offset, f, floating_mean=True)
  1106. # check if offset removal works as expected
  1107. assert_allclose(pgram, pgram_offset)
  1108. def test_floating_mean_false(self):
  1109. # Verify that when disabling the floating_mean, the calculations are correct
  1110. # Input parameters
  1111. ampl = 2.
  1112. w = 1.
  1113. phi = 0
  1114. nin = 1000
  1115. nout = 1000
  1116. p = 0.7 # Fraction of points to select
  1117. offset = 2 # Large offset
  1118. # Randomly select a fraction of an array with timesteps
  1119. rng = np.random.RandomState(2353425)
  1120. r = rng.rand(nin)
  1121. t = np.linspace(0.01*np.pi, 10.*np.pi, nin)[r >= p]
  1122. # Plot a cos wave for the selected times
  1123. y = ampl * np.cos(w*t + phi)
  1124. # Define the array of frequencies for which to compute the periodogram
  1125. f = np.linspace(0.01, 10., nout)
  1126. # Calculate Lomb-Scargle periodogram
  1127. pgram = lombscargle(t, y, f, normalize=True, floating_mean=False)
  1128. pgram_offset = lombscargle(t, y + offset, f, normalize=True,
  1129. floating_mean=False)
  1130. # check if disabling floating_mean works as expected
  1131. # nearly-zero for no offset, exact value will change based on seed
  1132. assert(pgram[0] < 0.01)
  1133. # significant value with offset, exact value will change based on seed
  1134. assert(pgram_offset[0] > 0.5)
  1135. def test_amplitude_is_correct(self):
  1136. # Verify that the amplitude is correct (when normalize='amplitude')
  1137. # Input parameters
  1138. ampl = 2.
  1139. w = 1.
  1140. phi = 0.12
  1141. nin = 100
  1142. nout = 1000
  1143. p = 0.7 # Fraction of points to select
  1144. offset = 2.15 # Large offset
  1145. # Randomly select a fraction of an array with timesteps
  1146. rng = np.random.RandomState(2353425)
  1147. r = rng.rand(nin)
  1148. t = np.linspace(0.01*np.pi, 10.*np.pi, nin)[r >= p]
  1149. # Plot a sine wave for the selected times
  1150. y = ampl * np.cos(w*t + phi) + offset
  1151. # Define the array of frequencies for which to compute the periodogram
  1152. f = np.linspace(0.01, 10., nout)
  1153. # Get the index of where the exact result should be
  1154. f_indx = np.where(f==w)[0][0]
  1155. # Calculate Lomb-Scargle periodogram (amplitude + phase)
  1156. pgram = lombscargle(t, y, f, normalize='amplitude', floating_mean=True)
  1157. # Check if amplitude is correct
  1158. assert_allclose(np.abs(pgram[f_indx]), ampl)
  1159. # Check if phase is correct
  1160. # (phase angle is the negative of the phase offset)
  1161. assert_allclose(-np.angle(pgram[f_indx]), phi)
  1162. def test_negative_weight(self):
  1163. # Test that a negative weight produces an error
  1164. t = np.zeros(1)
  1165. y = np.zeros(1)
  1166. f = np.ones(1)
  1167. weights = -np.ones(1)
  1168. assert_raises(ValueError, lombscargle, t, y, f, weights=weights)
  1169. @pytest.mark.filterwarnings("ignore::DeprecationWarning")
  1170. def test_list_input(self):
  1171. # Test that input can be passsed in as lists and with a numerical issue
  1172. # https://github.com/scipy/scipy/issues/8787
  1173. t = [1.98201652e+09, 1.98201752e+09, 1.98201852e+09, 1.98201952e+09,
  1174. 1.98202052e+09, 1.98202152e+09, 1.98202252e+09, 1.98202352e+09,
  1175. 1.98202452e+09, 1.98202552e+09, 1.98202652e+09, 1.98202752e+09,
  1176. 1.98202852e+09, 1.98202952e+09, 1.98203052e+09, 1.98203152e+09,
  1177. 1.98203252e+09, 1.98203352e+09, 1.98203452e+09, 1.98203552e+09,
  1178. 1.98205452e+09, 1.98205552e+09, 1.98205652e+09, 1.98205752e+09,
  1179. 1.98205852e+09, 1.98205952e+09, 1.98206052e+09, 1.98206152e+09,
  1180. 1.98206252e+09, 1.98206352e+09, 1.98206452e+09, 1.98206552e+09,
  1181. 1.98206652e+09, 1.98206752e+09, 1.98206852e+09, 1.98206952e+09,
  1182. 1.98207052e+09, 1.98207152e+09, 1.98207252e+09, 1.98207352e+09,
  1183. 1.98209652e+09, 1.98209752e+09, 1.98209852e+09, 1.98209952e+09,
  1184. 1.98210052e+09, 1.98210152e+09, 1.98210252e+09, 1.98210352e+09,
  1185. 1.98210452e+09, 1.98210552e+09, 1.98210652e+09, 1.98210752e+09,
  1186. 1.98210852e+09, 1.98210952e+09, 1.98211052e+09, 1.98211152e+09,
  1187. 1.98211252e+09, 1.98211352e+09, 1.98211452e+09, 1.98211552e+09,
  1188. 1.98217252e+09, 1.98217352e+09, 1.98217452e+09, 1.98217552e+09,
  1189. 1.98217652e+09, 1.98217752e+09, 1.98217852e+09, 1.98217952e+09,
  1190. 1.98218052e+09, 1.98218152e+09, 1.98218252e+09, 1.98218352e+09,
  1191. 1.98218452e+09, 1.98218552e+09, 1.98218652e+09, 1.98218752e+09,
  1192. 1.98218852e+09, 1.98218952e+09, 1.98219052e+09, 1.98219152e+09,
  1193. 1.98219352e+09, 1.98219452e+09, 1.98219552e+09, 1.98219652e+09,
  1194. 1.98219752e+09, 1.98219852e+09, 1.98219952e+09, 1.98220052e+09,
  1195. 1.98220152e+09, 1.98220252e+09, 1.98220352e+09, 1.98220452e+09,
  1196. 1.98220552e+09, 1.98220652e+09, 1.98220752e+09, 1.98220852e+09,
  1197. 1.98220952e+09, 1.98221052e+09, 1.98221152e+09, 1.98221252e+09,
  1198. 1.98222752e+09, 1.98222852e+09, 1.98222952e+09, 1.98223052e+09,
  1199. 1.98223152e+09, 1.98223252e+09, 1.98223352e+09, 1.98223452e+09,
  1200. 1.98223552e+09, 1.98223652e+09, 1.98223752e+09, 1.98223852e+09,
  1201. 1.98223952e+09, 1.98224052e+09, 1.98224152e+09, 1.98224252e+09,
  1202. 1.98224352e+09, 1.98224452e+09, 1.98224552e+09, 1.98224652e+09,
  1203. 1.98224752e+09]
  1204. y = [2.97600000e+03, 3.18200000e+03, 3.74900000e+03, 4.53500000e+03,
  1205. 5.43300000e+03, 6.38000000e+03, 7.34000000e+03, 8.29200000e+03,
  1206. 9.21900000e+03, 1.01120000e+04, 1.09620000e+04, 1.17600000e+04,
  1207. 1.25010000e+04, 1.31790000e+04, 1.37900000e+04, 1.43290000e+04,
  1208. 1.47940000e+04, 1.51800000e+04, 1.54870000e+04, 1.57110000e+04,
  1209. 5.74200000e+03, 4.82300000e+03, 3.99100000e+03, 3.33600000e+03,
  1210. 2.99600000e+03, 3.08400000e+03, 3.56700000e+03, 4.30700000e+03,
  1211. 5.18200000e+03, 6.11900000e+03, 7.07900000e+03, 8.03400000e+03,
  1212. 8.97000000e+03, 9.87300000e+03, 1.07350000e+04, 1.15480000e+04,
  1213. 1.23050000e+04, 1.30010000e+04, 1.36300000e+04, 1.41890000e+04,
  1214. 6.00000000e+03, 5.06800000e+03, 4.20500000e+03, 3.49000000e+03,
  1215. 3.04900000e+03, 3.01600000e+03, 3.40400000e+03, 4.08800000e+03,
  1216. 4.93500000e+03, 5.86000000e+03, 6.81700000e+03, 7.77500000e+03,
  1217. 8.71800000e+03, 9.63100000e+03, 1.05050000e+04, 1.13320000e+04,
  1218. 1.21050000e+04, 1.28170000e+04, 1.34660000e+04, 1.40440000e+04,
  1219. 1.32730000e+04, 1.26040000e+04, 1.18720000e+04, 1.10820000e+04,
  1220. 1.02400000e+04, 9.35300000e+03, 8.43000000e+03, 7.48100000e+03,
  1221. 6.52100000e+03, 5.57000000e+03, 4.66200000e+03, 3.85400000e+03,
  1222. 3.24600000e+03, 2.97900000e+03, 3.14700000e+03, 3.68800000e+03,
  1223. 4.45900000e+03, 5.35000000e+03, 6.29400000e+03, 7.25400000e+03,
  1224. 9.13800000e+03, 1.00340000e+04, 1.08880000e+04, 1.16910000e+04,
  1225. 1.24370000e+04, 1.31210000e+04, 1.37380000e+04, 1.42840000e+04,
  1226. 1.47550000e+04, 1.51490000e+04, 1.54630000e+04, 1.56950000e+04,
  1227. 1.58430000e+04, 1.59070000e+04, 1.58860000e+04, 1.57800000e+04,
  1228. 1.55910000e+04, 1.53190000e+04, 1.49650000e+04, 1.45330000e+04,
  1229. 3.01000000e+03, 3.05900000e+03, 3.51200000e+03, 4.23400000e+03,
  1230. 5.10000000e+03, 6.03400000e+03, 6.99300000e+03, 7.95000000e+03,
  1231. 8.88800000e+03, 9.79400000e+03, 1.06600000e+04, 1.14770000e+04,
  1232. 1.22400000e+04, 1.29410000e+04, 1.35770000e+04, 1.41430000e+04,
  1233. 1.46350000e+04, 1.50500000e+04, 1.53850000e+04, 1.56400000e+04,
  1234. 1.58110000e+04]
  1235. periods = np.linspace(400, 120, 1000)
  1236. angular_freq = 2 * np.pi / periods
  1237. lombscargle(t, y, angular_freq, precenter=True, normalize=True)
  1238. def test_zero_freq(self):
  1239. # Verify that function works when freqs includes 0
  1240. # The value at f=0 will depend on the seed
  1241. # Input parameters
  1242. ampl = 2.
  1243. w = 1.
  1244. phi = 0.12
  1245. nin = 100
  1246. nout = 1001
  1247. p = 0.7 # Fraction of points to select
  1248. offset = 0
  1249. # Randomly select a fraction of an array with timesteps
  1250. rng = np.random.RandomState(2353425)
  1251. r = rng.rand(nin)
  1252. t = np.linspace(0.01*np.pi, 10.*np.pi, nin)[r >= p]
  1253. # Plot a sine wave for the selected times
  1254. y = ampl * np.cos(w*t + phi) + offset
  1255. # Define the array of frequencies for which to compute the periodogram
  1256. f = np.linspace(0, 10., nout)
  1257. # Calculate Lomb-Scargle periodogram
  1258. pgram = lombscargle(t, y, f, normalize=True, floating_mean=True)
  1259. # exact value will change based on seed
  1260. # testing to make sure it is very small
  1261. assert(pgram[0] < 1e-4)
  1262. def test_simple_div_zero(self):
  1263. # these are bare-minimum examples that would, without the eps adjustments,
  1264. # cause division-by-zero errors
  1265. # first, test with example that will cause first SS sum to be 0.0
  1266. t = [t + 1 for t in range(0, 32)]
  1267. y = np.ones(len(t))
  1268. freqs = [2.0*np.pi] * 2 # must have 2+ elements
  1269. lombscargle(t, y, freqs)
  1270. # second, test with example that will cause first CC sum to be 0.0
  1271. t = [t*4 + 1 for t in range(0, 32)]
  1272. y = np.ones(len(t))
  1273. freqs = [np.pi/2.0] * 2 # must have 2+ elements
  1274. lombscargle(t, y, freqs)
  1275. @pytest.mark.filterwarnings("ignore::DeprecationWarning")
  1276. def test_input_mutation(self):
  1277. # this tests for mutation of the input arrays
  1278. # https://github.com/scipy/scipy/issues/23474
  1279. # Input parameters
  1280. ampl = 2.
  1281. w = 1.
  1282. phi = 0.5 * np.pi
  1283. nin = 100
  1284. nout = 1000
  1285. p = 0.7 # Fraction of points to select
  1286. # Randomly select a fraction of an array with timesteps
  1287. rng = np.random.default_rng()
  1288. r = rng.random(nin)
  1289. t = np.linspace(0.01*np.pi, 10.*np.pi, nin)[r >= p]
  1290. # Plot a sine wave for the selected times
  1291. y = ampl * np.sin(w*t + phi)
  1292. # Define the array of frequencies for which to compute the periodogram
  1293. f = np.linspace(0.01, 10., nout)
  1294. weights = np.ones_like(y)
  1295. # create original copies before passing
  1296. t_org = t.copy()
  1297. y_org = y.copy()
  1298. f_org = f.copy()
  1299. weights_org = weights.copy()
  1300. lombscargle(t, y, f, precenter=True, weights=weights)
  1301. # check all 4 array inputs
  1302. assert_array_equal(t, t_org)
  1303. assert_array_equal(y, y_org)
  1304. assert_array_equal(f, f_org)
  1305. assert_array_equal(weights, weights_org)
  1306. def test_precenter_deprecation(self):
  1307. # test that precenter deprecation warning is raised
  1308. # Input parameters
  1309. ampl = 2.
  1310. w = 1.
  1311. phi = 0.5 * np.pi
  1312. nin = 100
  1313. nout = 1000
  1314. p = 0.7 # Fraction of points to select
  1315. offset = 0.15 # Offset to be subtracted in pre-centering
  1316. # Randomly select a fraction of an array with timesteps
  1317. rng = np.random.default_rng()
  1318. r = rng.random(nin)
  1319. t = np.linspace(0.01*np.pi, 10.*np.pi, nin)[r >= p]
  1320. # Plot a sine wave for the selected times
  1321. y = ampl * np.sin(w*t + phi) + offset
  1322. # Define the array of frequencies for which to compute the periodogram
  1323. f = np.linspace(0.01, 10., nout)
  1324. # Calculate Lomb-Scargle periodogram
  1325. with pytest.deprecated_call(match="leave 'precenter' unspecified"):
  1326. lombscargle(t, y, f, precenter=True)
  1327. # Should warn for explicit `False` too
  1328. with pytest.deprecated_call(match="leave 'precenter' unspecified"):
  1329. lombscargle(t, y, f, precenter=False)
  1330. @pytest.mark.filterwarnings(
  1331. "ignore:.*leave 'precenter' unspecified.*:DeprecationWarning"
  1332. )
  1333. def test_positional_args_deprecation(self):
  1334. with pytest.deprecated_call(match="use keyword arguments"):
  1335. one = np.asarray([1.0])
  1336. lombscargle(one, one, one, False)
  1337. class TestSTFT:
  1338. def test_input_validation(self):
  1339. def chk_VE(match):
  1340. """Assert for a ValueError matching regexp `match`.
  1341. This little wrapper allows a more concise code layout.
  1342. """
  1343. return pytest.raises(ValueError, match=match)
  1344. # Checks for check_COLA():
  1345. with chk_VE('nperseg must be a positive integer'):
  1346. check_COLA('hann', -10, 0)
  1347. with chk_VE('noverlap must be less than nperseg.'):
  1348. check_COLA('hann', 10, 20)
  1349. with chk_VE('window must be 1-D'):
  1350. check_COLA(np.ones((2, 2)), 10, 0)
  1351. with chk_VE('window must have length of nperseg'):
  1352. check_COLA(np.ones(20), 10, 0)
  1353. # Checks for check_NOLA():
  1354. with chk_VE('nperseg must be a positive integer'):
  1355. check_NOLA('hann', -10, 0)
  1356. with chk_VE('noverlap must be less than nperseg'):
  1357. check_NOLA('hann', 10, 20)
  1358. with chk_VE('window must be 1-D'):
  1359. check_NOLA(np.ones((2, 2)), 10, 0)
  1360. with chk_VE('window must have length of nperseg'):
  1361. check_NOLA(np.ones(20), 10, 0)
  1362. with chk_VE('noverlap must be a nonnegative integer'):
  1363. check_NOLA('hann', 64, -32)
  1364. x = np.zeros(1024)
  1365. z = stft(x)[2]
  1366. # Checks for stft():
  1367. with chk_VE('window must be 1-D'):
  1368. stft(x, window=np.ones((2, 2)))
  1369. with chk_VE('value specified for nperseg is different ' +
  1370. 'from length of window'):
  1371. stft(x, window=np.ones(10), nperseg=256)
  1372. with chk_VE('nperseg must be a positive integer'):
  1373. stft(x, nperseg=-256)
  1374. with chk_VE('noverlap must be less than nperseg.'):
  1375. stft(x, nperseg=256, noverlap=1024)
  1376. with chk_VE('nfft must be greater than or equal to nperseg.'):
  1377. stft(x, nperseg=256, nfft=8)
  1378. # Checks for istft():
  1379. with chk_VE('Input stft must be at least 2d!'):
  1380. istft(x)
  1381. with chk_VE('window must be 1-D'):
  1382. istft(z, window=np.ones((2, 2)))
  1383. with chk_VE('window must have length of 256'):
  1384. istft(z, window=np.ones(10), nperseg=256)
  1385. with chk_VE('nperseg must be a positive integer'):
  1386. istft(z, nperseg=-256)
  1387. with chk_VE('noverlap must be less than nperseg.'):
  1388. istft(z, nperseg=256, noverlap=1024)
  1389. with chk_VE('nfft must be greater than or equal to nperseg.'):
  1390. istft(z, nperseg=256, nfft=8)
  1391. with pytest.warns(UserWarning, match="NOLA condition failed, " +
  1392. "STFT may not be invertible"):
  1393. istft(z, nperseg=256, noverlap=0, window='hann')
  1394. with chk_VE('Must specify differing time and frequency axes!'):
  1395. istft(z, time_axis=0, freq_axis=0)
  1396. # Checks for _spectral_helper():
  1397. with chk_VE("Unknown value for mode foo, must be one of: " +
  1398. r"\{'psd', 'stft'\}"):
  1399. _spectral_helper(x, x, mode='foo')
  1400. with chk_VE("x and y must be equal if mode is 'stft'"):
  1401. _spectral_helper(x[:512], x[512:], mode='stft')
  1402. with chk_VE("Unknown boundary option 'foo', must be one of: " +
  1403. r"\['even', 'odd', 'constant', 'zeros', None\]"):
  1404. _spectral_helper(x, x, boundary='foo')
  1405. scaling = "not_valid"
  1406. with chk_VE(fr"Parameter {scaling=} not in \['spectrum', 'psd'\]!"):
  1407. stft(x, scaling=scaling)
  1408. with chk_VE(fr"Parameter {scaling=} not in \['spectrum', 'psd'\]!"):
  1409. istft(z, scaling=scaling)
  1410. def test_check_COLA(self):
  1411. settings = [
  1412. ('boxcar', 10, 0),
  1413. ('boxcar', 10, 9),
  1414. ('bartlett', 51, 26),
  1415. ('hann', 256, 128),
  1416. ('hann', 256, 192),
  1417. ('blackman', 300, 200),
  1418. (('tukey', 0.5), 256, 64),
  1419. ('hann', 256, 255),
  1420. ]
  1421. for setting in settings:
  1422. msg = '{}, {}, {}'.format(*setting)
  1423. assert_equal(True, check_COLA(*setting), err_msg=msg)
  1424. def test_check_NOLA(self):
  1425. settings_pass = [
  1426. ('boxcar', 10, 0),
  1427. ('boxcar', 10, 9),
  1428. ('boxcar', 10, 7),
  1429. ('bartlett', 51, 26),
  1430. ('bartlett', 51, 10),
  1431. ('hann', 256, 128),
  1432. ('hann', 256, 192),
  1433. ('hann', 256, 37),
  1434. ('blackman', 300, 200),
  1435. ('blackman', 300, 123),
  1436. (('tukey', 0.5), 256, 64),
  1437. (('tukey', 0.5), 256, 38),
  1438. ('hann', 256, 255),
  1439. ('hann', 256, 39),
  1440. ]
  1441. for setting in settings_pass:
  1442. msg = '{}, {}, {}'.format(*setting)
  1443. assert_equal(True, check_NOLA(*setting), err_msg=msg)
  1444. w_fail = np.ones(16)
  1445. w_fail[::2] = 0
  1446. settings_fail = [
  1447. (w_fail, len(w_fail), len(w_fail) // 2),
  1448. ('hann', 64, 0),
  1449. ]
  1450. for setting in settings_fail:
  1451. msg = '{}, {}, {}'.format(*setting)
  1452. assert_equal(False, check_NOLA(*setting), err_msg=msg)
  1453. def test_average_all_segments(self):
  1454. rng = np.random.RandomState(1234)
  1455. x = rng.randn(1024)
  1456. fs = 1.0
  1457. window = 'hann'
  1458. nperseg = 16
  1459. noverlap = 8
  1460. # Compare twosided, because onesided welch doubles non-DC terms to
  1461. # account for power at negative frequencies. stft doesn't do this,
  1462. # because it breaks invertibility.
  1463. f, _, Z = stft(x, fs, window, nperseg, noverlap, padded=False,
  1464. return_onesided=False, boundary=None)
  1465. fw, Pw = welch(x, fs, window, nperseg, noverlap, return_onesided=False,
  1466. scaling='spectrum', detrend=False)
  1467. assert_allclose(f, fw)
  1468. assert_allclose(np.mean(np.abs(Z)**2, axis=-1), Pw)
  1469. def test_permute_axes(self):
  1470. rng = np.random.RandomState(1234)
  1471. x = rng.randn(1024)
  1472. fs = 1.0
  1473. window = 'hann'
  1474. nperseg = 16
  1475. noverlap = 8
  1476. f1, t1, Z1 = stft(x, fs, window, nperseg, noverlap)
  1477. f2, t2, Z2 = stft(x.reshape((-1, 1, 1)), fs, window, nperseg, noverlap,
  1478. axis=0)
  1479. t3, x1 = istft(Z1, fs, window, nperseg, noverlap)
  1480. t4, x2 = istft(Z2.T, fs, window, nperseg, noverlap, time_axis=0,
  1481. freq_axis=-1)
  1482. assert_allclose(f1, f2)
  1483. assert_allclose(t1, t2)
  1484. assert_allclose(t3, t4)
  1485. assert_allclose(Z1, Z2[:, 0, 0, :])
  1486. assert_allclose(x1, x2[:, 0, 0])
  1487. @pytest.mark.parametrize('scaling', ['spectrum', 'psd'])
  1488. def test_roundtrip_real(self, scaling):
  1489. rng = np.random.RandomState(1234)
  1490. settings = [
  1491. ('boxcar', 100, 10, 0), # Test no overlap
  1492. ('boxcar', 100, 10, 9), # Test high overlap
  1493. ('bartlett', 101, 51, 26), # Test odd nperseg
  1494. ('hann', 1024, 256, 128), # Test defaults
  1495. (('tukey', 0.5), 1152, 256, 64), # Test Tukey
  1496. ('hann', 1024, 256, 255), # Test overlapped hann
  1497. ]
  1498. for window, N, nperseg, noverlap in settings:
  1499. t = np.arange(N)
  1500. x = 10*rng.randn(t.size)
  1501. _, _, zz = stft(x, nperseg=nperseg, noverlap=noverlap,
  1502. window=window, detrend=None, padded=False,
  1503. scaling=scaling)
  1504. tr, xr = istft(zz, nperseg=nperseg, noverlap=noverlap,
  1505. window=window, scaling=scaling)
  1506. msg = f'{window}, {noverlap}'
  1507. assert_allclose(t, tr, err_msg=msg)
  1508. assert_allclose(x, xr, err_msg=msg)
  1509. def test_roundtrip_not_nola(self):
  1510. rng = np.random.RandomState(1234)
  1511. w_fail = np.ones(16)
  1512. w_fail[::2] = 0
  1513. settings = [
  1514. (w_fail, 256, len(w_fail), len(w_fail) // 2),
  1515. ('hann', 256, 64, 0),
  1516. ]
  1517. for window, N, nperseg, noverlap in settings:
  1518. msg = f'{window}, {N}, {nperseg}, {noverlap}'
  1519. assert not check_NOLA(window, nperseg, noverlap), msg
  1520. t = np.arange(N)
  1521. x = 10 * rng.randn(t.size)
  1522. _, _, zz = stft(x, nperseg=nperseg, noverlap=noverlap,
  1523. window=window, detrend=None, padded=True,
  1524. boundary='zeros')
  1525. with pytest.warns(UserWarning, match='NOLA'):
  1526. tr, xr = istft(zz, nperseg=nperseg, noverlap=noverlap,
  1527. window=window, boundary=True)
  1528. assert np.allclose(t, tr[:len(t)]), msg
  1529. assert not np.allclose(x, xr[:len(x)]), msg
  1530. def test_roundtrip_nola_not_cola(self):
  1531. rng = np.random.RandomState(1234)
  1532. settings = [
  1533. ('boxcar', 100, 10, 3), # NOLA True, COLA False
  1534. ('bartlett', 101, 51, 37), # NOLA True, COLA False
  1535. ('hann', 1024, 256, 127), # NOLA True, COLA False
  1536. (('tukey', 0.5), 1152, 256, 14), # NOLA True, COLA False
  1537. ('hann', 1024, 256, 5), # NOLA True, COLA False
  1538. ]
  1539. for window, N, nperseg, noverlap in settings:
  1540. msg = f'{window}, {nperseg}, {noverlap}'
  1541. assert check_NOLA(window, nperseg, noverlap), msg
  1542. assert not check_COLA(window, nperseg, noverlap), msg
  1543. t = np.arange(N)
  1544. x = 10 * rng.randn(t.size)
  1545. _, _, zz = stft(x, nperseg=nperseg, noverlap=noverlap,
  1546. window=window, detrend=None, padded=True,
  1547. boundary='zeros')
  1548. tr, xr = istft(zz, nperseg=nperseg, noverlap=noverlap,
  1549. window=window, boundary=True)
  1550. msg = f'{window}, {noverlap}'
  1551. assert_allclose(t, tr[:len(t)], err_msg=msg)
  1552. assert_allclose(x, xr[:len(x)], err_msg=msg)
  1553. def test_roundtrip_float32(self):
  1554. rng = np.random.RandomState(1234)
  1555. settings = [('hann', 1024, 256, 128)]
  1556. for window, N, nperseg, noverlap in settings:
  1557. t = np.arange(N)
  1558. x = 10*rng.randn(t.size)
  1559. x = x.astype(np.float32)
  1560. _, _, zz = stft(x, nperseg=nperseg, noverlap=noverlap,
  1561. window=window, detrend=None, padded=False)
  1562. tr, xr = istft(zz, nperseg=nperseg, noverlap=noverlap,
  1563. window=window)
  1564. msg = f'{window}, {noverlap}'
  1565. assert_allclose(t, t, err_msg=msg)
  1566. assert_allclose(x, xr, err_msg=msg, rtol=1e-4, atol=1e-5)
  1567. assert_(x.dtype == xr.dtype)
  1568. @pytest.mark.parametrize('scaling', ['spectrum', 'psd'])
  1569. def test_roundtrip_complex(self, scaling):
  1570. rng = np.random.RandomState(1234)
  1571. settings = [
  1572. ('boxcar', 100, 10, 0), # Test no overlap
  1573. ('boxcar', 100, 10, 9), # Test high overlap
  1574. ('bartlett', 101, 51, 26), # Test odd nperseg
  1575. ('hann', 1024, 256, 128), # Test defaults
  1576. (('tukey', 0.5), 1152, 256, 64), # Test Tukey
  1577. ('hann', 1024, 256, 255), # Test overlapped hann
  1578. ]
  1579. for window, N, nperseg, noverlap in settings:
  1580. t = np.arange(N)
  1581. x = 10*rng.randn(t.size) + 10j*rng.randn(t.size)
  1582. _, _, zz = stft(x, nperseg=nperseg, noverlap=noverlap,
  1583. window=window, detrend=None, padded=False,
  1584. return_onesided=False, scaling=scaling)
  1585. tr, xr = istft(zz, nperseg=nperseg, noverlap=noverlap,
  1586. window=window, input_onesided=False,
  1587. scaling=scaling)
  1588. msg = f'{window}, {nperseg}, {noverlap}'
  1589. assert_allclose(t, tr, err_msg=msg)
  1590. assert_allclose(x, xr, err_msg=msg)
  1591. # Check that asking for onesided switches to twosided
  1592. with warnings.catch_warnings():
  1593. warnings.filterwarnings(
  1594. "ignore",
  1595. "Input data is complex, switching to return_onesided=False",
  1596. UserWarning,
  1597. )
  1598. _, _, zz = stft(x, nperseg=nperseg, noverlap=noverlap,
  1599. window=window, detrend=None, padded=False,
  1600. return_onesided=True, scaling=scaling)
  1601. tr, xr = istft(zz, nperseg=nperseg, noverlap=noverlap,
  1602. window=window, input_onesided=False, scaling=scaling)
  1603. msg = f'{window}, {nperseg}, {noverlap}'
  1604. assert_allclose(t, tr, err_msg=msg)
  1605. assert_allclose(x, xr, err_msg=msg)
  1606. def test_roundtrip_boundary_extension(self):
  1607. rng = np.random.RandomState(1234)
  1608. # Test against boxcar, since window is all ones, and thus can be fully
  1609. # recovered with no boundary extension
  1610. settings = [
  1611. ('boxcar', 100, 10, 0), # Test no overlap
  1612. ('boxcar', 100, 10, 9), # Test high overlap
  1613. ]
  1614. for window, N, nperseg, noverlap in settings:
  1615. t = np.arange(N)
  1616. x = 10*rng.randn(t.size)
  1617. _, _, zz = stft(x, nperseg=nperseg, noverlap=noverlap,
  1618. window=window, detrend=None, padded=True,
  1619. boundary=None)
  1620. _, xr = istft(zz, noverlap=noverlap, window=window, boundary=False)
  1621. for boundary in ['even', 'odd', 'constant', 'zeros']:
  1622. _, _, zz_ext = stft(x, nperseg=nperseg, noverlap=noverlap,
  1623. window=window, detrend=None, padded=True,
  1624. boundary=boundary)
  1625. _, xr_ext = istft(zz_ext, noverlap=noverlap, window=window,
  1626. boundary=True)
  1627. msg = f'{window}, {noverlap}, {boundary}'
  1628. assert_allclose(x, xr, err_msg=msg)
  1629. assert_allclose(x, xr_ext, err_msg=msg)
  1630. def test_roundtrip_padded_signal(self):
  1631. rng = np.random.RandomState(1234)
  1632. settings = [
  1633. ('boxcar', 101, 10, 0),
  1634. ('hann', 1000, 256, 128),
  1635. ]
  1636. for window, N, nperseg, noverlap in settings:
  1637. t = np.arange(N)
  1638. x = 10*rng.randn(t.size)
  1639. _, _, zz = stft(x, nperseg=nperseg, noverlap=noverlap,
  1640. window=window, detrend=None, padded=True)
  1641. tr, xr = istft(zz, noverlap=noverlap, window=window)
  1642. msg = f'{window}, {noverlap}'
  1643. # Account for possible zero-padding at the end
  1644. assert_allclose(t, tr[:t.size], err_msg=msg)
  1645. assert_allclose(x, xr[:x.size], err_msg=msg)
  1646. def test_roundtrip_padded_FFT(self):
  1647. rng = np.random.RandomState(1234)
  1648. settings = [
  1649. ('hann', 1024, 256, 128, 512),
  1650. ('hann', 1024, 256, 128, 501),
  1651. ('boxcar', 100, 10, 0, 33),
  1652. (('tukey', 0.5), 1152, 256, 64, 1024),
  1653. ]
  1654. for window, N, nperseg, noverlap, nfft in settings:
  1655. t = np.arange(N)
  1656. x = 10*rng.randn(t.size)
  1657. xc = x*np.exp(1j*np.pi/4)
  1658. # real signal
  1659. _, _, z = stft(x, nperseg=nperseg, noverlap=noverlap, nfft=nfft,
  1660. window=window, detrend=None, padded=True)
  1661. # complex signal
  1662. _, _, zc = stft(xc, nperseg=nperseg, noverlap=noverlap, nfft=nfft,
  1663. window=window, detrend=None, padded=True,
  1664. return_onesided=False)
  1665. tr, xr = istft(z, nperseg=nperseg, noverlap=noverlap, nfft=nfft,
  1666. window=window)
  1667. tr, xcr = istft(zc, nperseg=nperseg, noverlap=noverlap, nfft=nfft,
  1668. window=window, input_onesided=False)
  1669. msg = f'{window}, {noverlap}'
  1670. assert_allclose(t, tr, err_msg=msg)
  1671. assert_allclose(x, xr, err_msg=msg)
  1672. assert_allclose(xc, xcr, err_msg=msg)
  1673. def test_axis_rolling(self):
  1674. rng = np.random.RandomState(1234)
  1675. x_flat = rng.randn(1024)
  1676. _, _, z_flat = stft(x_flat)
  1677. for a in range(3):
  1678. newshape = [1,]*3
  1679. newshape[a] = -1
  1680. x = x_flat.reshape(newshape)
  1681. _, _, z_plus = stft(x, axis=a) # Positive axis index
  1682. _, _, z_minus = stft(x, axis=a-x.ndim) # Negative axis index
  1683. assert_equal(z_flat, z_plus.squeeze(), err_msg=a)
  1684. assert_equal(z_flat, z_minus.squeeze(), err_msg=a-x.ndim)
  1685. # z_flat has shape [n_freq, n_time]
  1686. # Test vs. transpose
  1687. _, x_transpose_m = istft(z_flat.T, time_axis=-2, freq_axis=-1)
  1688. _, x_transpose_p = istft(z_flat.T, time_axis=0, freq_axis=1)
  1689. assert_allclose(x_flat, x_transpose_m, err_msg='istft transpose minus')
  1690. assert_allclose(x_flat, x_transpose_p, err_msg='istft transpose plus')
  1691. def test_roundtrip_scaling(self):
  1692. """Verify behavior of scaling parameter. """
  1693. # Create 1024 sample cosine signal with amplitude 2:
  1694. X = np.zeros(513, dtype=complex)
  1695. X[256] = 1024
  1696. x = np.fft.irfft(X)
  1697. power_x = sum(x**2) / len(x) # power of signal x is 2
  1698. # Calculate magnitude-scaled STFT:
  1699. Zs = stft(x, boundary='even', scaling='spectrum')[2]
  1700. # Test round trip:
  1701. x1 = istft(Zs, boundary=True, scaling='spectrum')[1]
  1702. assert_allclose(x1, x)
  1703. # For a Hann-windowed 256 sample length FFT, we expect a peak at
  1704. # frequency 64 (since it is 1/4 the length of X) with a height of 1
  1705. # (half the amplitude). A Hann window of a perfectly centered sine has
  1706. # the magnitude [..., 0, 0, 0.5, 1, 0.5, 0, 0, ...].
  1707. # Note that in this case the 'even' padding works for the beginning
  1708. # but not for the end of the STFT.
  1709. assert_allclose(abs(Zs[63, :-1]), 0.5)
  1710. assert_allclose(abs(Zs[64, :-1]), 1)
  1711. assert_allclose(abs(Zs[65, :-1]), 0.5)
  1712. # All other values should be zero:
  1713. Zs[63:66, :-1] = 0
  1714. # Note since 'rtol' does not have influence here, atol needs to be set:
  1715. assert_allclose(Zs[:, :-1], 0, atol=np.finfo(Zs.dtype).resolution)
  1716. # Calculate two-sided psd-scaled STFT:
  1717. # - using 'even' padding since signal is axis symmetric - this ensures
  1718. # stationary behavior on the boundaries
  1719. # - using the two-sided transform allows determining the spectral
  1720. # power by `sum(abs(Zp[:, k])**2) / len(f)` for the k-th time slot.
  1721. Zp = stft(x, return_onesided=False, boundary='even', scaling='psd')[2]
  1722. # Calculate spectral power of Zd by summing over the frequency axis:
  1723. psd_Zp = np.sum(Zp.real**2 + Zp.imag**2, axis=0) / Zp.shape[0]
  1724. # Spectral power of Zp should be equal to the signal's power:
  1725. assert_allclose(psd_Zp, power_x)
  1726. # Test round trip:
  1727. x1 = istft(Zp, input_onesided=False, boundary=True, scaling='psd')[1]
  1728. assert_allclose(x1, x)
  1729. # The power of the one-sided psd-scaled STFT can be determined
  1730. # analogously (note that the two sides are not of equal shape):
  1731. Zp0 = stft(x, return_onesided=True, boundary='even', scaling='psd')[2]
  1732. # Since x is real, its Fourier transform is conjugate symmetric, i.e.,
  1733. # the missing 'second side' can be expressed through the 'first side':
  1734. Zp1 = np.conj(Zp0[-2:0:-1, :]) # 'second side' is conjugate reversed
  1735. assert_allclose(Zp[:129, :], Zp0)
  1736. assert_allclose(Zp[129:, :], Zp1)
  1737. # Calculate the spectral power:
  1738. s2 = (np.sum(Zp0.real ** 2 + Zp0.imag ** 2, axis=0) +
  1739. np.sum(Zp1.real ** 2 + Zp1.imag ** 2, axis=0))
  1740. psd_Zp01 = s2 / (Zp0.shape[0] + Zp1.shape[0])
  1741. assert_allclose(psd_Zp01, power_x)
  1742. # Test round trip:
  1743. x1 = istft(Zp0, input_onesided=True, boundary=True, scaling='psd')[1]
  1744. assert_allclose(x1, x)
  1745. class TestSampledSpectralRepresentations:
  1746. """Check energy/power relations from `Spectral Analysis` section in the user guide.
  1747. A 32 sample cosine signal is used to compare the numerical to the expected results
  1748. stated in :ref:`tutorial_SpectralAnalysis` in
  1749. file ``doc/source/tutorial/signal.rst``
  1750. """
  1751. n: int = 32 #: number of samples
  1752. T: float = 1/16 #: sampling interval
  1753. a_ref: float = 3 #: amplitude of reference
  1754. l_a: int = 3 #: index in fft for defining frequency of test signal
  1755. x_ref: np.ndarray #: reference signal
  1756. X_ref: np.ndarray #: two-sided FFT of x_ref
  1757. E_ref: float #: energy of signal
  1758. P_ref: float #: power of signal
  1759. def setup_method(self):
  1760. """Create Cosine signal with amplitude a from spectrum. """
  1761. f = rfftfreq(self.n, self.T)
  1762. X_ref = np.zeros_like(f)
  1763. self.l_a = 3
  1764. X_ref[self.l_a] = self.a_ref/2 * self.n # set amplitude
  1765. self.x_ref = irfft(X_ref)
  1766. self.X_ref = fft(self.x_ref)
  1767. # Closed form expression for continuous-time signal:
  1768. self.E_ref = self.tau * self.a_ref**2 / 2 # energy of signal
  1769. self.P_ref = self.a_ref**2 / 2 # power of signal
  1770. @property
  1771. def tau(self) -> float:
  1772. """Duration of signal. """
  1773. return self.n * self.T
  1774. @property
  1775. def delta_f(self) -> float:
  1776. """Bin width """
  1777. return 1 / (self.n * self.T)
  1778. def test_reference_signal(self):
  1779. """Test energy and power formulas. """
  1780. # Verify that amplitude is a:
  1781. assert_allclose(2*self.a_ref, np.ptp(self.x_ref), rtol=0.1)
  1782. # Verify that energy expression for sampled signal:
  1783. assert_allclose(self.T * sum(self.x_ref ** 2), self.E_ref)
  1784. # Verify that spectral energy and power formulas are correct:
  1785. sum_X_ref_squared = sum(self.X_ref.real**2 + self.X_ref.imag**2)
  1786. assert_allclose(self.T/self.n * sum_X_ref_squared, self.E_ref)
  1787. assert_allclose(1/self.n**2 * sum_X_ref_squared, self.P_ref)
  1788. def test_windowed_DFT(self):
  1789. """Verify spectral representations of windowed DFT.
  1790. Furthermore, the scalings of `periodogram` and `welch` are verified.
  1791. """
  1792. w = hann(self.n, sym=False)
  1793. c_amp, c_rms = abs(sum(w)), np.sqrt(sum(w.real**2 + w.imag**2))
  1794. Xw = fft(self.x_ref*w) # unnormalized windowed DFT
  1795. # Verify that the *spectrum* peak is consistent:
  1796. assert_allclose(self.tau * Xw[self.l_a] / c_amp, self.a_ref * self.tau / 2)
  1797. # Verify that the *amplitude spectrum* peak is consistent:
  1798. assert_allclose(Xw[self.l_a] / c_amp, self.a_ref/2)
  1799. # Verify spectral power/energy equals signal's power/energy:
  1800. X_ESD = self.tau * self.T * abs(Xw / c_rms)**2 # Energy Spectral Density
  1801. X_PSD = self.T * abs(Xw / c_rms)**2 # Power Spectral Density
  1802. assert_allclose(self.delta_f * sum(X_ESD), self.E_ref)
  1803. assert_allclose(self.delta_f * sum(X_PSD), self.P_ref)
  1804. # Verify scalings of periodogram:
  1805. kw = dict(fs=1/self.T, window=w, detrend=False, return_onesided=False)
  1806. _, P_mag = periodogram(self.x_ref, scaling='spectrum', **kw)
  1807. _, P_psd = periodogram(self.x_ref, scaling='density', **kw)
  1808. # Verify that periodogram calculates a squared magnitude spectrum:
  1809. float_res = np.finfo(P_mag.dtype).resolution
  1810. assert_allclose(P_mag, abs(Xw/c_amp)**2, atol=float_res*max(P_mag))
  1811. # Verify that periodogram calculates a PSD:
  1812. assert_allclose(P_psd, X_PSD, atol=float_res*max(P_psd))
  1813. # Ensure that scaling of welch is the same as of periodogram:
  1814. kw = dict(nperseg=len(self.x_ref), noverlap=0, **kw)
  1815. assert_allclose(welch(self.x_ref, scaling='spectrum', **kw)[1], P_mag,
  1816. atol=float_res*max(P_mag))
  1817. assert_allclose(welch(self.x_ref, scaling='density', **kw)[1], P_psd,
  1818. atol=float_res*max(P_psd))