Advertisement
Guest User

translate between DFT coefficients and amplitude estimates

a guest
Dec 21st, 2015
611
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 5.91 KB | None | 0 0
  1. #!/usr/bin/env python
  2. import numpy
  3. """
  4. Demonstrate and work around the fiddliness of translating between DFT
  5. coefficients and accurate amplitude estimates (scaling; special handling
  6. of DC component; signal-length-dependent special handling of Nyquist
  7. component).
  8. """
  9.  
  10.  
  11. def fft2ap(X, samplingfreq_hz=2.0, axis=0):
  12.     """
  13.     Given discrete Fourier transform(s) <X> (with frequency along the
  14.     specified <axis>), return a dict containing a properly scaled
  15.     amplitude spectrum, a phase spectrum in degrees and in radians,
  16.     and a frequency axis. Copes with all the edge conditions and
  17.     fiddly bits that tend to go wrong when you try to do it by hand,
  18.     namely:
  19.         (1) scaling according to Parseval's Theorem;
  20.         (2) special rescaling of the DC component;
  21.         (3) signal-length-dependent special rescaling of the Nyquist
  22.             component;
  23.         (4) removing the negative-frequency part of the spectrum.
  24.    
  25.     The inverse of   d=fft2ap(X)  is  X = ap2fft(**d)
  26.     """
  27.     fs = float(samplingfreq_hz)
  28.     nsamp = int(X.shape[axis])
  29.     biggest_pos_freq = float(numpy.floor(nsamp/2))       # floor(nsamp/2)
  30.     biggest_neg_freq = -float(numpy.floor((nsamp-1)/2))  # -floor((nsamp-1)/2)
  31.     posfreq = numpy.arange(0.0, biggest_pos_freq+1.0) * (float(fs) / float(nsamp))
  32.     negfreq = numpy.arange(biggest_neg_freq, 0.0) * (float(fs) / float(nsamp))
  33.     fullfreq = numpy.concatenate((posfreq,negfreq))
  34.     sub = [slice(None)] * max(axis+1, len(X.shape))
  35.     sub[axis] = slice(0,len(posfreq))
  36.     X = project(X, axis)[sub]
  37.     ph = numpy.angle(X)
  38.     amp = numpy.abs(X) * (2.0 / float(nsamp))  # scaling according to Parseval's Theorem
  39.     sub[axis] = 0
  40.     amp[sub] /= 2.0  # DC is represented only once, and hence at double amplitude relative to others
  41.     if nsamp%2 == 0:    # ...and if-and-only-if the number of samples is even, the same logic applies to the Nyquist frequency
  42.         sub[axis] = -1  #    
  43.         amp[sub] /= 2.0 #
  44.     return {'amplitude':amp, 'phase_rad':ph, 'phase_deg':ph*(180.0/numpy.pi), 'freq_hz':posfreq, 'fullfreq_hz':fullfreq, 'samplingfreq_hz':fs, 'axis':axis}
  45.  
  46. def ap2fft(amplitude,phase_rad=None,phase_deg=None,samplingfreq_hz=2.0,axis=0,freq_hz=None,fullfreq_hz=None,nsamp=None):
  47.     """
  48.     Keyword arguments match the fields of the dict
  49.     output by fft2ap().
  50.  
  51.     The inverse of   d=fft2ap(X)  is  X = ap2fft(**d)
  52.     """
  53.     fs = float(samplingfreq_hz)
  54.     if nsamp==None:
  55.         if fullfreq_hz is not None: nsamp = len(fullfreq_hz)
  56.         elif freq_hz is not None:   nsamp = len(freq_hz) * 2 - 2 # assume even number of samples if no other info
  57.         else: nsamp = amplitude.shape[axis] * 2 - 2 # assume even number of samples if no other info
  58.    
  59.     amplitude = project(numpy.array(amplitude,dtype='float'), axis)
  60.     if phase_rad is None and phase_deg is None: phase_rad = numpy.zeros(shape=amplitude.shape,dtype='float')
  61.     if phase_rad is not None:
  62.         if not isinstance(phase_rad, numpy.ndarray) or phase_rad.dtype != 'float': phase_rad = numpy.array(phase_rad,dtype='float')
  63.         phase_rad = project(phase_rad, axis)
  64.     if phase_deg is not None:
  65.         if not isinstance(phase_deg, numpy.ndarray) or phase_deg.dtype != 'float': phase_deg = numpy.array(phase_deg,dtype='float')
  66.         phase_deg = project(phase_deg, axis)
  67.     if phase_rad is not None and phase_deg is not None:
  68.         if phase_rad.shape != phase_deg.shape: raise ValueError, "conflicting phase_rad and phase_deg arguments"
  69.         if numpy.max(numpy.abs(phase_rad * (180.0/numpy.pi) - phase_deg) > 1e-10): raise ValueError, "conflicting phase_rad and phase_deg arguments"
  70.        
  71.     if phase_rad is None:
  72.         phase_rad = phase_deg * (numpy.pi/180.0)
  73.     f = phase_rad * 1j
  74.     f = numpy.exp(f)
  75.     f = f * amplitude
  76.     f *= float(nsamp)/2.0 # scaling according to Parseval's Theorem
  77.     sub = [slice(None)] * max(axis+1, len(f.shape))
  78.     sub[axis] = 0
  79.     f[sub] *= 2.0
  80.     if nsamp%2 == 0:
  81.         sub[axis] = -1
  82.         f[sub] *= 2.0
  83.     sub[axis] = slice((nsamp%2)-2, 0, -1)
  84.     f = numpy.concatenate((f, numpy.conj(f[sub])), axis=axis)
  85.     return f
  86.  
  87. # helper function
  88. def project(a, maxdim):
  89.     """
  90.     Return a view of the numpy array <a> that has at least <maxdim>+1
  91.     dimensions.
  92.     """
  93.     if isinstance(a, numpy.matrix) and maxdim > 1: a = numpy.asarray(a)
  94.     else: a = a.view()
  95.     a.shape += (1,) * (maxdim-len(a.shape)+1)
  96.     return a
  97.  
  98. if __name__ == '__main__':
  99.     # Test code.
  100.    
  101.     # The following three parameters can be overridden by command-line args:
  102.     freq_hz = 2.0  # with samplingfreq_hz=10 or 11, set freq_hz to 5.0 to test the Nyquist
  103.     seconds = 1.0  #
  104.     samplingfreq_hz = 10.0 # with default seconds=1.0, set this to 11 instead of 10 to test the odd-number-of-samples case
  105.    
  106.     import sys
  107.     args = getattr( sys, 'argv', [] )[ 1: ]
  108.     if args: freq_hz = float( args.pop( 0 ) )
  109.     if args: seconds = float( args.pop( 0 ) )
  110.     if args: samplingfreq_hz = float( args.pop( 0 ) )
  111.    
  112.     nsamples = int( round( samplingfreq_hz * seconds ) )
  113.     theta = numpy.linspace( 0, 1, nsamples, endpoint=0 )
  114.     x = numpy.cos( 2 * numpy.pi * freq_hz * theta ) # generate wave
  115.     x = x[ numpy.newaxis, : ] * [ [ 1 ], [ 2 ] ] + [ [ 3 ], [ 5 ] ]  # create an array with two rows, each row a scaled shifted version of the wave
  116.     X = numpy.fft.fft( x, axis=1 )
  117.     d = fft2ap( X,  samplingfreq_hz=samplingfreq_hz , axis=1 )
  118.    
  119.     # reconstruct  
  120.     Xr = ap2fft( **d )
  121.     xr = numpy.fft.ifft( Xr, axis=1 ).real
  122.     print( 'err = %g' % numpy.abs(x-xr).max() ) # reconstruction error should be tiny (say 1e-14)
  123.    
  124.     # Check output:  DC components should be exactly 3 and 5; chosen freq_hz components
  125.     # should have amplitude 1 and 2, regardless of whether they're the Nyquist or not,
  126.     # and of whether nsamples is even or odd   
  127.     for k,v in sorted( d.items() ): print( '%s = %s' % ( k, str( v ) ) )
  128.     # plot amplitude spectrum.
  129.     import pylab
  130.     pylab.ion()
  131.     pylab.bar( d[ 'freq_hz' ]-0.5, d[ 'amplitude' ][ 0 ], width=0.5, color='b', hold=False )
  132.     pylab.bar( d[ 'freq_hz' ],     d[ 'amplitude' ][ 1 ], width=0.5, color='r', hold=True )
  133.     pylab.title( 'number of samples = %d' % nsamples )
  134.     pylab.ylabel( 'amplitude' )
  135.     pylab.xlabel( 'frequency (Hz)' )
  136.     pylab.grid( 'on' )
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement