3 # Wrap Numpy's fft module to produce 1D unitary transforms and power spectra.
5 # Copyright (C) 2008 W. Trevor King
8 # Redistribution and use in source and binary forms, with or without
9 # modification, are permitted provided that the following conditions
12 # * Redistributions of source code must retain the above copyright
13 # notice, this list of conditions and the following disclaimer.
15 # * Redistributions in binary form must reproduce the above copyright
16 # notice, this list of conditions and the following disclaimer in
17 # the documentation and/or other materials provided with the
20 # * Neither the name of the copyright holders nor the names of the
21 # contributors may be used to endorse or promote products derived
22 # from this software without specific prior written permission.
24 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
25 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
26 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
27 # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
28 # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
29 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
30 # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
31 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
32 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
33 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
34 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
35 # POSSIBILITY OF SUCH DAMAGE.
38 Define some FFT wrappers to reduce clutter.
39 Provides a unitary discrete FFT and a windowed version.
40 Based on :func:`numpy.fft.rfft`.
44 * :func:`unitary_rfft`
45 * :func:`power_spectrum`
46 * :func:`unitary_power_spectrum`
47 * :func:`avg_power_spectrum`
48 * :func:`unitary_avg_power_spectrum`
53 from numpy import log2, floor, round, ceil, abs, pi, exp, cos, sin, sqrt, \
54 sinc, arctan2, array, ones, arange, linspace, zeros, \
55 uint16, float, concatenate, fromfile, argmax, complex
56 from numpy.fft import rfft
61 def floor_pow_of_two(num):
62 """Round `num` down to the closest exact a power of two.
67 >>> floor_pow_of_two(3)
69 >>> floor_pow_of_two(11)
71 >>> floor_pow_of_two(15)
79 def round_pow_of_two(num):
80 """Round `num` to the closest exact a power of two on a log scale.
85 >>> round_pow_of_two(2.9) # Note rounding on *log scale*
87 >>> round_pow_of_two(11)
89 >>> round_pow_of_two(15)
97 def ceil_pow_of_two(num):
98 """Round `num` up to the closest exact a power of two.
103 >>> ceil_pow_of_two(3)
105 >>> ceil_pow_of_two(11)
107 >>> ceil_pow_of_two(15)
111 if int(lnum) != lnum:
115 def unitary_rfft(data, freq=1.0):
116 """Compute the unitary Fourier transform of real data.
118 Unitary = preserves power [Parseval's theorem].
123 Real (not complex) data taken with a sampling frequency `freq`.
129 freq_axis,trans : numpy.ndarray
130 Arrays ready for plotting.
134 If the units on your data are Volts,
135 and your sampling frequency is in Hz,
136 then `freq_axis` will be in Hz,
137 and `trans` will be in Volts.
139 nsamps = floor_pow_of_two(len(data))
140 # Which should satisfy the discrete form of Parseval's theorem
142 # SUM |x_m|^2 = 1/n SUM |X_k|^2.
144 # However, we want our FFT to satisfy the continuous Parseval eqn
145 # int_{-infty}^{infty} |x(t)|^2 dt = int_{-infty}^{infty} |X(f)|^2 df
146 # which has the discrete form
148 # SUM |x_m|^2 dt = SUM |X'_k|^2 df
150 # with X'_k = AX, this gives us
152 # SUM |x_m|^2 = A^2 df/dt SUM |X'_k|^2
157 # From Numerical Recipes (http://www.fizyka.umk.pl/nrbook/bookcpdf.html),
158 # Section 12.1, we see that for a sampling rate dt, the maximum frequency
159 # f_c in the transformed data is the Nyquist frequency (12.1.2)
161 # and the points are spaced out by (12.1.5)
167 # A = 1/ndf = ndt/n = dt
168 # so we can convert the Numpy transformed data to match our unitary
169 # continuous transformed data with (also NR 12.1.8)
170 # X'_k = dtX = X / <sampling freq>
171 trans = rfft(data[0:nsamps]) / float(freq)
172 freq_axis = linspace(0, freq/2, nsamps/2+1)
173 return (freq_axis, trans)
175 def power_spectrum(data, freq=1.0):
176 """Compute the power spectrum of the time series `data`.
181 Real (not complex) data taken with a sampling frequency `freq`.
187 freq_axis,power : numpy.ndarray
188 Arrays ready for plotting.
192 If the number of samples in `data` is not an integer power of two,
193 the FFT ignores some of the later points.
197 unitary_power_spectrum,avg_power_spectrum
199 nsamps = floor_pow_of_two(len(data))
201 freq_axis = linspace(0, freq/2, nsamps/2+1)
202 # nsamps/2+1 b/c zero-freq and nyqist-freq are both fully real.
203 # >>> help(numpy.fft.fftpack.rfft) for Numpy's explaination.
204 # See Numerical Recipies for a details.
205 trans = rfft(data[0:nsamps])
206 power = trans * trans.conj() # We want the square of the amplitude.
207 return (freq_axis, power)
209 def unitary_power_spectrum(data, freq=1.0):
210 """Compute the unitary power spectrum of the time series `data`.
214 power_spectrum,unitary_avg_power_spectrum
216 freq_axis,power = power_spectrum(data, freq)
217 # One sided power spectral density, so 2|H(f)|**2 (see NR 2nd edition 12.0.14, p498)
219 # numpy normalizes with 1/N on the inverse transform ifft,
220 # so we should normalize the freq-space representation with 1/sqrt(N).
221 # But we're using the rfft, where N points are like N/2 complex points, so 1/sqrt(N/2)
222 # So the power gets normalized by that twice and we have 2/N
224 # On top of this, the FFT assumes a sampling freq of 1 per second,
225 # and we want to preserve area under our curves.
226 # If our total time T = len(data)/freq is smaller than 1,
227 # our df_real = freq/len(data) is bigger that the FFT expects (dt_fft = 1/len(data)),
228 # and we need to scale the powers down to conserve area.
229 # df_fft * F_fft(f) = df_real *F_real(f)
230 # F_real = F_fft(f) * (1/len)/(freq/len) = F_fft(f)*freq
231 # So the power gets normalized by *that* twice and we have 2/N * freq**2
233 # power per unit time
234 # measure x(t) for time T
235 # X(f) = int_0^T x(t) exp(-2 pi ift) dt
236 # PSD(f) = 2 |X(f)|**2 / T
238 # total_time = len(data)/float(freq)
239 # power *= 2.0 / float(freq)**2 / total_time
240 # power *= 2.0 / freq**2 * freq / len(data)
241 power *= 2.0 / (freq * float(len(data)))
243 return (freq_axis, power)
245 def window_hann(length):
246 r"""Returns a Hann window array with length entries
250 The Hann window with length :math:`L` is defined as
252 .. math:: w_i = \frac{1}{2} (1-\cos(2\pi i/L))
254 win = zeros((length,), dtype=float)
255 for i in range(length):
256 win[i] = 0.5*(1.0-cos(2.0*pi*float(i)/(length)))
257 # avg value of cos over a period is 0
258 # so average height of Hann window is 0.5
261 def avg_power_spectrum(data, freq=1.0, chunk_size=2048,
262 overlap=True, window=window_hann):
263 """Compute the avgerage power spectrum of `data`.
268 Real (not complex) data taken with a sampling frequency `freq`.
272 Number of samples per chunk. Use a power of two.
273 overlap: {True,False}
274 If `True`, each chunk overlaps the previous chunk by half its
275 length. Otherwise, the chunks are end-to-end, and not
278 Weights used to "smooth" the chunks, there is a whole science
279 behind windowing, but if you're not trying to squeeze every
280 drop of information out of your data, you'll be OK with the
285 freq_axis,power : numpy.ndarray
286 Arrays ready for plotting.
290 The average power spectrum is computed by breaking `data` into
291 chunks of length `chunk_size`. These chunks are transformed
292 individually into frequency space and then averaged together.
294 See Numerical Recipes 2 section 13.4 for a good introduction to
297 If the number of samples in `data` is not a multiple of
298 `chunk_size`, we ignore the extra points.
300 assert chunk_size == floor_pow_of_two(chunk_size), \
301 "chunk_size %d should be a power of 2" % chunk_size
303 nchunks = len(data)/chunk_size # integer division = implicit floor
305 chunk_step = chunk_size/2
307 chunk_step = chunk_size
309 win = window(chunk_size) # generate a window of the appropriate size
310 freq_axis = linspace(0, freq/2, chunk_size/2+1)
311 # nsamps/2+1 b/c zero-freq and nyqist-freq are both fully real.
312 # >>> help(numpy.fft.fftpack.rfft) for Numpy's explaination.
313 # See Numerical Recipies for a details.
314 power = zeros((chunk_size/2+1,), dtype=float)
315 for i in range(nchunks):
316 starti = i*chunk_step
317 stopi = starti+chunk_size
318 fft_chunk = rfft(data[starti:stopi]*win)
319 p_chunk = fft_chunk * fft_chunk.conj()
320 power += p_chunk.astype(float)
321 power /= float(nchunks)
322 return (freq_axis, power)
324 def unitary_avg_power_spectrum(data, freq=1.0, chunk_size=2048,
325 overlap=True, window=window_hann):
326 """Compute the unitary average power spectrum of `data`.
330 avg_power_spectrum,unitary_power_spectrum
332 freq_axis,power = avg_power_spectrum(data, freq, chunk_size,
334 # 2.0 / (freq * chunk_size) |rfft()|**2 --> unitary_power_spectrum
335 power *= 2.0 / (freq*float(chunk_size)) * 8/3 # see unitary_power_spectrum()
336 # * 8/3 to remove power from windowing
337 # <[x(t)*w(t)]**2> = <x(t)**2 * w(t)**2> ~= <x(t)**2> * <w(t)**2>
338 # where the ~= is because the frequency of x(t) >> the frequency of w(t).
339 # So our calulated power has and extra <w(t)**2> in it.
340 # For the Hann window, <w(t)**2> = <0.5(1 + 2cos + cos**2)> = 1/4 + 0 + 1/8 = 3/8
341 # For low frequency components, where the frequency of x(t) is ~= the frequency of w(t),
342 # The normalization is not perfect. ??
343 # The normalization approaches perfection as chunk_size -> infinity.
344 return (freq_axis, power)
348 class TestRFFT (unittest.TestCase):
349 r"""Ensure Numpy's FFT algorithm acts as expected.
353 The expected return values are [#dft]_:
355 .. math:: X_k = \sum_{m=0}^{n-1} x_m \exp^{-2\pi imk/n}
357 .. [#dft] See the *Background information* section of :mod:`numpy.fft`.
359 def run_rfft(self, xs, Xs):
364 Xa.append(sum([x*exp(-2*pi*i*m*k/n) for x,m in zip(xs,range(n))]))
366 assert (Xs[k]-Xa[k])/abs(Xa[k]) < 1e-6, \
367 "rfft mismatch on element %d: %g != %g, relative error %g" \
368 % (k, Xs[k], Xa[k], (Xs[k]-Xa[k])/abs(Xa[k]))
369 # Which should satisfy the discrete form of Parseval's theorem
371 # SUM |x_m|^2 = 1/n SUM |X_k|^2.
373 timeSum = sum([abs(x)**2 for x in xs])
374 freqSum = sum([abs(X)**2 for X in Xa])
375 assert abs(freqSum/float(n) - timeSum)/timeSum < 1e-6, \
376 "Mismatch on Parseval's, %g != 1/%d * %g" % (timeSum, n, freqSum)
379 xs = [1,2,3,1,2,3,1,2,3,1,2,3,1,2,3,1]
380 self.run_rfft(xs, rfft(xs))
382 class TestUnitaryRFFT (unittest.TestCase):
383 """Verify `unitary_rfft`.
385 def run_unitary_rfft_parsevals(self, xs, freq, freqs, Xs):
386 """Check the discretized integral form of Parseval's theorem
392 .. math:: \sum_{m=0}^{n-1} |x_m|^2 dt = \sum_{k=0}^{n-1} |X_k|^2 df
395 df = freqs[1]-freqs[0]
396 assert (df - 1/(len(xs)*dt))/df < 1e-6, \
397 "Mismatch in spacing, %g != 1/(%d*%g)" % (df, len(xs), dt)
399 for k in range(len(Xs)-1,1,-1):
401 assert len(xs) == len(Xa), "Length mismatch %d != %d" % (len(xs), len(Xa))
402 lhs = sum([abs(x)**2 for x in xs]) * dt
403 rhs = sum([abs(X)**2 for X in Xa]) * df
404 assert abs(lhs - rhs)/lhs < 1e-4, "Mismatch on Parseval's, %g != %g" \
407 def test_unitary_rfft_parsevals(self):
408 "Test unitary rfft on Parseval's theorem"
409 xs = [1,2,3,1,2,3,1,2,3,1,2,3,1,2,3,1]
411 freqs,Xs = unitary_rfft(xs, 1.0/dt)
412 self.run_unitary_rfft_parsevals(xs, 1.0/dt, freqs, Xs)
415 r"""Rectangle function.
422 \rect(t) = \begin{cases}
423 1& \text{if $|t| < 0.5$}, \\
424 0& \text{if $|t| \ge 0.5$}.
432 def run_unitary_rfft_rect(self, a=1.0, time_shift=5.0, samp_freq=25.6,
434 r"""Test `unitary_rttf` on known function `rect(at)`.
440 .. math:: \rfft(\rect(at)) = 1/|a|\cdot\sinc(f/a)
442 samp_freq = float(samp_freq)
445 x = zeros((samples,), dtype=float)
447 for i in range(samples):
449 x[i] = self.rect(a*(t-time_shift))
450 freq_axis, X = unitary_rfft(x, samp_freq)
451 #_test_unitary_rfft_parsevals(x, samp_freq, freq_axis, X)
453 # remove the phase due to our time shift
454 j = complex(0.0,1.0) # sqrt(-1)
455 for i in range(len(freq_axis)):
457 inverse_phase_shift = exp(j*2.0*pi*time_shift*f)
458 X[i] *= inverse_phase_shift
460 expected = zeros((len(freq_axis),), dtype=float)
461 # normalized sinc(x) = sin(pi x)/(pi x)
462 # so sinc(0.5) = sin(pi/2)/(pi/2) = 2/pi
463 assert sinc(0.5) == 2.0/pi, "abnormal sinc()"
464 for i in range(len(freq_axis)):
466 expected[i] = 1.0/abs(a) * sinc(f/a)
471 pylab.plot(arange(0, dt*samples, dt), x)
472 pylab.title('time series')
474 pylab.plot(freq_axis, X.real, 'r.')
475 pylab.plot(freq_axis, X.imag, 'g.')
476 pylab.plot(freq_axis, expected, 'b-')
477 pylab.title('freq series')
479 def test_unitary_rfft_rect(self):
480 "Test unitary FFTs on variously shaped rectangular functions."
481 self.run_unitary_rfft_rect(a=0.5)
482 self.run_unitary_rfft_rect(a=2.0)
483 self.run_unitary_rfft_rect(a=0.7, samp_freq=50, samples=512)
484 self.run_unitary_rfft_rect(a=3.0, samp_freq=60, samples=1024)
486 def gaussian(self, a, t):
487 r"""Gaussian function.
492 .. math:: \gaussian(a,t) = \exp^{-at^2}
494 return exp(-a * t**2)
496 def run_unitary_rfft_gaussian(self, a=1.0, time_shift=5.0, samp_freq=25.6,
498 r"""Test `unitary_rttf` on known function `gaussian(a,t)`.
506 \rfft(\gaussian(a,t)) = \sqrt{\pi/a} \cdot \gaussian(1/a,\pi f)
508 samp_freq = float(samp_freq)
511 x = zeros((samples,), dtype=float)
513 for i in range(samples):
515 x[i] = self.gaussian(a, (t-time_shift))
516 freq_axis, X = unitary_rfft(x, samp_freq)
517 #_test_unitary_rfft_parsevals(x, samp_freq, freq_axis, X)
519 # remove the phase due to our time shift
520 j = complex(0.0,1.0) # sqrt(-1)
521 for i in range(len(freq_axis)):
523 inverse_phase_shift = exp(j*2.0*pi*time_shift*f)
524 X[i] *= inverse_phase_shift
526 expected = zeros((len(freq_axis),), dtype=float)
527 for i in range(len(freq_axis)):
529 expected[i] = sqrt(pi/a) * self.gaussian(1.0/a, pi*f) # see Wikipedia, or do the integral yourself.
534 pylab.plot(arange(0, dt*samples, dt), x)
535 pylab.title('time series')
537 pylab.plot(freq_axis, X.real, 'r.')
538 pylab.plot(freq_axis, X.imag, 'g.')
539 pylab.plot(freq_axis, expected, 'b-')
540 pylab.title('freq series')
542 def test_unitary_rfft_gaussian(self):
543 "Test unitary FFTs on variously shaped gaussian functions."
544 self.run_unitary_rfft_gaussian(a=0.5)
545 self.run_unitary_rfft_gaussian(a=2.0)
546 self.run_unitary_rfft_gaussian(a=0.7, samp_freq=50, samples=512)
547 self.run_unitary_rfft_gaussian(a=3.0, samp_freq=60, samples=1024)
549 class TestUnitaryPowerSpectrum (unittest.TestCase):
550 def run_unitary_power_spectrum_sin(self, sin_freq=10, samp_freq=512,
552 x = zeros((samples,), dtype=float)
553 samp_freq = float(samp_freq)
554 for i in range(samples):
555 x[i] = sin(2.0 * pi * (i/samp_freq) * sin_freq)
556 freq_axis, power = unitary_power_spectrum(x, samp_freq)
559 expected = zeros((len(freq_axis),), dtype=float)
560 df = samp_freq/float(samples) # df = 1/T, where T = total_time
562 # average power per unit time is
564 # average value of sin(t)**2 = 0.5 (b/c sin**2+cos**2 == 1)
565 # so average value of (int sin(t)**2 dt) per unit time is 0.5
567 # we spread that power over a frequency bin of width df, sp
569 # where f0 is the sin's frequency
572 # FFT of sin(2*pi*t*f0) gives
573 # X(f) = 0.5 i (delta(f-f0) - delta(f-f0)),
574 # (area under x(t) = 0, area under X(f) = 0)
575 # so one sided power spectral density (PSD) per unit time is
576 # P(f) = 2 |X(f)|**2 / T
577 # = 2 * |0.5 delta(f-f0)|**2 / T
578 # = 0.5 * |delta(f-f0)|**2 / T
579 # but we're discrete and want the integral of the 'delta' to be 1,
580 # so 'delta'*df = 1 --> 'delta' = 1/df, and
581 # P(f) = 0.5 / (df**2 * T)
582 # = 0.5 / df (T = 1/df)
583 expected[i] = 0.5 / df
585 print "The power should be a peak at %g Hz of %g (%g, %g)" % \
586 (sin_freq, expected[i], freq_axis[imax], power[imax])
589 for i in range(len(freq_axis)):
590 Pexp += expected[i] *df
592 print " The total power should be %g (%g)" % (Pexp, P)
597 pylab.plot(arange(0, samples/samp_freq, 1.0/samp_freq), x, 'b-')
598 pylab.title('time series')
600 pylab.plot(freq_axis, power, 'r.')
601 pylab.plot(freq_axis, expected, 'b-')
602 pylab.title('%g samples of sin at %g Hz' % (samples, sin_freq))
604 def test_unitary_power_spectrum_sin(self):
605 "Test unitary power spectrums on variously shaped sin functions"
606 self.run_unitary_power_spectrum_sin(sin_freq=5, samp_freq=512, samples=1024)
607 self.run_unitary_power_spectrum_sin(sin_freq=5, samp_freq=512, samples=2048)
608 self.run_unitary_power_spectrum_sin(sin_freq=5, samp_freq=512, samples=4098)
609 self.run_unitary_power_spectrum_sin(sin_freq=7, samp_freq=512, samples=1024)
610 self.run_unitary_power_spectrum_sin(sin_freq=5, samp_freq=1024, samples=2048)
611 # finally, with some irrational numbers, to check that I'm not getting lucky
612 self.run_unitary_power_spectrum_sin(sin_freq=pi, samp_freq=100*exp(1), samples=1024)
613 # test with non-integer number of periods
614 self.run_unitary_power_spectrum_sin(sin_freq=5, samp_freq=512, samples=256)
616 def run_unitary_power_spectrum_delta(self, amp=1, samp_freq=1,
620 x = zeros((samples,), dtype=float)
621 samp_freq = float(samp_freq)
623 freq_axis, power = unitary_power_spectrum(x, samp_freq)
625 # power = <x(t)**2> = (amp)**2 * dt/T
626 # we spread that power over the entire freq_axis [0,fN], so
627 # P(f) = (amp)**2 dt / (T fN)
629 # dt = 1/samp_freq (sample period)
630 # T = samples/samp_freq (total time of data aquisition)
631 # fN = 0.5 samp_freq (Nyquist frequency)
633 # P(f) = amp**2 / (samp_freq * samples/samp_freq * 0.5 samp_freq)
634 # = 2 amp**2 / (samp_freq*samples)
635 expected_amp = 2.0 * amp**2 / (samp_freq * samples)
636 expected = ones((len(freq_axis),), dtype=float) * expected_amp
638 print "The power should be flat at y = %g (%g)" % (expected_amp, power[0])
643 pylab.plot(arange(0, samples/samp_freq, 1.0/samp_freq), x, 'b-')
644 pylab.title('time series')
646 pylab.plot(freq_axis, power, 'r.')
647 pylab.plot(freq_axis, expected, 'b-')
648 pylab.title('%g samples of delta amp %g' % (samples, amp))
650 def _test_unitary_power_spectrum_delta(self):
651 "Test unitary power spectrums on various delta functions"
652 _test_unitary_power_spectrum_delta(amp=1, samp_freq=1.0, samples=1024)
653 _test_unitary_power_spectrum_delta(amp=1, samp_freq=1.0, samples=2048)
654 _test_unitary_power_spectrum_delta(amp=1, samp_freq=0.5, samples=2048)# expected = 2*computed
655 _test_unitary_power_spectrum_delta(amp=1, samp_freq=2.0, samples=2048)# expected = 0.5*computed
656 _test_unitary_power_spectrum_delta(amp=3, samp_freq=1.0, samples=1024)
657 _test_unitary_power_spectrum_delta(amp=pi, samp_freq=exp(1), samples=1024)
659 def gaussian(self, area, mean, std, t):
660 "Integral over all time = area (i.e. normalized for area=1)"
661 return area/(std*sqrt(2.0*pi)) * exp(-0.5*((t-mean)/std)**2)
663 def run_unitary_power_spectrum_gaussian(self, area=2.5, mean=5, std=1,
664 samp_freq=10.24 ,samples=512):
667 x = zeros((samples,), dtype=float)
669 for i in range(samples):
670 t = i/float(samp_freq)
671 x[i] = self.gaussian(area, mean, std, t)
672 freq_axis, power = unitary_power_spectrum(x, samp_freq)
674 # generate the predicted curve
675 # by comparing our self.gaussian() form to _gaussian(),
676 # we see that the Fourier transform of x(t) has parameters:
677 # std' = 1/(2 pi std) (references declaring std' = 1/std are converting to angular frequency, not frequency like we are)
678 # area' = area/[std sqrt(2*pi)] (plugging into FT of _gaussian() above)
679 # mean' = 0 (changing the mean in the time-domain just changes the phase in the freq-domain)
680 # So our power spectral density per unit time is given by
681 # P(f) = 2 |X(f)|**2 / T
683 # T = samples/samp_freq (total time of data aquisition)
685 area = area /(std*sqrt(2.0*pi))
686 std = 1.0/(2.0*pi*std)
687 expected = zeros((len(freq_axis),), dtype=float)
688 df = float(samp_freq)/samples # 1/total_time ( = freq_axis[1]-freq_axis[0] = freq_axis[1])
689 for i in range(len(freq_axis)):
691 gaus = self.gaussian(area, mean, std, f)
692 expected[i] = 2.0 * gaus**2 * samp_freq/samples
693 print "The power should be a half-gaussian, ",
694 print "with a peak at 0 Hz with amplitude %g (%g)" % (expected[0], power[0])
699 pylab.plot(arange(0, samples/samp_freq, 1.0/samp_freq), x, 'b-')
700 pylab.title('time series')
702 pylab.plot(freq_axis, power, 'r.')
703 pylab.plot(freq_axis, expected, 'b-')
704 pylab.title('freq series')
706 def test_unitary_power_spectrum_gaussian(self):
707 "Test unitary power spectrums on various gaussian functions"
709 for std in [1,sqrt(2)]:
710 for samp_freq in [10.0, exp(1)]:
711 for samples in [1024,2048]:
712 self.run_unitary_power_spectrum_gaussian(
713 area=area, std=std, samp_freq=samp_freq,
716 class TestUnitaryAvgPowerSpectrum (unittest.TestCase):
717 def run_unitary_avg_power_spectrum_sin(self, sin_freq=10, samp_freq=512,
718 samples=1024, chunk_size=512,
719 overlap=True, window=window_hann):
722 x = zeros((samples,), dtype=float)
723 samp_freq = float(samp_freq)
724 for i in range(samples):
725 x[i] = sin(2.0 * pi * (i/samp_freq) * sin_freq)
726 freq_axis, power = unitary_avg_power_spectrum(x, samp_freq, chunk_size,
730 expected = zeros((len(freq_axis),), dtype=float)
731 df = samp_freq/float(chunk_size) # df = 1/T, where T = total_time
733 expected[i] = 0.5 / df # see _test_unitary_power_spectrum_sin()
735 print "The power should be a peak at %g Hz of %g (%g, %g)" % \
736 (sin_freq, expected[i], freq_axis[imax], power[imax])
739 for i in range(len(freq_axis)):
740 Pexp += expected[i] * df
742 print " The total power should be %g (%g)" % (Pexp, P)
747 pylab.plot(arange(0, samples/samp_freq, 1.0/samp_freq), x, 'b-')
748 pylab.title('time series')
750 pylab.plot(freq_axis, power, 'r.')
751 pylab.plot(freq_axis, expected, 'b-')
752 pylab.title('%g samples of sin at %g Hz' % (samples, sin_freq))
754 def test_unitary_avg_power_spectrum_sin(self):
755 "Test unitary avg power spectrums on variously shaped sin functions."
756 self.run_unitary_avg_power_spectrum_sin(sin_freq=5, samp_freq=512, samples=1024)
757 self.run_unitary_avg_power_spectrum_sin(sin_freq=5, samp_freq=512, samples=2048)
758 self.run_unitary_avg_power_spectrum_sin(sin_freq=5, samp_freq=512, samples=4098)
759 self.run_unitary_avg_power_spectrum_sin(sin_freq=17, samp_freq=512, samples=1024)
760 self.run_unitary_avg_power_spectrum_sin(sin_freq=5, samp_freq=1024, samples=2048)
761 # test long wavelenth sin, so be closer to window frequency
762 self.run_unitary_avg_power_spectrum_sin(sin_freq=1, samp_freq=1024, samples=2048)
763 # finally, with some irrational numbers, to check that I'm not getting lucky
764 self.run_unitary_avg_power_spectrum_sin(sin_freq=pi, samp_freq=100*exp(1), samples=1024)