_spectral_py.py 95 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489
  1. """Tools for spectral analysis.
  2. """
  3. import numpy as np
  4. import numpy.typing as npt
  5. from scipy import fft as sp_fft
  6. from scipy._lib.deprecation import _deprecate_positional_args, _NoValue
  7. from . import _signaltools
  8. from ._short_time_fft import ShortTimeFFT, FFT_MODE_TYPE
  9. from .windows import get_window
  10. from ._arraytools import const_ext, even_ext, odd_ext, zero_ext
  11. import warnings
  12. from typing import cast, Literal
  13. __all__ = ['periodogram', 'welch', 'lombscargle', 'csd', 'coherence',
  14. 'spectrogram', 'stft', 'istft', 'check_COLA', 'check_NOLA']
  15. @_deprecate_positional_args(version="1.19.0")
  16. def lombscargle(
  17. x: npt.ArrayLike,
  18. y: npt.ArrayLike,
  19. freqs: npt.ArrayLike,
  20. *,
  21. precenter: bool = _NoValue,
  22. normalize: bool | Literal["power", "normalize", "amplitude"] = False,
  23. weights: npt.NDArray | None = None,
  24. floating_mean: bool = False,
  25. ) -> npt.NDArray:
  26. """
  27. Compute the generalized Lomb-Scargle periodogram.
  28. The Lomb-Scargle periodogram was developed by Lomb [1]_ and further
  29. extended by Scargle [2]_ to find, and test the significance of weak
  30. periodic signals with uneven temporal sampling. The algorithm used
  31. here is based on a weighted least-squares fit of the form
  32. ``y(ω) = a*cos(ω*x) + b*sin(ω*x) + c``, where the fit is calculated for
  33. each frequency independently. This algorithm was developed by Zechmeister
  34. and Kürster which improves the Lomb-Scargle periodogram by enabling
  35. the weighting of individual samples and calculating an unknown y offset
  36. (also called a "floating-mean" model) [3]_. For more details, and practical
  37. considerations, see the excellent reference on the Lomb-Scargle periodogram [4]_.
  38. When *normalize* is False (or "power") (default) the computed periodogram
  39. is unnormalized, it takes the value ``(A**2) * N/4`` for a harmonic
  40. signal with amplitude A for sufficiently large N. Where N is the length of x or y.
  41. When *normalize* is True (or "normalize") the computed periodogram is normalized
  42. by the residuals of the data around a constant reference model (at zero).
  43. When *normalize* is "amplitude" the computed periodogram is the complex
  44. representation of the amplitude and phase.
  45. Input arrays should be 1-D of a real floating data type, which are converted into
  46. float64 arrays before processing.
  47. Parameters
  48. ----------
  49. x : array_like
  50. Sample times.
  51. y : array_like
  52. Measurement values. Values are assumed to have a baseline of ``y = 0``. If
  53. there is a possibility of a y offset, it is recommended to set `floating_mean`
  54. to True.
  55. freqs : array_like
  56. Angular frequencies (e.g., having unit rad/s=2π/s for `x` having unit s) for
  57. output periodogram. Frequencies are normally >= 0, as any peak at ``-freq`` will
  58. also exist at ``+freq``.
  59. precenter : bool, optional
  60. Pre-center measurement values by subtracting the mean, if True. This is
  61. a legacy parameter and unnecessary if `floating_mean` is True.
  62. .. deprecated:: 1.17.0
  63. The `precenter` argument is deprecated and will be removed in SciPy 1.19.0.
  64. The functionality can be substituted by passing ``y - y.mean()`` to `y`.
  65. normalize : bool | str, optional
  66. Compute normalized or complex (amplitude + phase) periodogram.
  67. Valid options are: ``False``/``"power"``, ``True``/``"normalize"``, or
  68. ``"amplitude"``.
  69. weights : array_like, optional
  70. Weights for each sample. Weights must be nonnegative.
  71. floating_mean : bool, optional
  72. Determines a y offset for each frequency independently, if True.
  73. Else the y offset is assumed to be `0`.
  74. Returns
  75. -------
  76. pgram : array_like
  77. Lomb-Scargle periodogram.
  78. Raises
  79. ------
  80. ValueError
  81. If any of the input arrays x, y, freqs, or weights are not 1D, or if any are
  82. zero length. Or, if the input arrays x, y, and weights do not have the same
  83. shape as each other.
  84. ValueError
  85. If any weight is < 0, or the sum of the weights is <= 0.
  86. ValueError
  87. If the normalize parameter is not one of the allowed options.
  88. See Also
  89. --------
  90. periodogram: Power spectral density using a periodogram
  91. welch: Power spectral density by Welch's method
  92. csd: Cross spectral density by Welch's method
  93. Notes
  94. -----
  95. The algorithm used will not automatically account for any unknown y offset, unless
  96. `floating_mean` is ``True``. Therefore, for most use cases, if there is a
  97. possibility of a y offset, it is recommended to set `floating_mean` to ``True``.
  98. Furthermore, `floating_mean` accounts for sample weights, and will also correct for
  99. any bias due to consistently missing observations at peaks and/or troughs.
  100. The legacy concept of "pre-centering" entails removing the mean from parameter `y`
  101. before processing, i.e., passing ``y - y.mean()`` instead of setting the parameter
  102. `floating_mean` to ``True``.
  103. When the normalize parameter is "amplitude", for any frequency in freqs that is
  104. below ``(2*pi)/(x.max() - x.min())``, the predicted amplitude will tend towards
  105. infinity. The concept of a "Nyquist frequency" limit (see Nyquist-Shannon sampling
  106. theorem) is not generally applicable to unevenly sampled data. Therefore, with
  107. unevenly sampled data, valid frequencies in freqs can often be much higher than
  108. expected for those familiar with methods like FFT.
  109. References
  110. ----------
  111. .. [1] N.R. Lomb "Least-squares frequency analysis of unequally spaced
  112. data", Astrophysics and Space Science, vol 39, pp. 447-462, 1976
  113. :doi:`10.1007/bf00648343`
  114. .. [2] J.D. Scargle "Studies in astronomical time series analysis. II -
  115. Statistical aspects of spectral analysis of unevenly spaced data",
  116. The Astrophysical Journal, vol 263, pp. 835-853, 1982
  117. :doi:`10.1086/160554`
  118. .. [3] M. Zechmeister and M. Kürster, "The generalised Lomb-Scargle periodogram.
  119. A new formalism for the floating-mean and Keplerian periodograms,"
  120. Astronomy and Astrophysics, vol. 496, pp. 577-584, 2009
  121. :doi:`10.1051/0004-6361:200811296`
  122. .. [4] J.T. VanderPlas, "Understanding the Lomb-Scargle Periodogram,"
  123. The Astrophysical Journal Supplement Series, vol. 236, no. 1, p. 16,
  124. May 2018
  125. :doi:`10.3847/1538-4365/aab766`
  126. Examples
  127. --------
  128. >>> import numpy as np
  129. >>> rng = np.random.default_rng()
  130. First define some input parameters for the signal:
  131. >>> A = 2. # amplitude
  132. >>> c = 2. # offset
  133. >>> w0 = 1. # rad/sec
  134. >>> nin = 150
  135. >>> nout = 1002
  136. Randomly generate sample times:
  137. >>> x = rng.uniform(0, 10*np.pi, nin)
  138. Plot a sine wave for the selected times:
  139. >>> y = A * np.cos(w0*x) + c
  140. Define the array of frequencies for which to compute the periodogram:
  141. >>> w = np.linspace(0.25, 10, nout)
  142. Calculate Lomb-Scargle periodogram for each of the normalize options:
  143. >>> from scipy.signal import lombscargle
  144. >>> pgram_power = lombscargle(x, y, w, normalize=False)
  145. >>> pgram_norm = lombscargle(x, y, w, normalize=True)
  146. >>> pgram_amp = lombscargle(x, y, w, normalize='amplitude')
  147. ...
  148. >>> pgram_power_f = lombscargle(x, y, w, normalize=False, floating_mean=True)
  149. >>> pgram_norm_f = lombscargle(x, y, w, normalize=True, floating_mean=True)
  150. >>> pgram_amp_f = lombscargle(x, y, w, normalize='amplitude', floating_mean=True)
  151. Now make a plot of the input data:
  152. >>> import matplotlib.pyplot as plt
  153. >>> fig, (ax_t, ax_p, ax_n, ax_a) = plt.subplots(4, 1, figsize=(5, 6))
  154. >>> ax_t.plot(x, y, 'b+')
  155. >>> ax_t.set_xlabel('Time [s]')
  156. >>> ax_t.set_ylabel('Amplitude')
  157. Then plot the periodogram for each of the normalize options, as well as with and
  158. without floating_mean=True:
  159. >>> ax_p.plot(w, pgram_power, label='default')
  160. >>> ax_p.plot(w, pgram_power_f, label='floating_mean=True')
  161. >>> ax_p.set_xlabel('Angular frequency [rad/s]')
  162. >>> ax_p.set_ylabel('Power')
  163. >>> ax_p.legend(prop={'size': 7})
  164. ...
  165. >>> ax_n.plot(w, pgram_norm, label='default')
  166. >>> ax_n.plot(w, pgram_norm_f, label='floating_mean=True')
  167. >>> ax_n.set_xlabel('Angular frequency [rad/s]')
  168. >>> ax_n.set_ylabel('Normalized')
  169. >>> ax_n.legend(prop={'size': 7})
  170. ...
  171. >>> ax_a.plot(w, np.abs(pgram_amp), label='default')
  172. >>> ax_a.plot(w, np.abs(pgram_amp_f), label='floating_mean=True')
  173. >>> ax_a.set_xlabel('Angular frequency [rad/s]')
  174. >>> ax_a.set_ylabel('Amplitude')
  175. >>> ax_a.legend(prop={'size': 7})
  176. ...
  177. >>> plt.tight_layout()
  178. >>> plt.show()
  179. """
  180. # if no weights are provided, assume all data points are equally important
  181. if weights is None:
  182. weights = np.ones_like(y, dtype=np.float64)
  183. else:
  184. # if provided, make sure weights is an array and cast to float64
  185. weights = np.asarray(weights, dtype=np.float64)
  186. # make sure other inputs are arrays and cast to float64
  187. # done before validation, in case they were not arrays
  188. x = np.asarray(x, dtype=np.float64)
  189. y = np.asarray(y, dtype=np.float64)
  190. freqs = np.asarray(freqs, dtype=np.float64)
  191. # validate input shapes
  192. if not (x.ndim == 1 and x.size > 0 and x.shape == y.shape == weights.shape):
  193. raise ValueError("Parameters x, y, weights must be 1-D arrays of "
  194. "equal non-zero length!")
  195. if not (freqs.ndim == 1 and freqs.size > 0):
  196. raise ValueError("Parameter freqs must be a 1-D array of non-zero length!")
  197. # validate weights
  198. if not (np.all(weights >= 0) and np.sum(weights) > 0):
  199. raise ValueError("Parameter weights must have only non-negative entries "
  200. "which sum to a positive value!")
  201. # validate normalize parameter
  202. if isinstance(normalize, bool):
  203. # if bool, convert to str literal
  204. normalize = "normalize" if normalize else "power"
  205. if normalize not in ["power", "normalize", "amplitude"]:
  206. raise ValueError(
  207. "Normalize must be: False (or 'power'), True (or 'normalize'), "
  208. "or 'amplitude'."
  209. )
  210. # weight vector must sum to 1
  211. weights = weights * (1.0 / weights.sum())
  212. # if requested, perform precenter
  213. if precenter is not _NoValue:
  214. msg = ("Use of parameter 'precenter' is deprecated as of SciPy 1.17.0 and "
  215. "will be removed in 1.19.0. Please leave 'precenter' unspecified. "
  216. "Passing True to 'precenter' "
  217. "can be exactly substituted by passing 'y = (y - y.mean())' into "
  218. "the input. Consider setting `floating_mean` to True instead.")
  219. warnings.warn(msg, DeprecationWarning, stacklevel=2)
  220. if precenter:
  221. y = y - y.mean()
  222. # transform arrays
  223. # row vector
  224. freqs = freqs.reshape(1, -1)
  225. # column vectors
  226. x = x.reshape(-1, 1)
  227. y = y.reshape(-1, 1)
  228. weights = weights.reshape(-1, 1)
  229. # store frequent intermediates
  230. weights_y = weights * y
  231. freqst = freqs * x
  232. coswt = np.cos(freqst)
  233. sinwt = np.sin(freqst)
  234. Y = np.dot(weights.T, y) # Eq. 7
  235. CC = np.dot(weights.T, coswt * coswt) # Eq. 13
  236. SS = 1.0 - CC # trig identity: S^2 = 1 - C^2 Eq.14
  237. CS = np.dot(weights.T, coswt * sinwt) # Eq. 15
  238. if floating_mean:
  239. C = np.dot(weights.T, coswt) # Eq. 8
  240. S = np.dot(weights.T, sinwt) # Eq. 9
  241. CC -= C * C # Eq. 13
  242. SS -= S * S # Eq. 14
  243. CS -= C * S # Eq. 15
  244. # calculate tau (phase offset to eliminate CS variable)
  245. tau = 0.5 * np.arctan2(2.0 * CS, CC - SS) # Eq. 19
  246. freqst_tau = freqst - tau
  247. # coswt and sinwt are now offset by tau, which eliminates CS
  248. coswt_tau = np.cos(freqst_tau)
  249. sinwt_tau = np.sin(freqst_tau)
  250. YC = np.dot(weights_y.T, coswt_tau) # Eq. 11
  251. YS = np.dot(weights_y.T, sinwt_tau) # Eq. 12
  252. CC = np.dot(weights.T, coswt_tau * coswt_tau) # Eq. 13, CC range is [0.5, 1.0]
  253. SS = 1.0 - CC # trig identity: S^2 = 1 - C^2 Eq. 14, SS range is [0.0, 0.5]
  254. if floating_mean:
  255. C = np.dot(weights.T, coswt_tau) # Eq. 8
  256. S = np.dot(weights.T, sinwt_tau) # Eq. 9
  257. YC -= Y * C # Eq. 11
  258. YS -= Y * S # Eq. 12
  259. CC -= C * C # Eq. 13, CC range is now [0.0, 1.0]
  260. SS -= S * S # Eq. 14, SS range is now [0.0, 0.5]
  261. # to prevent division by zero errors with a and b, as well as correcting for
  262. # numerical precision errors that lead to CC or SS being approximately -0.0,
  263. # make sure CC and SS are both > 0
  264. epsneg = np.finfo(dtype=y.dtype).epsneg
  265. CC[CC < epsneg] = epsneg
  266. SS[SS < epsneg] = epsneg
  267. # calculate a and b
  268. # where: y(w) = a*cos(w) + b*sin(w) + c
  269. a = YC / CC # Eq. A.4 and 6, eliminating CS
  270. b = YS / SS # Eq. A.4 and 6, eliminating CS
  271. # c = Y - a * C - b * S
  272. # store final value as power in A^2 (i.e., (y units)^2)
  273. pgram = 2.0 * (a * YC + b * YS)
  274. # squeeze back to a vector
  275. pgram = np.squeeze(pgram)
  276. if normalize == "power": # (default)
  277. # return the legacy power units ((A**2) * N/4)
  278. pgram *= float(x.shape[0]) / 4.0
  279. elif normalize == "normalize":
  280. # return the normalized power (power at current frequency wrt the entire signal)
  281. # range will be [0, 1]
  282. YY = np.dot(weights_y.T, y) # Eq. 10
  283. if floating_mean:
  284. YY -= Y * Y # Eq. 10
  285. pgram *= 0.5 / np.squeeze(YY) # Eq. 20
  286. else: # normalize == "amplitude":
  287. # return the complex representation of the best-fit amplitude and phase
  288. # squeeze back to vectors
  289. a = np.squeeze(a)
  290. b = np.squeeze(b)
  291. tau = np.squeeze(tau)
  292. # calculate the complex representation, and correct for tau rotation
  293. pgram = (a + 1j * b) * np.exp(1j * tau)
  294. return pgram
  295. def periodogram(x, fs=1.0, window='boxcar', nfft=None, detrend='constant',
  296. return_onesided=True, scaling='density', axis=-1):
  297. """
  298. Estimate power spectral density using a periodogram.
  299. Parameters
  300. ----------
  301. x : array_like
  302. Time series of measurement values
  303. fs : float, optional
  304. Sampling frequency of the `x` time series. Defaults to 1.0.
  305. window : str or tuple or array_like, optional
  306. Desired window to use. If `window` is a string or tuple, it is
  307. passed to `get_window` to generate the window values, which are
  308. DFT-even by default. See `get_window` for a list of windows and
  309. required parameters. If `window` is array_like it will be used
  310. directly as the window and its length must be equal to the length
  311. of the axis over which the periodogram is computed. Defaults
  312. to 'boxcar'.
  313. nfft : int, optional
  314. Length of the FFT used. If `None` the length of `x` will be
  315. used.
  316. detrend : str or function or `False`, optional
  317. Specifies how to detrend each segment. If `detrend` is a
  318. string, it is passed as the `type` argument to the `detrend`
  319. function. If it is a function, it takes a segment and returns a
  320. detrended segment. If `detrend` is `False`, no detrending is
  321. done. Defaults to 'constant'.
  322. return_onesided : bool, optional
  323. If `True`, return a one-sided spectrum for real data. If
  324. `False` return a two-sided spectrum. Defaults to `True`, but for
  325. complex data, a two-sided spectrum is always returned.
  326. scaling : { 'density', 'spectrum' }, optional
  327. Selects between computing the power spectral density ('density')
  328. where `Pxx` has units of V²/Hz and computing the squared magnitude
  329. spectrum ('spectrum') where `Pxx` has units of V², if `x`
  330. is measured in V and `fs` is measured in Hz. Defaults to
  331. 'density'
  332. axis : int, optional
  333. Axis along which the periodogram is computed; the default is
  334. over the last axis (i.e. ``axis=-1``).
  335. Returns
  336. -------
  337. f : ndarray
  338. Array of sample frequencies.
  339. Pxx : ndarray
  340. Power spectral density or power spectrum of `x`.
  341. See Also
  342. --------
  343. welch: Estimate power spectral density using Welch's method
  344. lombscargle: Lomb-Scargle periodogram for unevenly sampled data
  345. Notes
  346. -----
  347. The ratio of the squared magnitude (``scaling='spectrum'``) divided by the spectral
  348. power density (``scaling='density'``) is the constant factor of
  349. ``sum(abs(window)**2)*fs / abs(sum(window))**2``.
  350. If `return_onesided` is ``True``, the values of the negative frequencies are added
  351. to values of the corresponding positive ones.
  352. Consult the :ref:`tutorial_SpectralAnalysis` section of the :ref:`user_guide`
  353. for a discussion of the scalings of the power spectral density and
  354. the magnitude (squared) spectrum.
  355. .. versionadded:: 0.12.0
  356. Examples
  357. --------
  358. >>> import numpy as np
  359. >>> from scipy import signal
  360. >>> import matplotlib.pyplot as plt
  361. >>> rng = np.random.default_rng()
  362. Generate a test signal, a 2 Vrms sine wave at 1234 Hz, corrupted by
  363. 0.001 V**2/Hz of white noise sampled at 10 kHz.
  364. >>> fs = 10e3
  365. >>> N = 1e5
  366. >>> amp = 2*np.sqrt(2)
  367. >>> freq = 1234.0
  368. >>> noise_power = 0.001 * fs / 2
  369. >>> time = np.arange(N) / fs
  370. >>> x = amp*np.sin(2*np.pi*freq*time)
  371. >>> x += rng.normal(scale=np.sqrt(noise_power), size=time.shape)
  372. Compute and plot the power spectral density.
  373. >>> f, Pxx_den = signal.periodogram(x, fs)
  374. >>> plt.semilogy(f, Pxx_den)
  375. >>> plt.ylim([1e-7, 1e2])
  376. >>> plt.xlabel('frequency [Hz]')
  377. >>> plt.ylabel('PSD [V**2/Hz]')
  378. >>> plt.show()
  379. If we average the last half of the spectral density, to exclude the
  380. peak, we can recover the noise power on the signal.
  381. >>> np.mean(Pxx_den[25000:])
  382. 0.000985320699252543
  383. Now compute and plot the power spectrum.
  384. >>> f, Pxx_spec = signal.periodogram(x, fs, 'flattop', scaling='spectrum')
  385. >>> plt.figure()
  386. >>> plt.semilogy(f, np.sqrt(Pxx_spec))
  387. >>> plt.ylim([1e-4, 1e1])
  388. >>> plt.xlabel('frequency [Hz]')
  389. >>> plt.ylabel('Linear spectrum [V RMS]')
  390. >>> plt.show()
  391. The peak height in the power spectrum is an estimate of the RMS
  392. amplitude.
  393. >>> np.sqrt(Pxx_spec.max())
  394. 2.0077340678640727
  395. """
  396. x = np.asarray(x)
  397. if x.size == 0:
  398. return np.empty(x.shape), np.empty(x.shape)
  399. if window is None:
  400. window = 'boxcar'
  401. if nfft is None:
  402. nperseg = x.shape[axis]
  403. elif nfft == x.shape[axis]:
  404. nperseg = nfft
  405. elif nfft > x.shape[axis]:
  406. nperseg = x.shape[axis]
  407. elif nfft < x.shape[axis]:
  408. s = [np.s_[:]]*len(x.shape)
  409. s[axis] = np.s_[:nfft]
  410. x = x[tuple(s)]
  411. nperseg = nfft
  412. nfft = None
  413. if hasattr(window, 'size'):
  414. if window.size != nperseg:
  415. raise ValueError('the size of the window must be the same size '
  416. 'of the input on the specified axis')
  417. return welch(x, fs=fs, window=window, nperseg=nperseg, noverlap=0,
  418. nfft=nfft, detrend=detrend, return_onesided=return_onesided,
  419. scaling=scaling, axis=axis)
  420. def welch(x, fs=1.0, window='hann_periodic', nperseg=None, noverlap=None, nfft=None,
  421. detrend='constant', return_onesided=True, scaling='density',
  422. axis=-1, average='mean'):
  423. r"""
  424. Estimate power spectral density using Welch's method.
  425. Welch's method [1]_ computes an estimate of the power spectral
  426. density by dividing the data into overlapping segments, computing a
  427. modified periodogram for each segment and averaging the
  428. periodograms.
  429. Parameters
  430. ----------
  431. x : array_like
  432. Time series of measurement values
  433. fs : float, optional
  434. Sampling frequency of the `x` time series. Defaults to 1.0.
  435. window : str or tuple or array_like, optional
  436. Desired window to use. If `window` is a string or tuple, it is
  437. passed to `get_window` to generate the window values, which are
  438. DFT-even by default. See `get_window` for a list of windows and
  439. required parameters. If `window` is array_like it will be used
  440. directly as the window and its length must be nperseg. Defaults
  441. to a periodic Hann window.
  442. nperseg : int, optional
  443. Length of each segment. Defaults to None, but if window is str or
  444. tuple, is set to 256, and if window is array_like, is set to the
  445. length of the window.
  446. noverlap : int, optional
  447. Number of points to overlap between segments. If `None`,
  448. ``noverlap = nperseg // 2``. Defaults to `None`.
  449. nfft : int, optional
  450. Length of the FFT used, if a zero padded FFT is desired. If
  451. `None`, the FFT length is `nperseg`. Defaults to `None`.
  452. detrend : str or function or `False`, optional
  453. Specifies how to detrend each segment. If `detrend` is a
  454. string, it is passed as the `type` argument to the `detrend`
  455. function. If it is a function, it takes a segment and returns a
  456. detrended segment. If `detrend` is `False`, no detrending is
  457. done. Defaults to 'constant'.
  458. return_onesided : bool, optional
  459. If `True`, return a one-sided spectrum for real data. If
  460. `False` return a two-sided spectrum. Defaults to `True`, but for
  461. complex data, a two-sided spectrum is always returned.
  462. scaling : { 'density', 'spectrum' }, optional
  463. Selects between computing the power spectral density ('density')
  464. where `Pxx` has units of V**2/Hz and computing the squared magnitude
  465. spectrum ('spectrum') where `Pxx` has units of V**2, if `x`
  466. is measured in V and `fs` is measured in Hz. Defaults to
  467. 'density'
  468. axis : int, optional
  469. Axis along which the periodogram is computed; the default is
  470. over the last axis (i.e. ``axis=-1``).
  471. average : { 'mean', 'median' }, optional
  472. Method to use when averaging periodograms. Defaults to 'mean'.
  473. .. versionadded:: 1.2.0
  474. Returns
  475. -------
  476. f : ndarray
  477. Array of sample frequencies.
  478. Pxx : ndarray
  479. Power spectral density or power spectrum of x.
  480. See Also
  481. --------
  482. csd: Cross power spectral density using Welch's method
  483. periodogram: Simple, optionally modified periodogram
  484. lombscargle: Lomb-Scargle periodogram for unevenly sampled data
  485. Notes
  486. -----
  487. An appropriate amount of overlap will depend on the choice of window
  488. and on your requirements. For the default Hann window an overlap of
  489. 50% is a reasonable trade-off between accurately estimating the
  490. signal power, while not over counting any of the data. Narrower
  491. windows may require a larger overlap. If `noverlap` is 0, this
  492. method is equivalent to Bartlett's method [2]_.
  493. The ratio of the squared magnitude (``scaling='spectrum'``) divided by the spectral
  494. power density (``scaling='density'``) is the constant factor of
  495. ``sum(abs(window)**2)*fs / abs(sum(window))**2``.
  496. If `return_onesided` is ``True``, the values of the negative frequencies are added
  497. to values of the corresponding positive ones.
  498. Consult the :ref:`tutorial_SpectralAnalysis` section of the :ref:`user_guide`
  499. for a discussion of the scalings of the power spectral density and
  500. the (squared) magnitude spectrum.
  501. .. versionadded:: 0.12.0
  502. References
  503. ----------
  504. .. [1] P. Welch, "The use of the fast Fourier transform for the
  505. estimation of power spectra: A method based on time averaging
  506. over short, modified periodograms", IEEE Trans. Audio
  507. Electroacoust. vol. 15, pp. 70-73, 1967.
  508. .. [2] M.S. Bartlett, "Periodogram Analysis and Continuous Spectra",
  509. Biometrika, vol. 37, pp. 1-16, 1950.
  510. Examples
  511. --------
  512. >>> import numpy as np
  513. >>> from scipy import signal
  514. >>> import matplotlib.pyplot as plt
  515. >>> rng = np.random.default_rng()
  516. Generate a test signal, a 2 Vrms sine wave at 1234 Hz, corrupted by
  517. 0.001 V**2/Hz of white noise sampled at 10 kHz.
  518. >>> fs = 10e3
  519. >>> N = 1e5
  520. >>> amp = 2*np.sqrt(2)
  521. >>> freq = 1234.0
  522. >>> noise_power = 0.001 * fs / 2
  523. >>> time = np.arange(N) / fs
  524. >>> x = amp*np.sin(2*np.pi*freq*time)
  525. >>> x += rng.normal(scale=np.sqrt(noise_power), size=time.shape)
  526. Compute and plot the power spectral density.
  527. >>> f, Pxx_den = signal.welch(x, fs, nperseg=1024)
  528. >>> plt.semilogy(f, Pxx_den)
  529. >>> plt.ylim([0.5e-3, 1])
  530. >>> plt.xlabel('frequency [Hz]')
  531. >>> plt.ylabel('PSD [V**2/Hz]')
  532. >>> plt.show()
  533. If we average the last half of the spectral density, to exclude the
  534. peak, we can recover the noise power on the signal.
  535. >>> np.mean(Pxx_den[256:])
  536. 0.0009924865443739191
  537. Now compute and plot the power spectrum.
  538. >>> f, Pxx_spec = signal.welch(x, fs, 'flattop', 1024, scaling='spectrum')
  539. >>> plt.figure()
  540. >>> plt.semilogy(f, np.sqrt(Pxx_spec))
  541. >>> plt.xlabel('frequency [Hz]')
  542. >>> plt.ylabel('Linear spectrum [V RMS]')
  543. >>> plt.show()
  544. The peak height in the power spectrum is an estimate of the RMS
  545. amplitude.
  546. >>> np.sqrt(Pxx_spec.max())
  547. 2.0077340678640727
  548. If we now introduce a discontinuity in the signal, by increasing the
  549. amplitude of a small portion of the signal by 50, we can see the
  550. corruption of the mean average power spectral density, but using a
  551. median average better estimates the normal behaviour.
  552. >>> x[int(N//2):int(N//2)+10] *= 50.
  553. >>> f, Pxx_den = signal.welch(x, fs, nperseg=1024)
  554. >>> f_med, Pxx_den_med = signal.welch(x, fs, nperseg=1024, average='median')
  555. >>> plt.semilogy(f, Pxx_den, label='mean')
  556. >>> plt.semilogy(f_med, Pxx_den_med, label='median')
  557. >>> plt.ylim([0.5e-3, 1])
  558. >>> plt.xlabel('frequency [Hz]')
  559. >>> plt.ylabel('PSD [V**2/Hz]')
  560. >>> plt.legend()
  561. >>> plt.show()
  562. """
  563. freqs, Pxx = csd(x, x, fs=fs, window=window, nperseg=nperseg,
  564. noverlap=noverlap, nfft=nfft, detrend=detrend,
  565. return_onesided=return_onesided, scaling=scaling,
  566. axis=axis, average=average)
  567. return freqs, Pxx.real
  568. def csd(x, y, fs=1.0, window='hann_periodic', nperseg=None, noverlap=None, nfft=None,
  569. detrend='constant', return_onesided=True, scaling='density',
  570. axis=-1, average='mean'):
  571. r"""
  572. Estimate the cross power spectral density, Pxy, using Welch's method.
  573. Parameters
  574. ----------
  575. x : array_like
  576. Time series of measurement values
  577. y : array_like
  578. Time series of measurement values
  579. fs : float, optional
  580. Sampling frequency of the `x` and `y` time series. Defaults
  581. to 1.0.
  582. window : str or tuple or array_like, optional
  583. Desired window to use. If `window` is a string or tuple, it is
  584. passed to `get_window` to generate the window values, which are
  585. DFT-even by default. See `get_window` for a list of windows and
  586. required parameters. If `window` is array_like it will be used
  587. directly as the window and its length must be nperseg. Defaults
  588. to a periodic Hann window.
  589. nperseg : int, optional
  590. Length of each segment. Defaults to None, but if window is str or
  591. tuple, is set to 256, and if window is array_like, is set to the
  592. length of the window.
  593. noverlap: int, optional
  594. Number of points to overlap between segments. If `None`,
  595. ``noverlap = nperseg // 2``. Defaults to `None` and may
  596. not be greater than `nperseg`.
  597. nfft : int, optional
  598. Length of the FFT used, if a zero padded FFT is desired. If
  599. `None`, the FFT length is `nperseg`. Defaults to `None`.
  600. detrend : str or function or `False`, optional
  601. Specifies how to detrend each segment. If `detrend` is a
  602. string, it is passed as the `type` argument to the `detrend`
  603. function. If it is a function, it takes a segment and returns a
  604. detrended segment. If `detrend` is `False`, no detrending is
  605. done. Defaults to 'constant'.
  606. return_onesided : bool, optional
  607. If `True`, return a one-sided spectrum for real data. If
  608. `False` return a two-sided spectrum. Defaults to `True`, but for
  609. complex data, a two-sided spectrum is always returned.
  610. scaling : { 'density', 'spectrum' }, optional
  611. Selects between computing the cross spectral density ('density')
  612. where `Pxy` has units of V**2/Hz and computing the cross spectrum
  613. ('spectrum') where `Pxy` has units of V**2, if `x` and `y` are
  614. measured in V and `fs` is measured in Hz. Defaults to 'density'
  615. axis : int, optional
  616. Axis along which the CSD is computed for both inputs; the
  617. default is over the last axis (i.e. ``axis=-1``).
  618. average : { 'mean', 'median' }, optional
  619. Method to use when averaging periodograms. If the spectrum is
  620. complex, the average is computed separately for the real and
  621. imaginary parts. Defaults to 'mean'.
  622. .. versionadded:: 1.2.0
  623. Returns
  624. -------
  625. f : ndarray
  626. Array of sample frequencies.
  627. Pxy : ndarray
  628. Cross spectral density or cross power spectrum of x,y.
  629. See Also
  630. --------
  631. periodogram: Simple, optionally modified periodogram
  632. lombscargle: Lomb-Scargle periodogram for unevenly sampled data
  633. welch: Power spectral density by Welch's method. [Equivalent to
  634. csd(x,x)]
  635. coherence: Magnitude squared coherence by Welch's method.
  636. Notes
  637. -----
  638. By convention, Pxy is computed with the conjugate FFT of X
  639. multiplied by the FFT of Y.
  640. If the input series differ in length, the shorter series will be
  641. zero-padded to match.
  642. An appropriate amount of overlap will depend on the choice of window
  643. and on your requirements. For the default Hann window an overlap of
  644. 50% is a reasonable trade-off between accurately estimating the
  645. signal power, while not over counting any of the data. Narrower
  646. windows may require a larger overlap.
  647. The ratio of the cross spectrum (``scaling='spectrum'``) divided by the cross
  648. spectral density (``scaling='density'``) is the constant factor of
  649. ``sum(abs(window)**2)*fs / abs(sum(window))**2``.
  650. If `return_onesided` is ``True``, the values of the negative frequencies are added
  651. to values of the corresponding positive ones.
  652. Consult the :ref:`tutorial_SpectralAnalysis` section of the :ref:`user_guide`
  653. for a discussion of the scalings of a spectral density and an (amplitude) spectrum.
  654. Welch's method may be interpreted as taking the average over the time slices of a
  655. (cross-) spectrogram. Internally, this function utilizes the `ShortTimeFFT` to
  656. determine the required (cross-) spectrogram. An example below illustrates that it
  657. is straightforward to calculate `Pxy` directly with the `ShortTimeFFT`. However,
  658. there are some notable differences in the behavior of the `ShortTimeFFT`:
  659. * There is no direct `ShortTimeFFT` equivalent for the `csd` parameter
  660. combination ``return_onesided=True, scaling='density'``, since
  661. ``fft_mode='onesided2X'`` requires ``'psd'`` scaling. The is due to `csd`
  662. returning the doubled squared magnitude in this case, which does not have a
  663. sensible interpretation.
  664. * `ShortTimeFFT` uses `float64` / `complex128` internally, which is due to the
  665. behavior of the utilized `~scipy.fft` module. Thus, those are the dtypes being
  666. returned. The `csd` function casts the return values to `float32` / `complex64`
  667. if the input is `float32` / `complex64` as well.
  668. * The `csd` function calculates ``np.conj(Sx[q,p]) * Sy[q,p]``, whereas
  669. `~ShortTimeFFT.spectrogram` calculates ``Sx[q,p] * np.conj(Sy[q,p])`` where
  670. ``Sx[q,p]``, ``Sy[q,p]`` are the STFTs of `x` and `y`. Also, the window
  671. positioning is different.
  672. .. versionadded:: 0.16.0
  673. References
  674. ----------
  675. .. [1] P. Welch, "The use of the fast Fourier transform for the
  676. estimation of power spectra: A method based on time averaging
  677. over short, modified periodograms", IEEE Trans. Audio
  678. Electroacoust. vol. 15, pp. 70-73, 1967.
  679. .. [2] Rabiner, Lawrence R., and B. Gold. "Theory and Application of
  680. Digital Signal Processing" Prentice-Hall, pp. 414-419, 1975
  681. Examples
  682. --------
  683. The following example plots the cross power spectral density of two signals with
  684. some common features:
  685. >>> import numpy as np
  686. >>> from scipy import signal
  687. >>> import matplotlib.pyplot as plt
  688. >>> rng = np.random.default_rng()
  689. ...
  690. ... # Generate two test signals with some common features:
  691. >>> N, fs = 100_000, 10e3 # number of samples and sampling frequency
  692. >>> amp, freq = 20, 1234.0 # amplitude and frequency of utilized sine signal
  693. >>> noise_power = 0.001 * fs / 2
  694. >>> time = np.arange(N) / fs
  695. >>> b, a = signal.butter(2, 0.25, 'low')
  696. >>> x = rng.normal(scale=np.sqrt(noise_power), size=time.shape)
  697. >>> y = signal.lfilter(b, a, x)
  698. >>> x += amp*np.sin(2*np.pi*freq*time)
  699. >>> y += rng.normal(scale=0.1*np.sqrt(noise_power), size=time.shape)
  700. ...
  701. ... # Compute and plot the magnitude of the cross spectral density:
  702. >>> nperseg, noverlap, win = 1024, 512, 'hann'
  703. >>> f, Pxy = signal.csd(x, y, fs, win, nperseg, noverlap)
  704. >>> fig0, ax0 = plt.subplots(tight_layout=True)
  705. >>> ax0.set_title(f"CSD ({win.title()}-window, {nperseg=}, {noverlap=})")
  706. >>> ax0.set(xlabel="Frequency $f$ in kHz", ylabel="CSD Magnitude in V²/Hz")
  707. >>> ax0.semilogy(f/1e3, np.abs(Pxy))
  708. >>> ax0.grid()
  709. >>> plt.show()
  710. The cross spectral density is calculated by taking the average over the time slices
  711. of a spectrogram:
  712. >>> SFT = signal.ShortTimeFFT.from_window('hann', fs, nperseg, noverlap,
  713. ... scale_to='psd', fft_mode='onesided2X',
  714. ... phase_shift=None)
  715. >>> Sxy1 = SFT.spectrogram(y, x, detr='constant', k_offset=nperseg//2,
  716. ... p0=0, p1=(N-noverlap) // SFT.hop)
  717. >>> Pxy1 = Sxy1.mean(axis=-1)
  718. >>> np.allclose(Pxy, Pxy1) # same result as with csd()
  719. True
  720. As discussed in the Notes section, the results of using an approach analogous to
  721. the code snippet above and the `csd` function may deviate due to implementation
  722. details.
  723. Note that the code snippet above can be easily adapted to determine other
  724. statistical properties than the mean value.
  725. """
  726. # The following lines are resembling the behavior of the originally utilized
  727. # `_spectral_helper()` function:
  728. same_data, axis = y is x, int(axis)
  729. x = np.asarray(x)
  730. if not same_data:
  731. y = np.asarray(y)
  732. # Check if we can broadcast the outer axes together
  733. x_outer, y_outer = list(x.shape), list(y.shape)
  734. x_outer.pop(axis)
  735. y_outer.pop(axis)
  736. try:
  737. outer_shape = np.broadcast_shapes(x_outer, y_outer)
  738. except ValueError as e:
  739. raise ValueError('x and y cannot be broadcast together.') from e
  740. if x.size == 0 or y.size == 0:
  741. out_shape = outer_shape + (min([x.shape[axis], y.shape[axis]]),)
  742. empty_out = np.moveaxis(np.empty(out_shape), -1, axis)
  743. return empty_out, empty_out
  744. out_dtype = np.result_type(x, y, np.complex64)
  745. else: # x is y:
  746. if x.size == 0:
  747. return np.empty(x.shape), np.empty(x.shape)
  748. out_dtype = np.result_type(x, np.complex64)
  749. n = x.shape[axis] if same_data else max(x.shape[axis], y.shape[axis])
  750. if isinstance(window, str) or isinstance(window, tuple):
  751. nperseg = int(nperseg) if nperseg is not None else 256
  752. if nperseg < 1:
  753. raise ValueError(f"Parameter {nperseg=} is not a positive integer!")
  754. elif n < nperseg:
  755. warnings.warn(f"{nperseg=} is greater than signal length max(len(x), " +
  756. f"len(y)) = {n}, using nperseg = {n}", stacklevel=3)
  757. nperseg = n
  758. win = get_window(window, nperseg)
  759. else:
  760. win = np.asarray(window)
  761. if nperseg is None:
  762. nperseg = len(win)
  763. if nperseg != len(win):
  764. raise ValueError(f"{nperseg=} does not equal {len(win)=}")
  765. nfft = int(nfft) if nfft is not None else nperseg
  766. if nfft < nperseg:
  767. raise ValueError(f"{nfft=} must be greater than or equal to {nperseg=}!")
  768. noverlap = int(noverlap) if noverlap is not None else nperseg // 2
  769. if noverlap >= nperseg:
  770. raise ValueError(f"{noverlap=} must be less than {nperseg=}!")
  771. if np.iscomplexobj(x) and return_onesided:
  772. return_onesided = False
  773. if x.shape[axis] < y.shape[axis]: # zero-pad x to shape of y:
  774. z_shape = list(y.shape)
  775. z_shape[axis] = y.shape[axis] - x.shape[axis]
  776. x = np.concatenate((x, np.zeros(z_shape)), axis=axis)
  777. elif y.shape[axis] < x.shape[axis]: # zero-pad y to shape of x:
  778. z_shape = list(x.shape)
  779. z_shape[axis] = x.shape[axis] - y.shape[axis]
  780. y = np.concatenate((y, np.zeros(z_shape)), axis=axis)
  781. # using cast() to make mypy happy:
  782. fft_mode = cast(FFT_MODE_TYPE, 'onesided' if return_onesided else 'twosided')
  783. if scaling not in (scales := {'spectrum': 'magnitude', 'density': 'psd'}):
  784. raise ValueError(f"Parameter {scaling=} not in {scales}!")
  785. SFT = ShortTimeFFT(win, nperseg - noverlap, fs, fft_mode=fft_mode, mfft=nfft,
  786. scale_to=scales[scaling], phase_shift=None)
  787. # csd() calculates X.conj()*Y instead of X*Y.conj():
  788. Pxy = SFT.spectrogram(y, x, detr=None if detrend is False else detrend,
  789. p0=0, p1=(n - noverlap) // SFT.hop, k_offset=nperseg // 2,
  790. axis=axis)
  791. # Note:
  792. # 'onesided2X' scaling of ShortTimeFFT conflicts with the
  793. # scaling='spectrum' parameter, since it doubles the squared magnitude,
  794. # which in the view of the ShortTimeFFT implementation does not make sense.
  795. # Hence, the doubling of the square is implemented here:
  796. if return_onesided:
  797. f_axis = Pxy.ndim - 1 + axis if axis < 0 else axis
  798. Pxy = np.moveaxis(Pxy, f_axis, -1)
  799. Pxy[..., 1:-1 if SFT.mfft % 2 == 0 else None] *= 2
  800. Pxy = np.moveaxis(Pxy, -1, f_axis)
  801. # Average over windows.
  802. if Pxy.shape[-1] > 1:
  803. if average == 'median':
  804. # np.median must be passed real arrays for the desired result
  805. bias = _median_bias(Pxy.shape[-1])
  806. if np.iscomplexobj(Pxy):
  807. Pxy = (np.median(np.real(Pxy), axis=-1) +
  808. np.median(np.imag(Pxy), axis=-1) * 1j)
  809. else:
  810. Pxy = np.median(Pxy, axis=-1)
  811. Pxy /= bias
  812. elif average == 'mean':
  813. Pxy = Pxy.mean(axis=-1)
  814. else:
  815. raise ValueError(f"Parameter {average=} must be 'median' or 'mean'!")
  816. else:
  817. Pxy = np.reshape(Pxy, Pxy.shape[:-1])
  818. # cast output type;
  819. Pxy = Pxy.astype(out_dtype)
  820. if same_data:
  821. Pxy = Pxy.real
  822. return SFT.f, Pxy
  823. def spectrogram(x, fs=1.0, window=('tukey_periodic', .25), nperseg=None, noverlap=None,
  824. nfft=None, detrend='constant', return_onesided=True,
  825. scaling='density', axis=-1, mode='psd'):
  826. """Compute a spectrogram with consecutive Fourier transforms (legacy function).
  827. Spectrograms can be used as a way of visualizing the change of a
  828. nonstationary signal's frequency content over time.
  829. .. legacy:: function
  830. :class:`ShortTimeFFT` is a newer STFT / ISTFT implementation with more
  831. features also including a :meth:`~ShortTimeFFT.spectrogram` method.
  832. A :ref:`comparison <tutorial_stft_legacy_stft>` between the
  833. implementations can be found in the :ref:`tutorial_stft` section of
  834. the :ref:`user_guide`.
  835. Parameters
  836. ----------
  837. x : array_like
  838. Time series of measurement values
  839. fs : float, optional
  840. Sampling frequency of the `x` time series. Defaults to 1.0.
  841. window : str or tuple or array_like, optional
  842. Desired window to use. If `window` is a string or tuple, it is
  843. passed to `get_window` to generate the window values, which are
  844. DFT-even by default. See `get_window` for a list of windows and
  845. required parameters. If `window` is array_like it will be used
  846. directly as the window and its length must be nperseg.
  847. Defaults to a periodic Tukey window with shape parameter of 0.25.
  848. nperseg : int, optional
  849. Length of each segment. Defaults to None, but if window is str or
  850. tuple, is set to 256, and if window is array_like, is set to the
  851. length of the window.
  852. noverlap : int, optional
  853. Number of points to overlap between segments. If `None`,
  854. ``noverlap = nperseg // 8``. Defaults to `None`.
  855. nfft : int, optional
  856. Length of the FFT used, if a zero padded FFT is desired. If
  857. `None`, the FFT length is `nperseg`. Defaults to `None`.
  858. detrend : str or function or `False`, optional
  859. Specifies how to detrend each segment. If `detrend` is a
  860. string, it is passed as the `type` argument to the `detrend`
  861. function. If it is a function, it takes a segment and returns a
  862. detrended segment. If `detrend` is `False`, no detrending is
  863. done. Defaults to 'constant'.
  864. return_onesided : bool, optional
  865. If `True`, return a one-sided spectrum for real data. If
  866. `False` return a two-sided spectrum. Defaults to `True`, but for
  867. complex data, a two-sided spectrum is always returned.
  868. scaling : { 'density', 'spectrum' }, optional
  869. Selects between computing the power spectral density ('density')
  870. where `Sxx` has units of V**2/Hz and computing the power
  871. spectrum ('spectrum') where `Sxx` has units of V**2, if `x`
  872. is measured in V and `fs` is measured in Hz. Defaults to
  873. 'density'.
  874. axis : int, optional
  875. Axis along which the spectrogram is computed; the default is over
  876. the last axis (i.e. ``axis=-1``).
  877. mode : str, optional
  878. Defines what kind of return values are expected. Options are
  879. ['psd', 'complex', 'magnitude', 'angle', 'phase']. 'complex' is
  880. equivalent to the output of `stft` with no padding or boundary
  881. extension. 'magnitude' returns the absolute magnitude of the
  882. STFT. 'angle' and 'phase' return the complex angle of the STFT,
  883. with and without unwrapping, respectively.
  884. Returns
  885. -------
  886. f : ndarray
  887. Array of sample frequencies.
  888. t : ndarray
  889. Array of segment times.
  890. Sxx : ndarray
  891. Spectrogram of x. By default, the last axis of Sxx corresponds
  892. to the segment times.
  893. See Also
  894. --------
  895. periodogram: Simple, optionally modified periodogram
  896. lombscargle: Lomb-Scargle periodogram for unevenly sampled data
  897. welch: Power spectral density by Welch's method.
  898. csd: Cross spectral density by Welch's method.
  899. ShortTimeFFT: Newer STFT/ISTFT implementation providing more features,
  900. which also includes a :meth:`~ShortTimeFFT.spectrogram`
  901. method.
  902. Notes
  903. -----
  904. An appropriate amount of overlap will depend on the choice of window
  905. and on your requirements. In contrast to welch's method, where the
  906. entire data stream is averaged over, one may wish to use a smaller
  907. overlap (or perhaps none at all) when computing a spectrogram, to
  908. maintain some statistical independence between individual segments.
  909. It is for this reason that the default window is a Tukey window with
  910. 1/8th of a window's length overlap at each end.
  911. .. versionadded:: 0.16.0
  912. References
  913. ----------
  914. .. [1] Oppenheim, Alan V., Ronald W. Schafer, John R. Buck
  915. "Discrete-Time Signal Processing", Prentice Hall, 1999.
  916. Examples
  917. --------
  918. >>> import numpy as np
  919. >>> from scipy import signal
  920. >>> from scipy.fft import fftshift
  921. >>> import matplotlib.pyplot as plt
  922. >>> rng = np.random.default_rng()
  923. Generate a test signal, a 2 Vrms sine wave whose frequency is slowly
  924. modulated around 3kHz, corrupted by white noise of exponentially
  925. decreasing magnitude sampled at 10 kHz.
  926. >>> fs = 10e3
  927. >>> N = 1e5
  928. >>> amp = 2 * np.sqrt(2)
  929. >>> noise_power = 0.01 * fs / 2
  930. >>> time = np.arange(N) / float(fs)
  931. >>> mod = 500*np.cos(2*np.pi*0.25*time)
  932. >>> carrier = amp * np.sin(2*np.pi*3e3*time + mod)
  933. >>> noise = rng.normal(scale=np.sqrt(noise_power), size=time.shape)
  934. >>> noise *= np.exp(-time/5)
  935. >>> x = carrier + noise
  936. Compute and plot the spectrogram.
  937. >>> f, t, Sxx = signal.spectrogram(x, fs)
  938. >>> plt.pcolormesh(t, f, Sxx, shading='gouraud')
  939. >>> plt.ylabel('Frequency [Hz]')
  940. >>> plt.xlabel('Time [sec]')
  941. >>> plt.show()
  942. Note, if using output that is not one sided, then use the following:
  943. >>> f, t, Sxx = signal.spectrogram(x, fs, return_onesided=False)
  944. >>> plt.pcolormesh(t, fftshift(f), fftshift(Sxx, axes=0), shading='gouraud')
  945. >>> plt.ylabel('Frequency [Hz]')
  946. >>> plt.xlabel('Time [sec]')
  947. >>> plt.show()
  948. """
  949. modelist = ['psd', 'complex', 'magnitude', 'angle', 'phase']
  950. if mode not in modelist:
  951. raise ValueError(f'unknown value for mode {mode}, must be one of {modelist}')
  952. # need to set default for nperseg before setting default for noverlap below
  953. window, nperseg = _triage_segments(window, nperseg,
  954. input_length=x.shape[axis])
  955. # Less overlap than welch, so samples are more statistically independent
  956. if noverlap is None:
  957. noverlap = nperseg // 8
  958. if mode == 'psd':
  959. freqs, time, Sxx = _spectral_helper(x, x, fs, window, nperseg,
  960. noverlap, nfft, detrend,
  961. return_onesided, scaling, axis,
  962. mode='psd')
  963. else:
  964. freqs, time, Sxx = _spectral_helper(x, x, fs, window, nperseg,
  965. noverlap, nfft, detrend,
  966. return_onesided, scaling, axis,
  967. mode='stft')
  968. if mode == 'magnitude':
  969. Sxx = np.abs(Sxx)
  970. elif mode in ['angle', 'phase']:
  971. Sxx = np.angle(Sxx)
  972. if mode == 'phase':
  973. # Sxx has one additional dimension for time strides
  974. if axis < 0:
  975. axis -= 1
  976. Sxx = np.unwrap(Sxx, axis=axis)
  977. # mode =='complex' is same as `stft`, doesn't need modification
  978. return freqs, time, Sxx
  979. def check_COLA(window, nperseg, noverlap, tol=1e-10):
  980. r"""Check whether the Constant OverLap Add (COLA) constraint is met
  981. (legacy function).
  982. .. legacy:: function
  983. The COLA constraint is equivalent of having a constant dual window, i.e.,
  984. ``all(ShortTimeFFT.dual_win == ShortTimeFFT.dual_win[0])``. Hence,
  985. `closest_STFT_dual_window` generalizes this function, as the following
  986. example shows:
  987. >>> import numpy as np
  988. >>> from scipy.signal import check_COLA, closest_STFT_dual_window, windows
  989. ...
  990. >>> w, w_rect, hop = windows.hann(12, sym=False), np.ones(12), 6
  991. >>> dual_win, alpha = closest_STFT_dual_window(w, hop, w_rect, scaled=True)
  992. >>> np.allclose(dual_win/alpha, w_rect, atol=1e-10, rtol=0)
  993. True
  994. >>> check_COLA(w, len(w), len(w) - hop) # equivalent legacy function call
  995. True
  996. Parameters
  997. ----------
  998. window : str or tuple or array_like
  999. Desired window to use. If `window` is a string or tuple, it is
  1000. passed to `get_window` to generate the window values, which are
  1001. DFT-even by default. See `get_window` for a list of windows and
  1002. required parameters. If `window` is array_like it will be used
  1003. directly as the window and its length must be nperseg.
  1004. nperseg : int
  1005. Length of each segment.
  1006. noverlap : int
  1007. Number of points to overlap between segments.
  1008. tol : float, optional
  1009. The allowed variance of a bin's weighted sum from the median bin
  1010. sum.
  1011. Returns
  1012. -------
  1013. verdict : bool
  1014. `True` if chosen combination satisfies COLA within `tol`,
  1015. `False` otherwise
  1016. See Also
  1017. --------
  1018. closest_STFT_dual_window: Allows determining the closest window meeting the
  1019. COLA constraint for a given window
  1020. check_NOLA: Check whether the Nonzero Overlap Add (NOLA) constraint is met
  1021. ShortTimeFFT: Provide short-time Fourier transform and its inverse
  1022. stft: Short-time Fourier transform (legacy)
  1023. istft: Inverse Short-time Fourier transform (legacy)
  1024. Notes
  1025. -----
  1026. In order to invert a short-time Fourier transfrom (STFT) with the so-called
  1027. "overlap-add method", the signal windowing must obey the constraint of
  1028. "Constant OverLap Add" (COLA). This ensures that every point in the input
  1029. data is equally weighted, thereby avoiding aliasing and allowing full
  1030. reconstruction. Note that the algorithms implemented in `ShortTimeFFT.istft`
  1031. and in `istft` (legacy) only require that the weaker "nonzero overlap-add"
  1032. condition (as in `check_NOLA`) is met.
  1033. Some examples of windows that satisfy COLA:
  1034. - Rectangular window at overlap of 0, 1/2, 2/3, 3/4, ...
  1035. - Bartlett window at overlap of 1/2, 3/4, 5/6, ...
  1036. - Hann window at 1/2, 2/3, 3/4, ...
  1037. - Any Blackman family window at 2/3 overlap
  1038. - Any window with ``noverlap = nperseg-1``
  1039. A very comprehensive list of other windows may be found in [2]_,
  1040. wherein the COLA condition is satisfied when the "Amplitude
  1041. Flatness" is unity.
  1042. .. versionadded:: 0.19.0
  1043. References
  1044. ----------
  1045. .. [1] Julius O. Smith III, "Spectral Audio Signal Processing", W3K
  1046. Publishing, 2011,ISBN 978-0-9745607-3-1.
  1047. .. [2] G. Heinzel, A. Ruediger and R. Schilling, "Spectrum and
  1048. spectral density estimation by the Discrete Fourier transform
  1049. (DFT), including a comprehensive list of window functions and
  1050. some new at-top windows", 2002,
  1051. http://hdl.handle.net/11858/00-001M-0000-0013-557A-5
  1052. Examples
  1053. --------
  1054. >>> from scipy import signal
  1055. Confirm COLA condition for rectangular window of 75% (3/4) overlap:
  1056. >>> signal.check_COLA(signal.windows.boxcar(100), 100, 75)
  1057. True
  1058. COLA is not true for 25% (1/4) overlap, though:
  1059. >>> signal.check_COLA(signal.windows.boxcar(100), 100, 25)
  1060. False
  1061. "Symmetrical" Hann window (for filter design) is not COLA:
  1062. >>> signal.check_COLA(signal.windows.hann(120, sym=True), 120, 60)
  1063. False
  1064. "Periodic" or "DFT-even" Hann window (for FFT analysis) is COLA for
  1065. overlap of 1/2, 2/3, 3/4, etc.:
  1066. >>> signal.check_COLA(signal.windows.hann(120, sym=False), 120, 60)
  1067. True
  1068. >>> signal.check_COLA(signal.windows.hann(120, sym=False), 120, 80)
  1069. True
  1070. >>> signal.check_COLA(signal.windows.hann(120, sym=False), 120, 90)
  1071. True
  1072. """
  1073. nperseg = int(nperseg)
  1074. if nperseg < 1:
  1075. raise ValueError('nperseg must be a positive integer')
  1076. if noverlap >= nperseg:
  1077. raise ValueError('noverlap must be less than nperseg.')
  1078. noverlap = int(noverlap)
  1079. if isinstance(window, str) or type(window) is tuple:
  1080. win = get_window(window, nperseg)
  1081. else:
  1082. win = np.asarray(window)
  1083. if len(win.shape) != 1:
  1084. raise ValueError('window must be 1-D')
  1085. if win.shape[0] != nperseg:
  1086. raise ValueError('window must have length of nperseg')
  1087. step = nperseg - noverlap
  1088. binsums = sum(win[ii*step:(ii+1)*step] for ii in range(nperseg//step))
  1089. if nperseg % step != 0:
  1090. binsums[:nperseg % step] += win[-(nperseg % step):]
  1091. deviation = binsums - np.median(binsums)
  1092. return np.max(np.abs(deviation)) < tol
  1093. def check_NOLA(window, nperseg, noverlap, tol=1e-10):
  1094. r"""Check whether the Nonzero Overlap Add (NOLA) constraint is met.
  1095. Parameters
  1096. ----------
  1097. window : str or tuple or array_like
  1098. Desired window to use. If `window` is a string or tuple, it is
  1099. passed to `get_window` to generate the window values, which are
  1100. DFT-even by default. See `get_window` for a list of windows and
  1101. required parameters. If `window` is array_like it will be used
  1102. directly as the window and its length must be nperseg.
  1103. nperseg : int
  1104. Length of each segment.
  1105. noverlap : int
  1106. Number of points to overlap between segments.
  1107. tol : float, optional
  1108. The allowed variance of a bin's weighted sum from the median bin
  1109. sum.
  1110. Returns
  1111. -------
  1112. verdict : bool
  1113. `True` if chosen combination satisfies the NOLA constraint within
  1114. `tol`, `False` otherwise
  1115. See Also
  1116. --------
  1117. check_COLA: Check whether the Constant OverLap Add (COLA) constraint is met
  1118. stft: Short Time Fourier Transform
  1119. istft: Inverse Short Time Fourier Transform
  1120. Notes
  1121. -----
  1122. In order to enable inversion of an STFT via the inverse STFT in
  1123. `istft`, the signal windowing must obey the constraint of "nonzero
  1124. overlap add" (NOLA):
  1125. .. math:: \sum_{t}w^{2}[n-tH] \ne 0
  1126. for all :math:`n`, where :math:`w` is the window function, :math:`t` is the
  1127. frame index, and :math:`H` is the hop size (:math:`H` = `nperseg` -
  1128. `noverlap`).
  1129. This ensures that the normalization factors in the denominator of the
  1130. overlap-add inversion equation are not zero. Only very pathological windows
  1131. will fail the NOLA constraint.
  1132. .. versionadded:: 1.2.0
  1133. References
  1134. ----------
  1135. .. [1] Julius O. Smith III, "Spectral Audio Signal Processing", W3K
  1136. Publishing, 2011,ISBN 978-0-9745607-3-1.
  1137. .. [2] G. Heinzel, A. Ruediger and R. Schilling, "Spectrum and
  1138. spectral density estimation by the Discrete Fourier transform
  1139. (DFT), including a comprehensive list of window functions and
  1140. some new at-top windows", 2002,
  1141. http://hdl.handle.net/11858/00-001M-0000-0013-557A-5
  1142. Examples
  1143. --------
  1144. >>> import numpy as np
  1145. >>> from scipy import signal
  1146. Confirm NOLA condition for rectangular window of 75% (3/4) overlap:
  1147. >>> signal.check_NOLA(signal.windows.boxcar(100), 100, 75)
  1148. True
  1149. NOLA is also true for 25% (1/4) overlap:
  1150. >>> signal.check_NOLA(signal.windows.boxcar(100), 100, 25)
  1151. True
  1152. "Symmetrical" Hann window (for filter design) is also NOLA:
  1153. >>> signal.check_NOLA(signal.windows.hann(120, sym=True), 120, 60)
  1154. True
  1155. As long as there is overlap, it takes quite a pathological window to fail
  1156. NOLA:
  1157. >>> w = np.ones(64, dtype="float")
  1158. >>> w[::2] = 0
  1159. >>> signal.check_NOLA(w, 64, 32)
  1160. False
  1161. If there is not enough overlap, a window with zeros at the ends will not
  1162. work:
  1163. >>> signal.check_NOLA(signal.windows.hann(64), 64, 0)
  1164. False
  1165. >>> signal.check_NOLA(signal.windows.hann(64), 64, 1)
  1166. False
  1167. >>> signal.check_NOLA(signal.windows.hann(64), 64, 2)
  1168. True
  1169. """
  1170. nperseg = int(nperseg)
  1171. if nperseg < 1:
  1172. raise ValueError('nperseg must be a positive integer')
  1173. if noverlap >= nperseg:
  1174. raise ValueError('noverlap must be less than nperseg')
  1175. if noverlap < 0:
  1176. raise ValueError('noverlap must be a nonnegative integer')
  1177. noverlap = int(noverlap)
  1178. if isinstance(window, str) or type(window) is tuple:
  1179. win = get_window(window, nperseg)
  1180. else:
  1181. win = np.asarray(window)
  1182. if len(win.shape) != 1:
  1183. raise ValueError('window must be 1-D')
  1184. if win.shape[0] != nperseg:
  1185. raise ValueError('window must have length of nperseg')
  1186. step = nperseg - noverlap
  1187. binsums = sum(win[ii*step:(ii+1)*step]**2 for ii in range(nperseg//step))
  1188. if nperseg % step != 0:
  1189. binsums[:nperseg % step] += win[-(nperseg % step):]**2
  1190. return np.min(binsums) > tol
  1191. def stft(x, fs=1.0, window='hann_periodic', nperseg=256, noverlap=None, nfft=None,
  1192. detrend=False, return_onesided=True, boundary='zeros', padded=True,
  1193. axis=-1, scaling='spectrum'):
  1194. r"""Compute the Short Time Fourier Transform (legacy function).
  1195. STFTs can be used as a way of quantifying the change of a
  1196. nonstationary signal's frequency and phase content over time.
  1197. .. legacy:: function
  1198. `ShortTimeFFT` is a newer STFT / ISTFT implementation with more
  1199. features. A :ref:`comparison <tutorial_stft_legacy_stft>` between the
  1200. implementations can be found in the :ref:`tutorial_stft` section of the
  1201. :ref:`user_guide`.
  1202. Parameters
  1203. ----------
  1204. x : array_like
  1205. Time series of measurement values
  1206. fs : float, optional
  1207. Sampling frequency of the `x` time series. Defaults to 1.0.
  1208. window : str or tuple or array_like, optional
  1209. Desired window to use. If `window` is a string or tuple, it is
  1210. passed to `get_window` to generate the window values, which are
  1211. DFT-even by default. See `get_window` for a list of windows and
  1212. required parameters. If `window` is array_like it will be used
  1213. directly as the window and its length must be nperseg. Defaults
  1214. to a periodic Hann window.
  1215. nperseg : int, optional
  1216. Length of each segment. Defaults to 256.
  1217. noverlap : int, optional
  1218. Number of points to overlap between segments. If `None`,
  1219. ``noverlap = nperseg // 2``. Defaults to `None`. When
  1220. specified, the COLA constraint must be met (see Notes below).
  1221. nfft : int, optional
  1222. Length of the FFT used, if a zero padded FFT is desired. If
  1223. `None`, the FFT length is `nperseg`. Defaults to `None`.
  1224. detrend : str or function or `False`, optional
  1225. Specifies how to detrend each segment. If `detrend` is a
  1226. string, it is passed as the `type` argument to the `detrend`
  1227. function. If it is a function, it takes a segment and returns a
  1228. detrended segment. If `detrend` is `False`, no detrending is
  1229. done. Defaults to `False`.
  1230. return_onesided : bool, optional
  1231. If `True`, return a one-sided spectrum for real data. If
  1232. `False` return a two-sided spectrum. Defaults to `True`, but for
  1233. complex data, a two-sided spectrum is always returned.
  1234. boundary : str or None, optional
  1235. Specifies whether the input signal is extended at both ends, and
  1236. how to generate the new values, in order to center the first
  1237. windowed segment on the first input point. This has the benefit
  1238. of enabling reconstruction of the first input point when the
  1239. employed window function starts at zero. Valid options are
  1240. ``['even', 'odd', 'constant', 'zeros', None]``. Defaults to
  1241. 'zeros', for zero padding extension. I.e. ``[1, 2, 3, 4]`` is
  1242. extended to ``[0, 1, 2, 3, 4, 0]`` for ``nperseg=3``.
  1243. padded : bool, optional
  1244. Specifies whether the input signal is zero-padded at the end to
  1245. make the signal fit exactly into an integer number of window
  1246. segments, so that all of the signal is included in the output.
  1247. Defaults to `True`. Padding occurs after boundary extension, if
  1248. `boundary` is not `None`, and `padded` is `True`, as is the
  1249. default.
  1250. axis : int, optional
  1251. Axis along which the STFT is computed; the default is over the
  1252. last axis (i.e. ``axis=-1``).
  1253. scaling: {'spectrum', 'psd'}
  1254. The default 'spectrum' scaling allows each frequency line of `Zxx` to
  1255. be interpreted as a magnitude spectrum. The 'psd' option scales each
  1256. line to a power spectral density - it allows to calculate the signal's
  1257. energy by numerically integrating over ``abs(Zxx)**2``.
  1258. .. versionadded:: 1.9.0
  1259. Returns
  1260. -------
  1261. f : ndarray
  1262. Array of sample frequencies.
  1263. t : ndarray
  1264. Array of segment times.
  1265. Zxx : ndarray
  1266. STFT of `x`. By default, the last axis of `Zxx` corresponds
  1267. to the segment times.
  1268. See Also
  1269. --------
  1270. istft: Inverse Short Time Fourier Transform
  1271. ShortTimeFFT: Newer STFT/ISTFT implementation providing more features.
  1272. check_COLA: Check whether the Constant OverLap Add (COLA) constraint
  1273. is met
  1274. check_NOLA: Check whether the Nonzero Overlap Add (NOLA) constraint is met
  1275. welch: Power spectral density by Welch's method.
  1276. spectrogram: Spectrogram by Welch's method.
  1277. csd: Cross spectral density by Welch's method.
  1278. lombscargle: Lomb-Scargle periodogram for unevenly sampled data
  1279. Notes
  1280. -----
  1281. In order to enable inversion of an STFT via the inverse STFT in
  1282. `istft`, the signal windowing must obey the constraint of "Nonzero
  1283. OverLap Add" (NOLA), and the input signal must have complete
  1284. windowing coverage (i.e. ``(x.shape[axis] - nperseg) %
  1285. (nperseg-noverlap) == 0``). The `padded` argument may be used to
  1286. accomplish this.
  1287. Given a time-domain signal :math:`x[n]`, a window :math:`w[n]`, and a hop
  1288. size :math:`H` = `nperseg - noverlap`, the windowed frame at time index
  1289. :math:`t` is given by
  1290. .. math:: x_{t}[n]=x[n]w[n-tH]
  1291. The overlap-add (OLA) reconstruction equation is given by
  1292. .. math:: x[n]=\frac{\sum_{t}x_{t}[n]w[n-tH]}{\sum_{t}w^{2}[n-tH]}
  1293. The NOLA constraint ensures that every normalization term that appears
  1294. in the denominator of the OLA reconstruction equation is nonzero. Whether a
  1295. choice of `window`, `nperseg`, and `noverlap` satisfy this constraint can
  1296. be tested with `check_NOLA`.
  1297. .. versionadded:: 0.19.0
  1298. References
  1299. ----------
  1300. .. [1] Oppenheim, Alan V., Ronald W. Schafer, John R. Buck
  1301. "Discrete-Time Signal Processing", Prentice Hall, 1999.
  1302. .. [2] Daniel W. Griffin, Jae S. Lim "Signal Estimation from
  1303. Modified Short-Time Fourier Transform", IEEE 1984,
  1304. 10.1109/TASSP.1984.1164317
  1305. Examples
  1306. --------
  1307. >>> import numpy as np
  1308. >>> from scipy import signal
  1309. >>> import matplotlib.pyplot as plt
  1310. >>> rng = np.random.default_rng()
  1311. Generate a test signal, a 2 Vrms sine wave whose frequency is slowly
  1312. modulated around 3kHz, corrupted by white noise of exponentially
  1313. decreasing magnitude sampled at 10 kHz.
  1314. >>> fs = 10e3
  1315. >>> N = 1e5
  1316. >>> amp = 2 * np.sqrt(2)
  1317. >>> noise_power = 0.01 * fs / 2
  1318. >>> time = np.arange(N) / float(fs)
  1319. >>> mod = 500*np.cos(2*np.pi*0.25*time)
  1320. >>> carrier = amp * np.sin(2*np.pi*3e3*time + mod)
  1321. >>> noise = rng.normal(scale=np.sqrt(noise_power),
  1322. ... size=time.shape)
  1323. >>> noise *= np.exp(-time/5)
  1324. >>> x = carrier + noise
  1325. Compute and plot the STFT's magnitude.
  1326. >>> f, t, Zxx = signal.stft(x, fs, nperseg=1000)
  1327. >>> plt.pcolormesh(t, f, np.abs(Zxx), vmin=0, vmax=amp, shading='gouraud')
  1328. >>> plt.title('STFT Magnitude')
  1329. >>> plt.ylabel('Frequency [Hz]')
  1330. >>> plt.xlabel('Time [sec]')
  1331. >>> plt.show()
  1332. Compare the energy of the signal `x` with the energy of its STFT:
  1333. >>> E_x = sum(x**2) / fs # Energy of x
  1334. >>> # Calculate a two-sided STFT with PSD scaling:
  1335. >>> f, t, Zxx = signal.stft(x, fs, nperseg=1000, return_onesided=False,
  1336. ... scaling='psd')
  1337. >>> # Integrate numerically over abs(Zxx)**2:
  1338. >>> df, dt = f[1] - f[0], t[1] - t[0]
  1339. >>> E_Zxx = sum(np.sum(Zxx.real**2 + Zxx.imag**2, axis=0) * df) * dt
  1340. >>> # The energy is the same, but the numerical errors are quite large:
  1341. >>> np.isclose(E_x, E_Zxx, rtol=1e-2)
  1342. True
  1343. """
  1344. if scaling == 'psd':
  1345. scaling = 'density'
  1346. elif scaling != 'spectrum':
  1347. raise ValueError(f"Parameter {scaling=} not in ['spectrum', 'psd']!")
  1348. freqs, time, Zxx = _spectral_helper(x, x, fs, window, nperseg, noverlap,
  1349. nfft, detrend, return_onesided,
  1350. scaling=scaling, axis=axis,
  1351. mode='stft', boundary=boundary,
  1352. padded=padded)
  1353. return freqs, time, Zxx
  1354. def istft(Zxx, fs=1.0, window='hann_periodic', nperseg=None, noverlap=None, nfft=None,
  1355. input_onesided=True, boundary=True, time_axis=-1, freq_axis=-2,
  1356. scaling='spectrum'):
  1357. r"""Perform the inverse Short Time Fourier transform (legacy function).
  1358. .. legacy:: function
  1359. `ShortTimeFFT` is a newer STFT / ISTFT implementation with more
  1360. features. A :ref:`comparison <tutorial_stft_legacy_stft>` between the
  1361. implementations can be found in the :ref:`tutorial_stft` section of the
  1362. :ref:`user_guide`.
  1363. Parameters
  1364. ----------
  1365. Zxx : array_like
  1366. STFT of the signal to be reconstructed. If a purely real array
  1367. is passed, it will be cast to a complex data type.
  1368. fs : float, optional
  1369. Sampling frequency of the time series. Defaults to 1.0.
  1370. window : str or tuple or array_like, optional
  1371. Desired window to use. If `window` is a string or tuple, it is
  1372. passed to `get_window` to generate the window values, which are
  1373. DFT-even by default. See `get_window` for a list of windows and
  1374. required parameters. If `window` is array_like it will be used
  1375. directly as the window and its length must be nperseg. Defaults
  1376. to a periodic Hann window. Must match the window used to generate the
  1377. STFT for faithful inversion.
  1378. nperseg : int, optional
  1379. Number of data points corresponding to each STFT segment. This
  1380. parameter must be specified if the number of data points per
  1381. segment is odd, or if the STFT was padded via ``nfft >
  1382. nperseg``. If `None`, the value depends on the shape of
  1383. `Zxx` and `input_onesided`. If `input_onesided` is `True`,
  1384. ``nperseg=2*(Zxx.shape[freq_axis] - 1)``. Otherwise,
  1385. ``nperseg=Zxx.shape[freq_axis]``. Defaults to `None`.
  1386. noverlap : int, optional
  1387. Number of points to overlap between segments. If `None`, half
  1388. of the segment length. Defaults to `None`. When specified, the
  1389. COLA constraint must be met (see Notes below), and should match
  1390. the parameter used to generate the STFT. Defaults to `None`.
  1391. nfft : int, optional
  1392. Number of FFT points corresponding to each STFT segment. This
  1393. parameter must be specified if the STFT was padded via ``nfft >
  1394. nperseg``. If `None`, the default values are the same as for
  1395. `nperseg`, detailed above, with one exception: if
  1396. `input_onesided` is True and
  1397. ``nperseg==2*Zxx.shape[freq_axis] - 1``, `nfft` also takes on
  1398. that value. This case allows the proper inversion of an
  1399. odd-length unpadded STFT using ``nfft=None``. Defaults to
  1400. `None`.
  1401. input_onesided : bool, optional
  1402. If `True`, interpret the input array as one-sided FFTs, such
  1403. as is returned by `stft` with ``return_onesided=True`` and
  1404. `numpy.fft.rfft`. If `False`, interpret the input as a a
  1405. two-sided FFT. Defaults to `True`.
  1406. boundary : bool, optional
  1407. Specifies whether the input signal was extended at its
  1408. boundaries by supplying a non-`None` ``boundary`` argument to
  1409. `stft`. Defaults to `True`.
  1410. time_axis : int, optional
  1411. Where the time segments of the STFT is located; the default is
  1412. the last axis (i.e. ``axis=-1``).
  1413. freq_axis : int, optional
  1414. Where the frequency axis of the STFT is located; the default is
  1415. the penultimate axis (i.e. ``axis=-2``).
  1416. scaling: {'spectrum', 'psd'}
  1417. The default 'spectrum' scaling allows each frequency line of `Zxx` to
  1418. be interpreted as a magnitude spectrum. The 'psd' option scales each
  1419. line to a power spectral density - it allows to calculate the signal's
  1420. energy by numerically integrating over ``abs(Zxx)**2``.
  1421. Returns
  1422. -------
  1423. t : ndarray
  1424. Array of output data times.
  1425. x : ndarray
  1426. iSTFT of `Zxx`.
  1427. See Also
  1428. --------
  1429. stft: Short Time Fourier Transform
  1430. ShortTimeFFT: Newer STFT/ISTFT implementation providing more features.
  1431. check_COLA: Check whether the Constant OverLap Add (COLA) constraint
  1432. is met
  1433. check_NOLA: Check whether the Nonzero Overlap Add (NOLA) constraint is met
  1434. Notes
  1435. -----
  1436. In order to enable inversion of an STFT via the inverse STFT with
  1437. `istft`, the signal windowing must obey the constraint of "nonzero
  1438. overlap add" (NOLA):
  1439. .. math:: \sum_{t}w^{2}[n-tH] \ne 0
  1440. This ensures that the normalization factors that appear in the denominator
  1441. of the overlap-add reconstruction equation
  1442. .. math:: x[n]=\frac{\sum_{t}x_{t}[n]w[n-tH]}{\sum_{t}w^{2}[n-tH]}
  1443. are not zero. The NOLA constraint can be checked with the `check_NOLA`
  1444. function.
  1445. An STFT which has been modified (via masking or otherwise) is not
  1446. guaranteed to correspond to an exactly realizible signal. This
  1447. function implements the iSTFT via the least-squares estimation
  1448. algorithm detailed in [2]_, which produces a signal that minimizes
  1449. the mean squared error between the STFT of the returned signal and
  1450. the modified STFT.
  1451. .. versionadded:: 0.19.0
  1452. References
  1453. ----------
  1454. .. [1] Oppenheim, Alan V., Ronald W. Schafer, John R. Buck
  1455. "Discrete-Time Signal Processing", Prentice Hall, 1999.
  1456. .. [2] Daniel W. Griffin, Jae S. Lim "Signal Estimation from
  1457. Modified Short-Time Fourier Transform", IEEE 1984,
  1458. 10.1109/TASSP.1984.1164317
  1459. Examples
  1460. --------
  1461. >>> import numpy as np
  1462. >>> from scipy import signal
  1463. >>> import matplotlib.pyplot as plt
  1464. >>> rng = np.random.default_rng()
  1465. Generate a test signal, a 2 Vrms sine wave at 50Hz corrupted by
  1466. 0.001 V**2/Hz of white noise sampled at 1024 Hz.
  1467. >>> fs = 1024
  1468. >>> N = 10*fs
  1469. >>> nperseg = 512
  1470. >>> amp = 2 * np.sqrt(2)
  1471. >>> noise_power = 0.001 * fs / 2
  1472. >>> time = np.arange(N) / float(fs)
  1473. >>> carrier = amp * np.sin(2*np.pi*50*time)
  1474. >>> noise = rng.normal(scale=np.sqrt(noise_power),
  1475. ... size=time.shape)
  1476. >>> x = carrier + noise
  1477. Compute the STFT, and plot its magnitude
  1478. >>> f, t, Zxx = signal.stft(x, fs=fs, nperseg=nperseg)
  1479. >>> plt.figure()
  1480. >>> plt.pcolormesh(t, f, np.abs(Zxx), vmin=0, vmax=amp, shading='gouraud')
  1481. >>> plt.ylim([f[1], f[-1]])
  1482. >>> plt.title('STFT Magnitude')
  1483. >>> plt.ylabel('Frequency [Hz]')
  1484. >>> plt.xlabel('Time [sec]')
  1485. >>> plt.yscale('log')
  1486. >>> plt.show()
  1487. Zero the components that are 10% or less of the carrier magnitude,
  1488. then convert back to a time series via inverse STFT
  1489. >>> Zxx = np.where(np.abs(Zxx) >= amp/10, Zxx, 0)
  1490. >>> _, xrec = signal.istft(Zxx, fs)
  1491. Compare the cleaned signal with the original and true carrier signals.
  1492. >>> plt.figure()
  1493. >>> plt.plot(time, x, time, xrec, time, carrier)
  1494. >>> plt.xlim([2, 2.1])
  1495. >>> plt.xlabel('Time [sec]')
  1496. >>> plt.ylabel('Signal')
  1497. >>> plt.legend(['Carrier + Noise', 'Filtered via STFT', 'True Carrier'])
  1498. >>> plt.show()
  1499. Note that the cleaned signal does not start as abruptly as the original,
  1500. since some of the coefficients of the transient were also removed:
  1501. >>> plt.figure()
  1502. >>> plt.plot(time, x, time, xrec, time, carrier)
  1503. >>> plt.xlim([0, 0.1])
  1504. >>> plt.xlabel('Time [sec]')
  1505. >>> plt.ylabel('Signal')
  1506. >>> plt.legend(['Carrier + Noise', 'Filtered via STFT', 'True Carrier'])
  1507. >>> plt.show()
  1508. """
  1509. # Make sure input is an ndarray of appropriate complex dtype
  1510. Zxx = np.asarray(Zxx) + 0j
  1511. freq_axis = int(freq_axis)
  1512. time_axis = int(time_axis)
  1513. if Zxx.ndim < 2:
  1514. raise ValueError('Input stft must be at least 2d!')
  1515. if freq_axis == time_axis:
  1516. raise ValueError('Must specify differing time and frequency axes!')
  1517. nseg = Zxx.shape[time_axis]
  1518. if input_onesided:
  1519. # Assume even segment length
  1520. n_default = 2*(Zxx.shape[freq_axis] - 1)
  1521. else:
  1522. n_default = Zxx.shape[freq_axis]
  1523. # Check windowing parameters
  1524. if nperseg is None:
  1525. nperseg = n_default
  1526. else:
  1527. nperseg = int(nperseg)
  1528. if nperseg < 1:
  1529. raise ValueError('nperseg must be a positive integer')
  1530. if nfft is None:
  1531. if (input_onesided) and (nperseg == n_default + 1):
  1532. # Odd nperseg, no FFT padding
  1533. nfft = nperseg
  1534. else:
  1535. nfft = n_default
  1536. elif nfft < nperseg:
  1537. raise ValueError('nfft must be greater than or equal to nperseg.')
  1538. else:
  1539. nfft = int(nfft)
  1540. if noverlap is None:
  1541. noverlap = nperseg//2
  1542. else:
  1543. noverlap = int(noverlap)
  1544. if noverlap >= nperseg:
  1545. raise ValueError('noverlap must be less than nperseg.')
  1546. nstep = nperseg - noverlap
  1547. # Rearrange axes if necessary
  1548. if time_axis != Zxx.ndim-1 or freq_axis != Zxx.ndim-2:
  1549. # Turn negative indices to positive for the call to transpose
  1550. if freq_axis < 0:
  1551. freq_axis = Zxx.ndim + freq_axis
  1552. if time_axis < 0:
  1553. time_axis = Zxx.ndim + time_axis
  1554. zouter = list(range(Zxx.ndim))
  1555. for ax in sorted([time_axis, freq_axis], reverse=True):
  1556. zouter.pop(ax)
  1557. Zxx = np.transpose(Zxx, zouter+[freq_axis, time_axis])
  1558. # Get window as array
  1559. if isinstance(window, str) or type(window) is tuple:
  1560. win = get_window(window, nperseg)
  1561. else:
  1562. win = np.asarray(window)
  1563. if len(win.shape) != 1:
  1564. raise ValueError('window must be 1-D')
  1565. if win.shape[0] != nperseg:
  1566. raise ValueError(f'window must have length of {nperseg}')
  1567. ifunc = sp_fft.irfft if input_onesided else sp_fft.ifft
  1568. xsubs = ifunc(Zxx, axis=-2, n=nfft)[..., :nperseg, :]
  1569. # Initialize output and normalization arrays
  1570. outputlength = nperseg + (nseg-1)*nstep
  1571. x = np.zeros(list(Zxx.shape[:-2])+[outputlength], dtype=xsubs.dtype)
  1572. norm = np.zeros(outputlength, dtype=xsubs.dtype)
  1573. if np.result_type(win, xsubs) != xsubs.dtype:
  1574. win = win.astype(xsubs.dtype)
  1575. if scaling == 'spectrum':
  1576. xsubs *= win.sum()
  1577. elif scaling == 'psd':
  1578. xsubs *= np.sqrt(fs * sum(win**2))
  1579. else:
  1580. raise ValueError(f"Parameter {scaling=} not in ['spectrum', 'psd']!")
  1581. # Construct the output from the ifft segments
  1582. # This loop could perhaps be vectorized/strided somehow...
  1583. for ii in range(nseg):
  1584. # Window the ifft
  1585. x[..., ii*nstep:ii*nstep+nperseg] += xsubs[..., ii] * win
  1586. norm[..., ii*nstep:ii*nstep+nperseg] += win**2
  1587. # Remove extension points
  1588. if boundary:
  1589. x = x[..., nperseg//2:-(nperseg//2)]
  1590. norm = norm[..., nperseg//2:-(nperseg//2)]
  1591. # Divide out normalization where non-tiny
  1592. if np.sum(norm > 1e-10) != len(norm):
  1593. warnings.warn(
  1594. "NOLA condition failed, STFT may not be invertible."
  1595. + (" Possibly due to missing boundary" if not boundary else ""),
  1596. stacklevel=2
  1597. )
  1598. x /= np.where(norm > 1e-10, norm, 1.0)
  1599. if input_onesided:
  1600. x = x.real
  1601. # Put axes back
  1602. if x.ndim > 1:
  1603. if time_axis != Zxx.ndim-1:
  1604. if freq_axis < time_axis:
  1605. time_axis -= 1
  1606. x = np.moveaxis(x, -1, time_axis)
  1607. time = np.arange(x.shape[0])/float(fs)
  1608. return time, x
  1609. def coherence(x, y, fs=1.0, window='hann_periodic', nperseg=None, noverlap=None,
  1610. nfft=None, detrend='constant', axis=-1):
  1611. r"""
  1612. Estimate the magnitude squared coherence estimate, Cxy, of
  1613. discrete-time signals X and Y using Welch's method.
  1614. ``Cxy = abs(Pxy)**2/(Pxx*Pyy)``, where `Pxx` and `Pyy` are power
  1615. spectral density estimates of X and Y, and `Pxy` is the cross
  1616. spectral density estimate of X and Y.
  1617. Parameters
  1618. ----------
  1619. x : array_like
  1620. Time series of measurement values
  1621. y : array_like
  1622. Time series of measurement values
  1623. fs : float, optional
  1624. Sampling frequency of the `x` and `y` time series. Defaults
  1625. to 1.0.
  1626. window : str or tuple or array_like, optional
  1627. Desired window to use. If `window` is a string or tuple, it is
  1628. passed to `get_window` to generate the window values, which are
  1629. DFT-even by default. See `get_window` for a list of windows and
  1630. required parameters. If `window` is array_like it will be used
  1631. directly as the window and its length must be nperseg. Defaults
  1632. to a periodic Hann window.
  1633. nperseg : int, optional
  1634. Length of each segment. Defaults to None, but if window is str or
  1635. tuple, is set to 256, and if window is array_like, is set to the
  1636. length of the window.
  1637. noverlap: int, optional
  1638. Number of points to overlap between segments. If `None`,
  1639. ``noverlap = nperseg // 2``. Defaults to `None`.
  1640. nfft : int, optional
  1641. Length of the FFT used, if a zero padded FFT is desired. If
  1642. `None`, the FFT length is `nperseg`. Defaults to `None`.
  1643. detrend : str or function or `False`, optional
  1644. Specifies how to detrend each segment. If `detrend` is a
  1645. string, it is passed as the `type` argument to the `detrend`
  1646. function. If it is a function, it takes a segment and returns a
  1647. detrended segment. If `detrend` is `False`, no detrending is
  1648. done. Defaults to 'constant'.
  1649. axis : int, optional
  1650. Axis along which the coherence is computed for both inputs; the
  1651. default is over the last axis (i.e. ``axis=-1``).
  1652. Returns
  1653. -------
  1654. f : ndarray
  1655. Array of sample frequencies.
  1656. Cxy : ndarray
  1657. Magnitude squared coherence of x and y.
  1658. See Also
  1659. --------
  1660. periodogram: Simple, optionally modified periodogram
  1661. lombscargle: Lomb-Scargle periodogram for unevenly sampled data
  1662. welch: Power spectral density by Welch's method.
  1663. csd: Cross spectral density by Welch's method.
  1664. Notes
  1665. -----
  1666. An appropriate amount of overlap will depend on the choice of window
  1667. and on your requirements. For the default Hann window an overlap of
  1668. 50% is a reasonable trade-off between accurately estimating the
  1669. signal power, while not over counting any of the data. Narrower
  1670. windows may require a larger overlap.
  1671. .. versionadded:: 0.16.0
  1672. References
  1673. ----------
  1674. .. [1] P. Welch, "The use of the fast Fourier transform for the
  1675. estimation of power spectra: A method based on time averaging
  1676. over short, modified periodograms", IEEE Trans. Audio
  1677. Electroacoust. vol. 15, pp. 70-73, 1967.
  1678. .. [2] Stoica, Petre, and Randolph Moses, "Spectral Analysis of
  1679. Signals" Prentice Hall, 2005
  1680. Examples
  1681. --------
  1682. >>> import numpy as np
  1683. >>> from scipy import signal
  1684. >>> import matplotlib.pyplot as plt
  1685. >>> rng = np.random.default_rng()
  1686. Generate two test signals with some common features.
  1687. >>> fs = 10e3
  1688. >>> N = 1e5
  1689. >>> amp = 20
  1690. >>> freq = 1234.0
  1691. >>> noise_power = 0.001 * fs / 2
  1692. >>> time = np.arange(N) / fs
  1693. >>> b, a = signal.butter(2, 0.25, 'low')
  1694. >>> x = rng.normal(scale=np.sqrt(noise_power), size=time.shape)
  1695. >>> y = signal.lfilter(b, a, x)
  1696. >>> x += amp*np.sin(2*np.pi*freq*time)
  1697. >>> y += rng.normal(scale=0.1*np.sqrt(noise_power), size=time.shape)
  1698. Compute and plot the coherence.
  1699. >>> f, Cxy = signal.coherence(x, y, fs, nperseg=1024)
  1700. >>> plt.semilogy(f, Cxy)
  1701. >>> plt.xlabel('frequency [Hz]')
  1702. >>> plt.ylabel('Coherence')
  1703. >>> plt.show()
  1704. """
  1705. freqs, Pxx = welch(x, fs=fs, window=window, nperseg=nperseg,
  1706. noverlap=noverlap, nfft=nfft, detrend=detrend,
  1707. axis=axis)
  1708. _, Pyy = welch(y, fs=fs, window=window, nperseg=nperseg, noverlap=noverlap,
  1709. nfft=nfft, detrend=detrend, axis=axis)
  1710. _, Pxy = csd(x, y, fs=fs, window=window, nperseg=nperseg,
  1711. noverlap=noverlap, nfft=nfft, detrend=detrend, axis=axis)
  1712. Cxy = np.abs(Pxy)**2 / Pxx / Pyy
  1713. return freqs, Cxy
  1714. def _spectral_helper(x, y, fs=1.0, window='hann', nperseg=None, noverlap=None,
  1715. nfft=None, detrend='constant', return_onesided=True,
  1716. scaling='density', axis=-1, mode='psd', boundary=None,
  1717. padded=False):
  1718. """Calculate various forms of windowed FFTs for PSD, CSD, etc.
  1719. .. legacy:: function
  1720. This function is soley used by the legacy functions `spectrogram` and `stft`
  1721. (which are also in this same source file `scipy/signal/_spectral_py.py`).
  1722. This is a helper function that implements the commonality between
  1723. the stft, psd, csd, and spectrogram functions. It is not designed to
  1724. be called externally. The windows are not averaged over; the result
  1725. from each window is returned.
  1726. Parameters
  1727. ----------
  1728. x : array_like
  1729. Array or sequence containing the data to be analyzed.
  1730. y : array_like
  1731. Array or sequence containing the data to be analyzed. If this is
  1732. the same object in memory as `x` (i.e. ``_spectral_helper(x,
  1733. x, ...)``), the extra computations are spared.
  1734. fs : float, optional
  1735. Sampling frequency of the time series. Defaults to 1.0.
  1736. window : str or tuple or array_like, optional
  1737. Desired window to use. If `window` is a string or tuple, it is
  1738. passed to `get_window` to generate the window values, which are
  1739. DFT-even by default. See `get_window` for a list of windows and
  1740. required parameters. If `window` is array_like it will be used
  1741. directly as the window and its length must be nperseg. Defaults
  1742. to a Hann window.
  1743. nperseg : int, optional
  1744. Length of each segment. Defaults to None, but if window is str or
  1745. tuple, is set to 256, and if window is array_like, is set to the
  1746. length of the window.
  1747. noverlap : int, optional
  1748. Number of points to overlap between segments. If `None`,
  1749. ``noverlap = nperseg // 2``. Defaults to `None`.
  1750. nfft : int, optional
  1751. Length of the FFT used, if a zero padded FFT is desired. If
  1752. `None`, the FFT length is `nperseg`. Defaults to `None`.
  1753. detrend : str or function or `False`, optional
  1754. Specifies how to detrend each segment. If `detrend` is a
  1755. string, it is passed as the `type` argument to the `detrend`
  1756. function. If it is a function, it takes a segment and returns a
  1757. detrended segment. If `detrend` is `False`, no detrending is
  1758. done. Defaults to 'constant'.
  1759. return_onesided : bool, optional
  1760. If `True`, return a one-sided spectrum for real data. If
  1761. `False` return a two-sided spectrum. Defaults to `True`, but for
  1762. complex data, a two-sided spectrum is always returned.
  1763. scaling : { 'density', 'spectrum' }, optional
  1764. Selects between computing the cross spectral density ('density')
  1765. where `Pxy` has units of V²/Hz and computing the cross
  1766. spectrum ('spectrum') where `Pxy` has units of V², if `x`
  1767. and `y` are measured in V and `fs` is measured in Hz.
  1768. Defaults to 'density'
  1769. axis : int, optional
  1770. Axis along which the FFTs are computed; the default is over the
  1771. last axis (i.e. ``axis=-1``).
  1772. mode: str {'psd', 'stft'}, optional
  1773. Defines what kind of return values are expected. Defaults to
  1774. 'psd'.
  1775. boundary : str or None, optional
  1776. Specifies whether the input signal is extended at both ends, and
  1777. how to generate the new values, in order to center the first
  1778. windowed segment on the first input point. This has the benefit
  1779. of enabling reconstruction of the first input point when the
  1780. employed window function starts at zero. Valid options are
  1781. ``['even', 'odd', 'constant', 'zeros', None]``. Defaults to
  1782. `None`.
  1783. padded : bool, optional
  1784. Specifies whether the input signal is zero-padded at the end to
  1785. make the signal fit exactly into an integer number of window
  1786. segments, so that all of the signal is included in the output.
  1787. Defaults to `False`. Padding occurs after boundary extension, if
  1788. `boundary` is not `None`, and `padded` is `True`.
  1789. Returns
  1790. -------
  1791. freqs : ndarray
  1792. Array of sample frequencies.
  1793. t : ndarray
  1794. Array of times corresponding to each data segment
  1795. result : ndarray
  1796. Array of output data, contents dependent on *mode* kwarg.
  1797. Notes
  1798. -----
  1799. Adapted from matplotlib.mlab
  1800. .. versionadded:: 0.16.0
  1801. """
  1802. if mode not in ['psd', 'stft']:
  1803. raise ValueError(f"Unknown value for mode {mode}, must be one of: "
  1804. "{'psd', 'stft'}")
  1805. boundary_funcs = {'even': even_ext,
  1806. 'odd': odd_ext,
  1807. 'constant': const_ext,
  1808. 'zeros': zero_ext,
  1809. None: None}
  1810. if boundary not in boundary_funcs:
  1811. raise ValueError(f"Unknown boundary option '{boundary}', "
  1812. f"must be one of: {list(boundary_funcs.keys())}")
  1813. # If x and y are the same object we can save ourselves some computation.
  1814. same_data = y is x
  1815. if not same_data and mode != 'psd':
  1816. raise ValueError("x and y must be equal if mode is 'stft'")
  1817. axis = int(axis)
  1818. # Ensure we have np.arrays, get outdtype
  1819. x = np.asarray(x)
  1820. if not same_data:
  1821. y = np.asarray(y)
  1822. outdtype = np.result_type(x, y, np.complex64)
  1823. else:
  1824. outdtype = np.result_type(x, np.complex64)
  1825. if not same_data:
  1826. # Check if we can broadcast the outer axes together
  1827. xouter = list(x.shape)
  1828. youter = list(y.shape)
  1829. xouter.pop(axis)
  1830. youter.pop(axis)
  1831. try:
  1832. outershape = np.broadcast(np.empty(xouter), np.empty(youter)).shape
  1833. except ValueError as e:
  1834. raise ValueError('x and y cannot be broadcast together.') from e
  1835. if same_data:
  1836. if x.size == 0:
  1837. return np.empty(x.shape), np.empty(x.shape), np.empty(x.shape)
  1838. else:
  1839. if x.size == 0 or y.size == 0:
  1840. outshape = outershape + (min([x.shape[axis], y.shape[axis]]),)
  1841. emptyout = np.moveaxis(np.empty(outshape), -1, axis)
  1842. return emptyout, emptyout, emptyout
  1843. if x.ndim > 1:
  1844. if axis != -1:
  1845. x = np.moveaxis(x, axis, -1)
  1846. if not same_data and y.ndim > 1:
  1847. y = np.moveaxis(y, axis, -1)
  1848. # Check if x and y are the same length, zero-pad if necessary
  1849. if not same_data:
  1850. if x.shape[-1] != y.shape[-1]:
  1851. if x.shape[-1] < y.shape[-1]:
  1852. pad_shape = list(x.shape)
  1853. pad_shape[-1] = y.shape[-1] - x.shape[-1]
  1854. x = np.concatenate((x, np.zeros(pad_shape)), -1)
  1855. else:
  1856. pad_shape = list(y.shape)
  1857. pad_shape[-1] = x.shape[-1] - y.shape[-1]
  1858. y = np.concatenate((y, np.zeros(pad_shape)), -1)
  1859. if nperseg is not None: # if specified by user
  1860. nperseg = int(nperseg)
  1861. if nperseg < 1:
  1862. raise ValueError('nperseg must be a positive integer')
  1863. # parse window; if array like, then set nperseg = win.shape
  1864. win, nperseg = _triage_segments(window, nperseg, input_length=x.shape[-1])
  1865. if nfft is None:
  1866. nfft = nperseg
  1867. elif nfft < nperseg:
  1868. raise ValueError('nfft must be greater than or equal to nperseg.')
  1869. else:
  1870. nfft = int(nfft)
  1871. if noverlap is None:
  1872. noverlap = nperseg//2
  1873. else:
  1874. noverlap = int(noverlap)
  1875. if noverlap >= nperseg:
  1876. raise ValueError('noverlap must be less than nperseg.')
  1877. nstep = nperseg - noverlap
  1878. # Padding occurs after boundary extension, so that the extended signal ends
  1879. # in zeros, instead of introducing an impulse at the end.
  1880. # I.e. if x = [..., 3, 2]
  1881. # extend then pad -> [..., 3, 2, 2, 3, 0, 0, 0]
  1882. # pad then extend -> [..., 3, 2, 0, 0, 0, 2, 3]
  1883. if boundary is not None:
  1884. ext_func = boundary_funcs[boundary]
  1885. x = ext_func(x, nperseg//2, axis=-1)
  1886. if not same_data:
  1887. y = ext_func(y, nperseg//2, axis=-1)
  1888. if padded:
  1889. # Pad to integer number of windowed segments
  1890. # I.e. make x.shape[-1] = nperseg + (nseg-1)*nstep, with integer nseg
  1891. nadd = (-(x.shape[-1]-nperseg) % nstep) % nperseg
  1892. zeros_shape = list(x.shape[:-1]) + [nadd]
  1893. x = np.concatenate((x, np.zeros(zeros_shape)), axis=-1)
  1894. if not same_data:
  1895. zeros_shape = list(y.shape[:-1]) + [nadd]
  1896. y = np.concatenate((y, np.zeros(zeros_shape)), axis=-1)
  1897. # Handle detrending and window functions
  1898. if not detrend:
  1899. def detrend_func(d):
  1900. return d
  1901. elif not hasattr(detrend, '__call__'):
  1902. def detrend_func(d):
  1903. return _signaltools.detrend(d, type=detrend, axis=-1)
  1904. elif axis != -1:
  1905. # Wrap this function so that it receives a shape that it could
  1906. # reasonably expect to receive.
  1907. def detrend_func(d):
  1908. d = np.moveaxis(d, -1, axis)
  1909. d = detrend(d)
  1910. return np.moveaxis(d, axis, -1)
  1911. else:
  1912. detrend_func = detrend
  1913. if np.result_type(win, np.complex64) != outdtype:
  1914. win = win.astype(outdtype)
  1915. if scaling == 'density':
  1916. scale = 1.0 / (fs * (win*win).sum())
  1917. elif scaling == 'spectrum':
  1918. scale = 1.0 / win.sum()**2
  1919. else:
  1920. raise ValueError(f'Unknown scaling: {scaling!r}')
  1921. if mode == 'stft':
  1922. scale = np.sqrt(scale)
  1923. if return_onesided:
  1924. if np.iscomplexobj(x):
  1925. sides = 'twosided'
  1926. warnings.warn('Input data is complex, switching to return_onesided=False',
  1927. stacklevel=3)
  1928. else:
  1929. sides = 'onesided'
  1930. if not same_data:
  1931. if np.iscomplexobj(y):
  1932. sides = 'twosided'
  1933. warnings.warn('Input data is complex, switching to '
  1934. 'return_onesided=False',
  1935. stacklevel=3)
  1936. else:
  1937. sides = 'twosided'
  1938. if sides == 'twosided':
  1939. freqs = sp_fft.fftfreq(nfft, 1/fs)
  1940. elif sides == 'onesided':
  1941. freqs = sp_fft.rfftfreq(nfft, 1/fs)
  1942. # Perform the windowed FFTs
  1943. result = _fft_helper(x, win, detrend_func, nperseg, noverlap, nfft, sides)
  1944. if not same_data:
  1945. # All the same operations on the y data
  1946. result_y = _fft_helper(y, win, detrend_func, nperseg, noverlap, nfft,
  1947. sides)
  1948. result = np.conjugate(result) * result_y
  1949. elif mode == 'psd':
  1950. result = np.conjugate(result) * result
  1951. result *= scale
  1952. if sides == 'onesided' and mode == 'psd':
  1953. if nfft % 2:
  1954. result[..., 1:] *= 2
  1955. else:
  1956. # Last point is unpaired Nyquist freq point, don't double
  1957. result[..., 1:-1] *= 2
  1958. time = np.arange(nperseg/2, x.shape[-1] - nperseg/2 + 1,
  1959. nperseg - noverlap)/float(fs)
  1960. if boundary is not None:
  1961. time -= (nperseg/2) / fs
  1962. result = result.astype(outdtype)
  1963. # All imaginary parts are zero anyways
  1964. if same_data and mode != 'stft':
  1965. result = result.real
  1966. # Output is going to have new last axis for time/window index, so a
  1967. # negative axis index shifts down one
  1968. if axis < 0:
  1969. axis -= 1
  1970. # Roll frequency axis back to axis where the data came from
  1971. result = np.moveaxis(result, -1, axis)
  1972. return freqs, time, result
  1973. def _fft_helper(x, win, detrend_func, nperseg, noverlap, nfft, sides):
  1974. """
  1975. Calculate windowed FFT, for internal use by
  1976. `scipy.signal._spectral_helper`.
  1977. .. legacy:: function
  1978. This function is solely used by the legacy `_spectral_helper` function,
  1979. which is located also in this file.
  1980. This is a helper function that does the main FFT calculation for
  1981. `_spectral helper`. All input validation is performed there, and the
  1982. data axis is assumed to be the last axis of x. It is not designed to
  1983. be called externally. The windows are not averaged over; the result
  1984. from each window is returned.
  1985. Returns
  1986. -------
  1987. result : ndarray
  1988. Array of FFT data
  1989. Notes
  1990. -----
  1991. Adapted from matplotlib.mlab
  1992. .. versionadded:: 0.16.0
  1993. """
  1994. # Created sliding window view of array
  1995. if nperseg == 1 and noverlap == 0:
  1996. result = x[..., np.newaxis]
  1997. else:
  1998. step = nperseg - noverlap
  1999. result = np.lib.stride_tricks.sliding_window_view(
  2000. x, window_shape=nperseg, axis=-1, writeable=True
  2001. )
  2002. result = result[..., 0::step, :]
  2003. # Detrend each data segment individually
  2004. result = detrend_func(result)
  2005. # Apply window by multiplication
  2006. result = win * result
  2007. # Perform the fft. Acts on last axis by default. Zero-pads automatically
  2008. if sides == 'twosided':
  2009. func = sp_fft.fft
  2010. else:
  2011. result = result.real
  2012. func = sp_fft.rfft
  2013. result = func(result, n=nfft)
  2014. return result
  2015. def _triage_segments(window, nperseg, input_length):
  2016. """
  2017. Parses window and nperseg arguments for spectrogram and _spectral_helper.
  2018. This is a helper function, not meant to be called externally.
  2019. .. legacy:: function
  2020. This function is soley used by the legacy functions `spectrogram` and
  2021. `_spectral_helper` (which are also in this file).
  2022. Parameters
  2023. ----------
  2024. window : string, tuple, or ndarray
  2025. If window is specified by a string or tuple and nperseg is not
  2026. specified, nperseg is set to the default of 256 and returns a window of
  2027. that length.
  2028. If instead the window is array_like and nperseg is not specified, then
  2029. nperseg is set to the length of the window. A ValueError is raised if
  2030. the user supplies both an array_like window and a value for nperseg but
  2031. nperseg does not equal the length of the window.
  2032. nperseg : int
  2033. Length of each segment
  2034. input_length: int
  2035. Length of input signal, i.e. x.shape[-1]. Used to test for errors.
  2036. Returns
  2037. -------
  2038. win : ndarray
  2039. window. If function was called with string or tuple than this will hold
  2040. the actual array used as a window.
  2041. nperseg : int
  2042. Length of each segment. If window is str or tuple, nperseg is set to
  2043. 256. If window is array_like, nperseg is set to the length of the
  2044. window.
  2045. """
  2046. # parse window; if array like, then set nperseg = win.shape
  2047. if isinstance(window, str) or isinstance(window, tuple):
  2048. # if nperseg not specified
  2049. if nperseg is None:
  2050. nperseg = 256 # then change to default
  2051. if nperseg > input_length:
  2052. warnings.warn(f'nperseg = {nperseg:d} is greater than input length '
  2053. f' = {input_length:d}, using nperseg = {input_length:d}',
  2054. stacklevel=3)
  2055. nperseg = input_length
  2056. win = get_window(window, nperseg)
  2057. else:
  2058. win = np.asarray(window)
  2059. if len(win.shape) != 1:
  2060. raise ValueError('window must be 1-D')
  2061. if input_length < win.shape[-1]:
  2062. raise ValueError('window is longer than input signal')
  2063. if nperseg is None:
  2064. nperseg = win.shape[0]
  2065. elif nperseg is not None:
  2066. if nperseg != win.shape[0]:
  2067. raise ValueError("value specified for nperseg is different"
  2068. " from length of window")
  2069. return win, nperseg
  2070. def _median_bias(n):
  2071. """
  2072. Returns the bias of the median of a set of periodograms relative to
  2073. the mean.
  2074. See Appendix B from [1]_ for details.
  2075. Parameters
  2076. ----------
  2077. n : int
  2078. Numbers of periodograms being averaged.
  2079. Returns
  2080. -------
  2081. bias : float
  2082. Calculated bias.
  2083. References
  2084. ----------
  2085. .. [1] B. Allen, W.G. Anderson, P.R. Brady, D.A. Brown, J.D.E. Creighton.
  2086. "FINDCHIRP: an algorithm for detection of gravitational waves from
  2087. inspiraling compact binaries", Physical Review D 85, 2012,
  2088. :arxiv:`gr-qc/0509116`
  2089. """
  2090. ii_2 = 2 * np.arange(1., (n-1) // 2 + 1)
  2091. return 1 + np.sum(1. / (ii_2 + 1) - 1. / ii_2)